'Januar',
2 => 'Februar',
3 => 'Maerz',
4 => 'April',
5 => 'Mai',
6 => 'Juni',
7 => 'Juli',
8 => 'August',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Dezember',
];
$monthSkyTimes = [
1 => ['hour' => 22, 'minute' => 0],
2 => ['hour' => 22, 'minute' => 0],
3 => ['hour' => 22, 'minute' => 0],
4 => ['hour' => 23, 'minute' => 0],
5 => ['hour' => 23, 'minute' => 0],
6 => ['hour' => 23, 'minute' => 30],
7 => ['hour' => 23, 'minute' => 30],
8 => ['hour' => 23, 'minute' => 0],
9 => ['hour' => 23, 'minute' => 0],
10 => ['hour' => 23, 'minute' => 0],
11 => ['hour' => 22, 'minute' => 0],
12 => ['hour' => 22, 'minute' => 0],
];
$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';
$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'));
$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python';
$pythonScriptPath = __DIR__ . '/py/api.py';
$pythonOutput = [];
$pythonExitCode = 0;
$planetErrorText = null;
$pythonArgs = [
$pythonScriptPath,
'month_sky_context',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
(string) $monthTime['hour'],
(string) $monthTime['minute'],
$chartTimezone,
];
$pythonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1';
exec($pythonCommand, $pythonOutput, $pythonExitCode);
$pythonRaw = trim(implode("\n", $pythonOutput));
$pythonDecoded = json_decode($pythonRaw, true);
if (is_array($pythonDecoded) && ($pythonDecoded['ok'] ?? false)) {
if (is_array($pythonDecoded['bodies']['moon'] ?? null)) {
$moon = $pythonDecoded['bodies']['moon'];
$moonData = [
'label' => (string) ($moon['label'] ?? 'Mond'),
'az' => isset($moon['azimuth_deg']) ? (float) $moon['azimuth_deg'] : null,
'alt' => isset($moon['altitude_deg']) ? (float) $moon['altitude_deg'] : null,
'visible' => !empty($moon['visible']),
'phaseFraction' => isset($moon['phase_fraction']) ? (float) $moon['phase_fraction'] : null,
'waxing' => isset($moon['waxing']) ? (bool) $moon['waxing'] : null,
];
}
foreach (($pythonDecoded['bodies']['planets'] ?? []) as $planet) {
if (!is_array($planet)) {
continue;
}
$planetData[] = [
'key' => (string) ($planet['key'] ?? ''),
'label' => (string) ($planet['label'] ?? ''),
'az' => isset($planet['azimuth_deg']) ? (float) $planet['azimuth_deg'] : null,
'alt' => isset($planet['altitude_deg']) ? (float) $planet['altitude_deg'] : null,
'mag' => isset($planet['magnitude']) ? (float) $planet['magnitude'] : null,
'visible' => !empty($planet['visible']),
];
}
} elseif ($pythonRaw !== '') {
$planetErrorText = $pythonRaw;
}
$moonPhaseOutput = [];
$moonPhaseExitCode = 0;
$moonPhaseArgs = [
$pythonScriptPath,
'moon_phases_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$moonPhaseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPhaseArgs)) . ' 2>&1';
exec($moonPhaseCommand, $moonPhaseOutput, $moonPhaseExitCode);
$moonPhaseRaw = trim(implode("\n", $moonPhaseOutput));
$moonPhaseDecoded = json_decode($moonPhaseRaw, true);
if (is_array($moonPhaseDecoded) && ($moonPhaseDecoded['ok'] ?? false)) {
$moonPhaseEvents = is_array($moonPhaseDecoded['phases'] ?? null) ? $moonPhaseDecoded['phases'] : [];
}
$goldenHandleOutput = [];
$goldenHandleExitCode = 0;
$goldenHandleArgs = [
$pythonScriptPath,
'golden_handle_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$goldenHandleCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $goldenHandleArgs)) . ' 2>&1';
exec($goldenHandleCommand, $goldenHandleOutput, $goldenHandleExitCode);
$goldenHandleRaw = trim(implode("\n", $goldenHandleOutput));
$goldenHandleDecoded = json_decode($goldenHandleRaw, true);
if (is_array($goldenHandleDecoded) && ($goldenHandleDecoded['ok'] ?? false)) {
$goldenHandleEvents = is_array($goldenHandleDecoded['events'] ?? null) ? $goldenHandleDecoded['events'] : [];
}
$seasonOutput = [];
$seasonExitCode = 0;
$seasonArgs = [
$pythonScriptPath,
'season_changes_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$seasonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $seasonArgs)) . ' 2>&1';
exec($seasonCommand, $seasonOutput, $seasonExitCode);
$seasonRaw = trim(implode("\n", $seasonOutput));
$seasonDecoded = json_decode($seasonRaw, true);
if (is_array($seasonDecoded) && ($seasonDecoded['ok'] ?? false)) {
$seasonChanges = is_array($seasonDecoded['changes'] ?? null) ? $seasonDecoded['changes'] : [];
}
$timeChangeOutput = [];
$timeChangeExitCode = 0;
$timeChangeArgs = [
$pythonScriptPath,
'time_changes_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$timeChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $timeChangeArgs)) . ' 2>&1';
exec($timeChangeCommand, $timeChangeOutput, $timeChangeExitCode);
$timeChangeRaw = trim(implode("\n", $timeChangeOutput));
$timeChangeDecoded = json_decode($timeChangeRaw, true);
if (is_array($timeChangeDecoded) && ($timeChangeDecoded['ok'] ?? false)) {
$timeChanges = is_array($timeChangeDecoded['events'] ?? null) ? $timeChangeDecoded['events'] : [];
}
$eclipseOutput = [];
$eclipseExitCode = 0;
$eclipseArgs = [
$pythonScriptPath,
'eclipses_for_month',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$eclipseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $eclipseArgs)) . ' 2>&1';
exec($eclipseCommand, $eclipseOutput, $eclipseExitCode);
$eclipseRaw = trim(implode("\n", $eclipseOutput));
$eclipseDecoded = json_decode($eclipseRaw, true);
if (is_array($eclipseDecoded) && ($eclipseDecoded['ok'] ?? false)) {
$eclipseEvents = is_array($eclipseDecoded['events'] ?? null) ? $eclipseDecoded['events'] : [];
}
$moonApsisOutput = [];
$moonApsisExitCode = 0;
$moonApsisArgs = [
$pythonScriptPath,
'moon_apsides_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$moonApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonApsisArgs)) . ' 2>&1';
exec($moonApsisCommand, $moonApsisOutput, $moonApsisExitCode);
$moonApsisRaw = trim(implode("\n", $moonApsisOutput));
$moonApsisDecoded = json_decode($moonApsisRaw, true);
if (is_array($moonApsisDecoded) && ($moonApsisDecoded['ok'] ?? false)) {
$moonApsisEvents = is_array($moonApsisDecoded['events'] ?? null) ? $moonApsisDecoded['events'] : [];
}
$sunApsisOutput = [];
$sunApsisExitCode = 0;
$sunApsisArgs = [
$pythonScriptPath,
'sun_apsides_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$sunApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $sunApsisArgs)) . ' 2>&1';
exec($sunApsisCommand, $sunApsisOutput, $sunApsisExitCode);
$sunApsisRaw = trim(implode("\n", $sunApsisOutput));
$sunApsisDecoded = json_decode($sunApsisRaw, true);
if (is_array($sunApsisDecoded) && ($sunApsisDecoded['ok'] ?? false)) {
$sunApsisEvents = is_array($sunApsisDecoded['events'] ?? null) ? $sunApsisDecoded['events'] : [];
}
$elongationOutput = [];
$elongationExitCode = 0;
$elongationArgs = [
$pythonScriptPath,
'inner_planet_elongations_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$elongationCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $elongationArgs)) . ' 2>&1';
exec($elongationCommand, $elongationOutput, $elongationExitCode);
$elongationRaw = trim(implode("\n", $elongationOutput));
$elongationDecoded = json_decode($elongationRaw, true);
if (is_array($elongationDecoded) && ($elongationDecoded['ok'] ?? false)) {
$innerPlanetElongationEvents = is_array($elongationDecoded['events'] ?? null) ? $elongationDecoded['events'] : [];
}
$venusPeakMagnitudeOutput = [];
$venusPeakMagnitudeExitCode = 0;
$venusPeakMagnitudeArgs = [
$pythonScriptPath,
'venus_peak_magnitude_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$venusPeakMagnitudeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $venusPeakMagnitudeArgs)) . ' 2>&1';
exec($venusPeakMagnitudeCommand, $venusPeakMagnitudeOutput, $venusPeakMagnitudeExitCode);
$venusPeakMagnitudeRaw = trim(implode("\n", $venusPeakMagnitudeOutput));
$venusPeakMagnitudeDecoded = json_decode($venusPeakMagnitudeRaw, true);
if (is_array($venusPeakMagnitudeDecoded) && ($venusPeakMagnitudeDecoded['ok'] ?? false)) {
$venusPeakMagnitudeEvents = is_array($venusPeakMagnitudeDecoded['events'] ?? null) ? $venusPeakMagnitudeDecoded['events'] : [];
}
$outerPlanetOutput = [];
$outerPlanetExitCode = 0;
$outerPlanetArgs = [
$pythonScriptPath,
'outer_planet_events_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$outerPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $outerPlanetArgs)) . ' 2>&1';
exec($outerPlanetCommand, $outerPlanetOutput, $outerPlanetExitCode);
$outerPlanetRaw = trim(implode("\n", $outerPlanetOutput));
$outerPlanetDecoded = json_decode($outerPlanetRaw, true);
if (is_array($outerPlanetDecoded) && ($outerPlanetDecoded['ok'] ?? false)) {
$outerPlanetEvents = is_array($outerPlanetDecoded['events'] ?? null) ? $outerPlanetDecoded['events'] : [];
}
$outerPlanetStationOutput = [];
$outerPlanetStationExitCode = 0;
$outerPlanetStationArgs = [
$pythonScriptPath,
'outer_planet_stations_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$outerPlanetStationCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $outerPlanetStationArgs)) . ' 2>&1';
exec($outerPlanetStationCommand, $outerPlanetStationOutput, $outerPlanetStationExitCode);
$outerPlanetStationRaw = trim(implode("\n", $outerPlanetStationOutput));
$outerPlanetStationDecoded = json_decode($outerPlanetStationRaw, true);
if (is_array($outerPlanetStationDecoded) && ($outerPlanetStationDecoded['ok'] ?? false)) {
$outerPlanetStationEvents = is_array($outerPlanetStationDecoded['events'] ?? null) ? $outerPlanetStationDecoded['events'] : [];
}
$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);
}
}
}
}
foreach ($moonPhaseEvents as $phaseEvent) {
if (!is_array($phaseEvent)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($phaseEvent['label'] ?? ''),
'date' => (string) ($phaseEvent['local_date'] ?? ''),
'time' => (string) ($phaseEvent['local_time'] ?? ''),
'local_iso' => (string) ($phaseEvent['local_iso'] ?? ''),
'type' => 'moon_phase',
];
}
foreach ($goldenHandleEvents as $goldenHandleEvent) {
if (!is_array($goldenHandleEvent)) {
continue;
}
$peakDate = (string) ($goldenHandleEvent['peak_local_date'] ?? '');
$peakTime = (string) ($goldenHandleEvent['peak_local_time'] ?? '');
$goldenHandleLabel = 'Goldener Henkel am Mond';
$monthEventList[] = [
'event' => $goldenHandleLabel,
'date' => $peakDate !== '' ? $peakDate : (string) ($goldenHandleEvent['local_date'] ?? ''),
'time' => $peakTime !== '' ? $peakTime : (string) ($goldenHandleEvent['local_time'] ?? ''),
'local_iso' => (string) ($goldenHandleEvent['peak_local_iso'] ?? $goldenHandleEvent['local_iso'] ?? ''),
'type' => 'golden_handle',
];
}
foreach ($seasonChanges as $seasonChange) {
if (!is_array($seasonChange)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($seasonChange['label'] ?? 'Jahreszeitenwechsel'),
'date' => (string) ($seasonChange['local_date'] ?? ''),
'time' => (string) ($seasonChange['local_time'] ?? ''),
'local_iso' => (string) ($seasonChange['local_iso'] ?? ''),
'type' => 'season_change',
];
}
foreach ($timeChanges as $timeChange) {
if (!is_array($timeChange)) {
continue;
}
$timeChangeKind = (string) ($timeChange['kind'] ?? '');
$timeChangeLabel = $timeChangeKind === 'dst_start'
? 'Beginn der Sommerzeit (Uhren von 02:00 auf 03:00 Uhr vorstellen)'
: ($timeChangeKind === 'dst_end'
? 'Beginn der Normalzeit (Uhren von 03:00 auf 02:00 Uhr zurueckstellen)'
: 'Zeitumstellung');
$timeChangeTime = $timeChangeKind === 'dst_start'
? '02:00'
: ($timeChangeKind === 'dst_end' ? '03:00' : (string) ($timeChange['local_time'] ?? ''));
$monthEventList[] = [
'event' => $timeChangeLabel,
'date' => (string) ($timeChange['local_date'] ?? ''),
'time' => $timeChangeTime,
'local_iso' => (string) ($timeChange['local_iso'] ?? ''),
'type' => 'time_change',
];
}
foreach ($eclipseEvents as $eclipseEvent) {
if (!is_array($eclipseEvent)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($eclipseEvent['label'] ?? 'Finsternis'),
'date' => (string) ($eclipseEvent['local_date'] ?? ''),
'time' => (string) ($eclipseEvent['local_time'] ?? ''),
'local_iso' => (string) ($eclipseEvent['local_iso'] ?? ''),
'type' => (string) ($eclipseEvent['category'] ?? 'eclipse'),
];
}
foreach ($moonApsisEvents as $moonApsisEvent) {
if (!is_array($moonApsisEvent)) {
continue;
}
$moonApsisKind = (string) ($moonApsisEvent['kind'] ?? '');
$distanceText = isset($moonApsisEvent['distance_km'])
? number_format((float) $moonApsisEvent['distance_km'], 0, ',', '.') . ' km'
: '';
$moonApsisLabel = $moonApsisKind === 'perigee'
? 'Mond in Erdnähe (Perigäum)'
: ($moonApsisKind === 'apogee' ? 'Mond in Erdferne (Apogäum)' : (string) ($moonApsisEvent['label'] ?? 'Mondabstand'));
$monthEventList[] = [
'event' => trim($moonApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($moonApsisEvent['local_date'] ?? ''),
'time' => (string) ($moonApsisEvent['local_time'] ?? ''),
'local_iso' => (string) ($moonApsisEvent['local_iso'] ?? ''),
'type' => 'moon_apsis',
];
}
foreach ($sunApsisEvents as $sunApsisEvent) {
if (!is_array($sunApsisEvent)) {
continue;
}
$sunApsisKind = (string) ($sunApsisEvent['kind'] ?? '');
$distanceText = isset($sunApsisEvent['distance_km'])
? number_format((float) $sunApsisEvent['distance_km'], 0, ',', '.') . ' km'
: '';
$sunApsisLabel = $sunApsisKind === 'perihelion'
? 'Erde erreicht sonnennächsten Punkt (Perihel)'
: ($sunApsisKind === 'aphelion' ? 'Erde erreicht sonnenfernsten Punkt (Aphel)' : (string) ($sunApsisEvent['label'] ?? 'Sonnenabstand'));
$monthEventList[] = [
'event' => trim($sunApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($sunApsisEvent['local_date'] ?? ''),
'time' => (string) ($sunApsisEvent['local_time'] ?? ''),
'local_iso' => (string) ($sunApsisEvent['local_iso'] ?? ''),
'type' => 'sun_apsis',
];
}
foreach ($innerPlanetElongationEvents as $elongationEvent) {
if (!is_array($elongationEvent)) {
continue;
}
$elongationPlanet = (string) ($elongationEvent['planet_label'] ?? 'Planet');
$elongationKind = (string) ($elongationEvent['kind'] ?? '');
$elongationText = isset($elongationEvent['elongation_deg'])
? number_format((float) $elongationEvent['elongation_deg'], 2, ',', '') . ' Grad'
: '';
$elongationLabel = $elongationKind === 'western'
? $elongationPlanet . ' größte westliche Elongation'
: ($elongationKind === 'eastern'
? $elongationPlanet . ' größte östliche Elongation'
: (string) ($elongationEvent['label'] ?? 'Elongation'));
$monthEventList[] = [
'event' => trim($elongationLabel . ($elongationText !== '' ? ' (' . $elongationText . ')' : '')),
'date' => (string) ($elongationEvent['local_date'] ?? ''),
'time' => (string) ($elongationEvent['local_time'] ?? ''),
'local_iso' => (string) ($elongationEvent['local_iso'] ?? ''),
'type' => 'planet_elongation',
];
}
foreach ($venusPeakMagnitudeEvents as $venusPeakMagnitudeEvent) {
if (!is_array($venusPeakMagnitudeEvent)) {
continue;
}
$magnitudeText = isset($venusPeakMagnitudeEvent['mag'])
? number_format((float) $venusPeakMagnitudeEvent['mag'], 1, ',', '') . ' mag'
: '';
$monthEventList[] = [
'event' => trim('Venus in groesster Helligkeit' . ($magnitudeText !== '' ? ' (' . $magnitudeText . ')' : '')),
'date' => (string) ($venusPeakMagnitudeEvent['local_date'] ?? ''),
'time' => (string) ($venusPeakMagnitudeEvent['local_time'] ?? ''),
'local_iso' => (string) ($venusPeakMagnitudeEvent['local_iso'] ?? ''),
'type' => 'venus_peak_magnitude',
];
}
foreach ($outerPlanetEvents as $outerPlanetEvent) {
if (!is_array($outerPlanetEvent)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($outerPlanetEvent['label'] ?? 'Planetenereignis'),
'date' => (string) ($outerPlanetEvent['local_date'] ?? ''),
'time' => (string) ($outerPlanetEvent['local_time'] ?? ''),
'local_iso' => (string) ($outerPlanetEvent['local_iso'] ?? ''),
'type' => 'outer_planet_event',
];
}
foreach ($outerPlanetStationEvents as $outerPlanetStationEvent) {
if (!is_array($outerPlanetStationEvent)) {
continue;
}
$stationPlanet = (string) ($outerPlanetStationEvent['planet_label'] ?? 'Planet');
$stationKind = (string) ($outerPlanetStationEvent['kind'] ?? '');
$stationLabel = $stationKind === 'retrograde'
? $stationPlanet . ' stationär, dann rückläufig'
: ($stationKind === 'prograde'
? $stationPlanet . ' stationär, dann rechtläufig'
: (string) ($outerPlanetStationEvent['label'] ?? 'Stationärer Punkt'));
$monthEventList[] = [
'event' => $stationLabel,
'date' => (string) ($outerPlanetStationEvent['local_date'] ?? ''),
'time' => (string) ($outerPlanetStationEvent['local_time'] ?? ''),
'local_iso' => (string) ($outerPlanetStationEvent['local_iso'] ?? ''),
'type' => 'outer_planet_station',
];
}
foreach ($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);
?>