diff --git a/public/meteorshowers.php b/public/meteorshowers.php index 2ed0a91..efc44e4 100644 --- a/public/meteorshowers.php +++ b/public/meteorshowers.php @@ -4,94 +4,434 @@ declare(strict_types=1); session_start(); $loggedIn = isset($_SESSION['user_id']); -$pageTitle = 'Meteorstroeme - AstroTools'; -$bodyClass = 'meteorshowers-page'; +$pageTitle = 'Monatsvorhersage - AstroTools'; +$bodyClass = 'admin-page monatsvorhersage-page'; $publicBasePath = './'; -$locationIsGlobal = true; -$pdo = null; -$defaultLocation = [ - 'name' => 'Sternwarte Sonneberg', - 'latitude' => 50.3547, - 'longitude' => 11.1900, - 'elevation' => 640.0, - 'timezone' => 'Europe/Berlin', + +$currentYear = (int) date('Y'); +$selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear; +$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n'); +// ------------------------------------------------------------------------- +// Grenzwerte fuer Sternkarte und Objektanzeige +// ------------------------------------------------------------------------- +// Maximale Helligkeit (Magnitude) der Sterne, die aus der Datenbank geladen +// werden. Hoehere Werte laden mehr, aber auch schwaechere Sterne. +$starDatabaseMagnitudeLimit = 6.0; + +// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar +// gezeichnet werden sollen. +$starDisplayMagnitudeLimit = 4.4; + +// Sterne bis zu dieser Helligkeit erhalten auf der Sternkarte einen Namen. +$starLabelMagnitudeLimit = 2.0; + +// Maximale Helligkeit der Messier-Objekte, die aus der Datenbank geladen +// werden. +$messierDatabaseMagnitudeLimit = 2.0; + +// Messier-Objekte bis zu dieser Helligkeit werden auf der Monatskarte +// eingeblendet. +$messierDisplayMagnitudeLimit = 1.0; + +// ------------------------------------------------------------------------- +// Grenzwerte fuer Monatsereignisse +// ------------------------------------------------------------------------- +// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und Planeten. +$moonPlanetMaxSeparationDeg = 1.5; + +// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und ausgewaehlten +// Sternhaufen wie Plejaden oder Praesepe. +$moonDeepSkyMaxSeparationDeg = 1.0; + +// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und +// ausgewaehlten hellen Sternen wie Regulus oder Spica. +$planetBrightStarMaxSeparationDeg = 1.0; + +// Maximaler Winkelabstand fuer Planet-Planet-Konjunktionen. +$planetConjunctionMaxSeparationDeg = 1.0; + +// Nur Sterne bis zu dieser Helligkeit werden fuer Mondbedeckungen +// beruecksichtigt. +$moonOccultationMaxMag = 2.0; + +// ------------------------------------------------------------------------- +// Schwellen fuer "Gute Merkur-Sichtbarkeit" +// ------------------------------------------------------------------------- +// Gute Merkur-Sichtbarkeit wird nicht nur ueber Elongation definiert, sondern +// ueber echte Beobachtbarkeit in der Daemmerung. +// Geprueft wird pro Tag: +// morgens eine feste Zahl Minuten VOR Sonnenaufgang, +// abends eine feste Zahl Minuten NACH Sonnenuntergang. +$mercuryGoodVisibilitySampleOffsetMinutes = 45; + +// Merkur muss zum Pruefzeitpunkt mindestens diese Hoehe ueber dem Horizont +// erreichen, damit der Tag als gut sichtbar gilt. +$mercuryGoodVisibilityMinAltitudeDeg = 7.0; + +// Gleichzeitig muss die Sonne ausreichend tief unter dem Horizont stehen. +// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar waere, aber +// noch zu stark in der hellen Daemmerung untergeht. +$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0; + +// Erst ab dieser Anzahl aufeinanderfolgender guter Tage erzeugen wir ein +// Monatsereignis. Einzelne Ausreisser-Tage werden dadurch herausgefiltert. +$mercuryGoodVisibilityMinConsecutiveDays = 3; + +// ------------------------------------------------------------------------- +// Schwellen fuer "Planetenparade" +// ------------------------------------------------------------------------- +// Geprueft wird morgens eine feste Zahl Minuten VOR Sonnenaufgang und abends +// eine feste Zahl Minuten NACH Sonnenuntergang. +$planetParadeSampleOffsetMinutes = 45; + +// Ein Planet zaehlt fuer die Parade nur dann mit, wenn er zum Pruefzeitpunkt +// mindestens diese Hoehe ueber dem Horizont erreicht. +$planetParadeMinAltitudeDeg = 7.0; + +// Gleichzeitig muss die Sonne bereits tief genug unter dem Horizont stehen, +// damit die Parade nicht nur rechnerisch, sondern auch praktisch sichtbar ist. +$planetParadeMaxSunAltitudeDeg = -4.0; + +// Die beteiligten Planeten muessen innerhalb dieses Azimutbogens liegen. +// So vermeiden wir Treffer, bei denen die Planeten zu weit ueber den Himmel +// verteilt sind und nicht mehr als zusammenhaengende Parade wirken. +$planetParadeMaxAzimuthSpanDeg = 120.0; + +// Mindestens so viele helle Planeten muessen gleichzeitig sichtbar sein, +// damit wir von einer Planetenparade sprechen. +$planetParadeMinPlanetCount = 4; + +// Erst ab dieser Anzahl aufeinanderfolgender Tage erzeugen wir ein Ereignis. +// Das filtert kurzlebige Ein-Tages-Treffer heraus. +$planetParadeMinConsecutiveDays = 2; + +$years = range($currentYear - 2, $currentYear + 5); +$months = [ + 1 => 'Januar', + 2 => 'Februar', + 3 => 'Maerz', + 4 => 'April', + 5 => 'Mai', + 6 => 'Juni', + 7 => 'Juli', + 8 => 'August', + 9 => 'September', + 10 => 'Oktober', + 11 => 'November', + 12 => 'Dezember', +]; +$monthSkyTimes = [ + 1 => ['hour' => 22, 'minute' => 0], + 2 => ['hour' => 22, 'minute' => 0], + 3 => ['hour' => 22, 'minute' => 0], + 4 => ['hour' => 23, 'minute' => 0], + 5 => ['hour' => 23, 'minute' => 0], + 6 => ['hour' => 23, 'minute' => 30], + 7 => ['hour' => 23, 'minute' => 30], + 8 => ['hour' => 23, 'minute' => 0], + 9 => ['hour' => 23, 'minute' => 0], + 10 => ['hour' => 23, 'minute' => 0], + 11 => ['hour' => 22, 'minute' => 0], + 12 => ['hour' => 22, 'minute' => 0], ]; -$meteorRows = []; -$pageError = null; -function normalizeMeteorDegree(float $value): float -{ - $normalized = fmod($value, 360.0); - if ($normalized < 0.0) { - $normalized += 360.0; - } +$starData = []; +$lineStarData = []; +$constellationLines = []; +$messierData = []; +$planetData = []; +$moonData = null; +$moonPhaseEvents = []; +$goldenHandleEvents = []; +$moonPlanetApproaches = []; +$moonDeepSkyApproaches = []; +$planetBrightStarApproaches = []; +$mercuryGoodVisibilityEvents = []; +$planetParadeEvents = []; +$planetConstellationChangeEvents = []; +$planetConjunctions = []; +$eclipseEvents = []; +$moonApsisEvents = []; +$sunApsisEvents = []; +$innerPlanetElongationEvents = []; +$venusPeakMagnitudeEvents = []; +$outerPlanetEvents = []; +$outerPlanetStationEvents = []; +$jupiterMoonSideEvents = []; +$moonOccultations = []; +$meteorShowerPeaks = []; +$seasonChanges = []; +$timeChanges = []; +$monthEventList = []; +$starDataError = null; - return $normalized; -} - -function meteorSolarLongitude(DateTimeImmutable $dateTime): float -{ - $julianDay = ((float) $dateTime->format('U')) / 86400.0 + 2440587.5; - $daysSinceJ2000 = $julianDay - 2451545.0; - $meanLongitude = normalizeMeteorDegree(280.46 + 0.9856474 * $daysSinceJ2000); - $meanAnomalyRad = deg2rad(normalizeMeteorDegree(357.528 + 0.9856003 * $daysSinceJ2000)); - - return normalizeMeteorDegree( - $meanLongitude - + 1.915 * sin($meanAnomalyRad) - + 0.020 * sin(2.0 * $meanAnomalyRad) - ); -} - -function meteorForwardDegreeDistance(float $from, float $to): float -{ - return normalizeMeteorDegree($to - $from); -} - -function meteorRadiantIsVisible(float $latitudeDeg, ?float $declinationDeg): bool -{ - if ($declinationDeg === null) { - return false; - } - - return abs($latitudeDeg - $declinationDeg) < 90.0; -} - -function findMeteorSolarLongitudeCrossing( - DateTimeImmutable $rangeStartLocal, - DateTimeImmutable $rangeEndLocal, - float $targetLongitude, - DateTimeZone $displayTimezone -): ?DateTimeImmutable { - $utc = new DateTimeZone('UTC'); - $rangeStartUtc = $rangeStartLocal->setTimezone($utc); - $rangeEndUtc = $rangeEndLocal->setTimezone($utc); - - $startLongitude = meteorSolarLongitude($rangeStartUtc); - $endLongitude = meteorSolarLongitude($rangeEndUtc); - $targetDelta = meteorForwardDegreeDistance($startLongitude, $targetLongitude); - $rangeDelta = meteorForwardDegreeDistance($startLongitude, $endLongitude); - - if ($targetDelta > $rangeDelta + 0.0001) { - return null; - } - - $low = (int) $rangeStartUtc->format('U'); - $high = (int) $rangeEndUtc->format('U'); - - for ($i = 0; $i < 40; $i++) { - $mid = (int) floor(($low + $high) / 2); - $midLongitude = meteorSolarLongitude(new DateTimeImmutable('@' . $mid)); - $midDelta = meteorForwardDegreeDistance($startLongitude, $midLongitude); - - if ($midDelta < $targetDelta) { - $low = $mid; - } else { - $high = $mid; +if (!function_exists('buildMonthOccultationStarLabel')) { + function buildMonthOccultationStarLabel(array $row): string + { + $proper = trim((string) ($row['proper'] ?? '')); + if ($proper !== '') { + return $proper; } - } - return (new DateTimeImmutable('@' . $high))->setTimezone($displayTimezone); + $bayer = trim((string) ($row['bayer'] ?? '')); + $con = trim((string) ($row['con'] ?? '')); + if ($bayer !== '') { + return $con !== '' ? $bayer . ' ' . $con : $bayer; + } + + $hip = isset($row['hip']) ? (int) $row['hip'] : 0; + return $hip > 0 ? 'HIP ' . $hip : 'Unbenannter Stern'; + } +} + +if (!function_exists('normalizeMonthForecastDegree')) { + function normalizeMonthForecastDegree(float $value): float + { + $normalized = fmod($value, 360.0); + if ($normalized < 0.0) { + $normalized += 360.0; + } + + return $normalized; + } +} + +if (!function_exists('monthForecastSolarLongitude')) { + function monthForecastSolarLongitude(DateTimeImmutable $dateTime): float + { + $julianDay = ((float) $dateTime->format('U')) / 86400.0 + 2440587.5; + $daysSinceJ2000 = $julianDay - 2451545.0; + $meanLongitude = normalizeMonthForecastDegree(280.46 + 0.9856474 * $daysSinceJ2000); + $meanAnomalyRad = deg2rad(normalizeMonthForecastDegree(357.528 + 0.9856003 * $daysSinceJ2000)); + + return normalizeMonthForecastDegree( + $meanLongitude + + 1.915 * sin($meanAnomalyRad) + + 0.020 * sin(2.0 * $meanAnomalyRad) + ); + } +} + +if (!function_exists('monthForecastForwardDegreeDistance')) { + function monthForecastForwardDegreeDistance(float $from, float $to): float + { + return normalizeMonthForecastDegree($to - $from); + } +} + +if (!function_exists('monthForecastRadiantIsVisible')) { + function monthForecastRadiantIsVisible(float $latitudeDeg, ?float $declinationDeg): bool + { + if ($declinationDeg === null) { + return false; + } + + return abs($latitudeDeg - $declinationDeg) < 90.0; + } +} + +if (!function_exists('findMonthForecastSolarLongitudeCrossing')) { + function findMonthForecastSolarLongitudeCrossing( + DateTimeImmutable $rangeStartLocal, + DateTimeImmutable $rangeEndLocal, + float $targetLongitude, + DateTimeZone $displayTimezone + ): ?DateTimeImmutable { + $utc = new DateTimeZone('UTC'); + $rangeStartUtc = $rangeStartLocal->setTimezone($utc); + $rangeEndUtc = $rangeEndLocal->setTimezone($utc); + + $startLongitude = monthForecastSolarLongitude($rangeStartUtc); + $endLongitude = monthForecastSolarLongitude($rangeEndUtc); + $targetDelta = monthForecastForwardDegreeDistance($startLongitude, $targetLongitude); + $rangeDelta = monthForecastForwardDegreeDistance($startLongitude, $endLongitude); + + if ($targetDelta > $rangeDelta + 0.0001) { + return null; + } + + $low = (int) $rangeStartUtc->format('U'); + $high = (int) $rangeEndUtc->format('U'); + + for ($i = 0; $i < 40; $i++) { + $mid = (int) floor(($low + $high) / 2); + $midLongitude = monthForecastSolarLongitude(new DateTimeImmutable('@' . $mid)); + $midDelta = monthForecastForwardDegreeDistance($startLongitude, $midLongitude); + + if ($midDelta < $targetDelta) { + $low = $mid; + } else { + $high = $mid; + } + } + + return (new DateTimeImmutable('@' . $high))->setTimezone($displayTimezone); + } +} + +if (!function_exists('buildObservationTipInsertSql')) { + function buildObservationTipInsertSql(array $events, DateTimeZone $timezone): string + { + $rows = []; + + foreach ($events as $event) { + if (!is_array($event)) { + continue; + } + + $tip = trim((string) ($event['event'] ?? '')); + if ($tip === '') { + continue; + } + + $moment = null; + $localIso = trim((string) ($event['local_iso'] ?? '')); + if ($localIso !== '') { + try { + $moment = new DateTimeImmutable($localIso); + } catch (Throwable $e) { + $moment = null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + $date = trim((string) ($event['date'] ?? '')); + $time = trim((string) ($event['time'] ?? '')); + if ($date !== '' && $time !== '') { + $moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + continue; + } + + $rows[] = sprintf( + "('%s', '%s')", + $moment->setTimezone($timezone)->format('Y-m-d H:i:s'), + str_replace("'", "''", $tip) + ); + } + + if ($rows === []) { + return ''; + } + + return "INSERT INTO `astro_sonobs_observationtips` (`moment`, `tip`)\nVALUES\n " + . implode(",\n ", $rows) + . ";\n"; + } +} + +if (!function_exists('escapeIcalText')) { + function escapeIcalText(string $value): string + { + return str_replace( + ["\\", ";", ",", "\r\n", "\n", "\r"], + ["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"], + $value + ); + } +} + +if (!function_exists('foldIcalLine')) { + function foldIcalLine(string $line): string + { + $chunks = preg_split('/(.{1,73})/u', $line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($chunks === false || $chunks === []) { + return $line; + } + + $firstChunk = array_shift($chunks); + return $firstChunk . ($chunks ? "\r\n " . implode("\r\n ", $chunks) : ''); + } +} + +if (!function_exists('buildObservationTipCalendarIcs')) { + function buildObservationTipCalendarIcs(array $events, DateTimeZone $timezone): string + { + $lines = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//AstroTools//Monatsvorhersage//DE', + 'CALSCALE:GREGORIAN', + 'METHOD:PUBLISH', + 'X-WR-CALNAME:AstroTools Monatsereignisse', + ]; + + $createdUtc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Ymd\THis\Z'); + + foreach ($events as $index => $event) { + if (!is_array($event)) { + continue; + } + + $summary = trim((string) ($event['event'] ?? '')); + if ($summary === '') { + continue; + } + + $moment = null; + $localIso = trim((string) ($event['local_iso'] ?? '')); + if ($localIso !== '') { + try { + $moment = new DateTimeImmutable($localIso); + } catch (Throwable $e) { + $moment = null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + $date = trim((string) ($event['date'] ?? '')); + $time = trim((string) ($event['time'] ?? '')); + if ($date !== '' && $time !== '') { + $moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + continue; + } + + $startUtc = $moment->setTimezone(new DateTimeZone('UTC')); + $endMoment = null; + $endLocalIso = trim((string) ($event['end_local_iso'] ?? '')); + if ($endLocalIso !== '') { + try { + $endMoment = new DateTimeImmutable($endLocalIso); + } catch (Throwable $e) { + $endMoment = null; + } + } + + if (!$endMoment instanceof DateTimeImmutable) { + $durationMinutes = isset($event['duration_minutes']) ? max(1, (int) $event['duration_minutes']) : 15; + $endMoment = $moment->modify('+' . $durationMinutes . ' minutes'); + } + + $endUtc = $endMoment->setTimezone(new DateTimeZone('UTC')); + $uidBase = sha1($startUtc->format(DateTimeInterface::ATOM) . '|' . $summary . '|' . (string) $index); + + $eventLines = [ + 'BEGIN:VEVENT', + 'UID:' . $uidBase . '@astrotools.local', + 'DTSTAMP:' . $createdUtc, + 'DTSTART:' . $startUtc->format('Ymd\THis\Z'), + 'DTEND:' . $endUtc->format('Ymd\THis\Z'), + 'SUMMARY:' . escapeIcalText($summary), + 'DESCRIPTION:' . escapeIcalText($summary), + 'END:VEVENT', + ]; + + foreach ($eventLines as $eventLine) { + $lines[] = foldIcalLine($eventLine); + } + } + + $lines[] = 'END:VCALENDAR'; + + return implode("\r\n", $lines) . "\r\n"; + } } try { @@ -102,163 +442,1295 @@ try { $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, - ] + $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + + $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) $starDatabaseMagnitudeLimit . " + ORDER BY mag ASC" ); - if ($loggedIn && isset($_SESSION['user_id'])) { - $stmtLocation = $pdo->prepare( - 'SELECT id, name, latitude, longitude, elevation, timezone - FROM app_user_locations - WHERE user_id = ? AND is_default = 1 - LIMIT 1' - ); - $stmtLocation->execute([(int) $_SESSION['user_id']]); - $userLocation = $stmtLocation->fetch(); - if (is_array($userLocation)) { - $defaultLocation = $userLocation; - $locationIsGlobal = false; - } + while ($row = $starStmt->fetch()) { + $starData[] = [ + '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, + ]; } - $stmt = $pdo->query( + $constellationIndex = []; + $ensureConstellation = static function (string $id) use (&$constellationIndex): void { + if (!isset($constellationIndex[$id])) { + $constellationIndex[$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) { + $constellationId = (string) $row['constellation_id']; + $ensureConstellation($constellationId); + $constellationIndex[$constellationId]['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) { + $constellationId = (string) $row['constellation_id']; + $ensureConstellation($constellationId); + $constellationIndex[$constellationId]['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) { + $constellationId = (string) $row['constellation_id']; + $lineIndex = (int) $row['line_index']; + + $ensureConstellation($constellationId); + + $constellationIndex[$constellationId]['lines'][$lineIndex] ??= []; + $constellationIndex[$constellationId]['lines'][$lineIndex][] = (int) $row['hip_number']; + } + + foreach ($constellationIndex as &$constellation) { + if ($constellation['lines']) { + ksort($constellation['lines']); + $constellation['lines'] = array_values($constellation['lines']); + } + } + unset($constellation); + + $constellationLines = array_values($constellationIndex); + + $lineStarStmt = $pdo->query( + "SELECT DISTINCT sh.hip, sh.proper, sh.bayer, sh.con, sh.spect, sh.mag, sh.ci, sh.rarad, sh.decrad + FROM constellation_line_points clp + INNER JOIN star_hipparcos sh ON sh.hip = clp.hip_number + WHERE sh.rarad IS NOT NULL + AND sh.decrad IS NOT NULL" + ); + + while ($row = $lineStarStmt->fetch()) { + $lineStarData[] = [ + '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' => $row['mag'] !== null ? (float) $row['mag'] : 99.0, + 'ci' => $row['ci'] !== null ? (float) $row['ci'] : null, + ]; + } + + $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 + AND v_mag IS NOT NULL + AND v_mag <= " . (float) $messierDatabaseMagnitudeLimit . " + ORDER BY v_mag ASC, CAST(REGEXP_REPLACE(messier, '[^0-9]', '') AS UNSIGNED)" + ); + + while ($row = $messierStmt->fetch()) { + if (!preg_match('/(\d+)/', (string) $row['messier'], $match)) { + continue; + } + + $messierData[] = [ + 'm' => (int) $match[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' => (float) $row['v_mag'], + 'size' => $row['major_axis'] !== null ? (float) $row['major_axis'] : 8.0, + ]; + } + + $meteorStmt = $pdo->query( "SELECT designation, shower_code, radiant_delta_deg, peak_sol, zhr, zhr_variable FROM meteorshowers WHERE peak_sol IS NOT NULL ORDER BY peak_sol ASC, designation ASC" ); - $meteorRows = $stmt->fetchAll(); + + while ($row = $meteorStmt->fetch()) { + $meteorShowerPeaks[] = [ + 'designation' => trim((string) ($row['designation'] ?? '')), + 'shower_code' => trim((string) ($row['shower_code'] ?? '')), + 'radiant_delta_deg' => $row['radiant_delta_deg'] !== null ? (float) $row['radiant_delta_deg'] : null, + 'peak_sol' => (float) $row['peak_sol'], + 'zhr' => $row['zhr'] !== null ? (int) $row['zhr'] : null, + 'zhr_variable' => trim((string) ($row['zhr_variable'] ?? '')), + ]; + } } catch (Throwable $e) { - $pageError = $e->getMessage(); + $starDataError = $e->getMessage(); } require __DIR__ . '/header.php'; -$displayTimezoneName = (string) (($currentLocation['timezone'] ?? null) ?: ($defaultLocation['timezone'] ?? 'Europe/Berlin')); -$displayTimezone = new DateTimeZone($displayTimezoneName); -$displayLocationName = (string) (($currentLocation['name'] ?? null) ?: ($defaultLocation['name'] ?? 'Sternwarte Sonneberg')); -$displayLatitude = isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : (float) $defaultLocation['latitude']; -$currentYear = (int) (new DateTimeImmutable('now', $displayTimezone))->format('Y'); -$yearStart = new DateTimeImmutable(sprintf('%04d-01-01 00:00:00', $currentYear), $displayTimezone); -$yearEnd = $yearStart->modify('+1 year'); -$meteorEvents = []; +$chartLocationName = (string) (($currentLocation['name'] ?? null) ?: 'Sternwarte Sonneberg'); +$chartLatitude = isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : 50.3547; +$chartLongitude = isset($currentLocation['longitude']) ? (float) $currentLocation['longitude'] : 11.1900; +$chartElevation = isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float) $currentLocation['elevation'] : 640.0; +$chartTimezone = (string) (($currentLocation['timezone'] ?? null) ?: 'Europe/Berlin'); -foreach ($meteorRows as $row) { - if (!is_array($row) || !isset($row['peak_sol'])) { +$monthTime = $monthSkyTimes[$selectedMonth]; +$chartLocalTime = new DateTimeImmutable( + sprintf('%04d-%02d-15 %02d:%02d:00', $selectedYear, $selectedMonth, $monthTime['hour'], $monthTime['minute']), + new DateTimeZone($chartTimezone) +); +$chartUtcTime = $chartLocalTime->setTimezone(new DateTimeZone('UTC')); + +$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python'; +$pythonScriptPath = __DIR__ . '/py/api.py'; +$pythonOutput = []; +$pythonExitCode = 0; +$planetErrorText = null; + +$pythonArgs = [ + $pythonScriptPath, + 'month_sky_context', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + (string) $monthTime['hour'], + (string) $monthTime['minute'], + $chartTimezone, +]; + +$pythonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1'; +exec($pythonCommand, $pythonOutput, $pythonExitCode); +$pythonRaw = trim(implode("\n", $pythonOutput)); +$pythonDecoded = json_decode($pythonRaw, true); + +if (is_array($pythonDecoded) && ($pythonDecoded['ok'] ?? false)) { + if (is_array($pythonDecoded['bodies']['moon'] ?? null)) { + $moon = $pythonDecoded['bodies']['moon']; + $moonData = [ + 'label' => (string) ($moon['label'] ?? 'Mond'), + 'az' => isset($moon['azimuth_deg']) ? (float) $moon['azimuth_deg'] : null, + 'alt' => isset($moon['altitude_deg']) ? (float) $moon['altitude_deg'] : null, + 'visible' => !empty($moon['visible']), + 'phaseFraction' => isset($moon['phase_fraction']) ? (float) $moon['phase_fraction'] : null, + 'waxing' => isset($moon['waxing']) ? (bool) $moon['waxing'] : null, + ]; + } + foreach (($pythonDecoded['bodies']['planets'] ?? []) as $planet) { + if (!is_array($planet)) { + continue; + } + $planetData[] = [ + 'key' => (string) ($planet['key'] ?? ''), + 'label' => (string) ($planet['label'] ?? ''), + 'az' => isset($planet['azimuth_deg']) ? (float) $planet['azimuth_deg'] : null, + 'alt' => isset($planet['altitude_deg']) ? (float) $planet['altitude_deg'] : null, + 'mag' => isset($planet['magnitude']) ? (float) $planet['magnitude'] : null, + 'visible' => !empty($planet['visible']), + ]; + } +} elseif ($pythonRaw !== '') { + $planetErrorText = $pythonRaw; +} + +$moonPhaseOutput = []; +$moonPhaseExitCode = 0; +$moonPhaseArgs = [ + $pythonScriptPath, + 'moon_phases_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$moonPhaseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPhaseArgs)) . ' 2>&1'; +exec($moonPhaseCommand, $moonPhaseOutput, $moonPhaseExitCode); +$moonPhaseRaw = trim(implode("\n", $moonPhaseOutput)); +$moonPhaseDecoded = json_decode($moonPhaseRaw, true); +if (is_array($moonPhaseDecoded) && ($moonPhaseDecoded['ok'] ?? false)) { + $moonPhaseEvents = is_array($moonPhaseDecoded['phases'] ?? null) ? $moonPhaseDecoded['phases'] : []; +} + +$goldenHandleOutput = []; +$goldenHandleExitCode = 0; +$goldenHandleArgs = [ + $pythonScriptPath, + 'golden_handle_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$goldenHandleCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $goldenHandleArgs)) . ' 2>&1'; +exec($goldenHandleCommand, $goldenHandleOutput, $goldenHandleExitCode); +$goldenHandleRaw = trim(implode("\n", $goldenHandleOutput)); +$goldenHandleDecoded = json_decode($goldenHandleRaw, true); +if (is_array($goldenHandleDecoded) && ($goldenHandleDecoded['ok'] ?? false)) { + $goldenHandleEvents = is_array($goldenHandleDecoded['events'] ?? null) ? $goldenHandleDecoded['events'] : []; +} + +$seasonOutput = []; +$seasonExitCode = 0; +$seasonArgs = [ + $pythonScriptPath, + 'season_changes_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$seasonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $seasonArgs)) . ' 2>&1'; +exec($seasonCommand, $seasonOutput, $seasonExitCode); +$seasonRaw = trim(implode("\n", $seasonOutput)); +$seasonDecoded = json_decode($seasonRaw, true); +if (is_array($seasonDecoded) && ($seasonDecoded['ok'] ?? false)) { + $seasonChanges = is_array($seasonDecoded['changes'] ?? null) ? $seasonDecoded['changes'] : []; +} + +$timeChangeOutput = []; +$timeChangeExitCode = 0; +$timeChangeArgs = [ + $pythonScriptPath, + 'time_changes_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$timeChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $timeChangeArgs)) . ' 2>&1'; +exec($timeChangeCommand, $timeChangeOutput, $timeChangeExitCode); +$timeChangeRaw = trim(implode("\n", $timeChangeOutput)); +$timeChangeDecoded = json_decode($timeChangeRaw, true); +if (is_array($timeChangeDecoded) && ($timeChangeDecoded['ok'] ?? false)) { + $timeChanges = is_array($timeChangeDecoded['events'] ?? null) ? $timeChangeDecoded['events'] : []; +} + +$eclipseOutput = []; +$eclipseExitCode = 0; +$eclipseArgs = [ + $pythonScriptPath, + 'eclipses_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$eclipseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $eclipseArgs)) . ' 2>&1'; +exec($eclipseCommand, $eclipseOutput, $eclipseExitCode); +$eclipseRaw = trim(implode("\n", $eclipseOutput)); +$eclipseDecoded = json_decode($eclipseRaw, true); +if (is_array($eclipseDecoded) && ($eclipseDecoded['ok'] ?? false)) { + $eclipseEvents = is_array($eclipseDecoded['events'] ?? null) ? $eclipseDecoded['events'] : []; +} + +$moonApsisOutput = []; +$moonApsisExitCode = 0; +$moonApsisArgs = [ + $pythonScriptPath, + 'moon_apsides_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$moonApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonApsisArgs)) . ' 2>&1'; +exec($moonApsisCommand, $moonApsisOutput, $moonApsisExitCode); +$moonApsisRaw = trim(implode("\n", $moonApsisOutput)); +$moonApsisDecoded = json_decode($moonApsisRaw, true); +if (is_array($moonApsisDecoded) && ($moonApsisDecoded['ok'] ?? false)) { + $moonApsisEvents = is_array($moonApsisDecoded['events'] ?? null) ? $moonApsisDecoded['events'] : []; +} + +$sunApsisOutput = []; +$sunApsisExitCode = 0; +$sunApsisArgs = [ + $pythonScriptPath, + 'sun_apsides_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$sunApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $sunApsisArgs)) . ' 2>&1'; +exec($sunApsisCommand, $sunApsisOutput, $sunApsisExitCode); +$sunApsisRaw = trim(implode("\n", $sunApsisOutput)); +$sunApsisDecoded = json_decode($sunApsisRaw, true); +if (is_array($sunApsisDecoded) && ($sunApsisDecoded['ok'] ?? false)) { + $sunApsisEvents = is_array($sunApsisDecoded['events'] ?? null) ? $sunApsisDecoded['events'] : []; +} + +$elongationOutput = []; +$elongationExitCode = 0; +$elongationArgs = [ + $pythonScriptPath, + 'inner_planet_elongations_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$elongationCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $elongationArgs)) . ' 2>&1'; +exec($elongationCommand, $elongationOutput, $elongationExitCode); +$elongationRaw = trim(implode("\n", $elongationOutput)); +$elongationDecoded = json_decode($elongationRaw, true); +if (is_array($elongationDecoded) && ($elongationDecoded['ok'] ?? false)) { + $innerPlanetElongationEvents = is_array($elongationDecoded['events'] ?? null) ? $elongationDecoded['events'] : []; +} + +$venusPeakMagnitudeOutput = []; +$venusPeakMagnitudeExitCode = 0; +$venusPeakMagnitudeArgs = [ + $pythonScriptPath, + 'venus_peak_magnitude_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$venusPeakMagnitudeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $venusPeakMagnitudeArgs)) . ' 2>&1'; +exec($venusPeakMagnitudeCommand, $venusPeakMagnitudeOutput, $venusPeakMagnitudeExitCode); +$venusPeakMagnitudeRaw = trim(implode("\n", $venusPeakMagnitudeOutput)); +$venusPeakMagnitudeDecoded = json_decode($venusPeakMagnitudeRaw, true); +if (is_array($venusPeakMagnitudeDecoded) && ($venusPeakMagnitudeDecoded['ok'] ?? false)) { + $venusPeakMagnitudeEvents = is_array($venusPeakMagnitudeDecoded['events'] ?? null) ? $venusPeakMagnitudeDecoded['events'] : []; +} + +$outerPlanetOutput = []; +$outerPlanetExitCode = 0; +$outerPlanetArgs = [ + $pythonScriptPath, + 'outer_planet_events_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$outerPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $outerPlanetArgs)) . ' 2>&1'; +exec($outerPlanetCommand, $outerPlanetOutput, $outerPlanetExitCode); +$outerPlanetRaw = trim(implode("\n", $outerPlanetOutput)); +$outerPlanetDecoded = json_decode($outerPlanetRaw, true); +if (is_array($outerPlanetDecoded) && ($outerPlanetDecoded['ok'] ?? false)) { + $outerPlanetEvents = is_array($outerPlanetDecoded['events'] ?? null) ? $outerPlanetDecoded['events'] : []; +} + +$outerPlanetStationOutput = []; +$outerPlanetStationExitCode = 0; +$outerPlanetStationArgs = [ + $pythonScriptPath, + 'outer_planet_stations_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$outerPlanetStationCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $outerPlanetStationArgs)) . ' 2>&1'; +exec($outerPlanetStationCommand, $outerPlanetStationOutput, $outerPlanetStationExitCode); +$outerPlanetStationRaw = trim(implode("\n", $outerPlanetStationOutput)); +$outerPlanetStationDecoded = json_decode($outerPlanetStationRaw, true); +if (is_array($outerPlanetStationDecoded) && ($outerPlanetStationDecoded['ok'] ?? false)) { + $outerPlanetStationEvents = is_array($outerPlanetStationDecoded['events'] ?? null) ? $outerPlanetStationDecoded['events'] : []; +} + +$jupiterMoonSideOutput = []; +$jupiterMoonSideExitCode = 0; +$jupiterMoonSideArgs = [ + $pythonScriptPath, + 'jupiter_moons_one_side_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$jupiterMoonSideCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $jupiterMoonSideArgs)) . ' 2>&1'; +exec($jupiterMoonSideCommand, $jupiterMoonSideOutput, $jupiterMoonSideExitCode); +$jupiterMoonSideRaw = trim(implode("\n", $jupiterMoonSideOutput)); +$jupiterMoonSideDecoded = json_decode($jupiterMoonSideRaw, true); +if (is_array($jupiterMoonSideDecoded) && ($jupiterMoonSideDecoded['ok'] ?? false)) { + $jupiterMoonSideEvents = is_array($jupiterMoonSideDecoded['events'] ?? null) ? $jupiterMoonSideDecoded['events'] : []; +} + +$moonPlanetOutput = []; +$moonPlanetExitCode = 0; +$moonPlanetArgs = [ + $pythonScriptPath, + 'moon_planet_approaches', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $moonPlanetMaxSeparationDeg, +]; +$moonPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPlanetArgs)) . ' 2>&1'; +exec($moonPlanetCommand, $moonPlanetOutput, $moonPlanetExitCode); +$moonPlanetRaw = trim(implode("\n", $moonPlanetOutput)); +$moonPlanetDecoded = json_decode($moonPlanetRaw, true); +if (is_array($moonPlanetDecoded) && ($moonPlanetDecoded['ok'] ?? false)) { + $moonPlanetApproaches = is_array($moonPlanetDecoded['approaches'] ?? null) ? $moonPlanetDecoded['approaches'] : []; +} + +$moonDeepSkyOutput = []; +$moonDeepSkyExitCode = 0; +$moonDeepSkyArgs = [ + $pythonScriptPath, + 'moon_deep_sky_approaches_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $moonDeepSkyMaxSeparationDeg, +]; +$moonDeepSkyCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonDeepSkyArgs)) . ' 2>&1'; +exec($moonDeepSkyCommand, $moonDeepSkyOutput, $moonDeepSkyExitCode); +$moonDeepSkyRaw = trim(implode("\n", $moonDeepSkyOutput)); +$moonDeepSkyDecoded = json_decode($moonDeepSkyRaw, true); +if (is_array($moonDeepSkyDecoded) && ($moonDeepSkyDecoded['ok'] ?? false)) { + $moonDeepSkyApproaches = is_array($moonDeepSkyDecoded['approaches'] ?? null) ? $moonDeepSkyDecoded['approaches'] : []; +} + +$planetBrightStarOutput = []; +$planetBrightStarExitCode = 0; +$planetBrightStarArgs = [ + $pythonScriptPath, + 'planet_bright_star_approaches_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $planetBrightStarMaxSeparationDeg, +]; +$planetBrightStarCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetBrightStarArgs)) . ' 2>&1'; +exec($planetBrightStarCommand, $planetBrightStarOutput, $planetBrightStarExitCode); +$planetBrightStarRaw = trim(implode("\n", $planetBrightStarOutput)); +$planetBrightStarDecoded = json_decode($planetBrightStarRaw, true); +if (is_array($planetBrightStarDecoded) && ($planetBrightStarDecoded['ok'] ?? false)) { + $planetBrightStarApproaches = is_array($planetBrightStarDecoded['approaches'] ?? null) ? $planetBrightStarDecoded['approaches'] : []; +} + +$planetConjunctionOutput = []; +$planetConjunctionExitCode = 0; +$planetConjunctionArgs = [ + $pythonScriptPath, + 'planet_conjunctions_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $planetConjunctionMaxSeparationDeg, +]; +$planetConjunctionCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConjunctionArgs)) . ' 2>&1'; +exec($planetConjunctionCommand, $planetConjunctionOutput, $planetConjunctionExitCode); +$planetConjunctionRaw = trim(implode("\n", $planetConjunctionOutput)); +$planetConjunctionDecoded = json_decode($planetConjunctionRaw, true); +if (is_array($planetConjunctionDecoded) && ($planetConjunctionDecoded['ok'] ?? false)) { + $planetConjunctions = is_array($planetConjunctionDecoded['conjunctions'] ?? null) ? $planetConjunctionDecoded['conjunctions'] : []; +} + +$mercuryGoodVisibilityOutput = []; +$mercuryGoodVisibilityExitCode = 0; +$mercuryGoodVisibilityArgs = [ + $pythonScriptPath, + 'mercury_good_visibility_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $mercuryGoodVisibilitySampleOffsetMinutes, + (string) $mercuryGoodVisibilityMinAltitudeDeg, + (string) $mercuryGoodVisibilityMaxSunAltitudeDeg, + (string) $mercuryGoodVisibilityMinConsecutiveDays, +]; +$mercuryGoodVisibilityCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $mercuryGoodVisibilityArgs)) . ' 2>&1'; +exec($mercuryGoodVisibilityCommand, $mercuryGoodVisibilityOutput, $mercuryGoodVisibilityExitCode); +$mercuryGoodVisibilityRaw = trim(implode("\n", $mercuryGoodVisibilityOutput)); +$mercuryGoodVisibilityDecoded = json_decode($mercuryGoodVisibilityRaw, true); +if (is_array($mercuryGoodVisibilityDecoded) && ($mercuryGoodVisibilityDecoded['ok'] ?? false)) { + $mercuryGoodVisibilityEvents = is_array($mercuryGoodVisibilityDecoded['events'] ?? null) ? $mercuryGoodVisibilityDecoded['events'] : []; +} + +$planetParadeOutput = []; +$planetParadeExitCode = 0; +$planetParadeArgs = [ + $pythonScriptPath, + 'planet_parades_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $planetParadeSampleOffsetMinutes, + (string) $planetParadeMinAltitudeDeg, + (string) $planetParadeMaxSunAltitudeDeg, + (string) $planetParadeMaxAzimuthSpanDeg, + (string) $planetParadeMinPlanetCount, + (string) $planetParadeMinConsecutiveDays, +]; +$planetParadeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetParadeArgs)) . ' 2>&1'; +exec($planetParadeCommand, $planetParadeOutput, $planetParadeExitCode); +$planetParadeRaw = trim(implode("\n", $planetParadeOutput)); +$planetParadeDecoded = json_decode($planetParadeRaw, true); +if (is_array($planetParadeDecoded) && ($planetParadeDecoded['ok'] ?? false)) { + $planetParadeEvents = is_array($planetParadeDecoded['events'] ?? null) ? $planetParadeDecoded['events'] : []; +} + +$planetConstellationChangeOutput = []; +$planetConstellationChangeExitCode = 0; +$planetConstellationChangeArgs = [ + $pythonScriptPath, + 'planet_constellation_changes_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$planetConstellationChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConstellationChangeArgs)) . ' 2>&1'; +exec($planetConstellationChangeCommand, $planetConstellationChangeOutput, $planetConstellationChangeExitCode); +$planetConstellationChangeRaw = trim(implode("\n", $planetConstellationChangeOutput)); +$planetConstellationChangeDecoded = json_decode($planetConstellationChangeRaw, true); +if (is_array($planetConstellationChangeDecoded) && ($planetConstellationChangeDecoded['ok'] ?? false)) { + $planetConstellationChangeEvents = is_array($planetConstellationChangeDecoded['events'] ?? null) ? $planetConstellationChangeDecoded['events'] : []; +} + +$moonOccultationStars = []; +try { + if (isset($pdo)) { + $stmtOccultStars = $pdo->prepare( + "SELECT hip, proper, bayer, con, mag, rarad, decrad, dist + FROM star_hipparcos + WHERE rarad IS NOT NULL + AND decrad IS NOT NULL + AND mag IS NOT NULL + AND mag <= :max_mag + ORDER BY mag ASC, proper ASC, bayer ASC, hip ASC" + ); + $stmtOccultStars->execute([':max_mag' => $moonOccultationMaxMag]); + + foreach ($stmtOccultStars->fetchAll() as $row) { + $moonOccultationStars[] = [ + 'hip' => isset($row['hip']) ? (int) $row['hip'] : 0, + 'label' => buildMonthOccultationStarLabel($row), + 'constellation' => trim((string) ($row['con'] ?? '')), + 'mag' => isset($row['mag']) ? (float) $row['mag'] : 99.0, + 'ra' => isset($row['rarad']) ? (float) $row['rarad'] * (12 / M_PI) : 0.0, + 'dec' => isset($row['decrad']) ? (float) $row['decrad'] * (180 / M_PI) : 0.0, + 'distLy' => isset($row['dist']) && $row['dist'] !== null && (float) $row['dist'] > 0.0 + ? (float) $row['dist'] * 3.26156 + : 1000.0, + ]; + } + } +} catch (Throwable $e) { +} + +if ($moonOccultationStars) { + $tempOccultationFile = tempnam(sys_get_temp_dir(), 'month_occult_'); + if ($tempOccultationFile !== false) { + try { + file_put_contents($tempOccultationFile, json_encode($moonOccultationStars, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); + $moonOccultOutput = []; + $moonOccultExitCode = 0; + $moonOccultArgs = [ + $pythonScriptPath, + 'moon_star_occultations_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + $tempOccultationFile, + ]; + $moonOccultCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonOccultArgs)) . ' 2>&1'; + exec($moonOccultCommand, $moonOccultOutput, $moonOccultExitCode); + $moonOccultRaw = trim(implode("\n", $moonOccultOutput)); + $moonOccultDecoded = json_decode($moonOccultRaw, true); + if (is_array($moonOccultDecoded) && ($moonOccultDecoded['ok'] ?? false)) { + $moonOccultations = is_array($moonOccultDecoded['results'] ?? null) ? $moonOccultDecoded['results'] : []; + } + } catch (Throwable $e) { + } finally { + if (is_file($tempOccultationFile)) { + @unlink($tempOccultationFile); + } + } + } +} + +foreach ($moonPhaseEvents as $phaseEvent) { + if (!is_array($phaseEvent)) { continue; } - $peakDateTime = findMeteorSolarLongitudeCrossing( - $yearStart, - $yearEnd, - (float) $row['peak_sol'], - $displayTimezone + $monthEventList[] = [ + 'event' => (string) ($phaseEvent['label'] ?? ''), + 'date' => (string) ($phaseEvent['local_date'] ?? ''), + 'time' => (string) ($phaseEvent['local_time'] ?? ''), + 'local_iso' => (string) ($phaseEvent['local_iso'] ?? ''), + 'type' => 'moon_phase', + ]; +} + +foreach ($goldenHandleEvents as $goldenHandleEvent) { + if (!is_array($goldenHandleEvent)) { + continue; + } + + $peakDate = (string) ($goldenHandleEvent['peak_local_date'] ?? ''); + $peakTime = (string) ($goldenHandleEvent['peak_local_time'] ?? ''); + $goldenHandleLabel = 'Goldener Henkel am Mond'; + + $monthEventList[] = [ + 'event' => $goldenHandleLabel, + 'date' => $peakDate !== '' ? $peakDate : (string) ($goldenHandleEvent['local_date'] ?? ''), + 'time' => $peakTime !== '' ? $peakTime : (string) ($goldenHandleEvent['local_time'] ?? ''), + 'local_iso' => (string) ($goldenHandleEvent['peak_local_iso'] ?? $goldenHandleEvent['local_iso'] ?? ''), + 'type' => 'golden_handle', + ]; +} + +foreach ($seasonChanges as $seasonChange) { + if (!is_array($seasonChange)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($seasonChange['label'] ?? 'Jahreszeitenwechsel'), + 'date' => (string) ($seasonChange['local_date'] ?? ''), + 'time' => (string) ($seasonChange['local_time'] ?? ''), + 'local_iso' => (string) ($seasonChange['local_iso'] ?? ''), + 'type' => 'season_change', + ]; +} + +foreach ($timeChanges as $timeChange) { + if (!is_array($timeChange)) { + continue; + } + + $timeChangeKind = (string) ($timeChange['kind'] ?? ''); + $timeChangeLabel = $timeChangeKind === 'dst_start' + ? 'Beginn der Sommerzeit (Uhren von 02:00 auf 03:00 Uhr vorstellen)' + : ($timeChangeKind === 'dst_end' + ? 'Beginn der Normalzeit (Uhren von 03:00 auf 02:00 Uhr zurueckstellen)' + : 'Zeitumstellung'); + $timeChangeTime = $timeChangeKind === 'dst_start' + ? '02:00' + : ($timeChangeKind === 'dst_end' ? '03:00' : (string) ($timeChange['local_time'] ?? '')); + + $monthEventList[] = [ + 'event' => $timeChangeLabel, + 'date' => (string) ($timeChange['local_date'] ?? ''), + 'time' => $timeChangeTime, + 'local_iso' => (string) ($timeChange['local_iso'] ?? ''), + 'type' => 'time_change', + ]; +} + +foreach ($eclipseEvents as $eclipseEvent) { + if (!is_array($eclipseEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($eclipseEvent['label'] ?? 'Finsternis'), + 'date' => (string) ($eclipseEvent['local_date'] ?? ''), + 'time' => (string) ($eclipseEvent['local_time'] ?? ''), + 'local_iso' => (string) ($eclipseEvent['local_iso'] ?? ''), + 'type' => (string) ($eclipseEvent['category'] ?? 'eclipse'), + ]; +} + +foreach ($moonApsisEvents as $moonApsisEvent) { + if (!is_array($moonApsisEvent)) { + continue; + } + + $moonApsisKind = (string) ($moonApsisEvent['kind'] ?? ''); + $distanceText = isset($moonApsisEvent['distance_km']) + ? number_format((float) $moonApsisEvent['distance_km'], 0, ',', '.') . ' km' + : ''; + $moonApsisLabel = $moonApsisKind === 'perigee' + ? 'Mond in Erdnähe (Perigäum)' + : ($moonApsisKind === 'apogee' ? 'Mond in Erdferne (Apogäum)' : (string) ($moonApsisEvent['label'] ?? 'Mondabstand')); + + $monthEventList[] = [ + 'event' => trim($moonApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($moonApsisEvent['local_date'] ?? ''), + 'time' => (string) ($moonApsisEvent['local_time'] ?? ''), + 'local_iso' => (string) ($moonApsisEvent['local_iso'] ?? ''), + 'type' => 'moon_apsis', + ]; +} + +foreach ($sunApsisEvents as $sunApsisEvent) { + if (!is_array($sunApsisEvent)) { + continue; + } + + $sunApsisKind = (string) ($sunApsisEvent['kind'] ?? ''); + $distanceText = isset($sunApsisEvent['distance_km']) + ? number_format((float) $sunApsisEvent['distance_km'], 0, ',', '.') . ' km' + : ''; + $sunApsisLabel = $sunApsisKind === 'perihelion' + ? 'Erde erreicht sonnennächsten Punkt (Perihel)' + : ($sunApsisKind === 'aphelion' ? 'Erde erreicht sonnenfernsten Punkt (Aphel)' : (string) ($sunApsisEvent['label'] ?? 'Sonnenabstand')); + + $monthEventList[] = [ + 'event' => trim($sunApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($sunApsisEvent['local_date'] ?? ''), + 'time' => (string) ($sunApsisEvent['local_time'] ?? ''), + 'local_iso' => (string) ($sunApsisEvent['local_iso'] ?? ''), + 'type' => 'sun_apsis', + ]; +} + +foreach ($innerPlanetElongationEvents as $elongationEvent) { + if (!is_array($elongationEvent)) { + continue; + } + + $elongationPlanet = (string) ($elongationEvent['planet_label'] ?? 'Planet'); + $elongationKind = (string) ($elongationEvent['kind'] ?? ''); + $elongationText = isset($elongationEvent['elongation_deg']) + ? number_format((float) $elongationEvent['elongation_deg'], 2, ',', '') . ' Grad' + : ''; + $elongationLabel = $elongationKind === 'western' + ? $elongationPlanet . ' größte westliche Elongation' + : ($elongationKind === 'eastern' + ? $elongationPlanet . ' größte östliche Elongation' + : (string) ($elongationEvent['label'] ?? 'Elongation')); + + $monthEventList[] = [ + 'event' => trim($elongationLabel . ($elongationText !== '' ? ' (' . $elongationText . ')' : '')), + 'date' => (string) ($elongationEvent['local_date'] ?? ''), + 'time' => (string) ($elongationEvent['local_time'] ?? ''), + 'local_iso' => (string) ($elongationEvent['local_iso'] ?? ''), + 'type' => 'planet_elongation', + ]; +} + +foreach ($venusPeakMagnitudeEvents as $venusPeakMagnitudeEvent) { + if (!is_array($venusPeakMagnitudeEvent)) { + continue; + } + + $magnitudeText = isset($venusPeakMagnitudeEvent['mag']) + ? number_format((float) $venusPeakMagnitudeEvent['mag'], 1, ',', '') . ' mag' + : ''; + + $monthEventList[] = [ + 'event' => trim('Venus in groesster Helligkeit' . ($magnitudeText !== '' ? ' (' . $magnitudeText . ')' : '')), + 'date' => (string) ($venusPeakMagnitudeEvent['local_date'] ?? ''), + 'time' => (string) ($venusPeakMagnitudeEvent['local_time'] ?? ''), + 'local_iso' => (string) ($venusPeakMagnitudeEvent['local_iso'] ?? ''), + 'type' => 'venus_peak_magnitude', + ]; +} + +foreach ($outerPlanetEvents as $outerPlanetEvent) { + if (!is_array($outerPlanetEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($outerPlanetEvent['label'] ?? 'Planetenereignis'), + 'date' => (string) ($outerPlanetEvent['local_date'] ?? ''), + 'time' => (string) ($outerPlanetEvent['local_time'] ?? ''), + 'local_iso' => (string) ($outerPlanetEvent['local_iso'] ?? ''), + 'type' => 'outer_planet_event', + ]; +} + +foreach ($outerPlanetStationEvents as $outerPlanetStationEvent) { + if (!is_array($outerPlanetStationEvent)) { + continue; + } + + $stationPlanet = (string) ($outerPlanetStationEvent['planet_label'] ?? 'Planet'); + $stationKind = (string) ($outerPlanetStationEvent['kind'] ?? ''); + $stationLabel = $stationKind === 'retrograde' + ? $stationPlanet . ' stationär, dann rückläufig' + : ($stationKind === 'prograde' + ? $stationPlanet . ' stationär, dann rechtläufig' + : (string) ($outerPlanetStationEvent['label'] ?? 'Stationärer Punkt')); + + $monthEventList[] = [ + 'event' => $stationLabel, + 'date' => (string) ($outerPlanetStationEvent['local_date'] ?? ''), + 'time' => (string) ($outerPlanetStationEvent['local_time'] ?? ''), + 'local_iso' => (string) ($outerPlanetStationEvent['local_iso'] ?? ''), + 'type' => 'outer_planet_station', + ]; +} + +foreach ($jupiterMoonSideEvents as $jupiterMoonSideEvent) { + if (!is_array($jupiterMoonSideEvent)) { + continue; + } + + $durationLabel = trim((string) ($jupiterMoonSideEvent['duration_label'] ?? '')); + $eventLabel = trim((string) ($jupiterMoonSideEvent['label'] ?? 'Alle 4 Jupitermonde auf einer Seite von Jupiter')); + if ($durationLabel !== '') { + $eventLabel .= ' (' . $durationLabel . ')'; + } + + $monthEventList[] = [ + 'event' => $eventLabel, + 'date' => (string) ($jupiterMoonSideEvent['local_date'] ?? ''), + 'time' => (string) ($jupiterMoonSideEvent['local_time'] ?? ''), + 'local_iso' => (string) ($jupiterMoonSideEvent['local_iso'] ?? ''), + 'end_local_iso' => (string) ($jupiterMoonSideEvent['end_local_iso'] ?? ''), + 'duration_minutes' => isset($jupiterMoonSideEvent['duration_minutes']) ? (int) $jupiterMoonSideEvent['duration_minutes'] : null, + 'type' => 'jupiter_moons_one_side', + ]; +} + +foreach ($moonPlanetApproaches as $approachEvent) { + if (!is_array($approachEvent)) { + continue; + } + + $distanceText = isset($approachEvent['separation_deg']) + ? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . ' Grad' + : ''; + + $monthEventList[] = [ + 'event' => trim((string) ($approachEvent['label'] ?? 'Mond nahe Planet') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($approachEvent['local_date'] ?? ''), + 'time' => (string) ($approachEvent['local_time'] ?? ''), + 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), + 'type' => 'moon_planet', + ]; +} + +foreach ($moonDeepSkyApproaches as $approachEvent) { + if (!is_array($approachEvent)) { + continue; + } + + $distanceText = isset($approachEvent['separation_deg']) + ? number_format((float) ($approachEvent['separation_deg']), 2, ',', '') . ' Grad' + : ''; + + $monthEventList[] = [ + 'event' => trim((string) ($approachEvent['label'] ?? 'Mond nahe Sternhaufen') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($approachEvent['local_date'] ?? ''), + 'time' => (string) ($approachEvent['local_time'] ?? ''), + 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), + 'type' => 'moon_deep_sky', + ]; +} + +foreach ($planetBrightStarApproaches as $approachEvent) { + if (!is_array($approachEvent)) { + continue; + } + + $distanceText = isset($approachEvent['separation_deg']) + ? number_format((float) ($approachEvent['separation_deg']), 2, ',', '') . ' Grad' + : ''; + + $monthEventList[] = [ + 'event' => trim((string) ($approachEvent['label'] ?? 'Planet nahe hellem Stern') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($approachEvent['local_date'] ?? ''), + 'time' => (string) ($approachEvent['local_time'] ?? ''), + 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), + 'type' => 'planet_bright_star', + ]; +} + +foreach ($planetConjunctions as $conjunctionEvent) { + if (!is_array($conjunctionEvent)) { + continue; + } + + $distanceText = isset($conjunctionEvent['separation_deg']) + ? number_format((float) $conjunctionEvent['separation_deg'], 2, ',', '') . ' Grad' + : ''; + + $monthEventList[] = [ + 'event' => trim((string) ($conjunctionEvent['label'] ?? 'Planetenkonjunktion') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($conjunctionEvent['local_date'] ?? ''), + 'time' => (string) ($conjunctionEvent['local_time'] ?? ''), + 'local_iso' => (string) ($conjunctionEvent['local_iso'] ?? ''), + 'type' => 'planet_conjunction', + ]; +} + +foreach ($mercuryGoodVisibilityEvents as $visibilityEvent) { + if (!is_array($visibilityEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($visibilityEvent['label'] ?? 'Gute Merkur-Sichtbarkeit'), + 'date' => (string) ($visibilityEvent['local_date'] ?? ''), + 'time' => (string) ($visibilityEvent['local_time'] ?? ''), + 'local_iso' => (string) ($visibilityEvent['local_iso'] ?? ''), + 'end_local_iso' => (string) ($visibilityEvent['end_local_iso'] ?? ''), + 'duration_minutes' => isset($visibilityEvent['duration_minutes']) ? (int) $visibilityEvent['duration_minutes'] : null, + 'type' => 'mercury_good_visibility', + ]; +} + +foreach ($planetParadeEvents as $paradeEvent) { + if (!is_array($paradeEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($paradeEvent['label'] ?? 'Planetenparade'), + 'date' => (string) ($paradeEvent['local_date'] ?? ''), + 'time' => (string) ($paradeEvent['local_time'] ?? ''), + 'local_iso' => (string) ($paradeEvent['local_iso'] ?? ''), + 'end_local_iso' => (string) ($paradeEvent['end_local_iso'] ?? ''), + 'duration_minutes' => isset($paradeEvent['duration_minutes']) ? (int) $paradeEvent['duration_minutes'] : null, + 'type' => 'planet_parade', + ]; +} + +foreach ($planetConstellationChangeEvents as $changeEvent) { + if (!is_array($changeEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($changeEvent['label'] ?? 'Planet wechselt Sternbild'), + 'date' => (string) ($changeEvent['local_date'] ?? ''), + 'time' => (string) ($changeEvent['local_time'] ?? ''), + 'local_iso' => (string) ($changeEvent['local_iso'] ?? ''), + 'type' => 'planet_constellation_change', + ]; +} + +foreach ($moonOccultations as $occultationEvent) { + if (!is_array($occultationEvent)) { + continue; + } + + $maximum = is_array($occultationEvent['maximum'] ?? null) ? $occultationEvent['maximum'] : null; + $star = is_array($occultationEvent['star'] ?? null) ? $occultationEvent['star'] : null; + if ($maximum === null || $star === null) { + continue; + } + + $starLabel = (string) ($star['label'] ?? 'Stern'); + $starMag = isset($star['mag']) ? number_format((float) $star['mag'], 1, ',', '') : ''; + $monthEventList[] = [ + 'event' => 'Mond bedeckt ' . $starLabel . ($starMag !== '' ? ' (' . $starMag . ' mag)' : ''), + 'date' => (string) ($maximum['local_date'] ?? ''), + 'time' => (string) ($maximum['local_time'] ?? ''), + 'local_iso' => (string) ($maximum['local_iso'] ?? ''), + 'type' => 'moon_occultation', + ]; +} + +$monthStartLocal = new DateTimeImmutable( + sprintf('%04d-%02d-01 00:00:00', $selectedYear, $selectedMonth), + new DateTimeZone($chartTimezone) +); +$monthEndLocal = $monthStartLocal->modify('first day of next month'); + +foreach ($meteorShowerPeaks as $meteorShowerPeak) { + if (!monthForecastRadiantIsVisible($chartLatitude, $meteorShowerPeak['radiant_delta_deg'] ?? null)) { + continue; + } + + $peakSol = isset($meteorShowerPeak['peak_sol']) ? (float) $meteorShowerPeak['peak_sol'] : null; + if ($peakSol === null) { + continue; + } + + $peakDateTime = findMonthForecastSolarLongitudeCrossing( + $monthStartLocal, + $monthEndLocal, + $peakSol, + new DateTimeZone($chartTimezone) ); if (!$peakDateTime instanceof DateTimeImmutable) { continue; } - $visible = meteorRadiantIsVisible( - $displayLatitude, - $row['radiant_delta_deg'] !== null ? (float) $row['radiant_delta_deg'] : null - ); + $designation = trim((string) ($meteorShowerPeak['designation'] ?? '')); + $showerCode = trim((string) ($meteorShowerPeak['shower_code'] ?? '')); + $zhr = $meteorShowerPeak['zhr'] ?? null; + $zhrVariable = trim((string) ($meteorShowerPeak['zhr_variable'] ?? '')); + $rateText = ''; - $zhrText = '-'; - if ($row['zhr'] !== null && (int) $row['zhr'] >= 0) { - $zhrText = 'ca. ' . (int) $row['zhr']; - } elseif (trim((string) ($row['zhr_variable'] ?? '')) !== '') { - $zhrText = 'ca. ' . trim((string) $row['zhr_variable']); + if (is_int($zhr) && $zhr >= 0) { + $rateText = 'ZHR ca. ' . $zhr; + } elseif ($zhrVariable !== '') { + $rateText = 'ZHR ca. ' . $zhrVariable; } - $meteorEvents[] = [ - 'designation' => trim((string) ($row['designation'] ?? '')), - 'shower_code' => trim((string) ($row['shower_code'] ?? '')), - 'peak' => $peakDateTime, - 'visible' => $visible, - 'visible_label' => $visible ? 'vom Standort sichtbar' : 'vom Standort nicht sichtbar', - 'zhr_text' => $zhrText, + $labelParts = []; + $labelParts[] = 'Maximum ' . ($designation !== '' ? $designation : 'Meteorstrom'); + if ($showerCode !== '') { + $labelParts[] = $showerCode; + } + if ($rateText !== '') { + $labelParts[] = $rateText; + } + + $monthEventList[] = [ + 'event' => implode(' - ', $labelParts), + 'date' => $peakDateTime->format('d.m.Y'), + 'time' => $peakDateTime->format('H:i'), + 'local_iso' => $peakDateTime->format(DateTimeInterface::ATOM), + 'type' => 'meteor_shower_peak', ]; } usort( - $meteorEvents, - static fn (array $a, array $b): int => $a['peak'] <=> $b['peak'] + $monthEventList, + static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? '')) ); + +$observationTipInsertSql = buildObservationTipInsertSql($monthEventList, new DateTimeZone($chartTimezone)); +$observationTipCalendarIcs = buildObservationTipCalendarIcs($monthEventList, new DateTimeZone($chartTimezone)); + +$chartConfig = [ + 'monthName' => $months[$selectedMonth], + 'year' => $selectedYear, + 'month' => $selectedMonth, + 'location' => [ + 'name' => $chartLocationName, + 'latitude' => $chartLatitude, + 'longitude' => $chartLongitude, + 'elevation' => $chartElevation, + 'timezone' => $chartTimezone, + ], + 'localTimeLabel' => $chartLocalTime->format('d.m.Y H:i') . ' ' . $chartTimezone, + 'utcTimeIso' => $chartUtcTime->format(DateTimeInterface::ATOM), + 'monthSkyHour' => $monthTime['hour'], + 'monthSkyMinute' => $monthTime['minute'], + 'starLimit' => $starDisplayMagnitudeLimit, + 'starLabelLimit' => $starLabelMagnitudeLimit, + 'messierLimit' => $messierDisplayMagnitudeLimit, +]; + +$chartConfigJson = json_encode($chartConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$starDataJson = json_encode($starData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$lineStarDataJson = json_encode($lineStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$constellationLinesJson = json_encode($constellationLines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$messierDataJson = json_encode($messierData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$planetDataJson = json_encode($planetData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$moonDataJson = json_encode($moonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$moonPhaseEventsJson = json_encode($moonPhaseEvents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$observationTipCalendarIcsJson = json_encode($observationTipCalendarIcs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatsereignisse-%04d-%02d.ics', $selectedYear, $selectedMonth), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?> -
-
-
-

Meteorstroeme

-

- Einfache Jahresliste der Meteorstrom-Maxima fuer - - in . -

-
+
+
+
+
+

Zeitraum waehlen

+

Die Sternkarte wird fuer den 15. des gewaehlten Monats mit der festen Monatsuhrzeit erzeugt.

+
- -
- Daten konnten nicht geladen werden: + + + + +
+
- -
- Fuer das aktuelle Jahr wurden keine Meteorstrom-Daten gefunden. + + +
+
+
+

Zenitprojektion

+

+ , + +

+
- -
- - - - - - - - - - - - - - - - - - - - - + +
+ +
+ + + + +
+
+
+

Südhimmel

+

+ , + +

+
+
+ +
+ +
+ + +
+ +
+
+
+

Mondphasen Im

+

Eine Monatszeile vom ersten bis zum letzten Tag mit der Mondtextur aus moon4geocron.png.

+
+
+ +
+
+ Mondphasen des Monats in einer Zeile +
+
+ + +
+ +
+
+
+

Ereignisse

+

Chronologische Monatsliste fuer

+
+
+ + + + + + +
+
+ +
-
DatumUhrzeitMeteorstromCodeZHRSichtbarkeit
format('d.m.Y'), ENT_QUOTES, 'UTF-8') ?>format('H:i'), ENT_QUOTES, 'UTF-8') ?>
+
+ +

Fuer diesen Monat wurden keine Ereignisse gefunden.

+
- +
+
+ + + diff --git a/public/monatsvorhersage_old.php b/public/monatsvorhersage_old.php index 0e50702..efc44e4 100644 --- a/public/monatsvorhersage_old.php +++ b/public/monatsvorhersage_old.php @@ -11,35 +11,99 @@ $publicBasePath = './'; $currentYear = (int) date('Y'); $selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear; $selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n'); +// ------------------------------------------------------------------------- +// Grenzwerte fuer Sternkarte und Objektanzeige +// ------------------------------------------------------------------------- +// Maximale Helligkeit (Magnitude) der Sterne, die aus der Datenbank geladen +// werden. Hoehere Werte laden mehr, aber auch schwaechere Sterne. $starDatabaseMagnitudeLimit = 6.0; + +// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar +// gezeichnet werden sollen. $starDisplayMagnitudeLimit = 4.4; + +// Sterne bis zu dieser Helligkeit erhalten auf der Sternkarte einen Namen. $starLabelMagnitudeLimit = 2.0; -$messierDatabaseMagnitudeLimit = 4.0; + +// Maximale Helligkeit der Messier-Objekte, die aus der Datenbank geladen +// werden. +$messierDatabaseMagnitudeLimit = 2.0; + +// Messier-Objekte bis zu dieser Helligkeit werden auf der Monatskarte +// eingeblendet. $messierDisplayMagnitudeLimit = 1.0; + +// ------------------------------------------------------------------------- +// Grenzwerte fuer Monatsereignisse +// ------------------------------------------------------------------------- +// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und Planeten. $moonPlanetMaxSeparationDeg = 1.5; + +// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und ausgewaehlten +// Sternhaufen wie Plejaden oder Praesepe. $moonDeepSkyMaxSeparationDeg = 1.0; -$planetBrightStarMaxSeparationDeg = 1.0; + // Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und // ausgewaehlten hellen Sternen wie Regulus oder Spica. +$planetBrightStarMaxSeparationDeg = 1.0; + +// Maximaler Winkelabstand fuer Planet-Planet-Konjunktionen. $planetConjunctionMaxSeparationDeg = 1.0; + +// Nur Sterne bis zu dieser Helligkeit werden fuer Mondbedeckungen +// beruecksichtigt. $moonOccultationMaxMag = 2.0; -// Gute Merkur-Sichtbarkeit wird nicht nur über Elongation, sondern über echte -// Beobachtbarkeit am Himmel definiert: Wir prüfen pro Tag zu einem festen -// Zeitpunkt relativ zu Sonnenauf- oder -untergang, ob Merkur hoch genug steht -// und die Sonne bereits tief genug unter dem Horizont ist. + +// ------------------------------------------------------------------------- +// Schwellen fuer "Gute Merkur-Sichtbarkeit" +// ------------------------------------------------------------------------- +// Gute Merkur-Sichtbarkeit wird nicht nur ueber Elongation definiert, sondern +// ueber echte Beobachtbarkeit in der Daemmerung. +// Geprueft wird pro Tag: +// morgens eine feste Zahl Minuten VOR Sonnenaufgang, +// abends eine feste Zahl Minuten NACH Sonnenuntergang. $mercuryGoodVisibilitySampleOffsetMinutes = 45; -// Morgens: so viele Minuten VOR Sonnenaufgang prüfen. -// Abends: so viele Minuten NACH Sonnenuntergang prüfen. + +// Merkur muss zum Pruefzeitpunkt mindestens diese Hoehe ueber dem Horizont +// erreichen, damit der Tag als gut sichtbar gilt. $mercuryGoodVisibilityMinAltitudeDeg = 7.0; -// Merkur muss bei diesem Prüfzeitpunkt mindestens so hoch über dem Horizont -// stehen, damit wir die Sichtbarkeit als "gut" einstufen. + +// Gleichzeitig muss die Sonne ausreichend tief unter dem Horizont stehen. +// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar waere, aber +// noch zu stark in der hellen Daemmerung untergeht. $mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0; -// Die Sonne muss gleichzeitig mindestens 4 Grad unter dem Horizont stehen. -// So vermeiden wir Treffer, bei denen Merkur zwar formal da ist, aber noch zu -// stark in der hellen Dämmerung untergeht. + +// Erst ab dieser Anzahl aufeinanderfolgender guter Tage erzeugen wir ein +// Monatsereignis. Einzelne Ausreisser-Tage werden dadurch herausgefiltert. $mercuryGoodVisibilityMinConsecutiveDays = 3; -// Einzelne gute Tage sind oft zu zufällig. Erst ab dieser Anzahl -// aufeinanderfolgender Tage erzeugen wir ein Monatsereignis. + +// ------------------------------------------------------------------------- +// Schwellen fuer "Planetenparade" +// ------------------------------------------------------------------------- +// Geprueft wird morgens eine feste Zahl Minuten VOR Sonnenaufgang und abends +// eine feste Zahl Minuten NACH Sonnenuntergang. +$planetParadeSampleOffsetMinutes = 45; + +// Ein Planet zaehlt fuer die Parade nur dann mit, wenn er zum Pruefzeitpunkt +// mindestens diese Hoehe ueber dem Horizont erreicht. +$planetParadeMinAltitudeDeg = 7.0; + +// Gleichzeitig muss die Sonne bereits tief genug unter dem Horizont stehen, +// damit die Parade nicht nur rechnerisch, sondern auch praktisch sichtbar ist. +$planetParadeMaxSunAltitudeDeg = -4.0; + +// Die beteiligten Planeten muessen innerhalb dieses Azimutbogens liegen. +// So vermeiden wir Treffer, bei denen die Planeten zu weit ueber den Himmel +// verteilt sind und nicht mehr als zusammenhaengende Parade wirken. +$planetParadeMaxAzimuthSpanDeg = 120.0; + +// Mindestens so viele helle Planeten muessen gleichzeitig sichtbar sein, +// damit wir von einer Planetenparade sprechen. +$planetParadeMinPlanetCount = 4; + +// Erst ab dieser Anzahl aufeinanderfolgender Tage erzeugen wir ein Ereignis. +// Das filtert kurzlebige Ein-Tages-Treffer heraus. +$planetParadeMinConsecutiveDays = 2; $years = range($currentYear - 2, $currentYear + 5); $months = [ @@ -83,6 +147,8 @@ $moonPlanetApproaches = []; $moonDeepSkyApproaches = []; $planetBrightStarApproaches = []; $mercuryGoodVisibilityEvents = []; +$planetParadeEvents = []; +$planetConstellationChangeEvents = []; $planetConjunctions = []; $eclipseEvents = []; $moonApsisEvents = []; @@ -203,6 +269,171 @@ if (!function_exists('findMonthForecastSolarLongitudeCrossing')) { } } +if (!function_exists('buildObservationTipInsertSql')) { + function buildObservationTipInsertSql(array $events, DateTimeZone $timezone): string + { + $rows = []; + + foreach ($events as $event) { + if (!is_array($event)) { + continue; + } + + $tip = trim((string) ($event['event'] ?? '')); + if ($tip === '') { + continue; + } + + $moment = null; + $localIso = trim((string) ($event['local_iso'] ?? '')); + if ($localIso !== '') { + try { + $moment = new DateTimeImmutable($localIso); + } catch (Throwable $e) { + $moment = null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + $date = trim((string) ($event['date'] ?? '')); + $time = trim((string) ($event['time'] ?? '')); + if ($date !== '' && $time !== '') { + $moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + continue; + } + + $rows[] = sprintf( + "('%s', '%s')", + $moment->setTimezone($timezone)->format('Y-m-d H:i:s'), + str_replace("'", "''", $tip) + ); + } + + if ($rows === []) { + return ''; + } + + return "INSERT INTO `astro_sonobs_observationtips` (`moment`, `tip`)\nVALUES\n " + . implode(",\n ", $rows) + . ";\n"; + } +} + +if (!function_exists('escapeIcalText')) { + function escapeIcalText(string $value): string + { + return str_replace( + ["\\", ";", ",", "\r\n", "\n", "\r"], + ["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"], + $value + ); + } +} + +if (!function_exists('foldIcalLine')) { + function foldIcalLine(string $line): string + { + $chunks = preg_split('/(.{1,73})/u', $line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($chunks === false || $chunks === []) { + return $line; + } + + $firstChunk = array_shift($chunks); + return $firstChunk . ($chunks ? "\r\n " . implode("\r\n ", $chunks) : ''); + } +} + +if (!function_exists('buildObservationTipCalendarIcs')) { + function buildObservationTipCalendarIcs(array $events, DateTimeZone $timezone): string + { + $lines = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//AstroTools//Monatsvorhersage//DE', + 'CALSCALE:GREGORIAN', + 'METHOD:PUBLISH', + 'X-WR-CALNAME:AstroTools Monatsereignisse', + ]; + + $createdUtc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Ymd\THis\Z'); + + foreach ($events as $index => $event) { + if (!is_array($event)) { + continue; + } + + $summary = trim((string) ($event['event'] ?? '')); + if ($summary === '') { + continue; + } + + $moment = null; + $localIso = trim((string) ($event['local_iso'] ?? '')); + if ($localIso !== '') { + try { + $moment = new DateTimeImmutable($localIso); + } catch (Throwable $e) { + $moment = null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + $date = trim((string) ($event['date'] ?? '')); + $time = trim((string) ($event['time'] ?? '')); + if ($date !== '' && $time !== '') { + $moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null; + } + } + + if (!$moment instanceof DateTimeImmutable) { + continue; + } + + $startUtc = $moment->setTimezone(new DateTimeZone('UTC')); + $endMoment = null; + $endLocalIso = trim((string) ($event['end_local_iso'] ?? '')); + if ($endLocalIso !== '') { + try { + $endMoment = new DateTimeImmutable($endLocalIso); + } catch (Throwable $e) { + $endMoment = null; + } + } + + if (!$endMoment instanceof DateTimeImmutable) { + $durationMinutes = isset($event['duration_minutes']) ? max(1, (int) $event['duration_minutes']) : 15; + $endMoment = $moment->modify('+' . $durationMinutes . ' minutes'); + } + + $endUtc = $endMoment->setTimezone(new DateTimeZone('UTC')); + $uidBase = sha1($startUtc->format(DateTimeInterface::ATOM) . '|' . $summary . '|' . (string) $index); + + $eventLines = [ + 'BEGIN:VEVENT', + 'UID:' . $uidBase . '@astrotools.local', + 'DTSTAMP:' . $createdUtc, + 'DTSTART:' . $startUtc->format('Ymd\THis\Z'), + 'DTEND:' . $endUtc->format('Ymd\THis\Z'), + 'SUMMARY:' . escapeIcalText($summary), + 'DESCRIPTION:' . escapeIcalText($summary), + 'END:VEVENT', + ]; + + foreach ($eventLines as $eventLine) { + $lines[] = foldIcalLine($eventLine); + } + } + + $lines[] = 'END:VCALENDAR'; + + return implode("\r\n", $lines) . "\r\n"; + } +} + try { $dbConfig = require __DIR__ . '/../config/database.php'; $dsn = sprintf( @@ -766,6 +997,49 @@ if (is_array($mercuryGoodVisibilityDecoded) && ($mercuryGoodVisibilityDecoded['o $mercuryGoodVisibilityEvents = is_array($mercuryGoodVisibilityDecoded['events'] ?? null) ? $mercuryGoodVisibilityDecoded['events'] : []; } +$planetParadeOutput = []; +$planetParadeExitCode = 0; +$planetParadeArgs = [ + $pythonScriptPath, + 'planet_parades_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $planetParadeSampleOffsetMinutes, + (string) $planetParadeMinAltitudeDeg, + (string) $planetParadeMaxSunAltitudeDeg, + (string) $planetParadeMaxAzimuthSpanDeg, + (string) $planetParadeMinPlanetCount, + (string) $planetParadeMinConsecutiveDays, +]; +$planetParadeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetParadeArgs)) . ' 2>&1'; +exec($planetParadeCommand, $planetParadeOutput, $planetParadeExitCode); +$planetParadeRaw = trim(implode("\n", $planetParadeOutput)); +$planetParadeDecoded = json_decode($planetParadeRaw, true); +if (is_array($planetParadeDecoded) && ($planetParadeDecoded['ok'] ?? false)) { + $planetParadeEvents = is_array($planetParadeDecoded['events'] ?? null) ? $planetParadeDecoded['events'] : []; +} + +$planetConstellationChangeOutput = []; +$planetConstellationChangeExitCode = 0; +$planetConstellationChangeArgs = [ + $pythonScriptPath, + 'planet_constellation_changes_for_month', + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$planetConstellationChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConstellationChangeArgs)) . ' 2>&1'; +exec($planetConstellationChangeCommand, $planetConstellationChangeOutput, $planetConstellationChangeExitCode); +$planetConstellationChangeRaw = trim(implode("\n", $planetConstellationChangeOutput)); +$planetConstellationChangeDecoded = json_decode($planetConstellationChangeRaw, true); +if (is_array($planetConstellationChangeDecoded) && ($planetConstellationChangeDecoded['ok'] ?? false)) { + $planetConstellationChangeEvents = is_array($planetConstellationChangeDecoded['events'] ?? null) ? $planetConstellationChangeDecoded['events'] : []; +} + $moonOccultationStars = []; try { if (isset($pdo)) { @@ -1054,6 +1328,8 @@ foreach ($jupiterMoonSideEvents as $jupiterMoonSideEvent) { 'date' => (string) ($jupiterMoonSideEvent['local_date'] ?? ''), 'time' => (string) ($jupiterMoonSideEvent['local_time'] ?? ''), 'local_iso' => (string) ($jupiterMoonSideEvent['local_iso'] ?? ''), + 'end_local_iso' => (string) ($jupiterMoonSideEvent['end_local_iso'] ?? ''), + 'duration_minutes' => isset($jupiterMoonSideEvent['duration_minutes']) ? (int) $jupiterMoonSideEvent['duration_minutes'] : null, 'type' => 'jupiter_moons_one_side', ]; } @@ -1140,10 +1416,42 @@ foreach ($mercuryGoodVisibilityEvents as $visibilityEvent) { 'date' => (string) ($visibilityEvent['local_date'] ?? ''), 'time' => (string) ($visibilityEvent['local_time'] ?? ''), 'local_iso' => (string) ($visibilityEvent['local_iso'] ?? ''), + 'end_local_iso' => (string) ($visibilityEvent['end_local_iso'] ?? ''), + 'duration_minutes' => isset($visibilityEvent['duration_minutes']) ? (int) $visibilityEvent['duration_minutes'] : null, 'type' => 'mercury_good_visibility', ]; } +foreach ($planetParadeEvents as $paradeEvent) { + if (!is_array($paradeEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($paradeEvent['label'] ?? 'Planetenparade'), + 'date' => (string) ($paradeEvent['local_date'] ?? ''), + 'time' => (string) ($paradeEvent['local_time'] ?? ''), + 'local_iso' => (string) ($paradeEvent['local_iso'] ?? ''), + 'end_local_iso' => (string) ($paradeEvent['end_local_iso'] ?? ''), + 'duration_minutes' => isset($paradeEvent['duration_minutes']) ? (int) $paradeEvent['duration_minutes'] : null, + 'type' => 'planet_parade', + ]; +} + +foreach ($planetConstellationChangeEvents as $changeEvent) { + if (!is_array($changeEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($changeEvent['label'] ?? 'Planet wechselt Sternbild'), + 'date' => (string) ($changeEvent['local_date'] ?? ''), + 'time' => (string) ($changeEvent['local_time'] ?? ''), + 'local_iso' => (string) ($changeEvent['local_iso'] ?? ''), + 'type' => 'planet_constellation_change', + ]; +} + foreach ($moonOccultations as $occultationEvent) { if (!is_array($occultationEvent)) { continue; @@ -1227,6 +1535,9 @@ usort( static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? '')) ); +$observationTipInsertSql = buildObservationTipInsertSql($monthEventList, new DateTimeZone($chartTimezone)); +$observationTipCalendarIcs = buildObservationTipCalendarIcs($monthEventList, new DateTimeZone($chartTimezone)); + $chartConfig = [ 'monthName' => $months[$selectedMonth], 'year' => $selectedYear, @@ -1256,6 +1567,8 @@ $planetDataJson = json_encode($planetData, JSON_UNESCAPED_UNICODE | JSON_UNESCAP $moonDataJson = json_encode($moonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $moonPhaseEventsJson = json_encode($moonPhaseEvents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$observationTipCalendarIcsJson = json_encode($observationTipCalendarIcs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +$observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatsereignisse-%04d-%02d.ics', $selectedYear, $selectedMonth), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>
@@ -1358,9 +1671,30 @@ $starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_U

Ereignisse

Chronologische Monatsliste fuer

+
+ + + + + + +