421 lines
18 KiB
PHP
421 lines
18 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);
|
||
}
|
||
}
|
||
|
||
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');
|
||
if (PHP_OS_FAMILY === 'Windows') {
|
||
$pythonBin = 'python';
|
||
} elseif (file_exists('/usr/bin/python3')) {
|
||
$pythonBin = '/usr/bin/python3';
|
||
} elseif (file_exists('/usr/bin/python')) {
|
||
$pythonBin = '/usr/bin/python';
|
||
} else {
|
||
$pythonBin = 'python';
|
||
}
|
||
putenv('PYTHONUTF8=1');
|
||
putenv('PYTHONIOENCODING=utf-8');
|
||
|
||
// Sonne + Mond
|
||
$args = [
|
||
$pythonBin,
|
||
escapeshellarg($scriptPath),
|
||
escapeshellarg('sun_moon_rise_set'),
|
||
escapeshellarg((string)$lat),
|
||
escapeshellarg((string)$lon),
|
||
escapeshellarg((string)$elev),
|
||
escapeshellarg($date),
|
||
escapeshellarg($tz),
|
||
];
|
||
$cmd = implode(' ', $args) . ' 2>&1';
|
||
$out = [];
|
||
$resultCode = 0;
|
||
exec($cmd, $out, $resultCode);
|
||
$rawText = trim(implode("\n", $out));
|
||
$raw = json_decode($rawText, true);
|
||
if (is_array($raw) && ($raw['ok'] ?? false)) {
|
||
$skyData = $raw;
|
||
$nextDate = (new DateTimeImmutable($date, new DateTimeZone($tz)))->modify('+1 day')->format('Y-m-d');
|
||
$argsNext = [
|
||
$pythonBin,
|
||
escapeshellarg($scriptPath),
|
||
escapeshellarg('sun_moon_rise_set'),
|
||
escapeshellarg((string)$lat),
|
||
escapeshellarg((string)$lon),
|
||
escapeshellarg((string)$elev),
|
||
escapeshellarg($nextDate),
|
||
escapeshellarg($tz),
|
||
];
|
||
$cmdNext = implode(' ', $argsNext) . ' 2>&1';
|
||
$outNext = [];
|
||
$resultCodeNext = 0;
|
||
exec($cmdNext, $outNext, $resultCodeNext);
|
||
$rawTextNext = trim(implode("\n", $outNext));
|
||
$rawNext = json_decode($rawTextNext, true);
|
||
if (is_array($rawNext) && ($rawNext['ok'] ?? false)) {
|
||
$skyDataNext = $rawNext;
|
||
}
|
||
} else {
|
||
$pyError = $raw['error'] ?? ($rawText !== '' ? $rawText : 'Fehler beim Berechnen der Aufgangszeiten.');
|
||
}
|
||
|
||
// Planeten
|
||
$args2 = [
|
||
$pythonBin,
|
||
escapeshellarg($scriptPath),
|
||
escapeshellarg('planet_rise_set'),
|
||
escapeshellarg((string)$lat),
|
||
escapeshellarg((string)$lon),
|
||
escapeshellarg((string)$elev),
|
||
escapeshellarg($date),
|
||
escapeshellarg($tz),
|
||
];
|
||
$cmd2 = implode(' ', $args2) . ' 2>&1';
|
||
$out2 = [];
|
||
$resultCode2 = 0;
|
||
exec($cmd2, $out2, $resultCode2);
|
||
$rawText2 = trim(implode("\n", $out2));
|
||
$raw2 = json_decode($rawText2, true);
|
||
if (is_array($raw2) && ($raw2['ok'] ?? false)) {
|
||
$planetData = $raw2;
|
||
} elseif ($pyError === null) {
|
||
$pyError = $raw2['error'] ?? ($rawText2 !== '' ? $rawText2 : '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);
|
||
|
||
$args3 = [
|
||
$pythonBin,
|
||
escapeshellarg($scriptPath),
|
||
escapeshellarg('satellite_passes'),
|
||
escapeshellarg((string)$lat),
|
||
escapeshellarg((string)$lon),
|
||
escapeshellarg((string)$elev),
|
||
escapeshellarg($tz),
|
||
escapeshellarg($dayStart->format('Y-m-d\TH:i:s\Z')),
|
||
escapeshellarg($dayEnd->format('Y-m-d\TH:i:s\Z')),
|
||
escapeshellarg($satsPayload),
|
||
];
|
||
$cmd3 = implode(' ', $args3) . ' 2>&1';
|
||
$out3 = [];
|
||
$resultCode3 = 0;
|
||
exec($cmd3, $out3, $resultCode3);
|
||
$rawText3 = trim(implode("\n", $out3));
|
||
$raw3 = json_decode($rawText3, true);
|
||
if (is_array($raw3) && ($raw3['ok'] ?? false)) {
|
||
$satData = $raw3;
|
||
} else {
|
||
$satError = $raw3['error'] ?? ($rawText3 !== '' ? $rawText3 : 'Fehler beim Berechnen der Satellitenüberflüge.');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
<?php require __DIR__ . '/header.php'; ?>
|
||
|
||
<?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'] ?? [];
|
||
$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): ?>
|
||
<span class="riseset-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'; ?>
|