setTimezone(new DateTimeZone($tz))->format('H:i'); } } if (!function_exists('azDir')) { function azDir(float $az): string { $dirs = ['N','NO','O','SO','S','SW','W','NW']; return $dirs[(int)round($az / 45) % 8]; } } if (!function_exists('formatDurationHoursMinutes')) { function formatDurationHoursMinutes(int $minutes): string { $minutes = max(0, $minutes); $hours = intdiv($minutes, 60); $restMinutes = $minutes % 60; if ($hours > 0 && $restMinutes > 0) { return sprintf('%d Stunden %d Minuten', $hours, $restMinutes); } if ($hours > 0) { return sprintf('%d Stunden', $hours); } return sprintf('%d Minuten', $restMinutes); } } if (!function_exists('eventDateTime')) { function eventDateTime(array $event): ?DateTimeImmutable { if (empty($event['found']) || empty($event['local_iso'])) { return null; } try { return new DateTimeImmutable((string)$event['local_iso']); } catch (Exception) { return null; } } } if (!function_exists('intervalOverlapSeconds')) { function intervalOverlapSeconds(?DateTimeImmutable $aStart, ?DateTimeImmutable $aEnd, ?DateTimeImmutable $bStart, ?DateTimeImmutable $bEnd): int { if ($aStart === null || $aEnd === null || $bStart === null || $bEnd === null) { return 0; } $start = max($aStart->getTimestamp(), $bStart->getTimestamp()); $end = min($aEnd->getTimestamp(), $bEnd->getTimestamp()); return max(0, $end - $start); } } if (!function_exists('intervalDurationSeconds')) { function intervalDurationSeconds(?DateTimeImmutable $start, ?DateTimeImmutable $end): int { if ($start === null || $end === null) { return 0; } return max(0, $end->getTimestamp() - $start->getTimestamp()); } } if (!function_exists('planetVisibleIntervalsForDay')) { function planetVisibleIntervalsForDay(array $planet, DateTimeImmutable $dayStart, DateTimeImmutable $dayEnd): array { $rise = eventDateTime($planet['rise'] ?? []); $set = eventDateTime($planet['set'] ?? []); if ($rise && $set) { if ($rise <= $set) { return [[$rise, $set]]; } return [ [$dayStart, $set], [$rise, $dayEnd], ]; } if ($rise) { return [[$rise, $dayEnd]]; } if ($set) { return [[$dayStart, $set]]; } return []; } } if (!function_exists('planetVisibilityClass')) { function planetVisibilityClass(array $planet, DateTimeImmutable $dayStart, DateTimeImmutable $dayEnd, array $ev, array $evNext): array { $intervals = planetVisibleIntervalsForDay($planet, $dayStart, $dayEnd); if ($intervals === []) { return ['class' => 'planet-visibility-bad', 'label' => 'am Tag']; } $astroDusk = eventDateTime($ev['astro_dusk'] ?? []); $astroDawnNext = eventDateTime($evNext['astro_dawn'] ?? []); $nauticalDusk = eventDateTime($ev['nautical_dusk'] ?? []); $civilDusk = eventDateTime($ev['civil_dusk'] ?? []); $nauticalDawnNext = eventDateTime($evNext['nautical_dawn'] ?? []); $civilDawnNext = eventDateTime($evNext['civil_dawn'] ?? []); $nightSeconds = 0; $nauticalSeconds = 0; $twilightSeconds = 0; $visibleSeconds = 0; foreach ($intervals as [$from, $to]) { $visibleSeconds += intervalDurationSeconds($from, $to); $nightSeconds += intervalOverlapSeconds($from, $to, $astroDusk, $astroDawnNext); $nauticalSeconds += intervalOverlapSeconds($from, $to, $nauticalDusk, $astroDusk); $nauticalSeconds += intervalOverlapSeconds($from, $to, $astroDawnNext, $nauticalDawnNext); $twilightSeconds += intervalOverlapSeconds($from, $to, $civilDusk, $nauticalDusk); $twilightSeconds += intervalOverlapSeconds($from, $to, $nauticalDawnNext, $civilDawnNext); } if ($visibleSeconds <= 0) { return ['class' => 'planet-visibility-bad', 'label' => 'am Tag']; } $nightRatio = $nightSeconds / $visibleSeconds; $darkRatio = ($nightSeconds + $nauticalSeconds) / $visibleSeconds; if ($nightSeconds >= 3600 && $nightRatio >= 0.35) { return ['class' => 'planet-visibility-good', 'label' => 'nachts gut sichtbar']; } if (($nightSeconds + $nauticalSeconds) >= 5400 && $darkRatio >= 0.35) { return ['class' => 'planet-visibility-good', 'label' => 'in dunkler Daemmerung gut sichtbar']; } if ($nightSeconds > 0) { return ['class' => 'planet-visibility-warn', 'label' => 'nur teilweise in der astronomischen Nacht']; } if ($nauticalSeconds > 0) { return ['class' => 'planet-visibility-warn', 'label' => 'nur in nautischer Daemmerung sichtbar']; } if (($nightSeconds + $twilightSeconds) > 0) { return ['class' => 'planet-visibility-warn', 'label' => 'nur teilweise nachts / in der Dämmerung']; } return ['class' => 'planet-visibility-bad', 'label' => 'am Tag']; } } if (!function_exists('indexPythonCandidates')) { function indexPythonCandidates(): array { if (PHP_OS_FAMILY === 'Windows') { $candidates = []; $localAppData = getenv('LOCALAPPDATA'); if (is_string($localAppData) && $localAppData !== '') { $localPython = $localAppData . '\\Python\\bin\\python.exe'; if (is_file($localPython)) { $candidates[] = escapeshellarg($localPython); } } return array_values(array_unique(array_merge( $candidates, ['py -3', 'py', 'python'] ))); } return ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python']; } } if (!function_exists('runIndexPythonCommand')) { function runIndexPythonCommand(string $scriptPath, array $args): array { putenv('PYTHONUTF8=1'); putenv('PYTHONIOENCODING=utf-8'); $escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath], $args)); $lastError = 'Python konnte nicht gestartet werden.'; foreach (indexPythonCandidates() 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, ]; } } function idx_tleChecksum(string $line): int { $sum = 0; for ($i = 0; $i < 68; $i++) { $c = $line[$i]; if (ctype_digit($c)) $sum += (int)$c; elseif ($c === '-') $sum += 1; } return $sum % 10; } function idx_tleExp(float $val): string { if (abs($val) < 1e-99) return ' 00000-0'; $sign = $val >= 0 ? '+' : '-'; $abs = abs($val); $exp = (int)floor(log10($abs)) + 1; $mantissa = (int)round($abs / pow(10, $exp) * 1e5); if ($mantissa >= 100000) { $mantissa = 10000; $exp++; } return sprintf('%s%05d%s%d', $sign, $mantissa, $exp >= 0 ? '+' : '-', abs($exp)); } function idx_tleMmDot(float $val): string { return sprintf('%s.%08d', $val >= 0 ? ' ' : '-', (int)round(abs($val) * 1e8)); } function idx_tleEpoch(string $epochStr): string { $dt = new DateTimeImmutable($epochStr, new DateTimeZone('UTC')); $yy = (int)$dt->format('Y') % 100; $jan1 = new DateTimeImmutable($dt->format('Y') . '-01-01 00:00:00', new DateTimeZone('UTC')); $day = ((float)$dt->format('U') - (float)$jan1->format('U')) / 86400.0 + 1.0; return sprintf('%02d%012.8f', $yy, $day); } function idx_tleIntlDesig(?string $id): string { if (!$id) return str_repeat(' ', 8); if (preg_match('/^(\d{4})-(\d+)([A-Z]*)$/i', trim($id), $m)) { $yy = substr($m[1], 2, 2); return substr(str_pad($yy . str_pad($m[2], 3, '0', STR_PAD_LEFT) . str_pad(strtoupper($m[3]), 3), 8), 0, 8); } return str_pad(substr($id, 0, 8), 8); } function buildTleLine1(array $sat, array $tle): string { $line = sprintf('1 %05d%1s %-8s %14s %10s %8s %8s %1d %4d', (int)$sat['norad_cat_id'], in_array($sat['classification_type'] ?? '', ['U','C','S'], true) ? $sat['classification_type'] : 'U', idx_tleIntlDesig($sat['object_id'] ?? null), idx_tleEpoch((string)$tle['epoch']), idx_tleMmDot((float)($tle['mean_motion_dot'] ?? 0)), idx_tleExp((float)($tle['mean_motion_ddot'] ?? 0)), idx_tleExp((float)($tle['bstar'] ?? 0)), (int)($tle['ephemeris_type'] ?? 0), (int)($tle['element_set_no'] ?? 0) ); $line = str_pad($line, 68); return $line . idx_tleChecksum($line); } function buildTleLine2(array $sat, array $tle): string { $ecc = (float)($tle['eccentricity'] ?? 0); $line = sprintf('2 %05d %8.4f %8.4f %7s %8.4f %8.4f %11.8f%5d', (int)$sat['norad_cat_id'], (float)($tle['inclination'] ?? 0), (float)($tle['ra_of_asc_node'] ?? 0), substr(sprintf('%.7f', $ecc), 2), (float)($tle['arg_of_pericenter'] ?? 0), (float)($tle['mean_anomaly'] ?? 0), (float)($tle['mean_motion'] ?? 0), (int)($tle['rev_at_epoch'] ?? 0) ); $line = str_pad($line, 68); return $line . idx_tleChecksum($line); } // Standort und astronomische Daten nur wenn eingeloggt und Standort vorhanden $skyData = null; $skyDataNext = null; $planetData = null; $satData = null; $pyError = null; $satError = null; if ($loggedIn) { // header.php lädt $currentLocation – wir brauchen ihn vorab $config = require __DIR__ . '/../config/database.php'; $pdo = new PDO( sprintf('mysql:host=%s;dbname=%s;charset=%s', $config['host'], $config['dbname'], $config['charset'] ?? 'utf8mb4'), $config['user'], $config['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] ); $stmtLoc = $pdo->prepare( 'SELECT 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; if ($defaultLocation) { $lat = (float)$defaultLocation['latitude']; $lon = (float)$defaultLocation['longitude']; $elev = (float)($defaultLocation['elevation'] ?? 0); $tz = $defaultLocation['timezone'] ?? 'Europe/Berlin'; $date = (new DateTimeImmutable('now', new DateTimeZone($tz)))->format('Y-m-d'); $scriptPath = realpath(__DIR__ . '/py/api.py'); // Sonne + Mond $pythonResult = runIndexPythonCommand($scriptPath, [ 'sun_moon_rise_set', (string)$lat, (string)$lon, (string)$elev, $date, $tz, ]); if ($pythonResult['ok'] ?? false) { $skyData = $pythonResult['data']; $nextDate = (new DateTimeImmutable($date, new DateTimeZone($tz)))->modify('+1 day')->format('Y-m-d'); $pythonResultNext = runIndexPythonCommand($scriptPath, [ 'sun_moon_rise_set', (string)$lat, (string)$lon, (string)$elev, $nextDate, $tz, ]); if ($pythonResultNext['ok'] ?? false) { $skyDataNext = $pythonResultNext['data']; } } else { $pyError = $pythonResult['data']['error'] ?? $pythonResult['error'] ?? 'Fehler beim Berechnen der Aufgangszeiten.'; } // Planeten $pythonResult2 = runIndexPythonCommand($scriptPath, [ 'planet_rise_set', (string)$lat, (string)$lon, (string)$elev, $date, $tz, ]); if ($pythonResult2['ok'] ?? false) { $planetData = $pythonResult2['data']; } elseif ($pyError === null) { $pyError = $pythonResult2['data']['error'] ?? $pythonResult2['error'] ?? 'Fehler beim Berechnen der Planetenzeiten.'; } // Satelliten-Überflüge heute — Favorit zuerst, dann alle gemerkten $stmtSat = $pdo->prepare( 'SELECT s.id, s.norad_cat_id, s.object_name, s.object_type, s.object_id, s.classification_type, t.epoch, t.mean_motion, t.eccentricity, t.inclination, t.ra_of_asc_node, t.arg_of_pericenter, t.mean_anomaly, t.ephemeris_type, t.element_set_no, t.rev_at_epoch, t.bstar, t.mean_motion_dot, t.mean_motion_ddot, us.is_favorite FROM app_user_satellites us JOIN sat_satellites s ON s.id = us.satellite_id JOIN sat_tle_current t ON t.satellite_id = s.id WHERE us.user_id = ? AND t.epoch IS NOT NULL ORDER BY us.is_favorite DESC, s.object_name ASC LIMIT 3' ); $stmtSat->execute([(int)$_SESSION['user_id']]); $userSats = $stmtSat->fetchAll(); if ($userSats) { $tzObj = new DateTimeZone($tz); $dayStart = new DateTime('today', $tzObj); $dayStart->setTimezone(new DateTimeZone('UTC')); $dayEnd = new DateTime('tomorrow', $tzObj); $dayEnd->setTimezone(new DateTimeZone('UTC')); $satsJson = json_encode(array_map(fn($r) => [ 'id' => $r['id'], 'norad_cat_id' => $r['norad_cat_id'], 'object_name' => $r['object_name'], 'tle_line1' => buildTleLine1($r, $r), 'tle_line2' => buildTleLine2($r, $r), 'is_favorite' => (bool)$r['is_favorite'], ], $userSats), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $satsPayload = base64_encode((string) $satsJson); $pythonResult3 = runIndexPythonCommand($scriptPath, [ 'satellite_passes', (string)$lat, (string)$lon, (string)$elev, $tz, $dayStart->format('Y-m-d\TH:i:s\Z'), $dayEnd->format('Y-m-d\TH:i:s\Z'), $satsPayload, ]); if ($pythonResult3['ok'] ?? false) { $satData = $pythonResult3['data']; } else { $satError = $pythonResult3['data']['error'] ?? $pythonResult3['error'] ?? 'Fehler beim Berechnen der Satellitenüberflüge.'; } } } } ?>
Satellitentracking, Himmelsvorschau und Astronomiewerkzeuge für Sonneberg und darüber hinaus.
Noch kein Standort hinterlegt. Jetzt anlegen
Aufgangs- und Untergangszeiten für = h($defaultLocation['name']) ?> — = h((new DateTimeImmutable('now', new DateTimeZone($tz)))->format('d.m.Y')) ?>
modify('+1 day'); $deepSkyStart = (!empty($ev['astro_dusk']['found']) && !empty($ev['astro_dusk']['local_iso'])) ? new DateTimeImmutable($ev['astro_dusk']['local_iso']) : null; $deepSkyEnd = (!empty($evNext['astro_dawn']['found']) && !empty($evNext['astro_dawn']['local_iso'])) ? new DateTimeImmutable($evNext['astro_dawn']['local_iso']) : null; $deepSkyDurationMinutes = ($deepSkyStart && $deepSkyEnd && $deepSkyEnd > $deepSkyStart) ? (int) floor(($deepSkyEnd->getTimestamp() - $deepSkyStart->getTimestamp()) / 60) : null; ?>Keine vollstaendige astronomische Nacht im berechneten Zeitraum.
= h($satError) ?>
Heute keine Überflüge.