Files

596 lines
23 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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('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.';
}
}
}
}
?>
<?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>
&mdash; <?= 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'; ?>