'Januar', 2 => 'Februar', 3 => 'März', 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], ]; $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 = []; $favoriteSatellitePasses = []; $favoriteSatelliteName = null; $favoriteSatellitePayload = []; $favoriteCometPayload = []; $favoriteCometEvents = []; $seasonChanges = []; $timeChanges = []; $monthEventList = []; $starDataError = null; if (!function_exists('buildMonthOccultationStarLabel')) { function buildMonthOccultationStarLabel(array $row): string { $proper = trim((string) ($row['proper'] ?? '')); if ($proper !== '') { return $proper; } $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('monthForecastParseMeteorZhrRange')) { function monthForecastParseMeteorZhrRange(?int $zhr, string $zhrVariable): array { if ($zhr !== null && $zhr >= 0) { return ['min' => $zhr, 'max' => $zhr]; } if ($zhrVariable === '') { return ['min' => null, 'max' => null]; } preg_match_all('/\d+/', $zhrVariable, $matches); $numbers = array_map('intval', $matches[0] ?? []); if ($numbers === []) { return ['min' => null, 'max' => null]; } return [ 'min' => min($numbers), 'max' => max($numbers), ]; } } 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('monthForecastTleChecksum')) { function monthForecastTleChecksum(string $line): int { $sum = 0; for ($i = 0; $i < 68; $i++) { $c = $line[$i] ?? ' '; if (ctype_digit($c)) { $sum += (int) $c; } elseif ($c === '-') { $sum += 1; } } return $sum % 10; } } if (!function_exists('monthForecastTleExp')) { function monthForecastTleExp(float $value): string { if (abs($value) < 1e-99) { return ' 00000-0'; } $sign = $value >= 0 ? '+' : '-'; $abs = abs($value); $exp = (int) floor(log10($abs)) + 1; $mantissa = (int) round($abs / pow(10, $exp) * 1e5); if ($mantissa >= 100000) { $mantissa = 10000; $exp++; } $expSign = $exp >= 0 ? '+' : '-'; return sprintf('%s%05d%s%d', $sign, $mantissa, $expSign, abs($exp)); } } if (!function_exists('monthForecastTleMmDot')) { function monthForecastTleMmDot(float $value): string { $sign = $value >= 0 ? ' ' : '-'; return sprintf('%s.%08d', $sign, (int) round(abs($value) * 1e8)); } } if (!function_exists('monthForecastTleEpoch')) { function monthForecastTleEpoch(string $epochStr): string { $dt = new DateTimeImmutable($epochStr, new DateTimeZone('UTC')); $yy = (int) $dt->format('Y') % 100; $jan1 = new DateTimeImmutable($dt->format('Y') . '-01-01 00:00:00', new DateTimeZone('UTC')); $secs = (float) $dt->format('U') - (float) $jan1->format('U'); $day = $secs / 86400.0 + 1.0; return sprintf('%02d%012.8f', $yy, $day); } } if (!function_exists('monthForecastTleIntlDesig')) { function monthForecastTleIntlDesig(?string $objectId): string { if (!$objectId) { return str_repeat(' ', 8); } if (preg_match('/^(\d{4})-(\d+)([A-Z]*)$/i', trim($objectId), $match)) { $yy = substr($match[1], 2, 2); $launch = str_pad($match[2], 3, '0', STR_PAD_LEFT); $piece = str_pad(strtoupper($match[3]), 3, ' ', STR_PAD_RIGHT); return substr(str_pad($yy . $launch . $piece, 8, ' '), 0, 8); } return str_pad(substr($objectId, 0, 8), 8); } } if (!function_exists('monthForecastBuildTleLine1')) { function monthForecastBuildTleLine1(array $sat, array $tle): string { $norad = (int) ($sat['norad_cat_id'] ?? 0); $class = in_array($sat['classification_type'] ?? '', ['U', 'C', 'S'], true) ? (string) $sat['classification_type'] : 'U'; $line = sprintf( '1 %05d%1s %-8s %14s %10s %8s %8s %1d %4d', $norad, $class, monthForecastTleIntlDesig($sat['object_id'] ?? null), monthForecastTleEpoch((string) ($tle['epoch'] ?? '')), monthForecastTleMmDot((float) ($tle['mean_motion_dot'] ?? 0)), monthForecastTleExp((float) ($tle['mean_motion_ddot'] ?? 0)), monthForecastTleExp((float) ($tle['bstar'] ?? 0)), (int) ($tle['ephemeris_type'] ?? 0), (int) ($tle['element_set_no'] ?? 0) ); $line = str_pad($line, 68); return $line . monthForecastTleChecksum($line); } } if (!function_exists('monthForecastBuildTleLine2')) { function monthForecastBuildTleLine2(array $sat, array $tle): string { $norad = (int) ($sat['norad_cat_id'] ?? 0); $ecc = (float) ($tle['eccentricity'] ?? 0); $eccStr = substr(sprintf('%.7f', $ecc), 2); $line = sprintf( '2 %05d %8.4f %8.4f %7s %8.4f %8.4f %11.8f%5d', $norad, (float) ($tle['inclination'] ?? 0), (float) ($tle['ra_of_asc_node'] ?? 0), $eccStr, (float) ($tle['arg_of_pericenter'] ?? 0), (float) ($tle['mean_anomaly'] ?? 0), (float) ($tle['mean_motion'] ?? 0), (int) ($tle['rev_at_epoch'] ?? 0) ); $line = str_pad($line, 68); return $line . monthForecastTleChecksum($line); } } 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"; } } if (!function_exists('monthForecastBuildNarrativeText')) { function monthForecastBuildNarrativeText(array $monthEventList, int $monthNumber, string $monthName, int $year): string { $filteredEvents = array_values(array_filter( $monthEventList, static fn (array $item): bool => (string) ($item['type'] ?? '') !== 'favorite_satellite_pass' )); $skyIntroByMonth = [ 1 => 'Der Sternenhimmel im Januar wird noch von den klassischen Wintersternbildern beherrscht. Orion, Stier, Fuhrmann, Zwillinge und der Grosse Hund praegen den Abendhimmel, waehrend im Osten bereits erste Fruehlingsboten hoeher steigen.', 2 => 'Der Februar zeigt den Winterhimmel in seiner vielleicht schoensten Form. Orion, Stier, Zwillinge, Fuhrmann und Grosser Hund stehen abends eindrucksvoll ueber dem Horizont, spaeter in der Nacht ruesten sich bereits Loewe und andere Fruehlingssternbilder fuer ihren Auftritt.', 3 => 'Der Sternenhimmel im Maerz bildet den Uebergang von den Winter- zu den Fruehlingssternbildern. Abends stehen Orion, Zwillinge, Fuhrmann und Stier noch markant am Himmel, waehrend gegen Mitternacht Loewe, Jungfrau und Bootes immer mehr Aufmerksamkeit auf sich ziehen.', 4 => 'Im April dominiert der Fruehlingshimmel den Abend. Loewe, Jungfrau und Bootes stehen guenstig, waehrend die Wintersternbilder nun rasch in den Westen abwandern und am spaeteren Abend bereits Platz fuer den kommenden Sommerhimmel machen.', 5 => 'Der Mai zeigt den Fruehlingshimmel in voller Pracht. Loewe, Jungfrau, Bootes und die ausgedehnte Wasserschlange praegen die Nacht, waehrend sich im Osten schon die ersten Sternbilder des Sommers bemerkbar machen.', 6 => 'Im Juni werden die Naechte kurz, doch gerade in der spaeteren Dunkelheit treten bereits die Sommersternbilder hervor. Schwan, Leier und Adler gewinnen an Hoehe, waehrend der Fruehlingshimmel langsam nach Westen rueckt.', 7 => 'Im Juli praegen die Sommersternbilder den Himmel. Das Sommerdreieck aus Wega, Deneb und Atair dominiert den Blick nach oben, daneben laden Schuetze, Skorpion und die reichen Sternfelder der Milchstrasse zu ausgedehnten Beobachtungen ein.', 8 => 'Im August zeigt sich der Sommerhimmel von seiner eindrucksvollsten Seite. Das Sommerdreieck steht hoch, die Milchstrasse zieht sich markant ueber den Himmel, und in der zweiten Nachthaelfte machen sich bereits erste Herbststernbilder bemerkbar.', 9 => 'Der September vermittelt zwischen Sommer- und Herbsthimmel. Das Sommerdreieck bleibt ein Blickfang, doch Pegasus, Andromeda und die herbstlichen Sternbilder steigen nun immer besser am Abendhimmel auf.', 10 => 'Im Oktober uebernimmt der Herbsthimmel die Hauptrolle. Pegasus, Andromeda, Perseus und Dreieck stehen guenstig, waehrend sich gegen Morgen schon die ersten Vorboten des Winterhimmels zeigen.', 11 => 'Der November bringt lange dunkle Naechte und einen Himmel zwischen Herbst und Winter. Pegasus und Andromeda stehen noch gut, waehrend Stier, Fuhrmann und Orion im Laufe der Nacht immer eindrucksvoller werden.', 12 => 'Im Dezember kehrt der Winterhimmel eindrucksvoll zurueck. Orion, Stier, Zwillinge, Fuhrmann und der Grosse Hund dominieren den Abend und machen den Monat zu einer besonders attraktiven Zeit fuer Sternbeobachtungen.', ]; $paragraphs = []; $paragraphs[] = $skyIntroByMonth[$monthNumber] ?? ($monthName . ' ' . $year . ' bietet einen abwechslungsreichen Himmel zwischen den jahreszeitlichen Sternbildern.'); $findEvents = static function (array $events, callable $filter, int $limit = 3): array { $matches = []; foreach ($events as $event) { if ($filter($event)) { $matches[] = $event; if (count($matches) >= $limit) { break; } } } return $matches; }; $describeEvents = static function (array $events): string { $parts = []; foreach ($events as $event) { $label = trim((string) ($event['event'] ?? '')); $date = trim((string) ($event['date'] ?? '')); if ($label === '' || $date === '') { continue; } $parts[] = $label . ' am ' . $date; } return implode(', ', $parts); }; $pickRandomText = static function (array $options, string $fallback): string { $options = array_values(array_filter($options, static fn ($option): bool => trim((string) $option) !== '')); if ($options === []) { return $fallback; } try { return (string) $options[random_int(0, count($options) - 1)]; } catch (Throwable) { return (string) $options[array_rand($options)]; } }; $buildSeasonNarrative = static function (array $event): ?string { $label = trim((string) ($event['event'] ?? '')); $date = trim((string) ($event['date'] ?? '')); if ($label === '' || $date === '') { return null; } return match ($label) { 'Fruehlingsanfang' => 'Der astronomische Frühling beginnt am ' . $date . '. Dann überschreitet die Sonne den Himmelsäquator nach Norden; Tag und Nacht sind zu diesem Zeitpunkt annähernd gleich lang.', 'Sommeranfang' => 'Der astronomische Sommer beginnt am ' . $date . '. Dann erreicht die Sonne ihren nördlichsten Punkt auf der scheinbaren Jahresbahn und steht senkrecht über dem Wendekreis des Krebses.', 'Herbstanfang' => 'Der astronomische Herbst beginnt am ' . $date . '. Dann überschreitet die Sonne den Himmelsäquator wieder nach Süden; Tag und Nacht sind dabei annähernd gleich lang.', 'Winteranfang' => 'Der astronomische Winter beginnt am ' . $date . '. Dann erreicht die Sonne ihren südlichsten Punkt auf der scheinbaren Jahresbahn und steht senkrecht über dem Wendekreis des Steinbocks.', default => null, }; }; $seasonEvents = $findEvents( $filteredEvents, static fn (array $event): bool => (string) ($event['type'] ?? '') === 'season_change', 1 ); if ($seasonEvents !== []) { $seasonNarrative = $buildSeasonNarrative($seasonEvents[0]); if ($seasonNarrative !== null) { $paragraphs[] = $seasonNarrative; } } $moonEvents = $findEvents( $filteredEvents, static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['moon_phase', 'golden_handle', 'moon_planet_approach', 'moon_deep_sky_approach', 'moon_occultation', 'moon_apsis'], true), 4 ); if ($moonEvents !== []) { $moonIntro = $pickRandomText([ 'Auch der Mond liefert im Laufe des Monats einige markante Beobachtungsmomente.', 'Der Mond zeigt im Monatsverlauf mehrere gut beobachtbare Phasen und Konstellationen.', 'Am Mond ergeben sich im Laufe des Monats einige auffaellige Termine und Begegnungen.', 'Der Mond bietet im Monatsverlauf mehrere charakteristische Beobachtungssituationen.', ], 'Auch der Mond liefert im Laufe des Monats einige markante Beobachtungsmomente.'); $paragraphs[] = $moonIntro . ' Besonders auffaellig sind ' . $describeEvents($moonEvents) . '.'; } $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'], 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 praegen 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; } $planetMotionEvents = $findEvents( $filteredEvents, static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['outer_planet_station', 'planet_constellation_change'], true), 4 ); if ($planetMotionEvents !== []) { $planetMotionIntro = $pickRandomText([ 'Im Monatsverlauf zeigen sich bei den Planeten auch auffaellige Positionswechsel.', 'Auch in ihrer Stellung am Himmel veraendern 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 gehoeren ' . $describeEvents($planetMotionEvents) . '.'; } $cometEvents = $findEvents( $filteredEvents, static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['favorite_comet_good_visibility', 'favorite_comet_perihelion', 'favorite_comet_geocentric_minimum'], true), 4 ); if ($cometEvents !== []) { $cometIntro = $pickRandomText([ 'Bei den Kometen sind in diesem Monat besonders', 'Auch bei den Kometen lohnt sich in diesem Monat der Blick auf', 'Unter den Kometen fallen in diesem Monat vor allem', 'Kometenfreunde koennen sich in diesem Monat besonders fuer', ], 'Bei den Kometen sind in diesem Monat besonders'); $cometOutro = $pickRandomText([ 'interessant.', 'beobachtenswert.', 'bemerkenswert.', 'einen genaueren Blick wert.', ], 'interessant.'); $paragraphs[] = $cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro; } $eclipseEvents = $findEvents( $filteredEvents, static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['eclipse', 'lunar_eclipse', 'solar_eclipse'], true), 3 ); if ($eclipseEvents !== []) { $eclipseIntro = $pickRandomText([ 'Besondere Aufmerksamkeit verdienen ausserdem die Finsternistermine.', 'Auch die Finsternisse setzen in diesem Monat markante Akzente.', 'Ein weiterer Hoehepunkt koennen in diesem Monat die Finsternistermine sein.', 'Nicht zuletzt lohnt sich in diesem Monat auch ein Blick auf moegliche Finsternisse.', ], 'Besondere Aufmerksamkeit verdienen ausserdem die Finsternistermine.'); $paragraphs[] = $eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.'; } $meteorEvents = $findEvents( $filteredEvents, static fn (array $event): bool => (string) ($event['type'] ?? '') === 'meteor_shower_peak' && ( (int) ($event['zhr_min_estimate'] ?? 0) >= 20 || (int) ($event['zhr_max_estimate'] ?? 0) >= 30 ), 2 ); if ($meteorEvents !== []) { $meteorIntro = $pickRandomText([ 'Freunde von Meteoren sollten zudem die Aktivitaet der Meteorstroeme im Blick behalten.', 'Auch die Meteorstroeme verdienen in diesem Monat Aufmerksamkeit.', 'Wer nach Sternschnuppen Ausschau halten moechte, sollte auch die Meteorstroeme beachten.', 'Ein Blick auf die Meteorstroeme kann sich in diesem Monat ebenfalls lohnen.', ], 'Freunde von Meteoren sollten zudem die Aktivitaet der Meteorstroeme im Blick behalten.'); $paragraphs[] = $meteorIntro . ' Erwähnenswert sind besonders ' . $describeEvents($meteorEvents) . '.'; } else { $meteorEvents = $findEvents( $filteredEvents, static fn (array $event): bool => (string) ($event['type'] ?? '') === 'meteor_shower_peak' && ( (int) ($event['zhr_min_estimate'] ?? 0) >= 10 || (int) ($event['zhr_max_estimate'] ?? 0) >= 20 ), 2 ); if ($meteorEvents !== []) { $meteorModerateIntro = $pickRandomText([ 'Bei den Meteorstroemen lohnt in diesem Monat vor allem ein Blick auf', 'Unter den Meteorstroemen verdienen in diesem Monat besonders', 'Auch bei den Meteorstroemen fallen in diesem Monat vor allem', 'Wer Meteorstroeme verfolgen moechte, sollte in diesem Monat vor allem auf', ], 'Bei den Meteorstroemen lohnt in diesem Monat vor allem ein Blick auf'); $paragraphs[] = $meteorModerateIntro . ' ' . $describeEvents($meteorEvents) . '.'; } } if (count($paragraphs) === 1 && $filteredEvents !== []) { $highlights = array_slice($filteredEvents, 0, 4); $highlightIntro = $pickRandomText([ 'Zu den auffaelligen Terminen des Monats gehoeren', 'Unter den markanten Terminen dieses Monats finden sich', 'Im Monatsverlauf fallen vor allem', 'Besonders bemerkenswert im Monatskalender sind', ], 'Zu den auffaelligen Terminen des Monats gehoeren'); $paragraphs[] = $highlightIntro . ' ' . $describeEvents($highlights) . '.'; } return implode("\n\n", $paragraphs); } } 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, ]); $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" ); 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, ]; } $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" ); 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'] ?? '')), ]; } if ($loggedIn && isset($_SESSION['user_id'])) { $favoriteCometStmt = $pdo->prepare( "SELECT c.id, c.designation_and_name, c.designation_packed, c.orbit_type, c.year_of_perihelion, c.month_of_perihelion, c.day_of_perihelion, c.perihelion_dist_au, c.eccentricity, c.arg_perihelion_deg, c.ascending_node_deg, c.inclination_deg, c.absolute_magnitude_h, c.slope_parameter_g FROM app_user_comets uc INNER JOIN comets_mpc c ON c.id = uc.comet_id WHERE uc.user_id = :user_id ORDER BY c.designation_and_name ASC, c.id ASC" ); $favoriteCometStmt->execute([ ':user_id' => (int) $_SESSION['user_id'], ]); foreach ($favoriteCometStmt->fetchAll() as $favoriteCometRow) { $favoriteCometPayload[] = [ 'id' => (int) ($favoriteCometRow['id'] ?? 0), 'designation_and_name' => trim((string) ($favoriteCometRow['designation_and_name'] ?? '')), 'designation_packed' => trim((string) ($favoriteCometRow['designation_packed'] ?? '')), 'orbit_type' => trim((string) ($favoriteCometRow['orbit_type'] ?? '')), 'year_of_perihelion' => (int) ($favoriteCometRow['year_of_perihelion'] ?? 0), 'month_of_perihelion' => (int) ($favoriteCometRow['month_of_perihelion'] ?? 0), 'day_of_perihelion' => (float) ($favoriteCometRow['day_of_perihelion'] ?? 0.0), 'perihelion_dist_au' => (float) ($favoriteCometRow['perihelion_dist_au'] ?? 0.0), 'eccentricity' => (float) ($favoriteCometRow['eccentricity'] ?? 0.0), 'arg_perihelion_deg' => (float) ($favoriteCometRow['arg_perihelion_deg'] ?? 0.0), 'ascending_node_deg' => (float) ($favoriteCometRow['ascending_node_deg'] ?? 0.0), 'inclination_deg' => (float) ($favoriteCometRow['inclination_deg'] ?? 0.0), 'absolute_magnitude_h' => $favoriteCometRow['absolute_magnitude_h'] !== null ? (float) $favoriteCometRow['absolute_magnitude_h'] : null, 'slope_parameter_g' => $favoriteCometRow['slope_parameter_g'] !== null ? (float) $favoriteCometRow['slope_parameter_g'] : null, 'is_favorite' => true, ]; } $favoriteSatelliteStmt = $pdo->prepare( "SELECT s.id, s.norad_cat_id, s.object_name, s.object_id, s.classification_type, s.std_mag, s.rcs_size, s.rcs, t.epoch, t.mean_motion, t.eccentricity, t.inclination, t.ra_of_asc_node, t.arg_of_pericenter, t.mean_anomaly, t.ephemeris_type, t.element_set_no, t.rev_at_epoch, t.bstar, t.mean_motion_dot, t.mean_motion_ddot FROM app_user_satellites us INNER JOIN sat_satellites s ON s.id = us.satellite_id INNER JOIN sat_tle_current t ON t.satellite_id = s.id WHERE us.user_id = :user_id AND us.is_favorite = 1 AND t.epoch IS NOT NULL AND t.mean_motion IS NOT NULL ORDER BY s.object_name ASC LIMIT 1" ); $favoriteSatelliteStmt->execute([ ':user_id' => (int) $_SESSION['user_id'], ]); $favoriteSatelliteRow = $favoriteSatelliteStmt->fetch(); if (is_array($favoriteSatelliteRow)) { $favoriteSatelliteName = trim((string) ($favoriteSatelliteRow['object_name'] ?? '')); $favoriteSatellitePayload = [[ 'id' => (int) ($favoriteSatelliteRow['id'] ?? 0), 'norad_cat_id' => (int) ($favoriteSatelliteRow['norad_cat_id'] ?? 0), 'object_name' => $favoriteSatelliteName, 'is_favorite' => true, 'std_mag' => $favoriteSatelliteRow['std_mag'] !== null ? (float) $favoriteSatelliteRow['std_mag'] : null, 'rcs_size' => $favoriteSatelliteRow['rcs_size'] !== null ? (string) $favoriteSatelliteRow['rcs_size'] : null, 'rcs' => $favoriteSatelliteRow['rcs'] !== null && (float) $favoriteSatelliteRow['rcs'] > 0 ? (float) $favoriteSatelliteRow['rcs'] : null, 'tle_line1' => monthForecastBuildTleLine1($favoriteSatelliteRow, $favoriteSatelliteRow), 'tle_line2' => monthForecastBuildTleLine2($favoriteSatelliteRow, $favoriteSatelliteRow), ]]; } } } catch (Throwable $e) { $starDataError = $e->getMessage(); } require __DIR__ . '/header.php'; $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'); $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')); $monthStartLocal = new DateTimeImmutable( sprintf('%04d-%02d-01 00:00:00', $selectedYear, $selectedMonth), new DateTimeZone($chartTimezone) ); $monthEndLocal = $monthStartLocal->modify('first day of next month'); $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; } if (!empty($favoriteSatellitePayload)) { $favoriteSatellitePassOutput = []; $favoriteSatellitePassArgs = [ $pythonScriptPath, 'satellite_passes', (string) $chartLatitude, (string) $chartLongitude, (string) $chartElevation, $chartTimezone, $monthStartLocal->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z'), $monthEndLocal->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z'), base64_encode((string) json_encode($favoriteSatellitePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)), ]; $favoriteSatellitePassCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $favoriteSatellitePassArgs)) . ' 2>&1'; exec($favoriteSatellitePassCommand, $favoriteSatellitePassOutput); $favoriteSatellitePassRaw = trim(implode("\n", $favoriteSatellitePassOutput)); $favoriteSatellitePassDecoded = json_decode($favoriteSatellitePassRaw, true); if (is_array($favoriteSatellitePassDecoded) && ($favoriteSatellitePassDecoded['ok'] ?? false)) { $favoriteSatelliteResults = is_array($favoriteSatellitePassDecoded['results'] ?? null) ? $favoriteSatellitePassDecoded['results'] : []; $favoriteSatellitePasses = is_array($favoriteSatelliteResults[0]['passes'] ?? null) ? $favoriteSatelliteResults[0]['passes'] : []; } } if (!empty($favoriteCometPayload)) { $tempFavoriteCometFile = tempnam(sys_get_temp_dir(), 'month_comets_'); if ($tempFavoriteCometFile !== false) { try { file_put_contents($tempFavoriteCometFile, json_encode($favoriteCometPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); $favoriteCometOutput = []; $favoriteCometArgs = [ $pythonScriptPath, 'favorite_comet_events_for_month', (string) $chartLatitude, (string) $chartLongitude, (string) $chartElevation, (string) $selectedYear, (string) $selectedMonth, $chartTimezone, (string) $favoriteCometVisibilitySampleOffsetMinutes, (string) $favoriteCometVisibilityMinAltitudeDeg, (string) $favoriteCometVisibilityMaxSunAltitudeDeg, (string) $favoriteCometVisibilityMaxMagnitude, (string) $favoriteCometVisibilityMinConsecutiveDays, '@' . $tempFavoriteCometFile, ]; $favoriteCometCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $favoriteCometArgs)) . ' 2>&1'; exec($favoriteCometCommand, $favoriteCometOutput); $favoriteCometRaw = trim(implode("\n", $favoriteCometOutput)); $favoriteCometDecoded = json_decode($favoriteCometRaw, true); if (is_array($favoriteCometDecoded) && ($favoriteCometDecoded['ok'] ?? false)) { $favoriteCometEvents = is_array($favoriteCometDecoded['events'] ?? null) ? $favoriteCometDecoded['events'] : []; } } catch (Throwable $e) { } finally { if (is_file($tempFavoriteCometFile)) { @unlink($tempFavoriteCometFile); } } } } $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; } $phaseLabel = trim((string) ($phaseEvent['label'] ?? '')); if ($phaseLabel === 'Erstes Viertel' || $phaseLabel === 'Letztes Viertel') { $phaseLabel = 'Mond ' . $phaseLabel; } $monthEventList[] = [ 'event' => $phaseLabel, '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 größter 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; } $eventLabel = trim((string) ($jupiterMoonSideEvent['label'] ?? 'Alle 4 Jupitermonde auf einer Seite von Jupiter')); $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; } $planetLabel = trim((string) ($changeEvent['planet_label'] ?? 'Planet')); $fromConstellationName = trim((string) ($changeEvent['from_constellation_name'] ?? '')); $toConstellationName = trim((string) ($changeEvent['to_constellation_name'] ?? '')); $changeLabel = (string) ($changeEvent['label'] ?? 'Planet wechselt Sternbild'); if ($fromConstellationName !== '' && $toConstellationName !== '') { $changeLabel = sprintf( '%s wechselt vom Sternbild %s in das Sternbild %s', $planetLabel, $fromConstellationName, $toConstellationName ); } $monthEventList[] = [ 'event' => $changeLabel, 'date' => (string) ($changeEvent['local_date'] ?? ''), 'time' => (string) ($changeEvent['local_time'] ?? ''), 'local_iso' => (string) ($changeEvent['local_iso'] ?? ''), 'type' => 'planet_constellation_change', ]; } foreach ($favoriteCometEvents as $favoriteCometEvent) { if (!is_array($favoriteCometEvent)) { continue; } $favoriteCometKind = trim((string) ($favoriteCometEvent['kind'] ?? '')); $favoriteCometType = match ($favoriteCometKind) { 'good_visibility' => 'favorite_comet_good_visibility', 'perihelion' => 'favorite_comet_perihelion', 'geocentric_minimum' => 'favorite_comet_geocentric_minimum', default => 'favorite_comet_event', }; $monthEventList[] = [ 'event' => (string) ($favoriteCometEvent['label'] ?? 'Kometenereignis'), 'date' => (string) ($favoriteCometEvent['local_date'] ?? ''), 'time' => (string) ($favoriteCometEvent['local_time'] ?? ''), 'local_iso' => (string) ($favoriteCometEvent['local_iso'] ?? ''), 'end_local_iso' => (string) ($favoriteCometEvent['end_local_iso'] ?? ''), 'duration_minutes' => isset($favoriteCometEvent['duration_minutes']) ? (int) $favoriteCometEvent['duration_minutes'] : null, 'type' => $favoriteCometType, ]; } 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, ',', '') : ''; $maximumLocalDate = (string) ($maximum['local_date'] ?? ''); if ($maximumLocalDate !== '') { $parsedMaximumDate = DateTimeImmutable::createFromFormat('Y-m-d', $maximumLocalDate); if ($parsedMaximumDate instanceof DateTimeImmutable) { $maximumLocalDate = $parsedMaximumDate->format('d.m.Y'); } } $monthEventList[] = [ 'event' => 'Mond bedeckt ' . $starLabel . ($starMag !== '' ? ' (' . $starMag . ' mag)' : ''), 'date' => $maximumLocalDate, 'time' => (string) ($maximum['local_time'] ?? ''), 'local_iso' => (string) ($maximum['local_iso'] ?? ''), 'type' => 'moon_occultation', ]; } 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; } $designation = trim((string) ($meteorShowerPeak['designation'] ?? '')); $showerCode = trim((string) ($meteorShowerPeak['shower_code'] ?? '')); $zhr = $meteorShowerPeak['zhr'] ?? null; $zhrVariable = trim((string) ($meteorShowerPeak['zhr_variable'] ?? '')); $zhrRange = monthForecastParseMeteorZhrRange(is_int($zhr) ? $zhr : null, $zhrVariable); $zhrMinEstimate = $zhrRange['min']; $zhrMaxEstimate = $zhrRange['max']; $rateText = ''; if (is_int($zhr) && $zhr >= 0) { $rateText = 'ZHR ca. ' . $zhr; } elseif ($zhrVariable !== '') { $rateText = 'ZHR ca. ' . $zhrVariable; } $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), 'zhr_min_estimate' => $zhrMinEstimate, 'zhr_max_estimate' => $zhrMaxEstimate, 'type' => 'meteor_shower_peak', ]; } foreach ($favoriteSatellitePasses as $satellitePass) { if (!is_array($satellitePass)) { continue; } $peakUtc = trim((string) ($satellitePass['peak_utc'] ?? '')); if ($peakUtc === '') { continue; } $peakAltitude = isset($satellitePass['peak_el']) ? (float) $satellitePass['peak_el'] : null; $peakSunAltitude = isset($satellitePass['peak_sun_altitude_deg']) ? (float) $satellitePass['peak_sun_altitude_deg'] : null; $peakMagnitude = isset($satellitePass['peak_mag']['mag']) ? (float) $satellitePass['peak_mag']['mag'] : null; if ($peakAltitude === null || $peakSunAltitude === null) { continue; } if ($peakAltitude < $favoriteSatelliteBestPassMinAltitudeDeg) { continue; } if ($peakSunAltitude > $favoriteSatelliteBestPassMaxSunAltitudeDeg) { continue; } try { $peakDateTime = (new DateTimeImmutable($peakUtc))->setTimezone(new DateTimeZone($chartTimezone)); } catch (Throwable) { continue; } if ($favoriteSatelliteBestPassOnlyFirstNightHalf && (int) $peakDateTime->format('G') < 12) { continue; } if ($peakMagnitude === null || $peakMagnitude > $favoriteSatelliteBestPassMaxPeakMagnitude) { continue; } $peakAltitudeText = number_format($peakAltitude, 0, ',', ''); $eventLabel = 'Guter Ueberflug ' . ($favoriteSatelliteName !== null && $favoriteSatelliteName !== '' ? $favoriteSatelliteName : 'Favoriten-Satellit'); $peakMagnitudeText = number_format($peakMagnitude, 1, ',', ''); $eventLabel .= ' (max. ' . $peakAltitudeText . ' Grad, ' . $peakMagnitudeText . ' mag)'; $monthEventList[] = [ 'event' => $eventLabel, 'date' => $peakDateTime->format('d.m.Y'), 'time' => $peakDateTime->format('H:i'), 'local_iso' => $peakDateTime->format(DateTimeInterface::ATOM), 'type' => 'favorite_satellite_pass', ]; } usort( $monthEventList, static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? '')) ); $monthNarrativeText = monthForecastBuildNarrativeText($monthEventList, $selectedMonth, $months[$selectedMonth], $selectedYear); $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); $monthChartComets = array_map( static function (array $comet): array { return [ 'id' => (int) ($comet['id'] ?? 0), 'name' => trim((string) (($comet['designation_and_name'] ?? '') !== '' ? $comet['designation_and_name'] : ($comet['designation_packed'] ?? 'Komet'))), 'q' => isset($comet['perihelion_dist_au']) ? (float) $comet['perihelion_dist_au'] : null, 'e' => isset($comet['eccentricity']) ? (float) $comet['eccentricity'] : null, 'w' => isset($comet['arg_perihelion_deg']) ? (float) $comet['arg_perihelion_deg'] : null, 'Om' => isset($comet['ascending_node_deg']) ? (float) $comet['ascending_node_deg'] : null, 'inc' => isset($comet['inclination_deg']) ? (float) $comet['inclination_deg'] : null, 'Tp_y' => isset($comet['year_of_perihelion']) ? (int) $comet['year_of_perihelion'] : null, 'Tp_m' => isset($comet['month_of_perihelion']) ? (int) $comet['month_of_perihelion'] : null, 'Tp_d' => isset($comet['day_of_perihelion']) ? (float) $comet['day_of_perihelion'] : null, 'H' => array_key_exists('absolute_magnitude_h', $comet) && $comet['absolute_magnitude_h'] !== null ? (float) $comet['absolute_magnitude_h'] : null, 'G' => array_key_exists('slope_parameter_g', $comet) && $comet['slope_parameter_g'] !== null ? (float) $comet['slope_parameter_g'] : null, ]; }, $favoriteCometPayload ); $monthChartCometsJson = json_encode($monthChartComets, 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); ?>

Zeitraum wählen

Die Sternkarte wird fuer den 15. des gewählten Monats mit der festen Monatsuhrzeit erzeugt.

Monat Im Ueberblick

Zenitprojektion

,

Südhimmel

,

Mondphasen des Monats in einer Zeile

Ereignisse

Chronologische Monatsliste fuer