diff --git a/public/monatsvorhersage.php b/public/monatsvorhersage.php index 622cf42..b82d2f6 100644 --- a/public/monatsvorhersage.php +++ b/public/monatsvorhersage.php @@ -11,12 +11,20 @@ $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'); +$starDatabaseMagnitudeLimit = 6.0; +$starDisplayMagnitudeLimit = 4.4; +$starLabelMagnitudeLimit = 2.0; +$messierDatabaseMagnitudeLimit = 4.0; +$messierDisplayMagnitudeLimit = 1.0; +$moonPlanetMaxSeparationDeg = 1.5; +$planetConjunctionMaxSeparationDeg = 1.0; +$moonOccultationMaxMag = 2.0; $years = range($currentYear - 2, $currentYear + 5); $months = [ 1 => 'Januar', 2 => 'Februar', - 3 => 'März', + 3 => 'Maerz', 4 => 'April', 5 => 'Mai', 6 => 'Juni', @@ -41,7 +49,314 @@ $monthSkyTimes = [ 11 => ['hour' => 22, 'minute' => 0], 12 => ['hour' => 22, 'minute' => 0], ]; -$selectedSkyTime = $monthSkyTimes[$selectedMonth] ?? ['hour' => 22, 'minute' => 0]; + +$starData = []; +$lineStarData = []; +$constellationLines = []; +$messierData = []; +$planetData = []; +$moonData = null; +$moonPhaseEvents = []; +$goldenHandleEvents = []; +$moonPlanetApproaches = []; +$planetConjunctions = []; +$eclipseEvents = []; +$moonApsisEvents = []; +$sunApsisEvents = []; +$innerPlanetElongationEvents = []; +$venusPeakMagnitudeEvents = []; +$outerPlanetEvents = []; +$outerPlanetStationEvents = []; +$moonOccultations = []; +$meteorShowerPeaks = []; +$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('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); + } +} + +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'] ?? '')), + ]; + } +} catch (Throwable $e) { + $starDataError = $e->getMessage(); +} require __DIR__ . '/header.php'; @@ -51,12 +366,18 @@ $chartLongitude = isset($currentLocation['longitude']) ? (float) $currentLocatio $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')); + $pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python'; $pythonScriptPath = __DIR__ . '/py/api.py'; $pythonOutput = []; $pythonExitCode = 0; -$pythonPayload = null; -$pythonError = null; +$planetErrorText = null; $pythonArgs = [ $pythonScriptPath, @@ -66,327 +387,2950 @@ $pythonArgs = [ (string) $chartElevation, (string) $selectedYear, (string) $selectedMonth, - (string) ((int) $selectedSkyTime['hour']), - (string) ((int) $selectedSkyTime['minute']), + (string) $monthTime['hour'], + (string) $monthTime['minute'], $chartTimezone, ]; -$command = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1'; -exec($command, $pythonOutput, $pythonExitCode); - +$pythonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1'; +exec($pythonCommand, $pythonOutput, $pythonExitCode); $pythonRaw = trim(implode("\n", $pythonOutput)); -if ($pythonExitCode !== 0) { - $pythonError = $pythonRaw !== '' ? $pythonRaw : 'Python-Aufruf fehlgeschlagen.'; -} else { - $decoded = json_decode($pythonRaw, true); - if (!is_array($decoded)) { - $pythonError = $pythonRaw !== '' ? $pythonRaw : 'Python lieferte kein gültiges JSON.'; - } elseif (!($decoded['ok'] ?? false)) { - $pythonError = (string) ($decoded['error'] ?? 'Python meldete einen Fehler.'); - } else { - $pythonPayload = $decoded; +$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'] : []; +} + +$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'] : []; +} + +$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'] : []; +} + +$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); + } + } } } -$bodyRows = []; -if (is_array($pythonPayload['bodies'] ?? null)) { - $bodyRows[] = $pythonPayload['bodies']['sun'] ?? null; - $bodyRows[] = $pythonPayload['bodies']['moon'] ?? null; - foreach (($pythonPayload['bodies']['planets'] ?? []) as $planet) { - $bodyRows[] = $planet; +foreach ($moonPhaseEvents as $phaseEvent) { + if (!is_array($phaseEvent)) { + continue; } - $bodyRows = array_values(array_filter($bodyRows, static fn ($row) => is_array($row))); + + $monthEventList[] = [ + 'event' => (string) ($phaseEvent['label'] ?? ''), + 'date' => (string) ($phaseEvent['local_date'] ?? ''), + 'time' => (string) ($phaseEvent['local_time'] ?? ''), + 'local_iso' => (string) ($phaseEvent['local_iso'] ?? ''), + 'type' => 'moon_phase', + ]; } -$formatNumber = static function ($value, int $decimals = 1): string { - if (!is_numeric($value)) { - return '–'; +foreach ($goldenHandleEvents as $goldenHandleEvent) { + if (!is_array($goldenHandleEvent)) { + continue; } - return number_format((float) $value, $decimals, ',', ''); -}; + + $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 ($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 ($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 ($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; + } + + $designation = trim((string) ($meteorShowerPeak['designation'] ?? '')); + $showerCode = trim((string) ($meteorShowerPeak['shower_code'] ?? '')); + $zhr = $meteorShowerPeak['zhr'] ?? null; + $zhrVariable = trim((string) ($meteorShowerPeak['zhr_variable'] ?? '')); + $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), + 'type' => 'meteor_shower_peak', + ]; +} + +usort( + $monthEventList, + static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? '')) +); + +$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); ?> +
+
+
+
+

Zeitraum waehlen

+

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

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

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

+
+
+ + +
+ +
+
+ -
-
-
-
-
- - -
+ + diff --git a/public/monatsvorhersage_old.php b/public/monatsvorhersage_old.php index b82d2f6..659d336 100644 --- a/public/monatsvorhersage_old.php +++ b/public/monatsvorhersage_old.php @@ -67,6 +67,7 @@ $innerPlanetElongationEvents = []; $venusPeakMagnitudeEvents = []; $outerPlanetEvents = []; $outerPlanetStationEvents = []; +$jupiterMoonSideEvents = []; $moonOccultations = []; $meteorShowerPeaks = []; $seasonChanges = []; @@ -616,6 +617,23 @@ if (is_array($outerPlanetStationDecoded) && ($outerPlanetStationDecoded['ok'] ?? $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 = [ @@ -930,6 +948,26 @@ foreach ($outerPlanetStationEvents as $outerPlanetStationEvent) { ]; } +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'] ?? ''), + 'type' => 'jupiter_moons_one_side', + ]; +} + foreach ($moonPlanetApproaches as $approachEvent) { if (!is_array($approachEvent)) { continue; diff --git a/public/py/__pycache__/api.cpython-312.pyc b/public/py/__pycache__/api.cpython-312.pyc index 2b43f51..8ccc3cd 100644 Binary files a/public/py/__pycache__/api.cpython-312.pyc and b/public/py/__pycache__/api.cpython-312.pyc differ diff --git a/public/py/api.py b/public/py/api.py index 4208e1f..875adf2 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -1759,6 +1759,326 @@ def action_outer_planet_stations_for_month(args: list[str]) -> dict: } +def action_jupiter_moons_one_side_for_month(args: list[str]) -> dict: + if len(args) != 3: + fail( + "Aktion jupiter_moons_one_side_for_month erwartet 3 Argumente: year month timezone", + extra={"argv": args}, + ) + + try: + year = int(args[0]) + month = int(args[1]) + except ValueError as exc: + fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args}) + + if month < 1 or month > 12: + fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month}) + + timezone_name = args[2] + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: + fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)}) + + local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz) + if month == 12: + local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz) + else: + local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz) + + utc_start = local_start.astimezone(timezone.utc) + utc_end = local_end.astimezone(timezone.utc) + scan_step = timedelta(minutes=10) + x_tolerance_au = 1.0e-6 + jupiter_radius_au = astronomy.JUPITER_EQUATORIAL_RADIUS_KM / astronomy.KM_PER_AU + moon_keys = ["io", "europa", "ganymede", "callisto"] + + def classify_side(dt_utc: datetime) -> tuple[str | None, str | None, dict[str, float], dict[str, float]]: + time_value = dt_to_time(dt_utc) + rotation_eqj_to_ecl = astronomy.Rotation_EQJ_ECL() + jupiter_geo = astronomy.GeoVector(astronomy.Body.Jupiter, time_value, True) + jupiter_helio = astronomy.HelioVector(astronomy.Body.Jupiter, time_value) + moon_states = astronomy.JupiterMoons(time_value) + to_earth_eqj = astronomy.Vector( + -float(jupiter_geo.x), + -float(jupiter_geo.y), + -float(jupiter_geo.z), + time_value, + ) + to_earth_ecl = astronomy.RotateVector(rotation_eqj_to_ecl, to_earth_eqj) + + camera_x = float(to_earth_ecl.x) + camera_y = float(to_earth_ecl.z) + camera_z = -float(to_earth_ecl.y) + camera_len = math.sqrt( + (camera_x * camera_x) + (camera_y * camera_y) + (camera_z * camera_z) + ) + if camera_len <= 1.0e-12: + return None, None, {}, {} + + # Dieselbe Welt-/Kameraorientierung wie in jupitersystem.php: + # Display-Koordinaten sind (x, z, -y), die Kamera schaut aus der Richtung "toEarth". + forward_x = -camera_x / camera_len + forward_y = -camera_y / camera_len + forward_z = -camera_z / camera_len + + up_x = 0.0 + up_y = 1.0 + up_z = 0.0 + + right_x = (up_y * forward_z) - (up_z * forward_y) + right_y = (up_z * forward_x) - (up_x * forward_z) + right_z = (up_x * forward_y) - (up_y * forward_x) + right_len = math.sqrt( + (right_x * right_x) + (right_y * right_y) + (right_z * right_z) + ) + if right_len <= 1.0e-12: + return None, None, {}, {} + + right_x /= right_len + right_y /= right_len + right_z /= right_len + + # Himmlische Ostrichtung am Jupiter-Ort fuer die textliche Richtung oestlich/westlich. + jx = float(jupiter_geo.x) + jy = float(jupiter_geo.y) + east_eqj_x = -jy + east_eqj_y = jx + east_eqj_z = 0.0 + east_eqj_len = math.sqrt( + (east_eqj_x * east_eqj_x) + (east_eqj_y * east_eqj_y) + (east_eqj_z * east_eqj_z) + ) + if east_eqj_len <= 1.0e-12: + return None, None, {}, {} + + east_eqj_x /= east_eqj_len + east_eqj_y /= east_eqj_len + east_eqj_z /= east_eqj_len + + east_ecl = astronomy.RotateVector( + rotation_eqj_to_ecl, + astronomy.Vector(east_eqj_x, east_eqj_y, east_eqj_z, time_value), + ) + east_display_x = float(east_ecl.x) + east_display_y = float(east_ecl.z) + east_display_z = -float(east_ecl.y) + east_screen_x = ( + (east_display_x * right_x) + + (east_display_y * right_y) + + (east_display_z * right_z) + ) + + sun_to_jupiter_x = float(jupiter_helio.x) + sun_to_jupiter_y = float(jupiter_helio.y) + sun_to_jupiter_z = float(jupiter_helio.z) + sun_to_jupiter_len = math.sqrt( + (sun_to_jupiter_x * sun_to_jupiter_x) + + (sun_to_jupiter_y * sun_to_jupiter_y) + + (sun_to_jupiter_z * sun_to_jupiter_z) + ) + if sun_to_jupiter_len <= 1.0e-12: + return None, None, {}, {} + + sun_to_jupiter_x /= sun_to_jupiter_len + sun_to_jupiter_y /= sun_to_jupiter_len + sun_to_jupiter_z /= sun_to_jupiter_len + + projected_x_positions = {} + east_offsets = {} + shadow_flags = {} + for key in moon_keys: + moon_state = getattr(moon_states, key) + moon_ecl = astronomy.RotateVector(rotation_eqj_to_ecl, moon_state) + moon_display_x = float(moon_ecl.x) + moon_display_y = float(moon_ecl.z) + moon_display_z = -float(moon_ecl.y) + projected_x = ( + (moon_display_x * right_x) + + (moon_display_y * right_y) + + (moon_display_z * right_z) + ) + projected_x_positions[key] = projected_x + east_offsets[key] = projected_x * east_screen_x + + moon_x = float(moon_state.x) + moon_y = float(moon_state.y) + moon_z = float(moon_state.z) + shadow_axis_distance = ( + (moon_x * sun_to_jupiter_x) + + (moon_y * sun_to_jupiter_y) + + (moon_z * sun_to_jupiter_z) + ) + perp_x = moon_x - (shadow_axis_distance * sun_to_jupiter_x) + perp_y = moon_y - (shadow_axis_distance * sun_to_jupiter_y) + perp_z = moon_z - (shadow_axis_distance * sun_to_jupiter_z) + shadow_flags[key] = ( + shadow_axis_distance > 0.0 + and math.sqrt((perp_x * perp_x) + (perp_y * perp_y) + (perp_z * perp_z)) < jupiter_radius_au + ) + + projected_side_kind = None + minimum_positive_x = jupiter_radius_au + x_tolerance_au + + if ( + all(x_value > minimum_positive_x for x_value in projected_x_positions.values()) + and not any(shadow_flags.values()) + ): + projected_side_kind = "right" + elif ( + all(x_value < -minimum_positive_x for x_value in projected_x_positions.values()) + and not any(shadow_flags.values()) + ): + projected_side_kind = "left" + + east_west_kind = None + if all(offset > x_tolerance_au for offset in east_offsets.values()): + east_west_kind = "east" + elif all(offset < -x_tolerance_au for offset in east_offsets.values()): + east_west_kind = "west" + + return projected_side_kind, east_west_kind, projected_x_positions, east_offsets + + def refine_transition( + left_utc: datetime, + right_utc: datetime, + target_kind: str, + *, + find_start: bool, + ) -> datetime: + left = left_utc + right = right_utc + + for _ in range(32): + mid = left + (right - left) / 2 + mid_kind, _, _, _ = classify_side(mid) + if find_start: + if mid_kind == target_kind: + right = mid + else: + left = mid + else: + if mid_kind == target_kind: + left = mid + else: + right = mid + + return left + (right - left) / 2 + + def format_duration_label(duration_minutes: int) -> str: + hours, minutes = divmod(max(0, duration_minutes), 60) + if hours > 0 and minutes > 0: + return f"{hours} h {minutes:02d} min" + if hours > 0: + return f"{hours} h" + return f"{minutes} min" + + def direction_text_from_kinds(raw_kind: str | None, east_west_kind: str | None) -> str: + if east_west_kind == "east": + return "oestlich" + if east_west_kind == "west": + return "westlich" + return "westlich" if raw_kind == "left" else "oestlich" + + events: list[dict] = [] + current = utc_start + active_kind, active_east_west_kind, _, _ = classify_side(current) + active_start_utc = utc_start if active_kind is not None else None + previous_time = current + previous_kind = active_kind + current += scan_step + + while current <= utc_end: + current_kind, current_east_west_kind, _, _ = classify_side(current) + + if active_kind is None and previous_kind != current_kind and current_kind is not None: + active_kind = current_kind + active_east_west_kind = current_east_west_kind + active_start_utc = refine_transition(previous_time, current, current_kind, find_start=True) + + elif active_kind is not None and current_kind != active_kind: + active_end_utc = refine_transition(previous_time, current, active_kind, find_start=False) + clamped_start_utc = max(active_start_utc or utc_start, utc_start) + clamped_end_utc = min(active_end_utc, utc_end) + + if clamped_end_utc > clamped_start_utc: + start_local = clamped_start_utc.astimezone(tz) + end_local = clamped_end_utc.astimezone(tz) + duration_minutes = max( + 1, + int(round((clamped_end_utc - clamped_start_utc).total_seconds() / 60.0)), + ) + side_text = direction_text_from_kinds(active_kind, active_east_west_kind) + events.append({ + "kind": active_kind, + "direction": active_east_west_kind, + "label": f"Alle 4 Jupitermonde {side_text} von Jupiter", + "duration_label": format_duration_label(duration_minutes), + "duration_minutes": duration_minutes, + "utc_iso": clamped_start_utc.isoformat().replace("+00:00", "Z"), + "local_iso": start_local.isoformat(), + "local_date": start_local.strftime("%d.%m.%Y"), + "local_time": start_local.strftime("%H:%M"), + "end_utc_iso": clamped_end_utc.isoformat().replace("+00:00", "Z"), + "end_local_iso": end_local.isoformat(), + "end_local_date": end_local.strftime("%d.%m.%Y"), + "end_local_time": end_local.strftime("%H:%M"), + }) + + active_kind = None + active_east_west_kind = None + active_start_utc = None + + if current_kind is not None: + active_kind = current_kind + active_east_west_kind = current_east_west_kind + active_start_utc = refine_transition(previous_time, current, current_kind, find_start=True) + + previous_time = current + previous_kind = current_kind + current += scan_step + + if active_kind is not None and active_start_utc is not None: + clamped_start_utc = max(active_start_utc, utc_start) + clamped_end_utc = utc_end + if clamped_end_utc > clamped_start_utc: + start_local = clamped_start_utc.astimezone(tz) + end_local = clamped_end_utc.astimezone(tz) + duration_minutes = max( + 1, + int(round((clamped_end_utc - clamped_start_utc).total_seconds() / 60.0)), + ) + side_text = direction_text_from_kinds(active_kind, active_east_west_kind) + events.append({ + "kind": active_kind, + "direction": active_east_west_kind, + "label": f"Alle 4 Jupitermonde {side_text} von Jupiter", + "duration_label": format_duration_label(duration_minutes), + "duration_minutes": duration_minutes, + "utc_iso": clamped_start_utc.isoformat().replace("+00:00", "Z"), + "local_iso": start_local.isoformat(), + "local_date": start_local.strftime("%d.%m.%Y"), + "local_time": start_local.strftime("%H:%M"), + "end_utc_iso": clamped_end_utc.isoformat().replace("+00:00", "Z"), + "end_local_iso": end_local.isoformat(), + "end_local_date": end_local.strftime("%d.%m.%Y"), + "end_local_time": end_local.strftime("%H:%M"), + }) + + events.sort(key=lambda item: item["local_iso"]) + + return { + "ok": True, + "action": "jupiter_moons_one_side_for_month", + "selected": { + "year": year, + "month": month, + "timezone": timezone_name, + }, + "events": events, + } + + def action_moon_star_occultations_for_month(args: list[str]) -> dict: if len(args) != 7: fail( @@ -2815,7 +3135,7 @@ def action_satellite_passes(args: list[str]) -> dict: def main() -> None: if len(sys.argv) < 2: - fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "moon_star_occultations_for_month"]}) + fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "moon_star_occultations_for_month"]}) action = sys.argv[1] args = sys.argv[2:] @@ -2915,12 +3235,17 @@ def main() -> None: print(json.dumps(result, ensure_ascii=False)) return + if action == "jupiter_moons_one_side_for_month": + result = action_jupiter_moons_one_side_for_month(args) + print(json.dumps(result, ensure_ascii=False)) + return + if action == "moon_star_occultations_for_month": result = action_moon_star_occultations_for_month(args) print(json.dumps(result, ensure_ascii=False)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "moon_star_occultations_for_month"]}) if __name__ == "__main__":