= '0' && $value <= '9') { return (int) $value; } $upper = strtoupper($value); if ($upper >= 'A' && $upper <= 'V') { return ord($upper) - ord('A') + 10; } return null; } function decodeMpcPackedDate(?string $packed): ?string { if ($packed === null || strlen($packed) !== 5) { return null; } $centuryMap = [ 'I' => 1800, 'J' => 1900, 'K' => 2000, ]; $centuryCode = strtoupper($packed[0]); if (!isset($centuryMap[$centuryCode])) { return null; } $yearPart = substr($packed, 1, 2); if (!ctype_digit($yearPart)) { return null; } $month = decodeMpcPackedDateChar($packed[3]); $day = decodeMpcPackedDateChar($packed[4]); if ($month === null || $day === null || $month < 1 || $month > 12 || $day < 1 || $day > 31) { return null; } return sprintf('%04d-%02d-%02d', $centuryMap[$centuryCode] + (int) $yearPart, $month, $day); } function buildCometPerihelionIso(array $comet): ?string { $year = isset($comet['year_of_perihelion']) ? (int) $comet['year_of_perihelion'] : 0; $month = isset($comet['month_of_perihelion']) ? (int) $comet['month_of_perihelion'] : 0; $dayValue = isset($comet['day_of_perihelion']) ? (float) $comet['day_of_perihelion'] : 0.0; if ($year <= 0 || $month < 1 || $month > 12 || $dayValue <= 0.0) { return null; } $day = (int) floor($dayValue); if ($day < 1 || $day > 31) { return null; } $fractionalDay = $dayValue - $day; $seconds = (int) round($fractionalDay * 86400.0); try { $date = new DateTimeImmutable(sprintf('%04d-%02d-%02d 00:00:00', $year, $month, $day), new DateTimeZone('UTC')); } catch (Throwable $e) { return null; } return $date->modify(sprintf('+%d seconds', $seconds))->format('c'); } if ($loggedIn && isset($_SESSION['user_id'])) { try { $cfg = require __DIR__ . '/../config/database.php'; $pdo = new PDO( sprintf('mysql:host=%s;dbname=%s;charset=%s', $cfg['host'], $cfg['dbname'], $cfg['charset'] ?? 'utf8mb4'), $cfg['user'], $cfg['pass'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); $stmtLoc = $pdo->prepare( 'SELECT id, name, latitude, longitude, elevation, timezone FROM app_user_locations WHERE user_id = ? AND is_default = 1 LIMIT 1' ); $stmtLoc->execute([(int) $_SESSION['user_id']]); $defaultLocation = $stmtLoc->fetch() ?: null; $stmtFavoriteMinorplanets = $pdo->prepare( 'SELECT mp.id, mp.mp_number, mp.designation_text, mp.desig_packed, mp.epoch_packed, mp.mean_anomaly_deg, mp.arg_perihelion_deg, mp.ascending_node_deg, mp.inclination_deg, mp.eccentricity, mp.mean_motion_deg_per_day, mp.semimajor_axis_au FROM app_user_minorplanets aum INNER JOIN minorplanets_mpc mp ON mp.id = aum.minorplanet_id WHERE aum.user_id = ? ORDER BY aum.is_favorite DESC, mp.mp_number IS NULL ASC, mp.mp_number ASC, mp.designation_text ASC' ); $stmtFavoriteMinorplanets->execute([(int) $_SESSION['user_id']]); foreach ($stmtFavoriteMinorplanets->fetchAll() as $minorplanet) { $minorplanetLabel = trim((string) ($minorplanet['designation_text'] ?? '')); if ($minorplanetLabel === '') { $minorplanetLabel = trim((string) ($minorplanet['desig_packed'] ?? '')); } if ($minorplanetLabel === '') { continue; } if ( $minorplanet['mp_number'] !== null && !str_contains($minorplanetLabel, '(' . (string) $minorplanet['mp_number'] . ')') ) { $minorplanetLabel = '(' . (string) $minorplanet['mp_number'] . ') ' . $minorplanetLabel; } $bodyKey = 'minorplanet:' . (string) $minorplanet['id']; $favoriteMinorplanetOptions[$bodyKey] = $minorplanetLabel; $minorplanetPayloads[$bodyKey] = [ 'id' => (int) $minorplanet['id'], 'designation' => $minorplanetLabel, 'epochDateIso' => decodeMpcPackedDate((string) ($minorplanet['epoch_packed'] ?? '')), 'meanAnomalyDeg' => isset($minorplanet['mean_anomaly_deg']) ? (float) $minorplanet['mean_anomaly_deg'] : null, 'argPerihelionDeg' => isset($minorplanet['arg_perihelion_deg']) ? (float) $minorplanet['arg_perihelion_deg'] : null, 'ascendingNodeDeg' => isset($minorplanet['ascending_node_deg']) ? (float) $minorplanet['ascending_node_deg'] : null, 'inclinationDeg' => isset($minorplanet['inclination_deg']) ? (float) $minorplanet['inclination_deg'] : null, 'eccentricity' => isset($minorplanet['eccentricity']) ? (float) $minorplanet['eccentricity'] : null, 'meanMotionDegPerDay' => isset($minorplanet['mean_motion_deg_per_day']) ? (float) $minorplanet['mean_motion_deg_per_day'] : null, 'semimajorAxisAu' => isset($minorplanet['semimajor_axis_au']) ? (float) $minorplanet['semimajor_axis_au'] : null, ]; } $stmtFavoriteComets = $pdo->prepare( 'SELECT c.id, c.designation_and_name, c.year_of_perihelion, c.month_of_perihelion, c.day_of_perihelion, c.perihelion_dist_au, c.eccentricity, c.arg_perihelion_deg, c.ascending_node_deg, c.inclination_deg FROM app_user_comets auc INNER JOIN comets_mpc c ON c.id = auc.comet_id WHERE auc.user_id = ? ORDER BY auc.is_favorite DESC, c.designation_and_name ASC' ); $stmtFavoriteComets->execute([(int) $_SESSION['user_id']]); foreach ($stmtFavoriteComets->fetchAll() as $comet) { $cometLabel = trim((string) ($comet['designation_and_name'] ?? '')); if ($cometLabel === '') { continue; } $bodyKey = 'comet:' . (string) $comet['id']; $favoriteCometOptions[$bodyKey] = $cometLabel; $cometPayloads[$bodyKey] = [ 'id' => (int) $comet['id'], 'designation' => $cometLabel, 'perihelionIso' => buildCometPerihelionIso($comet), 'perihelionDistanceAu' => isset($comet['perihelion_dist_au']) ? (float) $comet['perihelion_dist_au'] : null, 'eccentricity' => isset($comet['eccentricity']) ? (float) $comet['eccentricity'] : null, 'argPerihelionDeg' => isset($comet['arg_perihelion_deg']) ? (float) $comet['arg_perihelion_deg'] : null, 'ascendingNodeDeg' => isset($comet['ascending_node_deg']) ? (float) $comet['ascending_node_deg'] : null, 'inclinationDeg' => isset($comet['inclination_deg']) ? (float) $comet['inclination_deg'] : null, ]; } $stmtStars = $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 <= 6.0 ORDER BY mag ASC" ); while ($row = $stmtStars->fetch()) { $chartStarData[] = [ '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' => [], ]; } }; $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); $chartConstellationLines = 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()) { $chartLineStarData[] = [ '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, ]; } } catch (Throwable $e) { $defaultLocation = null; $favoriteMinorplanetOptions = []; $favoriteCometOptions = []; $minorplanetPayloads = []; $cometPayloads = []; $chartStarData = []; $chartLineStarData = []; $chartConstellationLines = []; } } $planetOptions = [ 'Sun' => 'Sonne', 'Moon' => 'Mond', 'Mercury' => 'Merkur', 'Venus' => 'Venus', 'Mars' => 'Mars', 'Jupiter' => 'Jupiter', 'Saturn' => 'Saturn', 'Uranus' => 'Uranus', 'Neptune' => 'Neptun', ]; $bodyOptions = $planetOptions + $favoriteMinorplanetOptions + $favoriteCometOptions; $intervalOptions = [ '1' => '1 Minute', '10' => '10 Minuten', '30' => '30 Minuten', '60' => '1 Stunde', '360' => '6 Stunden', '720' => '12 Stunden', '1440' => '1 Tag', '2880' => '2 Tage', '4320' => '3 Tage', '10080' => '7 Tage', '20160' => '14 Tage', '43200' => '30 Tage', ]; $selectedBody = (string) ($_POST['body'] ?? 'Mercury'); if (!isset($bodyOptions[$selectedBody])) { $selectedBody = 'Mercury'; } $selectedInterval = (string) ($_POST['interval_minutes'] ?? '1440'); if (!isset($intervalOptions[$selectedInterval])) { $selectedInterval = '1440'; } $startDtInput = trim((string) ($_POST['start_dt'] ?? '')); if ($startDtInput !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $startDtInput)) { $startDtInput = ''; } $endDtInput = trim((string) ($_POST['end_dt'] ?? '')); if ($endDtInput !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $endDtInput)) { $endDtInput = ''; } function ephemeriden_h(mixed $value): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } function ephemeriden_slug(string $value): string { $value = strtolower(trim($value)); $value = str_replace( ['ä', 'ö', 'ü', 'ß', ' '], ['ae', 'oe', 'ue', 'ss', '-'], $value ); $value = preg_replace('/[^a-z0-9\-]+/', '-', $value) ?? $value; $value = trim($value, '-'); return $value !== '' ? $value : 'ephemeriden'; } function runEphemeridenPythonApi(string $action, array $args): array { $scriptPath = __DIR__ . '/py/ephemeriden_api.py'; $pythonCandidates = PHP_OS_FAMILY === 'Windows' ? ['python', 'py'] : ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python']; $escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args)); $lastError = 'Python konnte nicht gestartet werden.'; foreach ($pythonCandidates as $pythonBinary) { $command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1'; $output = []; $resultCode = 0; exec($command, $output, $resultCode); $joined = trim(implode("\n", $output)); $decoded = json_decode($joined, true); if (is_array($decoded)) { return [ 'ok' => (bool) ($decoded['ok'] ?? false), 'data' => $decoded, 'raw' => $joined, 'command' => $command, ]; } if ($joined !== '') { $lastError = $joined; } elseif ($resultCode !== 0) { $lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary; } } return [ 'ok' => false, 'error' => $lastError, ]; } $pageLocation = null; if (is_array($defaultLocation) && $defaultLocation !== []) { $pageLocation = [ 'name' => trim((string) ($defaultLocation['name'] ?? '')), 'latitude' => isset($defaultLocation['latitude']) ? (float) $defaultLocation['latitude'] : null, 'longitude' => isset($defaultLocation['longitude']) ? (float) $defaultLocation['longitude'] : null, 'elevation' => isset($defaultLocation['elevation']) && $defaultLocation['elevation'] !== null ? (float) $defaultLocation['elevation'] : 0.0, 'timezone' => (string) ($defaultLocation['timezone'] ?? 'Europe/Berlin'), ]; } $defaultLocationName = $pageLocation['name'] ?? ''; $errorMessage = ''; $ephemerisRows = []; $ephemerisChartRowsJson = '[]'; $ephemerisWindowStartIso = null; $exportFormat = (string) ($_POST['export_format'] ?? ''); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) { $errorMessage = 'Kein gültiger Standard-Standort verfügbar. Bitte zuerst in den Einstellungen einen Standard-Standort setzen.'; } elseif ($endDtInput === '') { $errorMessage = 'Bitte ein Enddatum angeben.'; } else { $tz = new DateTimeZone($pageLocation['timezone']); $startDt = $startDtInput !== '' ? DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $startDtInput, $tz) : new DateTimeImmutable('now', $tz); $endDt = DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $endDtInput, $tz); if ($startDt === false || $endDt === false) { $errorMessage = 'Start- oder Enddatum ist ungültig.'; } elseif ($endDt <= $startDt) { $errorMessage = 'Das Enddatum muss nach dem Startdatum liegen.'; } else { $rangeMinutes = (string) (int) round(($endDt->getTimestamp() - $startDt->getTimestamp()) / 60); $pythonAction = 'planet_ephemeris'; $pythonArgs = [ (string) $pageLocation['latitude'], (string) $pageLocation['longitude'], (string) $pageLocation['elevation'], (string) $pageLocation['timezone'], $selectedBody, $selectedInterval, $rangeMinutes, 'minutes', $startDtInput, ]; if (isset($minorplanetPayloads[$selectedBody])) { $pythonAction = 'small_body_ephemeris'; $pythonArgs = [ (string) $pageLocation['latitude'], (string) $pageLocation['longitude'], (string) $pageLocation['elevation'], (string) $pageLocation['timezone'], 'minorplanet', base64_encode(json_encode($minorplanetPayloads[$selectedBody], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}'), $selectedInterval, $rangeMinutes, 'minutes', $startDtInput, ]; } elseif (isset($cometPayloads[$selectedBody])) { $pythonAction = 'small_body_ephemeris'; $pythonArgs = [ (string) $pageLocation['latitude'], (string) $pageLocation['longitude'], (string) $pageLocation['elevation'], (string) $pageLocation['timezone'], 'comet', base64_encode(json_encode($cometPayloads[$selectedBody], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}'), $selectedInterval, $rangeMinutes, 'minutes', $startDtInput, ]; } $pythonResult = runEphemeridenPythonApi($pythonAction, $pythonArgs); if (!$pythonResult['ok']) { $errorMessage = (string) ( $pythonResult['data']['error'] ?? $pythonResult['error'] ?? 'Die Ephemeriden konnten nicht berechnet werden.' ); } else { $ephemerisRows = $pythonResult['data']['rows'] ?? []; $ephemerisWindowStartIso = isset($pythonResult['data']['window']['local_start']) ? (string) $pythonResult['data']['window']['local_start'] : null; if (!is_array($ephemerisRows)) { $ephemerisRows = []; } if ($exportFormat === 'csv' && $ephemerisRows !== []) { $filename = 'ephemeriden-' . ephemeriden_slug((string) ($bodyOptions[$selectedBody] ?? 'objekt')) . '.csv'; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); echo "\xEF\xBB\xBF"; $handle = fopen('php://output', 'wb'); if ($handle === false) { exit; } fputcsv($handle, ['Name des Objekts', 'Datum', 'Uhrzeit', 'RA (J2000)', 'RA (J2000, dezimal h)', 'Dek (J2000)', 'Dek (J2000, dezimal °)', 'Aufgang', 'Untergang'], ';', '"', '\\'); foreach ($ephemerisRows as $row) { fputcsv($handle, [ (string) ($row['object_name'] ?? ''), (string) ($row['date_local'] ?? ''), (string) ($row['time_local'] ?? ''), (string) ($row['ra'] ?? ''), (string) ($row['ra_decimal_hours'] ?? ''), (string) ($row['dec'] ?? ''), (string) ($row['dec_decimal_deg'] ?? ''), (string) (($row['rise'] ?? null) ?? '—'), (string) (($row['set'] ?? null) ?? '—'), ], ';', '"', '\\'); } fclose($handle); exit; } } } } } if ($ephemerisRows !== []) { $chartRows = []; foreach ($ephemerisRows as $row) { $azimuthDeg = isset($row['azimuth_deg']) ? (float) $row['azimuth_deg'] : null; $altitudeDeg = isset($row['altitude_deg']) ? (float) $row['altitude_deg'] : null; if ($azimuthDeg === null || $altitudeDeg === null) { continue; } $chartRows[] = [ 'object_name' => (string) ($row['object_name'] ?? ''), 'date_local' => (string) ($row['date_local'] ?? ''), 'time_local' => (string) ($row['time_local'] ?? ''), 'local_iso' => (string) ($row['local_iso'] ?? ''), 'ra_decimal_hours' => isset($row['ra_decimal_hours']) ? (float) $row['ra_decimal_hours'] : null, 'dec_decimal_deg' => isset($row['dec_decimal_deg']) ? (float) $row['dec_decimal_deg'] : null, 'azimuth_deg' => $azimuthDeg, 'altitude_deg' => $altitudeDeg, ]; } $ephemerisChartRowsJson = json_encode($chartRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]'; $ephemerisChartConfigJson = json_encode([ 'location' => [ 'name' => $defaultLocationName, 'latitude' => $pageLocation['latitude'] ?? null, 'longitude' => $pageLocation['longitude'] ?? null, 'elevation' => $pageLocation['elevation'] ?? 0.0, 'timezone' => $pageLocation['timezone'] ?? 'Europe/Berlin', ], 'utcTimeIso' => $ephemerisWindowStartIso, 'starLimit' => 4.4, 'starLabelLimit' => 2.0, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: 'null'; $ephemerisStarDataJson = json_encode($chartStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]'; $ephemerisLineStarDataJson = json_encode($chartLineStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]'; $ephemerisConstellationLinesJson = json_encode($chartConstellationLines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]'; } require __DIR__ . '/header.php'; ?>

Ephemeriden

Standard-Standort:
Name des Objekts Datum Uhrzeit RA (J2000) Dek (J2000) Aufgang Untergang
Für die gewählte Anfrage wurden keine Tabellenzeilen erzeugt.
Noch keine Berechnung ausgeführt.