From 1fb86d241c07dd14dfd4113580bccae3e0a0e2b6 Mon Sep 17 00:00:00 2001 From: Eskimue Date: Thu, 30 Apr 2026 10:10:03 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Milchstrasse=20erst=20mal=20raus.=20Darstel?= =?UTF-8?q?lungsprobleme=20nicht=20l=C3=B6sbar=20aktuell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sternenhimmel.php | 104 --------------------------------------- 1 file changed, 104 deletions(-) diff --git a/public/sternenhimmel.php b/public/sternenhimmel.php index 3b66c57..b59f318 100644 --- a/public/sternenhimmel.php +++ b/public/sternenhimmel.php @@ -15,8 +15,6 @@ $constellationDataError = null; $starDataError = null; $messierData = []; $messierDataError = null; -$milkyWayData = null; -$milkyWayDataError = null; $availableLocations = []; $starDbMagLimit = 12.0; @@ -236,16 +234,6 @@ try { $messierDataError = $e->getMessage(); } -try { - $milkyWayPath = __DIR__ . '/js/mw.json'; - if (is_file($milkyWayPath)) { - $milkyWayData = json_decode((string) file_get_contents($milkyWayPath), true, 512, JSON_THROW_ON_ERROR); - } -} catch (Throwable $e) { - $milkyWayData = null; - $milkyWayDataError = $e->getMessage(); -} - $favCometDataJson = json_encode( $favCometData ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES @@ -271,14 +259,6 @@ $messierDataErrorJson = json_encode( $messierDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); -$milkyWayDataJson = json_encode( - $milkyWayData, - JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES -); -$milkyWayDataErrorJson = json_encode( - $milkyWayDataError, - JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES -); ?> -
- Milchstraße -
- -
-
-
Sternbilder
@@ -522,14 +495,6 @@ const CFG_DEFAULT = { ecliptic: { color: 'rgba(255,175,35,0.52)', width: 1.1, dash: [5,5] }, - milkyWay: { - fillSoft: 'rgba(180, 208, 255, 0.018)', - fillMain: 'rgba(210, 228, 255, 0.028)', - stroke: 'rgba(220, 235, 255, 0.035)', - glow: 'rgba(185, 210, 255, 0.10)', - blur: 16, - minPoints: 20 - }, constellations: { color: 'rgba(100,160,220,0.38)', width: 0.8, dash: [], colorLabels: 'rgba(120,175,230,0.50)', fontLabels: '9px Rajdhani, sans-serif' @@ -631,7 +596,6 @@ let showAltaz = false; let showPlanets = true; let showSunMoon = true; let showEclip = true; -let showMilkyWay = true; let showMessier = true; let showConst = true; let showComets = true; @@ -654,8 +618,6 @@ const STAR_DATA_URL = 'sternenhimmel.php?stars_api=1'; const EMBEDDED_STAR_ERROR = ; const EMBEDDED_MESSIER_DATA = ; const EMBEDDED_MESSIER_ERROR = ; -const EMBEDDED_MILKY_WAY_DATA = ; -const EMBEDDED_MILKY_WAY_ERROR = ; const MESSIER = Array.isArray(EMBEDDED_MESSIER_DATA) ? EMBEDDED_MESSIER_DATA : []; const FAV_COMETS = ; let constellationData = EMBEDDED_CONSTELLATION_DATA; @@ -674,66 +636,6 @@ function buildMStyle() { } let M_STYLE = buildMStyle(); -function buildMilkyWayRings(data) { - if (!data || !Array.isArray(data.features)) return []; - - const rings = []; - for (const feature of data.features) { - const geometry = feature && feature.geometry ? feature.geometry : null; - if (!geometry) continue; - - const polygons = geometry.type === 'Polygon' - ? [geometry.coordinates] - : geometry.type === 'MultiPolygon' - ? geometry.coordinates - : []; - - for (const polygon of polygons) { - if (!Array.isArray(polygon)) continue; - for (const rawRing of polygon) { - if (!Array.isArray(rawRing) || rawRing.length < CFG.milkyWay.minPoints) continue; - - const coords = []; - let area = 0; - - for (let i = 0; i < rawRing.length; i++) { - const point = rawRing[i]; - if (!Array.isArray(point) || point.length < 2) continue; - const lon = Number(point[0]); - const lat = Number(point[1]); - if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue; - - coords.push([lon, lat]); - - if (i < rawRing.length - 1) { - const next = rawRing[i + 1]; - const lon2 = Array.isArray(next) ? Number(next[0]) : NaN; - const lat2 = Array.isArray(next) ? Number(next[1]) : NaN; - if (Number.isFinite(lon2) && Number.isFinite(lat2)) { - area += (lon * lat2) - (lon2 * lat); - } - } - } - - if (coords.length < CFG.milkyWay.minPoints) continue; - - const areaAbs = Math.abs(area / 2); - if (!Number.isFinite(areaAbs)) continue; - - rings.push({ - coords, - areaAbs, - }); - } - } - } - - rings.sort((a, b) => b.areaAbs - a.areaAbs); - return rings; -} - -const MILKY_WAY_RINGS = buildMilkyWayRings(EMBEDDED_MILKY_WAY_DATA); - function getTime() { return liveMode ? new Date() : simTime; } // ═══════════════════════════════════════════════════════════════════════════════ @@ -1968,7 +1870,6 @@ function render() { ctx.fill(); ctx.clip(); - if (showMilkyWay) drawMilkyWay(date); if (showAltaz) drawAltazGrid(); if (showEq) drawEquatorialGrid(date); if (showEclip) drawEcliptic(date); @@ -2303,7 +2204,6 @@ spBind('cfgPlanetsShow', () => showPlanets, v => showPlanets = v); spBind('cfgCometsShow', () => showComets, v => showComets = v); spBind('cfgCometsLabels', () => labelComets, v => labelComets = v); spBind('cfgEclipShow', () => showEclip, v => showEclip = v); -spBind('cfgMilkyWayShow', () => showMilkyWay, v => showMilkyWay = v); spBind('cfgMessierShow', () => showMessier, v => showMessier = v); spBind('cfgSunMoonShow', () => showSunMoon, v => showSunMoon = v); @@ -2394,10 +2294,6 @@ if (EMBEDDED_MESSIER_ERROR) { console.warn('Messier-Daten aus der Datenbank nicht geladen:', EMBEDDED_MESSIER_ERROR); } -if (EMBEDDED_MILKY_WAY_ERROR) { - console.warn('Milchstraßen-Daten nicht geladen:', EMBEDDED_MILKY_WAY_ERROR); -} - function updateStarLoadingUi(done, total) { if (!starLoadingOverlay || !starLoadingFill || !starLoadingStatus || !starLoadingNote) return; const safeTotal = Math.max(1, total); From 0a9ce8be1e7bf8c70d188239c8ba87b2b880e0e8 Mon Sep 17 00:00:00 2001 From: Eskimue Date: Thu, 30 Apr 2026 10:57:56 +0200 Subject: [PATCH 2/4] Drehbare sternkarte eingebaut, aber flasch --- config/rechtesystem.json | 5 + public/auth_helpers.php | 1 + public/drehbare_sternkarte.php | 1251 ++++++++++++++++++++++++++++++++ public/header.php | 1 + 4 files changed, 1258 insertions(+) create mode 100644 public/drehbare_sternkarte.php diff --git a/config/rechtesystem.json b/config/rechtesystem.json index 8165f9e..cf111e2 100644 --- a/config/rechtesystem.json +++ b/config/rechtesystem.json @@ -76,6 +76,10 @@ "label": "Sternenhimmel", "menu_group": "Himmel & Planeten" }, + "drehbare_sternkarte.php": { + "label": "Drehbare Sternkarte", + "menu_group": "Himmel & Planeten" + }, "meteorshowers.php": { "label": "Meteorstroeme", "menu_group": "Himmel & Planeten" @@ -132,6 +136,7 @@ "jupitersystem.php", "tagbogen.php", "sternenhimmel.php", + "drehbare_sternkarte.php", "meteorshowers.php", "geocron/index.php", "geocron3d.php" diff --git a/public/auth_helpers.php b/public/auth_helpers.php index d83328a..15c706c 100644 --- a/public/auth_helpers.php +++ b/public/auth_helpers.php @@ -123,6 +123,7 @@ function manageablePublicPages(): array 'jupitersystem.php' => 'Jupitersystem', 'tagbogen.php' => 'Tagbogen', 'sternenhimmel.php' => 'Sternenhimmel', + 'drehbare_sternkarte.php' => 'Drehbare Sternkarte', 'meteorshowers.php' => 'Meteorströme', 'geocron/index.php' => 'Geocron', 'geocron3d.php' => 'Geocron 3D', diff --git a/public/drehbare_sternkarte.php b/public/drehbare_sternkarte.php new file mode 100644 index 0000000..33bd5d6 --- /dev/null +++ b/public/drehbare_sternkarte.php @@ -0,0 +1,1251 @@ + []]; +$constellationDataError = null; +$messierData = []; +$messierDataError = null; +$availableLocations = []; +$starDbMagLimit = 7.4; +$pdo = null; + +try { + $dbConfig = require __DIR__ . '/../config/database.php'; + $dsn = sprintf( + 'mysql:host=%s;dbname=%s;charset=%s', + $dbConfig['host'], + $dbConfig['dbname'], + $dbConfig['charset'] ?? 'utf8mb4' + ); + $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + + $constellations = []; + $ensureConstellation = static function (string $id) use (&$constellations): void { + if (!isset($constellations[$id])) { + $constellations[$id] = [ + 'id' => $id, + 'lines' => [], + 'label_positions' => [], + 'common_name' => [ + 'german' => null, + 'native' => null, + 'byname' => null, + ], + ]; + } + }; + + $nameStmt = $pdo->query( + "SELECT constellation_id, german_name, native_name, byname + FROM constellation_names + ORDER BY constellation_id" + ); + foreach ($nameStmt as $row) { + $id = (string) $row['constellation_id']; + $ensureConstellation($id); + $constellations[$id]['common_name'] = [ + 'german' => $row['german_name'] !== null ? (string) $row['german_name'] : null, + 'native' => $row['native_name'] !== null ? (string) $row['native_name'] : null, + 'byname' => $row['byname'] !== null ? (string) $row['byname'] : null, + ]; + } + + $labelStmt = $pdo->query( + "SELECT constellation_id, position_index, ra_hours, dec_degrees + FROM constellation_label_positions + ORDER BY constellation_id, position_index" + ); + foreach ($labelStmt as $row) { + $id = (string) $row['constellation_id']; + $ensureConstellation($id); + $constellations[$id]['label_positions'][] = [ + (float) $row['ra_hours'], + (float) $row['dec_degrees'], + ]; + } + + $lineStmt = $pdo->query( + "SELECT cl.constellation_id, cl.line_index, clp.point_index, clp.hip_number + FROM constellation_lines cl + INNER JOIN constellation_line_points clp ON clp.line_id = cl.id + ORDER BY cl.constellation_id, cl.line_index, clp.point_index" + ); + foreach ($lineStmt as $row) { + $id = (string) $row['constellation_id']; + $lineIndex = (int) $row['line_index']; + $ensureConstellation($id); + $constellations[$id]['lines'][$lineIndex] ??= []; + $constellations[$id]['lines'][$lineIndex][] = (int) $row['hip_number']; + } + + foreach ($constellations as &$constellation) { + if ($constellation['lines']) { + ksort($constellation['lines']); + $constellation['lines'] = array_values($constellation['lines']); + } + } + unset($constellation); + + uasort( + $constellations, + static fn(array $a, array $b): int => strcmp($a['id'], $b['id']) + ); + + if ($starsApiRequested) { + $stars = []; + $starStmt = $pdo->query( + "SELECT hip, proper, bayer, con, spect, mag, ci, rarad, decrad + FROM star_hipparcos + WHERE mag IS NOT NULL + AND rarad IS NOT NULL + AND decrad IS NOT NULL + AND mag <= " . (float) $starDbMagLimit . " + ORDER BY mag ASC" + ); + while ($row = $starStmt->fetch()) { + $stars[] = [ + 'hip' => $row['hip'] !== null ? (int) $row['hip'] : null, + 'proper' => trim((string) ($row['proper'] ?? '')), + 'bayer' => trim((string) ($row['bayer'] ?? '')), + 'con' => trim((string) ($row['con'] ?? '')), + 'spect' => trim((string) ($row['spect'] ?? '')), + 'ra' => (float) $row['rarad'], + 'dec' => (float) $row['decrad'], + 'mag' => (float) $row['mag'], + 'ci' => $row['ci'] !== null ? (float) $row['ci'] : null, + ]; + } + + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'ok' => true, + 'stars' => $stars, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + $constellationData['constellations'] = array_values($constellations); + + $messierStmt = $pdo->query( + "SELECT messier, name, object_type_abrev, ra_deg, dec_deg, v_mag, major_axis + FROM dso_objects + WHERE messier IS NOT NULL + AND messier <> '' + AND ra_deg IS NOT NULL + AND dec_deg IS NOT NULL + ORDER BY CAST(REGEXP_REPLACE(messier, '[^0-9]', '') AS UNSIGNED)" + ); + while ($row = $messierStmt->fetch()) { + if (!preg_match('/(\d+)/', (string) $row['messier'], $m)) { + continue; + } + $messierData[] = [ + 'm' => (int) $m[1], + 'name' => trim((string) ($row['name'] ?? '')), + 'type' => trim((string) ($row['object_type_abrev'] ?? 'As')), + 'ra' => ((float) $row['ra_deg']) / 15.0, + 'dec' => (float) $row['dec_deg'], + 'mag' => $row['v_mag'] !== null ? (float) $row['v_mag'] : 99.0, + 'size' => $row['major_axis'] !== null ? (float) $row['major_axis'] : 8.0, + ]; + } + + if ($loggedIn && isset($_SESSION['user_id'])) { + $locationStmt = $pdo->prepare( + "SELECT id, name, latitude, longitude, elevation, timezone, is_default + FROM app_user_locations + WHERE user_id = ? + ORDER BY is_default DESC, name ASC, id ASC" + ); + $locationStmt->execute([(int) $_SESSION['user_id']]); + while ($row = $locationStmt->fetch()) { + if ($row['latitude'] === null || $row['longitude'] === null) { + continue; + } + $availableLocations[] = [ + 'id' => $row['id'] !== null ? (int) $row['id'] : null, + 'name' => trim((string) ($row['name'] ?? 'Standort')), + 'latitude' => (float) $row['latitude'], + 'longitude' => (float) $row['longitude'], + 'elevation' => $row['elevation'] !== null ? (float) $row['elevation'] : 0.0, + 'timezone' => trim((string) ($row['timezone'] ?? 'Europe/Berlin')), + 'is_default' => !empty($row['is_default']), + ]; + } + } +} catch (Throwable $e) { + if ($starsApiRequested) { + header('Content-Type: application/json; charset=utf-8'); + http_response_code(500); + echo json_encode([ + 'ok' => false, + 'error' => $e->getMessage(), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + $constellationDataError = $e->getMessage(); + $messierDataError = $e->getMessage(); +} + +$constellationDataJson = json_encode($constellationData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$constellationDataErrorJson = json_encode($constellationDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$messierDataJson = json_encode($messierData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$messierDataErrorJson = json_encode($messierDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +require __DIR__ . '/header.php'; + +$chartLocation = [ + 'id' => null, + 'name' => 'Sternwarte Sonneberg', + 'latitude' => 50.3547, + 'longitude' => 11.1900, + 'elevation' => 640.0, + 'timezone' => 'Europe/Berlin', +]; + +if (!empty($currentLocation) && is_array($currentLocation)) { + $chartLocation = [ + 'id' => isset($currentLocation['id']) ? (int) $currentLocation['id'] : null, + 'name' => (string) ($currentLocation['name'] ?? 'Standard-Standort'), + 'latitude' => isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : 50.3547, + 'longitude' => isset($currentLocation['longitude']) ? (float) $currentLocation['longitude'] : 11.1900, + 'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float) $currentLocation['elevation'] : 0.0, + 'timezone' => trim((string) ($currentLocation['timezone'] ?? 'Europe/Berlin')), + ]; +} + +if (!$availableLocations) { + $availableLocations[] = [ + 'id' => $chartLocation['id'], + 'name' => $chartLocation['name'], + 'latitude' => $chartLocation['latitude'], + 'longitude' => $chartLocation['longitude'], + 'elevation' => $chartLocation['elevation'], + 'timezone' => $chartLocation['timezone'], + 'is_default' => true, + ]; +} +?> + + + + +
+ + +
+
Sternscheibe wird geladen…
+
Ziehen zum Drehen
+
+ + +
+ + + + diff --git a/public/header.php b/public/header.php index e4966a1..7a84208 100644 --- a/public/header.php +++ b/public/header.php @@ -152,6 +152,7 @@ $menuGroups = [ ['href' => 'jupitersystem.php', 'label' => 'Jupitersystem'], ['href' => 'tagbogen.php', 'label' => 'Tagbogen'], ['href' => 'sternenhimmel.php', 'label' => 'Sternenhimmel'], + ['href' => 'drehbare_sternkarte.php', 'label' => 'Drehbare Sternkarte'], ['href' => 'satellitenhimmel.php', 'label' => 'Satellitenhimmel'], ['href' => 'meteorshowers.php', 'label' => 'Meteorströme'], ['href' => 'asteroid_temp.php', 'label' => 'Asteroid-Temperatur'], From 91694867e4579969f7fefa8908563c8101d3011f Mon Sep 17 00:00:00 2001 From: Eskimue Date: Thu, 30 Apr 2026 15:31:48 +0200 Subject: [PATCH 3/4] Monatgsvorhersage verbessert --- public/monatsvorhersage.php | 438 ++++++++++++++++++++++++++++++------ 1 file changed, 364 insertions(+), 74 deletions(-) diff --git a/public/monatsvorhersage.php b/public/monatsvorhersage.php index 68358ee..05ce0c5 100644 --- a/public/monatsvorhersage.php +++ b/public/monatsvorhersage.php @@ -835,80 +835,391 @@ if (!function_exists('monthForecastBuildNarrativeText')) { static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['moon_phase', 'golden_handle', 'moon_planet', 'moon_planet_approach', 'moon_deep_sky', 'moon_deep_sky_approach', 'moon_occultation', 'moon_apsis'], true), 10 ); + // Jahreszahl aus Datum entfernen (z. B. "19.04.2026" → "19.04.") + $shortDate = static function (string $d): string { + return (string) preg_replace('/\.\d{4}$/', '.', $d); + }; + if ($moonEvents !== []) { - // Chronologisch sortieren nach local_iso $sortedMoonEvents = $moonEvents; usort($sortedMoonEvents, static fn (array $a, array $b): int => strcmp( (string) ($a['local_iso'] ?? $a['date'] ?? ''), (string) ($b['local_iso'] ?? $b['date'] ?? '') )); - // Natürlichsprachliche Liste: "Vollmond ist am 01.05., Apogäum am 05.05. und Neumond am 16.05." - $moonParts = []; - foreach ($sortedMoonEvents as $me) { - $label = trim((string) ($me['event'] ?? '')); - $date = trim((string) ($me['date'] ?? '')); - if ($label === '' || $date === '') { - continue; + $phaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'moon_phase')); + $nonPhaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') !== 'moon_phase')); + + $moonSentences = []; + + // 1. Mondphasen in einem natürlichen Satz zusammenfassen + if ($phaseEvents !== []) { + $phaseParts = []; + foreach ($phaseEvents as $pe) { + $rawLabel = trim((string) ($pe['event'] ?? '')); + $date = trim((string) ($pe['date'] ?? '')); + if ($rawLabel === '' || $date === '') { + continue; + } + // "Mond Erstes Viertel" → "Erstes Viertel" + $cleanLabel = (string) preg_replace('/^Mond\s+/', '', $rawLabel); + $phaseParts[] = $cleanLabel . ' am ' . $shortDate($date); } - // "Mond bedeckt X" → "der Mond bedeckt X" für natürlicheren Satzbau - if (str_starts_with($label, 'Mond bedeckt ')) { - $label = 'der ' . $label; + if ($phaseParts !== []) { + $lastPhase = array_pop($phaseParts); + $phaseList = $phaseParts !== [] ? implode(', ', $phaseParts) . ' und ' . $lastPhase : $lastPhase; + $moonSentences[] = 'Die Mondphasen im Überblick: ' . $phaseList . '.'; } - $moonParts[] = ['label' => $label, 'date' => $date]; } - if ($moonParts !== []) { - $moonSentenceParts = []; - foreach ($moonParts as $idx => $part) { - if ($idx === 0) { - $moonSentenceParts[] = $part['label'] . ' ist am ' . $part['date']; + // 2. Apsis, Begegnungen und Goldener Henkel – nach Datum gruppiert + $byDate = []; + foreach ($nonPhaseEvents as $ne) { + $date = trim((string) ($ne['date'] ?? '')); + if ($date === '') { + continue; + } + $byDate[$date][] = $ne; + } + + foreach ($byDate as $date => $dateEvents) { + $apsis = null; + $approaches = []; + $goldenHandle = null; + foreach ($dateEvents as $de) { + $type = (string) ($de['type'] ?? ''); + if ($type === 'moon_apsis') { + $apsis = $de; + } elseif ($type === 'golden_handle') { + $goldenHandle = $de; } else { - $moonSentenceParts[] = $part['label'] . ' am ' . $part['date']; + $approaches[] = $de; } } - $lastPart = array_pop($moonSentenceParts); - $moonSentence = $moonSentenceParts !== [] - ? implode(', ', $moonSentenceParts) . ' und ' . $lastPart - : $lastPart; - $paragraphs[] = $moonSentence . '.'; + + $sd = $shortDate($date); + + if ($goldenHandle !== null) { + $moonSentences[] = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel: Ein Gebirgskamm am Mondrand leuchtet bereits im Sonnenlicht, während das Tal dahinter noch im Schatten liegt – ein lohnender Anblick schon im kleinen Teleskop.'; + } + + if ($apsis !== null) { + $apsisLabel = trim((string) ($apsis['event'] ?? '')); + // Distanz aus dem Label extrahieren: "(404.960 km)" + $distanceMatch = []; + $distanceStr = ''; + if (preg_match('/\(([0-9.,]+\s*km)\)\s*$/', $apsisLabel, $distanceMatch)) { + $distanceStr = ' mit nur ' . $distanceMatch[1]; + } + $isPerigee = str_contains($apsisLabel, 'Erdnähe') || str_contains($apsisLabel, 'Perigäum'); + if ($isPerigee) { + $apsisText = 'Am ' . $sd . ' erreicht der Mond sein Perigäum' . $distanceStr . ' – er steht der Erde in diesem Monat am nächsten'; + } else { + $apsisText = 'Am ' . $sd . ' erreicht der Mond sein Apogäum' . $distanceStr . ' – er steht der Erde in diesem Monat am fernsten'; + } + if ($approaches !== []) { + // Begegnungen natürlich formulieren: "und zieht dabei in nur X Grad Abstand an Y vorbei" + $approachFragments = []; + foreach ($approaches as $ap) { + $apLabel = trim((string) ($ap['event'] ?? '')); + $sepMatch = []; + $sepStr = ''; + if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $apLabel, $sepMatch)) { + $apLabel = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $apLabel)); + $sepStr = ' in nur ' . $sepMatch[1] . ' Abstand'; + } + if (str_starts_with($apLabel, 'Mond nahe ')) { + $target = substr($apLabel, strlen('Mond nahe ')); + $approachFragments[] = $sepStr . ' an ' . $target . ' vorbei'; + } elseif (str_starts_with($apLabel, 'Mond bedeckt ')) { + $target = substr($apLabel, strlen('Mond bedeckt ')); + $approachFragments[] = ' und bedeckt dabei ' . $target; + } else { + $approachFragments[] = $sepStr !== '' ? $sepStr . ' bei ' . $apLabel : ' bei ' . $apLabel; + } + } + $approachClause = count($approachFragments) === 1 + ? ' und zieht dabei' . $approachFragments[0] + : ' und zieht dabei' . implode(' sowie', $approachFragments); + $moonSentences[] = $apsisText . $approachClause . '.'; + } else { + $moonSentences[] = $apsisText . '.'; + } + } elseif ($approaches !== []) { + foreach ($approaches as $ap) { + $apLabel = trim((string) ($ap['event'] ?? '')); + $apDate = trim((string) ($ap['date'] ?? '')); + $sepMatch = []; + $sepStr = ''; + if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $apLabel, $sepMatch)) { + $apLabel = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $apLabel)); + $sepStr = ' in nur ' . $sepMatch[1] . ' Abstand'; + } + $apSd = $shortDate($apDate !== '' ? $apDate : $date); + if (str_starts_with($apLabel, 'Mond nahe ')) { + $target = substr($apLabel, strlen('Mond nahe ')); + $moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.'; + } elseif (str_starts_with($apLabel, 'Mond bedeckt ')) { + $target = substr($apLabel, strlen('Mond bedeckt ')); + $moonSentences[] = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.'; + } else { + $moonSentences[] = $apLabel . ' am ' . $apSd . ($sepStr !== '' ? ' – ' . ltrim($sepStr) : '') . '.'; + } + } + } + } + + if ($moonSentences !== []) { + $paragraphs[] = implode(' ', $moonSentences); } } + // Hilfsfunktion: natürlichen Satz aus einem Planeten-Event bauen + $buildPlanetSentence = static function (array $event) use ($shortDate): ?string { + $type = (string) ($event['type'] ?? ''); + $label = trim((string) ($event['event'] ?? '')); + $date = trim((string) ($event['date'] ?? '')); + if ($label === '' || $date === '') { + return null; + } + $sd = $shortDate($date); + + if ($type === 'planet_conjunction') { + $sepMatch = []; + $sep = ''; + if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) { + $sep = $sepMatch[1]; + $label = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $label)); + } + if (preg_match('/^(.+?)\s+nahe\s+(.+)$/i', $label, $m)) { + $p1 = trim($m[1]); + $p2 = trim($m[2]); + $sepNote = $sep !== '' ? ' – der Abstand beträgt nur ' . $sep : ''; + return 'Am ' . $sd . ' rücken ' . $p1 . ' und ' . $p2 . ' eng zusammen' . $sepNote . '.'; + } + } + + if ($type === 'golden_gate_of_ecliptic') { + $planet = ''; + if (preg_match('/^(\S+)\s+im\s+Goldenen\s+Tor/i', $label, $m)) { + $planet = $m[1]; + } + if ($planet !== '') { + return 'Am ' . $sd . ' durchquert ' . $planet . ' das Goldene Tor der Ekliptik und zieht dabei zwischen Hyaden und Plejaden hindurch – ein lohnender Anblick im Fernglas.'; + } + } + + if ($type === 'inner_planet_elongation') { + $sepMatch = []; + $sepStr = ''; + if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) { + $sepStr = ' (' . $sepMatch[1] . ')'; + $label = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $label)); + } + $isWest = str_contains($label, 'westlich') || str_contains($label, 'West'); + $planet = ''; + if (preg_match('/^(\S+)\s+/', $label, $m)) { + $planet = $m[1]; + } + $visibility = $isWest ? 'als Morgenstern am besten zu beobachten' : 'als Abendstern am besten zu beobachten'; + return 'Am ' . $sd . ' erreicht ' . ($planet ?: 'der Planet') . ' seine größte Elongation' . $sepStr . ' und ist dann ' . $visibility . '.'; + } + + if ($type === 'venus_peak_magnitude') { + $magMatch = []; + $magStr = ''; + if (preg_match('/\(([^)]+)\)/', $label, $magMatch)) { + $magStr = ' mit ' . $magMatch[1]; + } + return 'Am ' . $sd . ' erreicht Venus ihre größte Helligkeit' . $magStr . ' – sie ist dann selbst in der Dämmerung kaum zu übersehen.'; + } + + if ($type === 'outer_planet_event') { + $planet = ''; + if (preg_match('/^(\S+)\s+/', $label, $m)) { + $planet = $m[1]; + } + if (str_contains($label, 'Opposition')) { + return 'Am ' . $sd . ' steht ' . ($planet ?: 'der Planet') . ' in Opposition – er geht bei Sonnenuntergang auf, ist die ganze Nacht sichtbar und leuchtet so hell wie selten.'; + } + if (str_contains($label, 'Konjunktion')) { + return 'Am ' . $sd . ' steht ' . ($planet ?: 'der Planet') . ' in Konjunktion mit der Sonne und ist für einige Wochen nicht zu beobachten.'; + } + if (str_contains($label, 'Quadratur')) { + return 'Am ' . $sd . ' befindet sich ' . ($planet ?: 'der Planet') . ' in Quadratur – er steht 90 Grad von der Sonne entfernt und ist in der ersten oder zweiten Nachthälfte gut sichtbar.'; + } + } + + if ($type === 'mercury_good_visibility') { + return 'Um den ' . $sd . ' zeigt sich Merkur günstig am Himmel und lässt sich in der Dämmerung tief über dem Horizont aufspüren.'; + } + + if ($type === 'planet_parade') { + return 'Am ' . $sd . ' sind mehrere Planeten gleichzeitig am Himmel zu sehen – eine seltene Planetenparade, die einen Blick Richtung Süden lohnt.'; + } + + // Fallback + return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.'; + }; + + $buildPlanetMotionSentence = static function (array $event) use ($shortDate): ?string { + $type = (string) ($event['type'] ?? ''); + $label = trim((string) ($event['event'] ?? '')); + $date = trim((string) ($event['date'] ?? '')); + if ($label === '' || $date === '') { + return null; + } + $sd = $shortDate($date); + + if ($type === 'outer_planet_station') { + $planet = ''; + $isRetrograde = str_contains($label, 'rückläufig') || str_contains($label, 'Rückläufig'); + $isPrograde = str_contains($label, 'rechtläufig') || str_contains($label, 'Rechtläufig') || str_contains($label, 'direkt'); + if (preg_match('/^(\S+)\s+/', $label, $m)) { + $planet = $m[1]; + } + if ($isRetrograde) { + return 'Ab dem ' . $sd . ' läuft ' . ($planet ?: 'der Planet') . ' rückläufig – er bewegt sich scheinbar rückwärts vor dem Sternhintergrund, was seine Beobachtung besonders interessant macht.'; + } + if ($isPrograde) { + return 'Ab dem ' . $sd . ' setzt ' . ($planet ?: 'der Planet') . ' seine normale Vorwärtsbewegung am Himmel wieder fort.'; + } + return 'Am ' . $sd . ' wird ' . ($planet ?: 'der Planet') . ' stationär und ändert seine Bewegungsrichtung am Himmel.'; + } + + if ($type === 'planet_constellation_change') { + // Label-Format: "Mars wechselt vom Sternbild Wassermann in das Sternbild Fische" + if (preg_match('/^(.+?)\s+wechselt\s+vom\s+Sternbild\s+(.+?)\s+in\s+das\s+Sternbild\s+(.+)$/i', $label, $m)) { + $planet = trim($m[1]); + $from = trim($m[2]); + $to = trim($m[3]); + // Formulierung deterministisch variieren (nach erstem Buchstaben des Planetennamens) + $variant = ord($planet[0]) % 3; + if ($variant === 1) { + return 'Am ' . $sd . ' verlässt ' . $planet . ' das Sternbild ' . $from . ' und zieht weiter in das Sternbild ' . $to . '.'; + } + if ($variant === 2) { + return 'Am ' . $sd . ' tritt ' . $planet . ' aus dem Sternbild ' . $from . ' in das Sternbild ' . $to . ' über.'; + } + return 'Am ' . $sd . ' wechselt ' . $planet . ' vom Sternbild ' . $from . ' in das Sternbild ' . $to . '.'; + } + // Fallback ohne "vom"-Angabe + if (preg_match('/^(.+?)\s+wechselt\s+(?:in\s+das\s+Sternbild\s+|ins?\s+(?:Sternbild\s+)?)(.+)$/i', $label, $m)) { + return 'Am ' . $sd . ' wechselt ' . trim($m[1]) . ' in das Sternbild ' . trim($m[2]) . '.'; + } + } + + return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.'; + }; + $planetObservationEvents = $findEvents( $filteredEvents, static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['inner_planet_elongation', 'venus_peak_magnitude', 'outer_planet_event', 'planet_conjunction', 'planet_parade', 'mercury_good_visibility', 'golden_gate_of_ecliptic'], true), 5 ); if ($planetObservationEvents !== []) { - $planetIntro = $pickRandomText([ - 'Bei den Planeten lohnt sich vor allem der Blick auf', - 'Planetarisch besonders interessant sind in diesem Monat', - 'Bei den Planeten stechen in diesem Monat vor allem', - 'Einen planetaren Schwerpunkt bilden in diesem Monat', - ], 'Bei den Planeten lohnt sich vor allem der Blick auf'); - $planetOutro = $pickRandomText([ - 'Diese Termine geben dem Monat seinen charakteristischen planetaren Akzent.', - 'Damit bekommt der Monat einen klaren planetaren Schwerpunkt.', - 'Solche Termine prägen den planetaren Charakter dieses Monats besonders deutlich.', - 'Sie setzen im Monatsverlauf markante planetare Akzente.', - ], 'Diese Termine geben dem Monat seinen charakteristischen planetaren Akzent.'); - $paragraphs[] = $planetIntro . ' ' . $describeEvents($planetObservationEvents) . '. ' . $planetOutro; + $planetSentences = []; + foreach ($planetObservationEvents as $pe) { + $sentence = $buildPlanetSentence($pe); + if ($sentence !== null) { + $planetSentences[] = $sentence; + } + } + if ($planetSentences !== []) { + $paragraphs[] = implode(' ', $planetSentences); + } } $planetMotionEvents = $findEvents( $filteredEvents, static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['outer_planet_station', 'planet_constellation_change'], true), - 4 + 8 ); if ($planetMotionEvents !== []) { - $planetMotionIntro = $pickRandomText([ - 'Im Monatsverlauf zeigen sich bei den Planeten auch auffällige Positionswechsel.', - 'Auch in ihrer Stellung am Himmel verändern sich die Planeten in diesem Monat sichtbar.', - 'Neben den Beobachtungshighlights fallen bei den Planeten auch einige Positionswechsel auf.', - 'Auch die Bewegung der Planeten vor dem Sternhintergrund macht sich in diesem Monat bemerkbar.', - ], 'Im Monatsverlauf zeigen sich bei den Planeten auch auffaellige Positionswechsel.'); - $paragraphs[] = $planetMotionIntro . ' Dazu gehören ' . $describeEvents($planetMotionEvents) . '.'; + // Sternbild-Akkusativformen für natürliche Sätze ohne "Sternbild"-Wiederholung + $constellationAkk = [ + 'Widder' => 'den Widder', 'Stier' => 'den Stier', 'Zwillinge' => 'die Zwillinge', + 'Krebs' => 'den Krebs', 'Löwe' => 'den Löwen', 'Jungfrau' => 'die Jungfrau', + 'Waage' => 'die Waage', 'Skorpion' => 'den Skorpion', 'Schütze' => 'den Schützen', + 'Steinbock' => 'den Steinbock', 'Wassermann' => 'den Wassermann', 'Fische' => 'die Fische', + 'Walfisch' => 'den Walfisch', 'Orion' => 'den Orion', 'Fuhrmann' => 'den Fuhrmann', + 'Stier' => 'den Stier', 'Kassiopeia' => 'die Kassiopeia', 'Andromeda' => 'die Andromeda', + 'Perseus' => 'den Perseus', 'Pegasus' => 'den Pegasus', 'Leier' => 'die Leier', + 'Schwan' => 'den Schwan', 'Adler' => 'den Adler', 'Schlange' => 'die Schlange', + 'Schlangenträger' => 'den Schlangenträger', 'Herkules' => 'den Herkules', + 'Bärenhüter' => 'den Bärenhüter', 'Großer Bär' => 'den Großen Bären', + 'Drache' => 'den Drachen', 'Nördliche Krone' => 'die Nördliche Krone', + 'Kleiner Löwe' => 'den Kleinen Löwen', 'Rabe' => 'den Raben', 'Becher' => 'den Becher', + 'Wasserschlange' => 'die Wasserschlange', 'Eridanus' => 'den Eridanus', + 'Hase' => 'den Hasen', 'Großer Hund' => 'den Großen Hund', + 'Kleiner Hund' => 'den Kleinen Hund', 'Einhorn' => 'das Einhorn', + 'Delfin' => 'den Delfin', 'Pfeil' => 'den Pfeil', 'Fuchs' => 'den Fuchs', + 'Kepheus' => 'den Kepheus', 'Südlicher Fisch' => 'den Südlichen Fisch', + 'Jagdhunde' => 'die Jagdhunde', 'Löwchen' => 'das Löwchen', + ]; + $akkusativ = static function (string $name) use ($constellationAkk): string { + return $constellationAkk[$name] ?? 'das Sternbild ' . $name; + }; + + $stationEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'outer_planet_station')); + $changeEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'planet_constellation_change')); + + $motionSentences = []; + + // Rückläufigkeits-Ereignisse einzeln als Satz + foreach ($stationEvents as $se) { + $sentence = $buildPlanetMotionSentence($se); + if ($sentence !== null) { + $motionSentences[] = $sentence; + } + } + + // Sternbild-Wechsel: nach Planet gruppieren, kompakt als eine Aussage + if ($changeEvents !== []) { + $byPlanet = []; + $planetOrder = []; + foreach ($changeEvents as $ce) { + $label = trim((string) ($ce['event'] ?? '')); + $date = trim((string) ($ce['date'] ?? '')); + if (!preg_match('/^(.+?)\s+wechselt\s+vom\s+Sternbild\s+(.+?)\s+in\s+das\s+Sternbild\s+(.+)$/i', $label, $m)) { + continue; + } + $planet = trim($m[1]); + $to = trim($m[3]); + if (!isset($byPlanet[$planet])) { + $byPlanet[$planet] = []; + $planetOrder[] = $planet; + } + $byPlanet[$planet][] = ['date' => $date, 'to' => $to]; + } + + if ($byPlanet !== []) { + $parts = []; + foreach ($planetOrder as $planet) { + $moves = $byPlanet[$planet]; + if (count($moves) === 1) { + $parts[] = $planet . ' am ' . $shortDate($moves[0]['date']) . ' in ' . $akkusativ($moves[0]['to']); + } else { + // Mehrfacher Wechsel desselben Planeten + $moveParts = []; + foreach ($moves as $move) { + $moveParts[] = 'am ' . $shortDate($move['date']) . ' in ' . $akkusativ($move['to']); + } + $lastMove = array_pop($moveParts); + $parts[] = $planet . ' ' . ($moveParts !== [] ? implode(', ', $moveParts) . ' und dann ' . $lastMove : $lastMove); + } + } + $lastPart = array_pop($parts); + $changeText = count($byPlanet) > 1 + ? implode(', ', $parts) . ' und ' . $lastPart + : $lastPart; + $motionSentences[] = 'Sternbildwechsel im Monatsverlauf: ' . $changeText . '.'; + } + } + + if ($motionSentences !== []) { + $paragraphs[] = implode(' ', $motionSentences); + } } $cometEvents = $findEvents( @@ -3040,40 +3351,13 @@ async function composeExportCanvas(sourceCanvas, logoPosition, options) { const caption = monthChartExportCaption(); if (showCaption && caption !== '') { const padding = Math.round(exportCanvas.width * 0.024); - const fontSize = Math.max(26, Math.round(exportCanvas.width * 0.022)); + const fontSize = Math.max(18, Math.round(exportCanvas.width * 0.015)); exportCtx.save(); exportCtx.font = '600 ' + fontSize + 'px Rajdhani, sans-serif'; exportCtx.textAlign = 'right'; exportCtx.textBaseline = 'bottom'; - - const metrics = exportCtx.measureText(caption); - const textWidth = metrics.width; - const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8; - const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2; - const boxPadX = Math.round(fontSize * 0.45); - const boxPadY = Math.round(fontSize * 0.3); - const boxWidth = textWidth + boxPadX * 2; - const boxHeight = ascent + descent + boxPadY * 2; - const boxX = exportCanvas.width - padding - boxWidth; - const boxY = exportCanvas.height - padding - boxHeight; - const radius = Math.round(fontSize * 0.28); - - exportCtx.fillStyle = 'rgba(3, 7, 14, 0.7)'; - exportCtx.beginPath(); - exportCtx.moveTo(boxX + radius, boxY); - exportCtx.lineTo(boxX + boxWidth - radius, boxY); - exportCtx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + radius); - exportCtx.lineTo(boxX + boxWidth, boxY + boxHeight - radius); - exportCtx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - radius, boxY + boxHeight); - exportCtx.lineTo(boxX + radius, boxY + boxHeight); - exportCtx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - radius); - exportCtx.lineTo(boxX, boxY + radius); - exportCtx.quadraticCurveTo(boxX, boxY, boxX + radius, boxY); - exportCtx.closePath(); - exportCtx.fill(); - exportCtx.fillStyle = 'rgba(235, 242, 255, 0.96)'; - exportCtx.fillText(caption, exportCanvas.width - padding - boxPadX, exportCanvas.height - padding - boxPadY); + exportCtx.fillText(caption, exportCanvas.width - padding, exportCanvas.height - padding); exportCtx.restore(); } @@ -3113,7 +3397,13 @@ function monthChartExportCaption() { return ''; } - return 'Sternhimmel für ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ' Uhr'; + const parts = String(MONTH_CHART_CONFIG.localTimeLabel || '').split(' '); + const date = parts[0] || ''; + const hh = String(hour).padStart(2, '0'); + const mm = String(minute).padStart(2, '0'); + return date !== '' + ? 'Sternhimmel am ' + date + ' um ' + hh + ':' + mm + ' Uhr' + : 'Sternhimmel um ' + hh + ':' + mm + ' Uhr'; } function formatGermanDate(date) { From 4455aa44d4a0d2a92b56d8e906d28deb5a2fc4c5 Mon Sep 17 00:00:00 2001 From: Eskimue Date: Thu, 30 Apr 2026 16:33:43 +0200 Subject: [PATCH 4/4] Header Menu --- public/css/style.css | 2 +- public/header.php | 62 +++++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index e87009f..8167aaf 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -653,7 +653,7 @@ nav a.nav-logout:hover { position: absolute; top: calc(100% + 0.8rem); right: 0; - width: 480px; + width: 620px; max-width: 96vw; display: flex; flex-direction: column; diff --git a/public/header.php b/public/header.php index 7a84208..a06cabf 100644 --- a/public/header.php +++ b/public/header.php @@ -138,37 +138,47 @@ if ($loggedIn && isset($_SESSION['user_id']) && $pdoHeader !== null) { $menuGroups = [ [ - 'title' => 'Himmel & Planeten', + 'title' => 'Mond & Finsternisse', 'links' => [ - ['href' => 'moonphase.php', 'label' => 'Mondphase'], - ['href' => 'moonyear.php', 'label' => 'Mondphasen-Jahresübersicht'], - ['href' => 'planetensichtbarkeit.php', 'label' => 'Planetensichtbarkeit'], - ['href' => 'ephemeriden.php', 'label' => 'Ephemeriden'], - ['href' => 'comets.php', 'label' => 'Kometen'], - ['href' => 'astronomical_conversions.php', 'label' => 'Astronomical Conversions'], - ['href' => 'tcrb_lightcurve.php', 'label' => 'T CrB Lichtkurve'], - ['href' => 'solarsystem.php', 'label' => 'Sonnensystem'], - ['href' => 'mars.php', 'label' => 'Mars'], - ['href' => 'jupitersystem.php', 'label' => 'Jupitersystem'], - ['href' => 'tagbogen.php', 'label' => 'Tagbogen'], - ['href' => 'sternenhimmel.php', 'label' => 'Sternenhimmel'], - ['href' => 'drehbare_sternkarte.php', 'label' => 'Drehbare Sternkarte'], - ['href' => 'satellitenhimmel.php', 'label' => 'Satellitenhimmel'], - ['href' => 'meteorshowers.php', 'label' => 'Meteorströme'], - ['href' => 'asteroid_temp.php', 'label' => 'Asteroid-Temperatur'], - ['href' => 'geocron/index.php', 'label' => 'Geocron'], - ['href' => 'geocron3d.php', 'label' => 'Geocron 3D'], - ['href' => 'geocron3d_C.php', 'label' => 'Geocron 3D Cesium'], - ['href' => 'universesandbox.php', 'label' => 'Universe Sandbox'], + ['href' => 'moonphase.php', 'label' => 'Mondphase'], + ['href' => 'moonyear.php', 'label' => 'Mondphasen-Jahresübersicht'], + ['href' => 'mondfinsternis.php', 'label' => 'Mondfinsternis'], + ['href' => 'sonnenfinsternis.php', 'label' => 'Sonnenfinsternis'], + ['href' => 'bedeckung.php', 'label' => 'Bedeckungsphänomene'], + ['href' => 'sternbedeckungen.php', 'label' => 'Mond-Stern-Bedeckungen'], ], ], [ - 'title' => 'Finsternisse & Bedeckungen', + 'title' => 'Planeten & Sonnensystem', 'links' => [ - ['href' => 'mondfinsternis.php', 'label' => 'Mondfinsternis'], - ['href' => 'sonnenfinsternis.php', 'label' => 'Sonnenfinsternis'], - ['href' => 'bedeckung.php', 'label' => 'Bedeckungsphänomene'], - ['href' => 'sternbedeckungen.php', 'label' => 'Mond-Stern-Bedeckungen'], + ['href' => 'planetensichtbarkeit.php', 'label' => 'Planetensichtbarkeit'], + ['href' => 'ephemeriden.php', 'label' => 'Ephemeriden'], + ['href' => 'solarsystem.php', 'label' => 'Sonnensystem'], + ['href' => 'mars.php', 'label' => 'Mars'], + ['href' => 'jupitersystem.php', 'label' => 'Jupitersystem'], + ['href' => 'asteroid_temp.php', 'label' => 'Asteroid-Temperatur'], + ], + ], + [ + 'title' => 'Sternenhimmel & Karten', + 'links' => [ + ['href' => 'sternenhimmel.php', 'label' => 'Sternenhimmel'], + ['href' => 'drehbare_sternkarte.php', 'label' => 'Drehbare Sternkarte'], + ['href' => 'satellitenhimmel.php', 'label' => 'Satellitenhimmel'], + ['href' => 'tagbogen.php', 'label' => 'Tagbogen'], + ['href' => 'meteorshowers.php', 'label' => 'Meteorströme'], + ['href' => 'comets.php', 'label' => 'Kometen'], + ], + ], + [ + 'title' => 'Tools & Simulationen', + 'links' => [ + ['href' => 'tcrb_lightcurve.php', 'label' => 'T CrB Lichtkurve'], + ['href' => 'astronomical_conversions.php', 'label' => 'Astronomical Conversions'], + ['href' => 'geocron/index.php', 'label' => 'Geocron'], + ['href' => 'geocron3d.php', 'label' => 'Geocron 3D'], + ['href' => 'geocron3d_C.php', 'label' => 'Geocron 3D Cesium'], + ['href' => 'universesandbox.php', 'label' => 'Universe Sandbox'], ], ], ];