Files
2026-04-05 14:01:17 +02:00

1040 lines
56 KiB
PHP

<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Tagbogen - AstroTools';
$bodyClass = 'tagbogen-page';
?>
<?php require __DIR__ . '/header.php'; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Space+Mono:wght@400;700&family=Orbitron:wght@400;600;700;900&display=swap" rel="stylesheet">
<script src="js/astronomy.browser.min.js"></script>
<div class="tagbogen-tool">
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-head">
<div>
<div class="modal-head-title">STANDORT AUSW?HLEN</div>
<div class="modal-head-sub">Beobachtungsort auf der Erde</div>
</div>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<!-- Suche + Geolocation -->
<div class="modal-search-row">
<div class="modal-search-field">
<div class="modal-sect-label">Suche</div>
<input class="coord-input" id="locSearch" type="text" placeholder="Name eingeben..." oninput="filterLocs(this.value)">
</div>
<button class="geo-btn" id="geoBtn" onclick="geoLocate()" title="Standort über Browser ermitteln">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="15" height="15">
<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
<path d="M12 8a4 4 0 100 8 4 4 0 000-8z" opacity=".3"/>
</svg>
<span id="geoBtnText">GPS</span>
</button>
</div>
<!-- Gruppen-Dropdown -->
<div id="locGroups"></div>
<hr class="modal-divider">
<div>
<div class="modal-sect-label">Eigene Koordinaten</div>
<div class="coord-row">
<div class="coord-field">
<div class="coord-label">Breitengrad &deg;N</div>
<input class="coord-input" id="inputLat" type="number" step="0.01" min="-90" max="90" placeholder="z.B. 48.14">
</div>
<div class="coord-field">
<div class="coord-label">L?ngengrad &deg;E</div>
<input class="coord-input" id="inputLon" type="number" step="0.01" min="-180" max="180" placeholder="z.B. 11.58">
</div>
</div>
<div class="coord-field mt-gap">
<div class="coord-label">Ortsname (optional)</div>
<input class="coord-input" id="inputName" type="text" placeholder="Eigener Ort">
</div>
<button class="apply-btn" onclick="applyCustomCoords()">KOORDINATEN ANWENDEN</button>
</div>
</div>
</div>
</div>
<div class="page">
<div class="header">
<div class="header-left">
<div class="header-pre">Astronomische Visualisierung - Astronomy Engine</div>
<div class="header-title">TAGBOGEN</div>
<div class="header-sub">Sonnenweg am Himmelsgew?lbe</div>
</div>
<div class="header-right">
<button class="loc-btn" onclick="openModal()">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 1118 0z"/><circle cx="12" cy="10" r="3"/>
</svg>
<span id="locBtnText">STANDORT</span>
</button>
<div class="date-ctrl">
<button class="day-btn" onclick="shiftDay(-1)" title="Einen Tag zur?ck">&lt;</button>
<input type="date" class="date-input-hdr" id="dateInput">
<button class="day-btn" onclick="shiftDay(+1)" title="Einen Tag vor">&gt;</button>
</div>
</div>
</div>
<div class="app-layout">
<div class="controls-panel">
<div class="ctrl-block">
<div class="ctrl-label">Standort</div>
<div class="info-row"><span class="info-lbl">Ort</span><span class="info-val" id="infoName">-</span></div>
<div class="info-row"><span class="info-lbl">Breite</span><span class="info-val" id="infoLat">-</span></div>
<div class="info-row"><span class="info-lbl">L?nge</span><span class="info-val" id="infoLon">-</span></div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Sonnenzeiten (UTC)</div>
<div class="stat-grid">
<div class="stat-item"><div class="stat-lbl">Aufgang</div><div class="stat-val sm" id="statRise">-</div></div>
<div class="stat-item"><div class="stat-lbl">Untergang</div><div class="stat-val sm" id="statSet">-</div></div>
<div class="stat-item"><div class="stat-lbl">Mittag</div><div class="stat-val sm" id="statNoon">-</div></div>
<div class="stat-item"><div class="stat-lbl">Taglaenge</div><div class="stat-val sm" id="statDay">-</div></div>
</div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Aktuelle Position</div>
<div class="stat-grid">
<div class="stat-item"><div class="stat-lbl">H?he</div><div class="stat-val" id="statElev">-&deg;</div></div>
<div class="stat-item"><div class="stat-lbl">Azimut</div><div class="stat-val" id="statAz">-&deg;</div></div>
</div>
<div class="mt-lg">
<div class="info-row"><span class="info-lbl">Himmelsrichtung</span><span class="info-val" id="statDir">-</span></div>
<div class="info-row"><span class="info-lbl">Tagesphase</span><span class="info-val" id="statPhase">-</span></div>
<div class="sun-phase-bar"><div class="sun-phase-fill" id="phaseBar"></div></div>
</div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Sonnenparameter</div>
<div class="info-row"><span class="info-lbl">Deklination</span><span class="info-val" id="statDecl">-&deg;</span></div>
<div class="info-row"><span class="info-lbl">Max. Elevation</span><span class="info-val" id="statMaxElev">-&deg;</span></div>
<div class="info-row"><span class="info-lbl">Aufgang-Azimut</span><span class="info-val" id="statRiseAz">-&deg;</span></div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Jahresvergleich</div>
<div class="overlay-note">?berlagerung der Sonnenb?gen f?r astronomisch wichtige Tage</div>
<div class="overlay-list">
<button class="overlay-btn" id="btn_sommer" onclick="toggleOverlay('sommer')" data-active="0">
<span class="overlay-dot is-summer"></span>
<span class="overlay-text">Sommersonnenwende</span>
<div class="overlay-tooltip" id="tip_sommer">
<div class="overlay-tooltip-title">SOMMERSONNENWENDE</div>
<div class="overlay-tooltip-row"><span>Datum</span><span id="tip_sommer_date">21. Jun.</span></div>
<div class="overlay-tooltip-row"><span>Aufgang</span><span id="tip_sommer_rise">-</span></div>
<div class="overlay-tooltip-row"><span>Untergang</span><span id="tip_sommer_set">-</span></div>
<div class="overlay-tooltip-row highlight"><span>Taglaenge</span><span id="tip_sommer_dur">-</span></div>
<div class="overlay-tooltip-row"><span>Max. Elevation</span><span id="tip_sommer_elev">-</span></div>
<div class="overlay-tooltip-row"><span>Aufgang-Azimut</span><span id="tip_sommer_az">-</span></div>
</div>
</button>
<button class="overlay-btn" id="btn_winter" onclick="toggleOverlay('winter')" data-active="0">
<span class="overlay-dot is-winter"></span>
<span class="overlay-text">Wintersonnenwende</span>
<div class="overlay-tooltip" id="tip_winter">
<div class="overlay-tooltip-title">WINTERSONNENWENDE</div>
<div class="overlay-tooltip-row"><span>Datum</span><span id="tip_winter_date">21. Dez.</span></div>
<div class="overlay-tooltip-row"><span>Aufgang</span><span id="tip_winter_rise">-</span></div>
<div class="overlay-tooltip-row"><span>Untergang</span><span id="tip_winter_set">-</span></div>
<div class="overlay-tooltip-row highlight"><span>Taglaenge</span><span id="tip_winter_dur">-</span></div>
<div class="overlay-tooltip-row"><span>Max. Elevation</span><span id="tip_winter_elev">-</span></div>
<div class="overlay-tooltip-row"><span>Aufgang-Azimut</span><span id="tip_winter_az">-</span></div>
</div>
</button>
<button class="overlay-btn" id="btn_fruehjahr" onclick="toggleOverlay('fruehjahr')" data-active="0">
<span class="overlay-dot is-equinox"></span>
<span class="overlay-text">Tag-/Nachtgleiche</span>
<div class="overlay-tooltip" id="tip_fruehjahr">
<div class="overlay-tooltip-title">TAG-/NACHTGLEICHE</div>
<div class="overlay-tooltip-row"><span>Datum</span><span id="tip_fruehjahr_date">20. M?r.</span></div>
<div class="overlay-tooltip-row"><span>Aufgang</span><span id="tip_fruehjahr_rise">-</span></div>
<div class="overlay-tooltip-row"><span>Untergang</span><span id="tip_fruehjahr_set">-</span></div>
<div class="overlay-tooltip-row highlight"><span>Taglaenge</span><span id="tip_fruehjahr_dur">-</span></div>
<div class="overlay-tooltip-row"><span>Max. Elevation</span><span id="tip_fruehjahr_elev">-</span></div>
<div class="overlay-tooltip-row"><span>Aufgang-Azimut</span><span id="tip_fruehjahr_az">-</span></div>
</div>
</button>
</div>
<button class="overlay-clear-btn mt-lg" onclick="clearOverlays()">Alle ausblenden</button>
</div>
</div>
<div class="viz-panel">
<div class="viz-wrap">
<canvas id="arcCanvas"></canvas>
<div class="arc-overlay"><div class="arc-badge" id="arcBadge">TAGBOGEN</div></div>
<div class="view-toggle"><button class="vtb active" id="vtb_allsky" onclick="setView('allsky')">Zenit</button><button class="vtb" id="vtb_horizon" onclick="setView('horizon')">Horizont</button></div>
<div class="time-display">
<div class="time-big" id="timeBig">--:--</div>
<div class="time-sub">UTC</div>
</div>
</div>
<div class="slider-wrap">
<div class="slider-header">
<span class="slider-label" id="sliderLabel">TAGESVERLAUF</span>
<span class="slider-time" id="sliderTimeLabel">--:--</span>
</div>
<div class="sun-slider-track">
<div class="sun-slider-bg"><div class="sun-gradient-fill"></div></div>
<div class="sun-track-labels">
<span class="sun-track-lbl" id="lbl_rise">- O (Aufgang)</span>
<span class="sun-track-lbl" id="lbl_noon">-</span>
<span class="sun-track-lbl" id="lbl_set">(Untergang) W -</span>
</div>
<input type="range" id="timeSlider" min="0" max="1000" value="500" oninput="onSlider()">
</div>
<div class="time-ticks" id="timeTicks"></div>
</div>
</div>
</div>
</div>
<script>
//
//
//
//
//
const LOC_GROUPS = [
{ group: 'Deutschland & Nachbarl?nder', items: [
{name:'Rödental', sub:'Bayern, DE', lat: 50.30, lon: 11.05},
{name:'Berlin', sub:'DE, 52°N', lat: 52.52, lon: 13.40},
{name:'München', sub:'Bayern, DE', lat: 48.14, lon: 11.58},
{name:'Hamburg', sub:'DE, 53°N', lat: 53.55, lon: 10.00},
{name:'Köln', sub:'NRW, DE', lat: 50.94, lon: 6.96},
{name:'Frankfurt', sub:'Hessen, DE', lat: 50.11, lon: 8.68},
{name:'Dresden', sub:'Sachsen, DE', lat: 51.05, lon: 13.74},
{name:'Flensburg', sub:'Norddeutschland', lat: 54.79, lon: 9.44},
{name:'Zugspitze', sub:'2962 m, Bayern', lat: 47.42, lon: 10.99},
{name:'Wien', sub:'Österreich', lat: 48.21, lon: 16.37},
{name:'Innsbruck', sub:'Tirol, AT', lat: 47.27, lon: 11.39},
{name:'Zürich', sub:'Schweiz', lat: 47.38, lon: 8.54},
{name:'Bern', sub:'Schweiz', lat: 46.95, lon: 7.44},
{name:'Genf', sub:'Schweiz, 46°N', lat: 46.20, lon: 6.15},
{name:'Prag', sub:'Tschechien', lat: 50.08, lon: 14.42},
{name:'Warschau', sub:'Polen', lat: 52.23, lon: 21.01},
{name:'Amsterdam', sub:'Niederlande', lat: 52.37, lon: 4.90},
{name:'Brüssel', sub:'Belgien', lat: 50.85, lon: 4.35},
]},
{ group: 'Sternwarten & Observatorien', items: [
{name:'Sternwarte Sonneberg', sub:'Thüringen, 50.4°N', lat: 50.36, lon: 11.19},
{name:'Archenhold Berlin', sub:'Treptow, 52.5°N', lat: 52.48, lon: 13.47},
{name:'Deutsches Museum München',sub:'Isartor', lat: 48.13, lon: 11.58},
{name:'Sternwarte Heidelberg', sub:'Königstuhl', lat: 49.40, lon: 8.72},
{name:'H?her List Eifel', sub:'Rheinland, 50°N', lat: 50.16, lon: 6.85},
{name:'Calar Alto', sub:'Spanien, 2168 m', lat: 37.22, lon: -2.55},
{name:'ESO La Silla', sub:'Chile, 2400 m', lat:-29.26, lon:-70.73},
{name:'ESO Paranal (VLT)', sub:'Chile, 2635 m', lat:-24.63, lon:-70.40},
{name:'Mauna Kea', sub:'Hawaii, 4205 m', lat: 19.82, lon:-155.47},
{name:'La Palma (Roque)', sub:'Kanaren, 2396 m', lat: 28.76, lon:-17.89},
{name:'Kitt Peak', sub:'Arizona, USA', lat: 31.96, lon:-111.60},
{name:'Greenwich', sub:'London, 0° Merid.',lat: 51.48, lon: 0.00},
{name:'Arecibo', sub:'Puerto Rico', lat: 18.34, lon:-66.75},
{name:'ALMA', sub:'Chile, 5058 m', lat:-23.03, lon:-67.75},
{name:'SKA Karoo', sub:'Südafrika', lat:-30.71, lon: 21.44},
{name:'FAST China', sub:'Guizhou, 25°N', lat: 25.65, lon:106.86},
{name:'Effelsberg', sub:'Eifel, 50.5°N', lat: 50.52, lon: 6.88},
{name:'Mt. Palomar', sub:'Kalifornien', lat: 33.36, lon:-116.86},
{name:'Cerro Pachón (LSST)', sub:'Chile, 2662 m', lat:-30.24, lon:-70.74},
{name:'Sternwarte Bochum', sub:'NRW, 51.5°N', lat: 51.48, lon: 7.22},
]},
{ group: 'Europa', items: [
{name:'Paris', sub:'Frankreich', lat: 48.85, lon: 2.35},
{name:'London', sub:'UK, 51.5°N', lat: 51.51, lon: -0.13},
{name:'Madrid', sub:'Spanien', lat: 40.42, lon: -3.70},
{name:'Barcelona', sub:'Katalonien', lat: 41.39, lon: 2.15},
{name:'Rom', sub:'Italien, 42°N', lat: 41.90, lon: 12.50},
{name:'Mailand', sub:'Norditalien', lat: 45.46, lon: 9.19},
{name:'Athen', sub:'Griechenland', lat: 37.98, lon: 23.73},
{name:'Istanbul', sub:'Türkei, 41°N', lat: 41.02, lon: 28.97},
{name:'Lissabon', sub:'Portugal, 38.7°N', lat: 38.72, lon: -9.14},
{name:'Oslo', sub:'Norwegen, 60°N', lat: 59.91, lon: 10.75},
{name:'Stockholm', sub:'Schweden, 59°N', lat: 59.33, lon: 18.07},
{name:'Helsinki', sub:'Finnland, 60°N', lat: 60.17, lon: 24.94},
{name:'Kopenhagen', sub:'Dänemark, 56°N', lat: 55.68, lon: 12.57},
{name:'Reykjavik', sub:'Island, 64°N', lat: 64.13, lon:-21.94},
{name:'Tromsø', sub:'Norwegen, 70°N', lat: 69.65, lon: 18.96},
{name:'Longyearbyen', sub:'Svalbard, 78°N', lat: 78.22, lon: 15.63},
{name:'Lofoten', sub:'Norwegen, 68°N', lat: 68.11, lon: 13.58},
{name:'Moskau', sub:'Russland, 56°N', lat: 55.75, lon: 37.62},
{name:'Murmansk', sub:'Russland, 69°N', lat: 68.97, lon: 33.07},
]},
{ group: 'Amerika', items: [
{name:'New York', sub:'USA, 40.7°N', lat: 40.71, lon:-74.01},
{name:'Los Angeles', sub:'Kalifornien', lat: 34.05, lon:-118.24},
{name:'Chicago', sub:'Illinois, 42°N', lat: 41.85, lon:-87.65},
{name:'Miami', sub:'Florida, 26°N', lat: 25.77, lon:-80.19},
{name:'Anchorage', sub:'Alaska, 61°N', lat: 61.22, lon:-149.90},
{name:'Barrow (Utqiaġvik)',sub:'Alaska, 71°N', lat: 71.30, lon:-156.79},
{name:'Mexiko-Stadt', sub:'Mexiko, 19°N', lat: 19.43, lon:-99.13},
{name:'Bogotá', sub:'Kolumbien, 4.7°N', lat: 4.71, lon:-74.07},
{name:'Quito', sub:'Ecuador, 0.2°S', lat: -0.23, lon:-78.52},
{name:'Lima', sub:'Peru, 12°S', lat:-12.05, lon:-77.04},
{name:'Santiago', sub:'Chile, 33.5°S', lat:-33.46, lon:-70.65},
{name:'Buenos Aires', sub:'Argentinien, 34°S', lat:-34.61, lon:-58.38},
{name:'Punta Arenas', sub:'Chile, 53°S', lat:-53.16, lon:-70.91},
{name:'Ushuaia', sub:'Argentinien, 55°S', lat:-54.80, lon:-68.30},
]},
{ group: 'Asien & Pazifik', items: [
{name:'Tokio', sub:'Japan, 35.7°N', lat: 35.68, lon:139.69},
{name:'Sapporo', sub:'Hokkaido, 43°N', lat: 43.06, lon:141.35},
{name:'Peking', sub:'China, 40°N', lat: 39.91, lon:116.39},
{name:'Shanghai', sub:'China, 31°N', lat: 31.23, lon:121.47},
{name:'Hongkong', sub:'22.3°N', lat: 22.32, lon:114.17},
{name:'Singapur', sub:'1.4°N, fast Äquator', lat: 1.35, lon:103.82},
{name:'Mumbai', sub:'Indien, 19°N', lat: 19.08, lon: 72.88},
{name:'Delhi', sub:'Indien, 28.6°N', lat: 28.61, lon: 77.21},
{name:'Riad', sub:'Saudi-Arabien, 25°N', lat: 24.68, lon: 46.72},
{name:'Dubai', sub:'VAE, 25.2°N', lat: 25.20, lon: 55.27},
{name:'Sydney', sub:'Australien, 34°S', lat:-33.87, lon:151.21},
{name:'Melbourne', sub:'Australien, 37.8°S', lat:-37.81, lon:144.96},
{name:'Auckland', sub:'Neuseeland, 37°S', lat:-36.86, lon:174.77},
{name:'Hobart', sub:'Tasmanien, 43°S', lat:-42.88, lon:147.32},
]},
{ group: 'Afrika & Naher Osten', items: [
{name:'Kairo', sub:'Ägypten, 30°N', lat: 30.05, lon: 31.24},
{name:'Kapstadt', sub:'Südafrika, 34°S', lat:-33.93, lon: 18.42},
{name:'Johannesburg', sub:'Südafrika, 26.2°S', lat:-26.20, lon: 28.04},
{name:'Nairobi', sub:'Kenia, 1.3°S', lat: -1.29, lon: 36.82},
{name:'Addis Abeba', sub:'Äthiopien, 9°N', lat: 9.03, lon: 38.74},
{name:'Casablanca', sub:'Marokko, 33.6°N', lat: 33.59, lon: -7.62},
]},
{ group: 'Geographische Extrempunkte', items: [
{name:'Nordpol', sub:'90.0°N - Polartag/nacht', lat: 89.99, lon: 0.00},
{name:'Südpol', sub:'90.0°S - Antarktis', lat:-89.99, lon: 0.00},
{name:'Äquator / 0°', sub:'0°N 0°E - Golf v. Guinea',lat: 0.00, lon: 0.00},
{name:'Äquator / Borneo', sub:'0°N 109°E', lat: 0.10, lon:109.00},
{name:'Polarkreis N', sub:'66.56°N', lat: 66.56, lon: 25.00},
{name:'Polarkreis S', sub:'66.56°S', lat:-66.56, lon: 25.00},
{name:'Wendekreis Krebs', sub:'23.44°N', lat: 23.44, lon: 0.00},
{name:'Wendekreis Steinbock', sub:'23.44°S', lat:-23.44, lon: 0.00},
{name:'Datum-Grenze 180°',sub:'0°N 180°E', lat: 0.00, lon:180.00},
{name:'Antimeridian', sub:'0°N 179.9°W', lat: 0.00, lon:-179.9},
{name:'Null-Meridian', sub:'51.5°N 0°E Greenwich', lat: 51.48, lon: 0.00},
{name:'Pol der Unzug?nglichkeit',sub:'Antarktis, 83.9°S', lat:-83.90, lon: 65.72},
]},
{ group: 'Historische & Besondere Orte', items: [
{name:'Stonehenge', sub:'England, 51.2°N', lat: 51.18, lon: -1.83},
{name:'Gizeh Pyramiden', sub:'Ägypten, 30°N', lat: 29.98, lon: 31.13},
{name:'Angkor Wat', sub:'Kambodscha, 13.4°N', lat: 13.41, lon:103.87},
{name:'Machu Picchu', sub:'Peru, 13.2°S', lat:-13.16, lon:-72.54},
{name:'Chichen Itza', sub:'Mexiko, 20.7°N', lat: 20.68, lon:-88.57},
{name:'Newgrange', sub:'Irland, 53.7°N', lat: 53.69, lon: -6.47},
{name:'Abu Simbel', sub:'Ägypten, 22.3°N', lat: 22.34, lon: 31.63},
{name:'Easter Island', sub:'Pazifik, 27.1°S', lat:-27.12, lon:-109.37},
{name:'Uluru (Ayers Rock)',sub:'Australien, 25.3°S', lat:-25.34, lon:131.04},
{name:'Nasca-Linien', sub:'Peru, 14.7°S', lat:-14.72, lon:-75.13},
]},
{ group: 'Raumfahrt & Wissenschaft', items: [
{name:'Cape Canaveral', sub:'Florida, 28.5°N', lat: 28.47, lon:-80.56},
{name:'Baikonur', sub:'Kasachstan, 46°N', lat: 45.96, lon: 63.31},
{name:'Kourou', sub:'Franz.-Guayana, 5.3°N',lat: 5.17, lon:-52.68},
{name:'CERN Genf', sub:'Schweiz, 46.2°N', lat: 46.23, lon: 6.05},
{name:'ISS-Bahn (0°N)', sub:'Äquatorebene 51.6° i', lat: 0.00, lon: 0.00},
{name:'McMurdo Station', sub:'Antarktis, 77.8°S', lat:-77.85, lon:166.67},
{name:'Amundsen-Scott SP',sub:'Südpol, 90°S', lat:-89.99, lon: 0.00},
{name:'Alert Kanada', sub:'82.5°N, noerdlichste Siedlung',lat:82.50,lon:-62.33},
]},
];
//
const ALL_LOCS = LOC_GROUPS.flatMap(g=>g.items);
//
const locGroupsEl = document.getElementById('locGroups');
LOC_GROUPS.forEach(g=>{
const section = document.createElement('div');
section.className = 'loc-group';
section.innerHTML = `<div class="loc-group-title">${g.group}</div>`;
const grid = document.createElement('div');
grid.className = 'loc-grid';
g.items.forEach(item=>{
const idx = ALL_LOCS.indexOf(item);
const b = document.createElement('button');
b.className = 'city-btn';
b.dataset.idx = idx;
b.dataset.name = item.name.toLowerCase();
b.dataset.sub = (item.sub||'').toLowerCase();
b.innerHTML = `${item.name}<span class="loc-sub">${item.sub||''}</span>`;
b.onclick = ()=>selectLoc(idx);
grid.appendChild(b);
});
section.appendChild(grid);
locGroupsEl.appendChild(section);
});
function filterLocs(q){
q=q.toLowerCase();
document.querySelectorAll('.city-btn').forEach(b=>{
const match=!q||b.dataset.name.includes(q)||b.dataset.sub.includes(q);
b.classList.toggle('hidden',!match);
});
document.querySelectorAll('.loc-group').forEach(g=>{
const anyVisible=[...g.querySelectorAll('.city-btn')].some(b=>!b.classList.contains('hidden'));
g.style.display=anyVisible?'':'none';
});
}
//
let loc={name:'Berlin',lat:52.52,lon:13.40};
let currentDate=new Date(); currentDate.setUTCHours(12,0,0,0);
let sliderFrac=0.5;
let sunData=null;
let viewMode='allsky'; // 'allsky' | 'horizon'
function setView(v){
viewMode=v;
document.getElementById('vtb_allsky').classList.toggle('active',v==='allsky');
document.getElementById('vtb_horizon').classList.toggle('active',v==='horizon');
drawArc();
}
function toISO(d){return d.getUTCFullYear()+'-'+String(d.getUTCMonth()+1).padStart(2,'0')+'-'+String(d.getUTCDate()).padStart(2,'0');}
//
const OVERLAYS={
sommer: {active:false, color:'rgba(255,100,30,', label:'Sommersonnenwende', month:6, day:21},
winter: {active:false, color:'rgba(60,130,255,', label:'Wintersonnenwende', month:12, day:21},
fruehjahr: {active:false, color:'rgba(50,210,110,', label:'Tag-/Nachtgleiche', month:3, day:20},
};
function toggleOverlay(key){
OVERLAYS[key].active=!OVERLAYS[key].active;
const btn=document.getElementById('btn_'+key);
btn.dataset.active=OVERLAYS[key].active?'1':'0';
drawArc();
}
function clearOverlays(){
Object.keys(OVERLAYS).forEach(k=>{
OVERLAYS[k].active=false;
document.getElementById('btn_'+k).dataset.active='0';
});
drawArc();
}
function overlayDate(key){
const o=OVERLAYS[key];
const y=currentDate.getUTCFullYear();
return new Date(Date.UTC(y,o.month-1,o.day,12,0,0));
}
//
function openModal(){document.getElementById('modalOverlay').classList.add('vis');}
function closeModal(){document.getElementById('modalOverlay').classList.remove('vis');}
document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target===e.currentTarget)closeModal();});
function selectLoc(i){
const c=ALL_LOCS[i];
loc={name:c.name,lat:c.lat,lon:c.lon};
document.querySelectorAll('.city-btn').forEach(b=>b.classList.toggle('on',parseInt(b.dataset.idx)===i));
document.getElementById('inputLat').value=c.lat;
document.getElementById('inputLon').value=c.lon;
document.getElementById('inputName').value='';
closeModal(); update();
}
function applyCustomCoords(){
const lat=parseFloat(document.getElementById('inputLat').value);
const lon=parseFloat(document.getElementById('inputLon').value);
const name=document.getElementById('inputName').value.trim()||`${lat.toFixed(2)}°, ${lon.toFixed(2)}°`;
if(isNaN(lat)||isNaN(lon))return;
loc={name,lat,lon};
document.querySelectorAll('.city-btn').forEach(b=>b.classList.remove('on'));
closeModal(); update();
}
//
function hourToDate(baseDate, utcHour){
// baseDate: JS Date used only fuer Y/M/D (UTC)
const d=new Date(baseDate);
d.setUTCHours(0,0,0,0);
d.setTime(d.getTime()+utcHour*3600000);
return d;
}
function sunAltAz(lat,lon,jsDate){
const obs=new Astronomy.Observer(lat,lon,0);
const eq=Astronomy.Equator(Astronomy.Body.Sun,jsDate,obs,true,true);
const hz=Astronomy.Horizon(jsDate,obs,eq.ra,eq.dec,'normal');
return {altitude:hz.altitude, azimuth:hz.azimuth};
}
function computeSunData(lat,lon,date){
const obs=new Astronomy.Observer(lat,lon,0);
// Always search from UTC midnight of the selected day
const midnight=new Date(date); midnight.setUTCHours(0,0,0,0);
let riseUTC=null, setUTC=null, noonUTC=12;
let polarDay=false, polarNight=false;
// Solar transit (noon) - search fuerward from midnight
try{
const tr=Astronomy.SearchHourAngle(Astronomy.Body.Sun,obs,0,midnight,+1);
if(tr){
const td=tr.time.date;
noonUTC=td.getUTCHours()+td.getUTCMinutes()/60+td.getUTCSeconds()/3600;
}
}catch(e){ console.warn('noon search error',e); }
// Sunrise: search from midnight fuerward within 1 day
try{
const r=Astronomy.SearchRiseSet(Astronomy.Body.Sun,obs,+1,midnight,1,0);
if(r){const rd=r.date; riseUTC=rd.getUTCHours()+rd.getUTCMinutes()/60+rd.getUTCSeconds()/3600;}
}catch(e){ console.warn('rise search error',e); }
// Sunset: search from midnight fuerward within 1 day
try{
const s=Astronomy.SearchRiseSet(Astronomy.Body.Sun,obs,-1,midnight,1,0);
if(s){const sd2=s.date; setUTC=sd2.getUTCHours()+sd2.getUTCMinutes()/60+sd2.getUTCSeconds()/3600;}
}catch(e){ console.warn('set search error',e); }
// Polar cases: rise or set not found during the day
if(riseUTC===null||setUTC===null){
const noonPos=sunAltAz(lat,lon,hourToDate(date,noonUTC));
polarDay = noonPos.altitude>0;
polarNight = !polarDay;
riseUTC=0; setUTC=24;
}
// Fix rare midnight-crossing (e.g. very high latitudes in summer)
if(!polarDay&&!polarNight&&setUTC<=riseUTC) setUTC+=24;
// Declination at solar noon
let decl=null;
try{
const eq=Astronomy.Equator(Astronomy.Body.Sun,hourToDate(date,noonUTC),obs,true,true);
decl=eq.dec;
}catch(e){}
// Max elevation at transit
const maxPos=sunAltAz(lat,lon,hourToDate(date,noonUTC));
const maxElev=maxPos.altitude;
// Rise azimuth
let riseAz=null;
if(!polarDay&&!polarNight&&riseUTC!==null){
riseAz=sunAltAz(lat,lon,hourToDate(date,riseUTC)).azimuth;
}
return{riseUTC,setUTC,noonUTC,polarDay,polarNight,decl,maxElev,riseAz};
}
function fmtH(h){
if(h===null||isNaN(h))return'-';
h=((h%24)+24)%24;
const hh=Math.floor(h), mm=Math.round((h-hh)*60);
if(mm===60)return String((hh+1)%24).padStart(2,'0')+':00';
return String(hh).padStart(2,'0')+':'+String(mm).padStart(2,'0');
}
//
//
function drawWrappedPath(ctx, pts, W, closePath){
if(!pts.length)return;
ctx.beginPath();
let penDown=false;
for(let i=0;i<pts.length;i++){
const p=pts[i];
if(i===0){ctx.moveTo(p.x,p.y);penDown=true;continue;}
const prev=pts[i-1];
if(Math.abs(p.x-prev.x)>W*0.5){
// Wrap detected - start new sub-path
ctx.moveTo(p.x,p.y);
} else {
ctx.lineTo(p.x,p.y);
}
}
if(closePath)ctx.closePath();
}
function azDir(az){
return['N','NNO','NO','ONO','O','OSO','SO','SSO','S','SSW','SW','WSW','W','WNW','NW','NNW'][Math.round(az/22.5)%16];
}
//
const arcCanvas=document.getElementById('arcCanvas');
const ctx=arcCanvas.getContext('2d');
function resizeCanvas(){arcCanvas.width=arcCanvas.offsetWidth*devicePixelRatio;arcCanvas.height=arcCanvas.offsetHeight*devicePixelRatio;drawArc();}
function drawArc(){
if(!sunData)return;
const W=arcCanvas.width,H=arcCanvas.height,dpr=devicePixelRatio;
ctx.clearRect(0,0,W,H);
const sd=sunData;
const nPoints=500;
const isSH=loc.lat<0; //
// ══ PROJEKTION ════════════════════════════════════════════════════════════
// Gemeinsame Geometrie-Konstanten (werden je nach Modus ?berschrieben)
let cx,cy,R,groundY,rX,rY,clipFn,proj;
if(viewMode==='allsky'){
// ── ALL-SKY ZENITH-DRAUFSICHT ──────────────────────────────────────────
// Beobachter schaut nach oben. N=oben, S=unten, O=links, W=rechts
const pad=52*dpr;
R=Math.min(W,H)*0.5-pad;
cx=W*0.5; cy=H*0.5;
proj=(alt,az)=>{
const ar=az*Math.PI/180,er=alt*Math.PI/180;
const r=Math.cos(er)*R;
return{x:cx-Math.sin(ar)*r, y:cy-Math.cos(ar)*r};
};
// Hintergrund
ctx.fillStyle='#04060f';ctx.fillRect(0,0,W,H);
const skyG=ctx.createRadialGradient(cx,cy,0,cx,cy,R);
skyG.addColorStop(0,'#0e1535');skyG.addColorStop(0.7,'#080d28');skyG.addColorStop(1,'#040816');
ctx.save();ctx.beginPath();ctx.arc(cx,cy,R,0,Math.PI*2);ctx.clip();
ctx.fillStyle=skyG;ctx.fillRect(0,0,W,H);ctx.restore();
// Horizont-Ring
ctx.save();
ctx.beginPath();ctx.arc(cx,cy,R,0,Math.PI*2);
ctx.strokeStyle='rgba(200,168,75,.30)';ctx.lineWidth=1.5*dpr;ctx.stroke();
ctx.beginPath();ctx.rect(0,0,W,H);ctx.arc(cx,cy,R,0,Math.PI*2,true);
ctx.fillStyle='rgba(5,10,25,0.85)';ctx.fill();ctx.restore();
// Raster
[15,30,45,60,75].forEach(el=>{
const r2=Math.cos(el*Math.PI/180)*R;
ctx.beginPath();ctx.arc(cx,cy,r2,0,Math.PI*2);
ctx.strokeStyle='rgba(200,168,75,.06)';ctx.lineWidth=1*dpr;
ctx.setLineDash([4*dpr,8*dpr]);ctx.stroke();ctx.setLineDash([]);
ctx.font=`${8*dpr}px Space Mono`;ctx.fillStyle='rgba(200,168,75,.30)';ctx.textAlign='left';
ctx.fillText(el+'°',cx+r2+3*dpr,cy+4*dpr);
});
[0,45,90,135,180,225,270,315].forEach(az=>{
const p0=proj(0,az),p1=proj(89,az);
ctx.beginPath();ctx.moveTo(p0.x,p0.y);ctx.lineTo(p1.x,p1.y);
ctx.strokeStyle='rgba(200,168,75,.05)';ctx.lineWidth=1*dpr;ctx.stroke();
});
// Achsenkreuz
ctx.beginPath();
ctx.moveTo(cx,cy-R);ctx.lineTo(cx,cy+R);
ctx.moveTo(cx-R,cy);ctx.lineTo(cx+R,cy);
ctx.strokeStyle='rgba(200,168,75,.07)';ctx.lineWidth=1*dpr;ctx.stroke();
// Himmelsrichtungen
const cf=`bold ${11*dpr}px Orbitron`, cc='rgba(200,168,75,.70)';
ctx.font=cf;ctx.fillStyle=cc;
ctx.textAlign='center';ctx.textBaseline='bottom';ctx.fillText('N',cx,cy-R-4*dpr);
ctx.textBaseline='top';ctx.fillText('S',cx,cy+R+4*dpr);
ctx.textAlign='right';ctx.textBaseline='middle';ctx.fillText('O',cx-R-4*dpr,cy);
ctx.textAlign='left';ctx.fillText('W',cx+R+4*dpr,cy);
ctx.textBaseline='alphabetic';
// Zenit
ctx.beginPath();ctx.arc(cx,cy,3*dpr,0,Math.PI*2);ctx.fillStyle='rgba(200,168,75,.25)';ctx.fill();
ctx.font=`${8*dpr}px Orbitron`;ctx.fillStyle='rgba(200,168,75,.35)';
ctx.textAlign='center';ctx.fillText('Z',cx,cy-6*dpr);
clipFn=()=>{ctx.beginPath();ctx.arc(cx,cy,R+2*dpr,0,Math.PI*2);ctx.clip();};
}else{
// ── HORIZONT-PANORAMA (zylindrisch, 360?) ──────────────────────────────────
// Vollstaendige Rundumsicht: az=0(N) links, az=180(S) Mitte, az=360(N) rechts
// NH: Sommerbogen von NO (links-Mitte) ?ber S (Mitte-oben) nach NW (rechts-Mitte)
// SH: az verschoben um 180? ? N Mitte, S an Raendern
const vPad=36*dpr;
rY=(H-vPad*2)*0.80;
cx=W*0.5; groundY=H*0.75;
R=W*0.5; rX=W*0.5;
proj=(alt,az)=>{
const normAz=isSH?(az+180)%360:az;
return{ x:(normAz/360)*W, y:groundY-Math.sin(alt*Math.PI/180)*rY };
};
// Himmel
const skyG=ctx.createLinearGradient(0,0,0,groundY);
skyG.addColorStop(0,'#020210');skyG.addColorStop(0.4,'#060820');skyG.addColorStop(1,'#0b1230');
ctx.fillStyle=skyG;ctx.fillRect(0,0,W,groundY);
const gndG=ctx.createLinearGradient(0,groundY,0,H);
gndG.addColorStop(0,'#0a1428');gndG.addColorStop(1,'#060c1c');
ctx.fillStyle=gndG;ctx.fillRect(0,groundY,W,H-groundY);
ctx.beginPath();ctx.moveTo(0,groundY);ctx.lineTo(W,groundY);
ctx.strokeStyle='rgba(200,168,75,.30)';ctx.lineWidth=1.5*dpr;ctx.stroke();
// Elevation-Gitter
[15,30,45,60,75].forEach(el=>{
const yEl=groundY-Math.sin(el*Math.PI/180)*rY;
ctx.beginPath();ctx.moveTo(0,yEl);ctx.lineTo(W,yEl);
ctx.strokeStyle='rgba(200,168,75,.06)';ctx.lineWidth=1*dpr;
ctx.setLineDash([4*dpr,8*dpr]);ctx.stroke();ctx.setLineDash([]);
ctx.font=`${8*dpr}px Space Mono`;ctx.fillStyle='rgba(200,168,75,.28)';
ctx.textAlign='left';ctx.fillText(el+'°',4*dpr,yEl-3*dpr);
});
// Azimut-Gitter + Labels
const aziLabelsNH={0:'N',45:'NO',90:'O',135:'SO',180:'S',225:'SW',270:'W',315:'NW',360:'N'};
const aziLabelsSH={0:'S',45:'SW',90:'W',135:'NW',180:'N',225:'NO',270:'O',315:'SO',360:'S'};
const aziLabels=isSH?aziLabelsSH:aziLabelsNH;
[0,45,90,135,180,225,270,315,360].forEach(az=>{
const xAz=(az/360)*W;
const isMajor=(az%90===0);
ctx.beginPath();ctx.moveTo(xAz,0);ctx.lineTo(xAz,groundY);
ctx.strokeStyle=isMajor?'rgba(200,168,75,.12)':'rgba(200,168,75,.04)';
ctx.lineWidth=isMajor?1.5*dpr:1*dpr;ctx.stroke();
ctx.font=`bold ${isMajor?11:8}px Orbitron`;
ctx.fillStyle=isMajor?'rgba(200,168,75,.75)':'rgba(200,168,75,.35)';
ctx.textAlign='center';ctx.textBaseline='top';
ctx.fillText(aziLabels[az]||'',xAz,groundY+5*dpr);
ctx.textBaseline='alphabetic';
});
// Zenit-Strich ?ber S(NH)/N(SH) = Mitte
ctx.beginPath();ctx.moveTo(W*0.5,groundY);ctx.lineTo(W*0.5,vPad*0.5);
ctx.strokeStyle='rgba(200,168,75,.08)';ctx.lineWidth=1*dpr;
ctx.setLineDash([3*dpr,6*dpr]);ctx.stroke();ctx.setLineDash([]);
ctx.font=`${9*dpr}px Orbitron`;ctx.fillStyle='rgba(200,168,75,.35)';
ctx.textAlign='center';ctx.textBaseline='top';
ctx.fillText('ZENIT',W*0.5,4*dpr);ctx.textBaseline='alphabetic';
clipFn=()=>{ctx.rect(0,0,W,H);};
}
// ── Boegenpunkte bauen ────────────────────────────────────────────────────
const h0=sd.polarDay?0:sd.riseUTC;
const h1=sd.polarDay?24:sd.setUTC;
const pts=[];
for(let i=0;i<=nPoints;i++){
const h=h0+(h1-h0)*i/nPoints;
const{altitude,azimuth}=sunAltAz(loc.lat,loc.lon,hourToDate(currentDate,h));
// Nur Punkte ?ber dem Horizont (oder alle bei Polartag)
if(sd.polarDay||altitude>=-0.5)
pts.push({...proj(altitude,azimuth),alt:altitude,az:azimuth,h});
}
// ── Boegen zeichnen ────────────────────────────────────────────────────────
// Clip innerhalb des Himmelskreises fuer saubere Darstellung
ctx.save();
clipFn();
ctx.clip();
if(!sd.polarNight){
// Äußerer Gluehschein
ctx.beginPath();
drawWrappedPath(ctx,pts,W,sd.polarDay);
ctx.strokeStyle=sd.polarDay?'rgba(120,210,255,.10)':'rgba(255,150,20,.10)';
ctx.lineWidth=16*dpr;ctx.stroke();
// Farbiger Boegen (Segmente)
for(let i=1;i<pts.length;i++){
const t=i/pts.length;
let col;
if(sd.polarDay){
col='rgba(160,220,255,.80)';
}else{
const g=t<.5?Math.floor(60+t*380):Math.floor(440-t*380);
col=`rgba(255,${Math.max(0,Math.min(255,g))},20,.90)`;
}
if(Math.abs(pts[i].x-pts[i-1].x)<W*0.5){
ctx.beginPath();ctx.moveTo(pts[i-1].x,pts[i-1].y);ctx.lineTo(pts[i].x,pts[i].y);
ctx.strokeStyle=col;ctx.lineWidth=3*dpr;ctx.stroke();
}
}
// Mittag-Marker
const noonPt=sunAltAz(loc.lat,loc.lon,hourToDate(currentDate,sd.noonUTC));
const np=proj(noonPt.altitude,noonPt.azimuth);
ctx.beginPath();ctx.arc(np.x,np.y,5*dpr,0,Math.PI*2);ctx.fillStyle='rgba(200,168,75,.7)';ctx.fill();
// Linie zum Zenit
ctx.beginPath();ctx.moveTo(np.x,np.y);ctx.lineTo(cx,cy);
ctx.strokeStyle='rgba(200,168,75,.12)';ctx.lineWidth=1*dpr;ctx.setLineDash([3*dpr,6*dpr]);ctx.stroke();ctx.setLineDash([]);
ctx.font=`bold ${9*dpr}px Orbitron`;ctx.fillStyle='rgba(200,168,75,.85)';ctx.textAlign='center';
// Label außerhalb nahe dem Punkt, leicht versetzt
const nAngle=Math.atan2(np.x-cx,np.y-cy); // Winkel vom Zenit
ctx.fillText('MITTAG '+fmtH(sd.noonUTC), np.x+Math.sin(nAngle)*14*dpr, np.y+Math.cos(nAngle)*14*dpr);
// Aufgang / Untergang
if(!sd.polarDay){
const rP=sunAltAz(loc.lat,loc.lon,hourToDate(currentDate,sd.riseUTC));
const rp=proj(0,rP.azimuth);
ctx.beginPath();ctx.arc(rp.x,rp.y,6*dpr,0,Math.PI*2);ctx.fillStyle='#ff8040';ctx.fill();
ctx.font=`bold ${9*dpr}px Orbitron`;ctx.fillStyle='rgba(255,165,60,.95)';
// Label radial nach außen
const rAng=Math.atan2(rp.x-cx,rp.y-cy);
const lx=rp.x+Math.sin(rAng)*20*dpr, ly=rp.y+Math.cos(rAng)*20*dpr;
ctx.textAlign=rp.x<cx?'right':'left';
ctx.fillText('↑ '+fmtH(sd.riseUTC),lx,ly);
const sP=sunAltAz(loc.lat,loc.lon,hourToDate(currentDate,sd.setUTC));
const sp=proj(0,sP.azimuth);
ctx.beginPath();ctx.arc(sp.x,sp.y,6*dpr,0,Math.PI*2);ctx.fillStyle='#ff5010';ctx.fill();
ctx.font=`bold ${9*dpr}px Orbitron`;ctx.fillStyle='rgba(255,130,40,.95)';
const sAng=Math.atan2(sp.x-cx,sp.y-cy);
const sx2=sp.x+Math.sin(sAng)*20*dpr, sy2=sp.y+Math.cos(sAng)*20*dpr;
ctx.textAlign=sp.x>cx?'left':'right';
ctx.fillText(fmtH(sd.setUTC)+' ↓',sx2,sy2);
}
if(sd.polarDay){
ctx.font=`bold ${11*dpr}px Orbitron`;ctx.fillStyle='rgba(140,210,255,.8)';ctx.textAlign='center';
ctx.fillText('POLARTAG · MITTERNACHTSSONNE',cx,cy+R-20*dpr);
}
}else{
// Polarnacht: Boegen unter dem Horizont gestrichelt
ctx.beginPath();drawWrappedPath(ctx,pts,W,true);
ctx.strokeStyle='rgba(80,120,200,.25)';ctx.lineWidth=2*dpr;
ctx.setLineDash([6*dpr,10*dpr]);ctx.stroke();ctx.setLineDash([]);
ctx.font=`bold ${11*dpr}px Orbitron`;ctx.fillStyle='rgba(100,150,230,.65)';ctx.textAlign='center';
ctx.fillText('POLARNACHT',cx,cy+20*dpr);
}
ctx.restore(); // end clip
// ── Jahres-?berlagerungs-Boegen ───────────────────────────────────────────────
Object.keys(OVERLAYS).forEach(key=>{
const ov=OVERLAYS[key];
if(!ov.active)return;
const oDate=overlayDate(key);
const oSd=computeSunData(loc.lat,loc.lon,oDate);
const oh0=oSd.polarDay?0:oSd.riseUTC;
const oh1=oSd.polarDay?24:oSd.setUTC;
const opts=[];
for(let i=0;i<=500;i++){
const h=oh0+(oh1-oh0)*i/500;
const{altitude,azimuth}=sunAltAz(loc.lat,loc.lon,hourToDate(oDate,h));
if(oSd.polarDay||altitude>=-0.5)
opts.push(proj(altitude,azimuth));
}
// Gluehhalo + Boegen
ctx.save();
clipFn(); ctx.clip();
ctx.beginPath();
drawWrappedPath(ctx,opts,W,oSd.polarDay);
ctx.strokeStyle=ov.color+'0.12)';ctx.lineWidth=10*dpr;ctx.stroke();
ctx.beginPath();
drawWrappedPath(ctx,opts,W,oSd.polarDay);
ctx.strokeStyle=ov.color+'0.65)';ctx.lineWidth=2*dpr;
ctx.setLineDash([6*dpr,4*dpr]);ctx.stroke();ctx.setLineDash([]);
// Label
if(!oSd.polarNight&&!oSd.polarDay){
const midIdx=Math.floor(opts.length/2);
const mp=opts[midIdx];
ctx.font=`bold ${9*dpr}px Orbitron`;
ctx.fillStyle=ov.color+'0.90)';ctx.textAlign='center';
if(viewMode==='allsky'){
const ang=Math.atan2(mp.x-cx,mp.y-cy);
ctx.fillText(ov.label,mp.x+Math.sin(ang)*18*dpr,mp.y+Math.cos(ang)*18*dpr);
}else{
ctx.fillText(ov.label,mp.x,mp.y-14*dpr);
}
}
// Aufgangs-/Untergangs-Dots
if(!oSd.polarDay){
const rPos=sunAltAz(loc.lat,loc.lon,hourToDate(oDate,oSd.riseUTC));
const rp=proj(0,rPos.azimuth);
ctx.beginPath();ctx.arc(rp.x,rp.y,4*dpr,0,Math.PI*2);
ctx.fillStyle=ov.color+'0.80)';ctx.fill();
const sPos=sunAltAz(loc.lat,loc.lon,hourToDate(oDate,oSd.setUTC));
const sp=proj(0,sPos.azimuth);
ctx.beginPath();ctx.arc(sp.x,sp.y,4*dpr,0,Math.PI*2);
ctx.fillStyle=ov.color+'0.80)';ctx.fill();
}
ctx.restore();
});
// ── Aktuelle Sonnenposition ────────────────────────────────────────────────
const curH=sd.riseUTC+sliderFrac*(sd.setUTC-sd.riseUTC);
const sunPos=sunAltAz(loc.lat,loc.lon,hourToDate(currentDate,curH));
const sp2=proj(sunPos.altitude,sunPos.azimuth);
const below=sunPos.altitude<0;
// Linie Sonne ? Zenit
ctx.beginPath();ctx.moveTo(sp2.x,sp2.y);ctx.lineTo(cx,cy);
ctx.strokeStyle='rgba(255,220,60,.07)';ctx.lineWidth=1*dpr;ctx.stroke();
// Gluehkraenze
[80,45,22,11].forEach((r,i)=>{
const a=[.03,.08,.16,.42][i]*(below ? .10 : 1);
const g2=ctx.createRadialGradient(sp2.x,sp2.y,0,sp2.x,sp2.y,r*dpr);
g2.addColorStop(0,`rgba(255,240,120,${a})`);g2.addColorStop(1,'rgba(255,200,60,0)');
ctx.beginPath();ctx.arc(sp2.x,sp2.y,r*dpr,0,Math.PI*2);ctx.fillStyle=g2;ctx.fill();
});
// Sonnenscheibe
const discR=below?5*dpr:10*dpr;
const sg=ctx.createRadialGradient(sp2.x-2*dpr,sp2.y-2*dpr,0,sp2.x,sp2.y,discR);
sg.addColorStop(0,below?'#ffe0b0':'#fffce0');
sg.addColorStop(.4,below?'#ff9040':'#ffd860');
sg.addColorStop(1,'#ff7010');
ctx.globalAlpha=below ? .18 : 1;
ctx.beginPath();ctx.arc(sp2.x,sp2.y,discR,0,Math.PI*2);ctx.fillStyle=sg;ctx.fill();
ctx.globalAlpha=1;
// ── Stats ─────────────────────────────────────────────────────────────────
document.getElementById('statElev').textContent=sunPos.altitude.toFixed(1)+'°';
document.getElementById('statAz').textContent=sunPos.azimuth.toFixed(1)+'°';
document.getElementById('statDir').textContent=azDir(sunPos.azimuth);
const pct=sliderFrac*100;
document.getElementById('phaseBar').style.width=pct+'%';
document.getElementById('statPhase').textContent=
sd.polarNight?'Polarnacht':sd.polarDay?'Polartag':
pct<10?'Morgenr?te':pct<35?'Vormittag':pct<55?'Sonnenmittag':pct<82?'Nachmittag':'Abendd?mmerung';
const hh=((Math.floor(curH)%24)+24)%24;
const mm=Math.round((curH-Math.floor(curH))*60)%60;
const ts=String(hh).padStart(2,'0')+':'+String(mm).padStart(2,'0');
document.getElementById('timeBig').textContent=ts;
document.getElementById('sliderTimeLabel').textContent=ts;
}
function onSlider(){sliderFrac=parseInt(document.getElementById('timeSlider').value)/1000;drawArc();}
function buildTicks(){
if(!sunData)return;
const sd=sunData;
document.getElementById('timeTicks').innerHTML='';
const h0=sd.riseUTC,h1=sd.setUTC;
for(let i=0;i<=6;i++){
const d=document.createElement('div');
d.className='tick';
d.textContent=fmtH(h0+(h1-h0)*i/6);
document.getElementById('timeTicks').appendChild(d);
}
document.getElementById('lbl_rise').textContent=sd.polarDay?'00:00':fmtH(sd.riseUTC);
document.getElementById('lbl_noon').textContent='| '+fmtH(sd.noonUTC);
document.getElementById('lbl_set').textContent=sd.polarDay?'24:00':fmtH(sd.setUTC);
document.getElementById('sliderLabel').textContent=
sd.polarDay?'POLARTAG - GANZER TAG':sd.polarNight?'POLARNACHT':
'TAGESVERLAUF · '+fmtH(sd.riseUTC)+' - '+fmtH(sd.setUTC)+' UTC';
}
function update(){
dateInput.value=toISO(currentDate);
document.getElementById('infoName').textContent=loc.name;
document.getElementById('infoLat').textContent=loc.lat.toFixed(2)+'°';
document.getElementById('infoLon').textContent=loc.lon.toFixed(2)+'°';
document.getElementById('locBtnText').textContent=loc.name.toUpperCase().slice(0,14);
sunData=computeSunData(loc.lat,loc.lon,currentDate);
const sd=sunData;
if(!sd.polarDay&&!sd.polarNight){
document.getElementById('statRise').textContent=fmtH(sd.riseUTC);
document.getElementById('statSet').textContent=fmtH(sd.setUTC);
document.getElementById('statNoon').textContent=fmtH(sd.noonUTC);
const dur=sd.setUTC-sd.riseUTC;
document.getElementById('statDay').textContent=Math.floor(dur)+'h '+Math.round((dur%1)*60)+'m';
}else{
document.getElementById('statRise').textContent=sd.polarDay?'ganztags':'-';
document.getElementById('statSet').textContent=sd.polarDay?'ganztags':'-';
document.getElementById('statNoon').textContent=fmtH(sd.noonUTC);
document.getElementById('statDay').textContent=sd.polarDay?'24h 0m':'0h 0m';
}
document.getElementById('statDecl').textContent=sd.decl!==null?sd.decl.toFixed(2)+'°':'-°';
document.getElementById('statMaxElev').textContent=sd.maxElev!==null?sd.maxElev.toFixed(1)+'°':'-°';
document.getElementById('statRiseAz').textContent=sd.riseAz!==null?sd.riseAz.toFixed(1)+'°':'-°';
const curH=sd.riseUTC+sliderFrac*(sd.setUTC-sd.riseUTC);
const sunPos=sunAltAz(loc.lat,loc.lon,hourToDate(currentDate,curH));
document.getElementById('statElev').textContent=sunPos.altitude.toFixed(1)+'°';
document.getElementById('statAz').textContent=sunPos.azimuth.toFixed(1)+'°';
document.getElementById('statDir').textContent=azDir(sunPos.azimuth);
const pct=sliderFrac*100;
document.getElementById('phaseBar').style.width=pct+'%';
document.getElementById('statPhase').textContent=
sd.polarNight?'Polarnacht':sd.polarDay?'Polartag':
pct<10?'Morgenr?te':pct<35?'Vormittag':pct<55?'Sonnenmittag':pct<82?'Nachmittag':'Abendd?mmerung';
const hh=((Math.floor(curH)%24)+24)%24;
const mm=Math.round((curH-Math.floor(curH))*60)%60;
const ts=String(hh).padStart(2,'0')+':'+String(mm).padStart(2,'0');
document.getElementById('timeBig').textContent=ts;
document.getElementById('sliderTimeLabel').textContent=ts;
buildTicks();
drawArc();
updateOverlayTooltips();
}
function updateOverlayTooltips(){
const year=currentDate.getUTCFullYear();
[{key:'sommer',month:6,day:21},{key:'winter',month:12,day:21},{key:'fruehjahr',month:3,day:20}]
.forEach(({key,month,day})=>{
const oDate=new Date(Date.UTC(year,month-1,day,12,0,0));
const sd=computeSunData(loc.lat,loc.lon,oDate);
const dateStr=String(day).padStart(2,'0')+'.'+String(month).padStart(2,'0')+'.'+year;
document.getElementById('tip_'+key+'_date').textContent=dateStr;
if(sd.polarDay){
['rise','set','elev','az'].forEach(f=>document.getElementById('tip_'+key+'_'+f).textContent='-');
document.getElementById('tip_'+key+'_dur').textContent='24h 0m (Polartag)';
} else if(sd.polarNight){
['rise','set','elev','az'].forEach(f=>document.getElementById('tip_'+key+'_'+f).textContent='-');
document.getElementById('tip_'+key+'_dur').textContent='0h 0m (Polarnacht)';
} else {
const dur=sd.setUTC-sd.riseUTC;
document.getElementById('tip_'+key+'_rise').textContent=fmtH(sd.riseUTC)+' UTC';
document.getElementById('tip_'+key+'_set').textContent=fmtH(sd.setUTC)+' UTC';
document.getElementById('tip_'+key+'_dur').textContent=Math.floor(dur)+'h '+Math.round((dur%1)*60)+'m';
document.getElementById('tip_'+key+'_elev').textContent=sd.maxElev.toFixed(1)+'°';
document.getElementById('tip_'+key+'_az').textContent=sd.riseAz.toFixed(1)+'°';
}
});
}
//
let _resizeTimer;
window.addEventListener('resize',()=>{clearTimeout(_resizeTimer);_resizeTimer=setTimeout(()=>{resizeCanvas();},60);});
const ro=new ResizeObserver(()=>requestAnimationFrame(resizeCanvas));
ro.observe(arcCanvas.parentElement);
//
const dateInput=document.getElementById('dateInput');
dateInput.value=toISO(currentDate);
dateInput.addEventListener('change',()=>{
if(!dateInput.value)return;
currentDate=new Date(dateInput.value+'T12:00:00Z');
update();
});
function shiftDay(n){
currentDate=new Date(currentDate.getTime()+n*86400000);
dateInput.value=toISO(currentDate);
update();
}
//
function geoLocate(){
if(!navigator.geolocation){alert('Geolocation wird von diesem Browser nicht unterst?tzt.');return;}
const btn=document.getElementById('geoBtn');
const lbl=document.getElementById('geoBtnText');
btn.classList.add('loading');lbl.textContent='...';
navigator.geolocation.getCurrentPosition(
pos=>{
const lat=Math.round(pos.coords.latitude*10000)/10000;
const lon=Math.round(pos.coords.longitude*10000)/10000;
loc={name:'Aktueller Standort',lat,lon};
document.getElementById('inputLat').value=lat;
document.getElementById('inputLon').value=lon;
document.getElementById('inputName').value='Aktueller Standort';
document.querySelectorAll('.city-btn').forEach(b=>b.classList.remove('on'));
btn.classList.remove('loading');lbl.textContent='GPS';
closeModal();update();
},
err=>{
btn.classList.remove('loading');lbl.textContent='GPS';
alert('Standort konnte nicht ermittelt werden: '+err.message);
},
{timeout:10000,enableHighAccuracy:true}
);
}
//
//
selectLoc(0);
document.getElementById('timeSlider').value=500;
resizeCanvas();
update();
</script>
</div>
<?php require __DIR__ . '/footer.php'; ?>