560 lines
22 KiB
PHP
560 lines
22 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
session_start();
|
||
|
||
$loggedIn = isset($_SESSION['user_id']);
|
||
|
||
if (!function_exists('h')) {
|
||
function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); }
|
||
}
|
||
if (!function_exists('risesetTime')) {
|
||
function risesetTime(array $ev, string $key): string { return (!empty($ev[$key]['found'])) ? htmlspecialchars($ev[$key]['local_time'], ENT_QUOTES, 'UTF-8') : '—'; }
|
||
}
|
||
if (!function_exists('fmtLocalTime')) {
|
||
function fmtLocalTime(string $utcIso, string $tz): string { return (new DateTime($utcIso))->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('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'] ?? []);
|
||
$civilDusk = eventDateTime($ev['civil_dusk'] ?? []);
|
||
$civilDawnNext = eventDateTime($evNext['civil_dawn'] ?? []);
|
||
|
||
$nightSeconds = 0;
|
||
$twilightSeconds = 0;
|
||
|
||
foreach ($intervals as [$from, $to]) {
|
||
$nightSeconds += intervalOverlapSeconds($from, $to, $astroDusk, $astroDawnNext);
|
||
$twilightSeconds += intervalOverlapSeconds($from, $to, $civilDusk, $astroDusk);
|
||
$twilightSeconds += intervalOverlapSeconds($from, $to, $astroDawnNext, $civilDawnNext);
|
||
}
|
||
|
||
if ($nightSeconds > 0) {
|
||
return ['class' => 'planet-visibility-good', 'label' => 'nachts gut'];
|
||
}
|
||
|
||
if ($twilightSeconds > 0) {
|
||
return ['class' => 'planet-visibility-warn', 'label' => 'in der Daemmerung'];
|
||
}
|
||
|
||
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.';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
<?php require __DIR__ . '/header.php'; ?>
|
||
<style>
|
||
.planet-visibility-good {
|
||
color: #4fb36a;
|
||
}
|
||
|
||
.planet-visibility-good + .riseset-val,
|
||
.planet-visibility-good + .riseset-val + .riseset-val {
|
||
color: #4fb36a;
|
||
}
|
||
|
||
.planet-visibility-warn {
|
||
color: #d9a441;
|
||
}
|
||
|
||
.planet-visibility-warn + .riseset-val,
|
||
.planet-visibility-warn + .riseset-val + .riseset-val {
|
||
color: #d9a441;
|
||
}
|
||
|
||
.planet-visibility-bad {
|
||
color: #cf5a5a;
|
||
}
|
||
|
||
.planet-visibility-bad + .riseset-val,
|
||
.planet-visibility-bad + .riseset-val + .riseset-val {
|
||
color: #cf5a5a;
|
||
}
|
||
|
||
</style>
|
||
|
||
<?php if (!$loggedIn): ?>
|
||
|
||
<div class="public-hero">
|
||
<h2>Das Universum erwartet dich</h2>
|
||
<p>Satellitentracking, Himmelsvorschau und Astronomiewerkzeuge für Sonneberg und darüber hinaus.</p>
|
||
<div class="btn-row">
|
||
<a href="login.php" class="btn btn-primary">Anmelden</a>
|
||
<a href="register.php" class="btn btn-secondary">Registrieren</a>
|
||
</div>
|
||
</div>
|
||
|
||
<?php else: ?>
|
||
|
||
<?php if (!$defaultLocation): ?>
|
||
<p class="hint">Noch kein Standort hinterlegt. <a href="settings.php">Jetzt anlegen</a></p>
|
||
<?php elseif ($pyError): ?>
|
||
<ul class="msg-error"><li><?= h($pyError) ?></li></ul>
|
||
<?php else: ?>
|
||
|
||
<p class="hint rise-set-location">
|
||
Aufgangs- und Untergangszeiten für
|
||
<strong><?= h($defaultLocation['name']) ?></strong>
|
||
— <?= h((new DateTimeImmutable('now', new DateTimeZone($tz)))->format('d.m.Y')) ?>
|
||
</p>
|
||
|
||
<?php
|
||
$ev = $skyData['events'] ?? [];
|
||
$evNext = $skyDataNext['events'] ?? [];
|
||
$dayStartLocal = new DateTimeImmutable($date . ' 00:00:00', new DateTimeZone($tz));
|
||
$dayEndLocal = $dayStartLocal->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;
|
||
?>
|
||
<div class="card riseset-card">
|
||
<h2>Heute</h2>
|
||
<div class="riseset-grid">
|
||
<span class="riseset-label riseset-main">☀ Sonne</span>
|
||
<span class="riseset-val riseset-main">↑ <?= risesetTime($ev, 'sunrise') ?></span>
|
||
<span class="riseset-val riseset-main">↓ <?= risesetTime($ev, 'sunset') ?></span>
|
||
|
||
<span class="riseset-label riseset-dim">Bürgerl.</span>
|
||
<span class="riseset-val riseset-dim"><?= risesetTime($ev, 'civil_dawn') ?></span>
|
||
<span class="riseset-val riseset-dim"><?= risesetTime($ev, 'civil_dusk') ?></span>
|
||
|
||
<span class="riseset-label riseset-dim">Nautisch</span>
|
||
<span class="riseset-val riseset-dim"><?= risesetTime($ev, 'nautical_dawn') ?></span>
|
||
<span class="riseset-val riseset-dim"><?= risesetTime($ev, 'nautical_dusk') ?></span>
|
||
|
||
<span class="riseset-label riseset-dim">Astronom.</span>
|
||
<span class="riseset-val riseset-dim"><?= risesetTime($ev, 'astro_dawn') ?></span>
|
||
<span class="riseset-val riseset-dim"><?= risesetTime($ev, 'astro_dusk') ?></span>
|
||
|
||
<span class="riseset-sep"></span><span class="riseset-sep"></span><span class="riseset-sep"></span>
|
||
|
||
<span class="riseset-label riseset-moon">☽ Mond</span>
|
||
<span class="riseset-val riseset-moon">↑ <?= risesetTime($ev, 'moonrise') ?></span>
|
||
<span class="riseset-val riseset-moon">↓ <?= risesetTime($ev, 'moonset') ?></span>
|
||
|
||
<span class="riseset-sep"></span><span class="riseset-sep"></span><span class="riseset-sep"></span>
|
||
|
||
<?php foreach (($planetData['planets'] ?? []) as $planet): ?>
|
||
<?php $planetVisibility = planetVisibilityClass($planet, $dayStartLocal, $dayEndLocal, $ev, $evNext); ?>
|
||
<span class="riseset-label <?= h($planetVisibility['class']) ?>" title="<?= h($planetVisibility['label']) ?>"><?= h($planet['name']) ?></span>
|
||
<span class="riseset-val">↑ <?= ($planet['rise']['found'] ?? false) ? h($planet['rise']['local_time']) : '—' ?></span>
|
||
<span class="riseset-val">↓ <?= ($planet['set']['found'] ?? false) ? h($planet['set']['local_time']) : '—' ?></span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card riseset-card">
|
||
<h2>Deep Sky</h2>
|
||
<?php if ($deepSkyDurationMinutes === null): ?>
|
||
<p class="hint">Keine vollstaendige astronomische Nacht im berechneten Zeitraum.</p>
|
||
<?php else: ?>
|
||
<div class="riseset-grid">
|
||
<span class="riseset-label riseset-main">Beginn</span>
|
||
<span class="riseset-val riseset-main"><?= h($deepSkyStart->format('H:i')) ?></span>
|
||
<span class="riseset-val riseset-main"><?= h($deepSkyStart->format('d.m.')) ?></span>
|
||
|
||
<span class="riseset-label riseset-main">Ende</span>
|
||
<span class="riseset-val riseset-main"><?= h($deepSkyEnd->format('H:i')) ?></span>
|
||
<span class="riseset-val riseset-main"><?= h($deepSkyEnd->format('d.m.')) ?></span>
|
||
|
||
<span class="riseset-label riseset-dim">Dauer</span>
|
||
<span class="riseset-val riseset-dim"><?= h(formatDurationHoursMinutes($deepSkyDurationMinutes)) ?></span>
|
||
<span class="riseset-val riseset-dim">astronomische Nacht</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($satError): ?>
|
||
<div class="card riseset-card">
|
||
<h2>☆ Satellit</h2>
|
||
<p class="hint"><?= h($satError) ?></p>
|
||
</div>
|
||
<?php elseif ($satData): ?>
|
||
<?php
|
||
$favName = '';
|
||
$favPasses = [];
|
||
foreach (($satData['results'] ?? []) as $sat) {
|
||
if ($sat['is_favorite']) {
|
||
$favName = $sat['name'];
|
||
$favPasses = $sat['passes'] ?? [];
|
||
break;
|
||
}
|
||
}
|
||
usort($favPasses, fn($a, $b) => strcmp($a['aos_utc'], $b['aos_utc']));
|
||
?>
|
||
<div class="card riseset-card">
|
||
<h2>☆ <?= h($favName ?: 'Favorit') ?></h2>
|
||
<?php if (empty($favPasses)): ?>
|
||
<p class="hint">Heute keine Überflüge.</p>
|
||
<?php else: ?>
|
||
<div class="riseset-grid sat-passes-grid">
|
||
<span class="riseset-label riseset-dim">AOS</span>
|
||
<span class="riseset-label riseset-dim">LOS</span>
|
||
<span class="riseset-label riseset-dim">Max</span>
|
||
<?php foreach ($favPasses as $p): ?>
|
||
<span class="riseset-val riseset-main"><?= fmtLocalTime($p['aos_utc'], $tz) ?> <small><?= $p['aos_az'] !== null ? azDir((float)$p['aos_az']) : '' ?></small></span>
|
||
<span class="riseset-val"><?= fmtLocalTime($p['los_utc'], $tz) ?> <small><?= $p['los_az'] !== null ? azDir((float)$p['los_az']) : '' ?></small></span>
|
||
<span class="riseset-val riseset-dim"><?= round((float)$p['peak_el']) ?>°</span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php require __DIR__ . '/footer.php'; ?>
|