Files

870 lines
32 KiB
PHP

<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Kometen - AstroTools';
$headerIntroPre = 'Himmel & Planeten';
$headerIntroTitle = 'Kometen';
$headerIntroSub = 'Interessante Kometen aus der MPC-Datenbank mit Fokus auf bevorstehende und aktuelle Periheldurchgaenge.';
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function runPythonApi(string $action, array $args): array
{
$scriptPath = __DIR__ . '/py/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,
];
}
function runPythonApiWithJsonPayloadFile(string $action, array $payload): array
{
$tempFile = tempnam(sys_get_temp_dir(), 'skyview_py_');
if ($tempFile === false) {
return [
'ok' => false,
'error' => 'Temporäre Datei für Python-Payload konnte nicht erstellt werden.',
];
}
$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($jsonPayload === false) {
@unlink($tempFile);
return [
'ok' => false,
'error' => 'Python-Payload konnte nicht als JSON kodiert werden.',
];
}
if (file_put_contents($tempFile, $jsonPayload) === false) {
@unlink($tempFile);
return [
'ok' => false,
'error' => 'Temporäre Datei für Python-Payload konnte nicht geschrieben werden.',
];
}
try {
return runPythonApi($action, ['@' . $tempFile]);
} finally {
@unlink($tempFile);
}
}
function formatDecimalGerman(?float $value, int $decimals = 3): string
{
if ($value === null || !is_finite($value)) {
return '-';
}
return number_format($value, $decimals, ',', '.');
}
function formatRaHours(?float $hours): string
{
if ($hours === null || !is_finite($hours)) {
return '-';
}
$normalized = fmod($hours, 24.0);
if ($normalized < 0.0) {
$normalized += 24.0;
}
$totalSeconds = (int) round($normalized * 3600.0);
$raHours = intdiv($totalSeconds, 3600) % 24;
$raMinutes = intdiv($totalSeconds % 3600, 60);
$raSeconds = $totalSeconds % 60;
return sprintf('%02dh %02dm %02ds', $raHours, $raMinutes, $raSeconds);
}
function formatDeclination(?float $degrees): string
{
if ($degrees === null || !is_finite($degrees)) {
return '-';
}
$sign = $degrees < 0.0 ? '-' : '+';
$absolute = abs($degrees);
$deg = (int) floor($absolute);
$minutesFloat = ($absolute - $deg) * 60.0;
$minutes = (int) floor($minutesFloat);
$seconds = (int) round(($minutesFloat - $minutes) * 60.0);
if ($seconds >= 60) {
$seconds -= 60;
$minutes += 1;
}
if ($minutes >= 60) {
$minutes -= 60;
$deg += 1;
}
return sprintf('%s%02d° %02d\' %02d"', $sign, $deg, $minutes, $seconds);
}
function buildPerihelionDate(array $row): ?DateTimeImmutable
{
$year = isset($row['year_of_perihelion']) ? (int) $row['year_of_perihelion'] : 0;
$month = isset($row['month_of_perihelion']) ? (int) $row['month_of_perihelion'] : 0;
$dayValue = isset($row['day_of_perihelion']) ? (float) $row['day_of_perihelion'] : 0.0;
if ($year === 0 || $month < 1 || $month > 12 || $dayValue <= 0) {
return null;
}
$day = (int) floor($dayValue);
if ($day < 1 || $day > 31) {
return null;
}
$fractionalDay = $dayValue - $day;
$seconds = (int) round($fractionalDay * 86400);
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));
}
function classifyOrbitType(string $orbitType): string
{
return match (strtoupper(trim($orbitType))) {
'P' => 'Periodisch',
'C' => 'Nicht periodisch',
'I' => 'Interstellar',
'D' => 'Verschollen / zerfallen',
'A' => 'Asteroidal',
default => trim($orbitType) !== '' ? trim($orbitType) : '-',
};
}
$config = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$config['host'],
$config['dbname'],
$config['charset'] ?? 'utf8mb4'
);
$pdo = new PDO(
$dsn,
$config['user'],
$config['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$userId = isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $userId > 0) {
$action = $_POST['action'] ?? '';
$cometId = (int) ($_POST['comet_id'] ?? 0);
if ($action === 'add_comet' && $cometId > 0) {
$stmtCheck = $pdo->prepare("
SELECT `id`
FROM `comets_mpc`
WHERE `id` = :id
LIMIT 1
");
$stmtCheck->execute([':id' => $cometId]);
if ($stmtCheck->fetch()) {
$stmtInsert = $pdo->prepare("
INSERT INTO `app_user_comets` (
`user_id`,
`comet_id`,
`is_favorite`,
`created_at`
) VALUES (
:user_id,
:comet_id,
0,
NOW()
)
ON DUPLICATE KEY UPDATE
`comet_id` = VALUES(`comet_id`)
");
$stmtInsert->execute([
':user_id' => $userId,
':comet_id' => $cometId,
]);
}
}
if ($action === 'remove_comet' && $cometId > 0) {
$stmtCount = $pdo->prepare("
SELECT COUNT(*)
FROM `app_user_comets`
WHERE `user_id` = :user_id
");
$stmtCount->execute([':user_id' => $userId]);
$cometCount = (int) $stmtCount->fetchColumn();
if ($cometCount > 1) {
$stmtDelete = $pdo->prepare("
DELETE FROM `app_user_comets`
WHERE `user_id` = :user_id
AND `comet_id` = :comet_id
");
$stmtDelete->execute([
':user_id' => $userId,
':comet_id' => $cometId,
]);
}
}
header('Location: comets.php');
exit;
}
$today = new DateTimeImmutable('today', new DateTimeZone('UTC'));
$windowStart = $today->modify('-180 days');
$windowEnd = $today->modify('+365 days');
$queryYearMin = (int) $windowStart->format('Y') - 1;
$queryYearMax = (int) $windowEnd->format('Y') + 1;
$stmtComets = $pdo->prepare("
SELECT
c.*,
CASE WHEN auc.`comet_id` IS NULL THEN 0 ELSE 1 END AS `is_tracked`,
CASE WHEN auc.`is_favorite` = 1 THEN 1 ELSE 0 END AS `is_favorite`
FROM `comets_mpc` c
LEFT JOIN `app_user_comets` auc
ON auc.`comet_id` = c.`id`
AND auc.`user_id` = :user_id
WHERE c.`year_of_perihelion` BETWEEN :year_min AND :year_max
OR auc.`comet_id` IS NOT NULL
ORDER BY c.`year_of_perihelion` ASC, c.`month_of_perihelion` ASC, c.`day_of_perihelion` ASC, c.`designation_and_name` ASC
");
$stmtComets->execute([
':user_id' => $userId,
':year_min' => $queryYearMin,
':year_max' => $queryYearMax,
]);
$cometRows = $stmtComets->fetchAll();
$allComets = [];
$trackedComets = [];
$interestingComets = [];
foreach ($cometRows as $row) {
$perihelionDate = buildPerihelionDate($row);
$daysFromToday = $perihelionDate ? (int) floor(($perihelionDate->getTimestamp() - $today->getTimestamp()) / 86400) : null;
$row['perihelion_date_object'] = $perihelionDate;
$row['perihelion_date_display'] = $perihelionDate ? $perihelionDate->format('d.m.Y H:i') . ' UTC' : '-';
$row['days_from_today'] = $daysFromToday;
$row['orbit_type_label'] = classifyOrbitType((string) ($row['orbit_type'] ?? ''));
$row['absolute_magnitude_display'] = isset($row['absolute_magnitude_h']) && $row['absolute_magnitude_h'] !== null
? formatDecimalGerman((float) $row['absolute_magnitude_h'], 1)
: '-';
$row['photometric_index_display'] = isset($row['slope_parameter_g']) && $row['slope_parameter_g'] !== null
? formatDecimalGerman((float) $row['slope_parameter_g'], 1)
: '-';
$row['estimated_magnitude'] = null;
$row['estimated_magnitude_display'] = '-';
$row['brightness_error'] = null;
$allComets[] = $row;
if ((int) ($row['is_tracked'] ?? 0) === 1) {
$trackedComets[] = $row;
}
if ($perihelionDate !== null && $perihelionDate >= $windowStart && $perihelionDate <= $windowEnd) {
$interestingComets[] = $row;
}
}
usort($trackedComets, static function (array $left, array $right): int {
$favoriteCompare = ((int) $right['is_favorite']) <=> ((int) $left['is_favorite']);
if ($favoriteCompare !== 0) {
return $favoriteCompare;
}
return ((int) ($left['days_from_today'] ?? PHP_INT_MAX)) <=> ((int) ($right['days_from_today'] ?? PHP_INT_MAX));
});
usort($interestingComets, static function (array $left, array $right): int {
$leftDistance = abs((int) ($left['days_from_today'] ?? PHP_INT_MAX));
$rightDistance = abs((int) ($right['days_from_today'] ?? PHP_INT_MAX));
if ($leftDistance !== $rightDistance) {
return $leftDistance <=> $rightDistance;
}
return strcmp((string) $left['designation_and_name'], (string) $right['designation_and_name']);
});
$interestingComets = array_slice($interestingComets, 0, 24);
$visibleCometIds = [];
foreach (array_merge($trackedComets, $interestingComets) as $visibleComet) {
$visibleId = isset($visibleComet['id']) ? (int) $visibleComet['id'] : 0;
if ($visibleId > 0) {
$visibleCometIds[$visibleId] = true;
}
}
$calculationMoment = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$brightnessByCometId = [];
$brightnessError = null;
if ($visibleCometIds !== []) {
$pythonPayload = [
'date' => $calculationMoment->format('Y-m-d\TH:i:s\Z'),
'comets' => array_values(array_map(static function (array $row): array {
return [
'id' => $row['id'] ?? null,
'year_of_perihelion' => $row['year_of_perihelion'] ?? null,
'month_of_perihelion' => $row['month_of_perihelion'] ?? null,
'day_of_perihelion' => $row['day_of_perihelion'] ?? null,
'perihelion_dist_au' => $row['perihelion_dist_au'] ?? null,
'eccentricity' => $row['eccentricity'] ?? null,
'arg_perihelion_deg' => $row['arg_perihelion_deg'] ?? null,
'ascending_node_deg' => $row['ascending_node_deg'] ?? null,
'inclination_deg' => $row['inclination_deg'] ?? null,
'absolute_magnitude_h' => $row['absolute_magnitude_h'] ?? null,
'slope_parameter_g' => $row['slope_parameter_g'] ?? null,
];
}, array_filter($cometRows, static function (array $row) use ($visibleCometIds): bool {
$rowId = isset($row['id']) ? (int) $row['id'] : 0;
return $rowId > 0 && isset($visibleCometIds[$rowId]);
}))),
];
$pythonResult = runPythonApiWithJsonPayloadFile('comet_brightnesses', $pythonPayload);
if (($pythonResult['ok'] ?? false) && isset($pythonResult['data']['results']) && is_array($pythonResult['data']['results'])) {
foreach ($pythonResult['data']['results'] as $result) {
$resultId = isset($result['id']) ? (int) $result['id'] : 0;
if ($resultId > 0) {
$brightnessByCometId[$resultId] = $result;
}
}
} else {
$brightnessError = (string) ($pythonResult['error'] ?? 'Python-Berechnung nicht verfuegbar.');
}
}
$applyBrightness = static function (array $row) use ($brightnessByCometId): array {
$brightnessData = isset($row['id']) ? ($brightnessByCometId[(int) $row['id']] ?? null) : null;
$estimatedMagnitude = isset($brightnessData['estimated_magnitude']) && is_numeric($brightnessData['estimated_magnitude'])
? (float) $brightnessData['estimated_magnitude']
: null;
$raHours = isset($brightnessData['ra_hours']) && is_numeric($brightnessData['ra_hours'])
? (float) $brightnessData['ra_hours']
: null;
$decDeg = isset($brightnessData['dec_deg']) && is_numeric($brightnessData['dec_deg'])
? (float) $brightnessData['dec_deg']
: null;
$heliocentricDistance = isset($brightnessData['heliocentric_distance_au']) && is_numeric($brightnessData['heliocentric_distance_au'])
? (float) $brightnessData['heliocentric_distance_au']
: null;
$geocentricDistance = isset($brightnessData['geocentric_distance_au']) && is_numeric($brightnessData['geocentric_distance_au'])
? (float) $brightnessData['geocentric_distance_au']
: null;
$row['estimated_magnitude'] = $estimatedMagnitude;
$row['estimated_magnitude_display'] = $estimatedMagnitude !== null
? formatDecimalGerman($estimatedMagnitude, 1)
: '-';
$row['ra_hours'] = $raHours;
$row['ra_display'] = formatRaHours($raHours);
$row['dec_deg'] = $decDeg;
$row['dec_display'] = formatDeclination($decDeg);
$row['heliocentric_distance_au'] = $heliocentricDistance;
$row['heliocentric_distance_display'] = formatDecimalGerman($heliocentricDistance, 3);
$row['geocentric_distance_au'] = $geocentricDistance;
$row['geocentric_distance_display'] = formatDecimalGerman($geocentricDistance, 3);
$row['brightness_error'] = is_array($brightnessData) && !empty($brightnessData['error'])
? (string) $brightnessData['error']
: null;
return $row;
};
$trackedComets = array_map($applyBrightness, $trackedComets);
$interestingComets = array_map($applyBrightness, $interestingComets);
$orbitTypeCounts = [];
foreach ($allComets as $comet) {
$orbitKey = (string) ($comet['orbit_type'] ?? '?');
$orbitTypeCounts[$orbitKey] = ($orbitTypeCounts[$orbitKey] ?? 0) + 1;
}
arsort($orbitTypeCounts);
?>
<?php require __DIR__ . '/header.php'; ?>
<style>
.comets-layout {
display: grid;
gap: 1.25rem;
}
.comets-intro {
color: #d9e7ff;
line-height: 1.65;
margin: 0;
}
.comets-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.9rem;
}
.comets-stat {
padding: 1rem 1.1rem;
border-radius: 1rem;
border: 1px solid rgba(130, 170, 255, 0.16);
background:
radial-gradient(circle at top left, rgba(82, 153, 255, 0.12), transparent 36%),
linear-gradient(180deg, rgba(13, 19, 33, 0.96), rgba(8, 11, 20, 0.98));
}
.comets-stat-label {
color: #9db2d4;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.comets-stat-value {
margin-top: 0.45rem;
color: #ffffff;
font-size: 1.8rem;
font-weight: 700;
}
.comets-stat-help {
margin-top: 0.35rem;
color: #c7d6f4;
font-size: 0.92rem;
}
.comets-card {
padding: 1.2rem;
}
.comets-card h2 {
margin-bottom: 0.45rem;
}
.comets-card .hint {
margin-top: 0;
}
.comets-chip-row {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 1rem;
}
.comets-chip {
padding: 0.45rem 0.7rem;
border-radius: 999px;
border: 1px solid rgba(130, 170, 255, 0.18);
background: rgba(255, 255, 255, 0.04);
color: #dce8ff;
font-size: 0.92rem;
}
.comets-table-wrap {
overflow-x: auto;
}
.comets-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.comets-table th,
.comets-table td {
padding: 0.78rem 0.8rem;
border-bottom: 1px solid rgba(130, 170, 255, 0.12);
text-align: left;
vertical-align: top;
}
.comets-table th {
color: #a9bee0;
font-size: 0.84rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.comets-table th[data-sortable] {
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.comets-table th[data-sortable]::after {
content: ' ⇅';
opacity: 0.35;
font-size: 0.78em;
}
.comets-table th[data-sortable][data-sort-dir="asc"]::after {
content: ' ↑';
opacity: 1;
color: #c9a84c;
}
.comets-table th[data-sortable][data-sort-dir="desc"]::after {
content: ' ↓';
opacity: 1;
color: #c9a84c;
}
.comets-table td {
color: #eef4ff;
}
.comets-name {
font-weight: 600;
}
.comets-meta {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin-top: 0.35rem;
}
.comets-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.22rem 0.5rem;
border-radius: 999px;
font-size: 0.8rem;
color: #eef4ff;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.comets-badge.is-favorite {
background: rgba(255, 208, 102, 0.12);
border-color: rgba(255, 208, 102, 0.28);
color: #ffd98b;
}
.comets-badge.is-tracked {
background: rgba(130, 170, 255, 0.12);
border-color: rgba(130, 170, 255, 0.26);
color: #bfd6ff;
}
.comets-days {
font-weight: 600;
}
.comets-days.is-past {
color: #ffb2a8;
}
.comets-days.is-upcoming {
color: #b8f0c8;
}
@media (max-width: 960px) {
.comets-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.comets-summary {
grid-template-columns: 1fr;
}
}
</style>
<div class="comets-layout">
<section class="card comets-card">
<p class="comets-intro">
Diese erste Version liest die vorhandenen Kometen direkt aus <code>comets_mpc</code> und hebt Objekte hervor,
deren Perihel zwischen <strong><?= h($windowStart->format('d.m.Y')) ?></strong> und
<strong><?= h($windowEnd->format('d.m.Y')) ?></strong> liegt.
Nutzer-Favoriten aus <code>app_user_comets</code> werden zusaetzlich separat gezeigt.
</p>
<p class="hint">
Die Spalte <strong>m grob</strong> nutzt jetzt die uebliche MPC-Kometenformel fuer <strong>heute</strong>:
<code>H + 5 log(Delta) + 2,5 * n * log(r)</code>. Sie bleibt trotzdem nur eine Orientierung.
</p>
<p class="hint">
Position und Helligkeit werden fuer <strong><?= h($calculationMoment->format('d.m.Y H:i')) ?> UTC</strong> berechnet.
</p>
<?php if ($brightnessError !== null): ?>
<p class="hint">Die Python-Berechnung der Helligkeit war nicht verfuegbar: <?= h($brightnessError) ?></p>
<?php endif; ?>
<div class="comets-chip-row">
<?php foreach (array_slice($orbitTypeCounts, 0, 5, true) as $orbitType => $count): ?>
<span class="comets-chip"><?= h(classifyOrbitType((string) $orbitType)) ?>: <?= (int) $count ?></span>
<?php endforeach; ?>
</div>
</section>
<section class="comets-summary">
<div class="comets-stat">
<div class="comets-stat-label">Kometen im Blickfenster</div>
<div class="comets-stat-value"><?= count($interestingComets) ?></div>
<div class="comets-stat-help">Perihel im aktuellen Zeitfenster</div>
</div>
<div class="comets-stat">
<div class="comets-stat-label">Meine Kometen</div>
<div class="comets-stat-value"><?= count($trackedComets) ?></div>
<div class="comets-stat-help">Aus den Favoriten uebernommen</div>
</div>
<div class="comets-stat">
<div class="comets-stat-label">Gesamt in Datenbank</div>
<div class="comets-stat-value"><?= count($allComets) ?></div>
<div class="comets-stat-help">Geladene Datensaetze im relevanten Jahresbereich</div>
</div>
<div class="comets-stat">
<div class="comets-stat-label">Zeitraum</div>
<div class="comets-stat-value"><?= h($today->format('Y')) ?></div>
<div class="comets-stat-help"><?= h($windowStart->format('d.m.Y')) ?> bis <?= h($windowEnd->format('d.m.Y')) ?></div>
</div>
</section>
<section class="card comets-card">
<h2>Meine Kometen</h2>
<?php if (empty($trackedComets)): ?>
<p class="hint">Du hast aktuell noch keine Kometen in deinen Favoriten. In <a href="my_favorites.php">Favoriten</a> kannst du welche markieren.</p>
<?php else: ?>
<div class="comets-table-wrap">
<table class="comets-table">
<thead>
<tr>
<th data-sortable>Komet</th>
<th data-sortable>Typ</th>
<th data-sortable>RA</th>
<th data-sortable>Dec</th>
<th data-sortable>Perihel</th>
<th data-sortable>Abstand</th>
<th data-sortable title="Grobe Helligkeitsschaetzung fuer heute nach H + 5 log(Delta) + 2,5 * n * log(r).">m grob</th>
<?php if ($loggedIn): ?>
<th></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($trackedComets as $comet): ?>
<?php
$daysClass = ((int) ($comet['days_from_today'] ?? 0)) < 0 ? 'is-past' : 'is-upcoming';
$daysLabel = $comet['days_from_today'] === null
? '-'
: ((int) $comet['days_from_today'] === 0
? 'Heute'
: (((int) $comet['days_from_today'] > 0 ? '+' : '') . (int) $comet['days_from_today'] . ' Tage'));
$perihelionTs = $comet['perihelion_date_object'] instanceof DateTimeImmutable ? $comet['perihelion_date_object']->getTimestamp() : '';
?>
<tr>
<td>
<div class="comets-name"><?= h((string) $comet['designation_and_name']) ?></div>
<div class="comets-meta">
<?php if ((int) $comet['is_favorite'] === 1): ?>
<span class="comets-badge is-favorite">Favorit</span>
<?php endif; ?>
<span class="comets-badge is-tracked">Beobachtet</span>
<?php if (!empty($comet['reference_text'])): ?>
<span class="comets-badge"><?= h((string) $comet['reference_text']) ?></span>
<?php endif; ?>
</div>
</td>
<td><?= h((string) $comet['orbit_type_label']) ?></td>
<td data-sort-value="<?= h((string) ($comet['ra_hours'] ?? '')) ?>"><?= h((string) $comet['ra_display']) ?></td>
<td data-sort-value="<?= h((string) ($comet['dec_deg'] ?? '')) ?>"><?= h((string) $comet['dec_display']) ?></td>
<td data-sort-value="<?= h((string) $perihelionTs) ?>"><?= h((string) $comet['perihelion_date_display']) ?></td>
<td data-sort-value="<?= h((string) ($comet['days_from_today'] ?? '')) ?>"><span class="comets-days <?= $daysClass ?>"><?= h($daysLabel) ?></span></td>
<td data-sort-value="<?= h((string) ($comet['estimated_magnitude'] ?? '')) ?>" title="r = <?= h((string) $comet['heliocentric_distance_display']) ?> AE, Δ = <?= h((string) $comet['geocentric_distance_display']) ?> AE"><?= h((string) $comet['estimated_magnitude_display']) ?></td>
<?php if ($loggedIn): ?>
<td class="action-cell">
<?php if (count($trackedComets) > 1): ?>
<form method="post" action="comets.php" onsubmit="return confirm('Komet wirklich aus deiner Liste entfernen?');">
<input type="hidden" name="action" value="remove_comet">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php else: ?>
<span class="hint">Letzter Komet</span>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<section class="card comets-card">
<h2>Interessante Kometen</h2>
<p class="hint">Sortiert nach Naehe zum heutigen Datum bezogen auf den Periheldurchgang.</p>
<?php if (empty($interestingComets)): ?>
<p class="hint">Im aktuellen Zeitfenster wurden keine Kometen gefunden.</p>
<?php else: ?>
<div class="comets-table-wrap">
<table class="comets-table">
<thead>
<tr>
<th data-sortable>Komet</th>
<th data-sortable>Typ</th>
<th data-sortable>RA</th>
<th data-sortable>Dec</th>
<th data-sortable>Perihel</th>
<th data-sortable>Abstand</th>
<th data-sortable title="Grobe Helligkeitsschaetzung fuer heute nach H + 5 log(Delta) + 2,5 * n * log(r).">m grob</th>
<?php if ($loggedIn): ?>
<th></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($interestingComets as $comet): ?>
<?php
$daysClass = ((int) ($comet['days_from_today'] ?? 0)) < 0 ? 'is-past' : 'is-upcoming';
$daysLabel = $comet['days_from_today'] === null
? '-'
: ((int) $comet['days_from_today'] === 0
? 'Heute'
: (((int) $comet['days_from_today'] > 0 ? '+' : '') . (int) $comet['days_from_today'] . ' Tage'));
$perihelionTs = $comet['perihelion_date_object'] instanceof DateTimeImmutable ? $comet['perihelion_date_object']->getTimestamp() : '';
?>
<tr>
<td>
<div class="comets-name"><?= h((string) $comet['designation_and_name']) ?></div>
<div class="comets-meta">
<?php if ((int) $comet['is_favorite'] === 1): ?>
<span class="comets-badge is-favorite">Favorit</span>
<?php elseif ((int) $comet['is_tracked'] === 1): ?>
<span class="comets-badge is-tracked">Beobachtet</span>
<?php endif; ?>
<?php if (!empty($comet['reference_text'])): ?>
<span class="comets-badge"><?= h((string) $comet['reference_text']) ?></span>
<?php endif; ?>
</div>
</td>
<td><?= h((string) $comet['orbit_type_label']) ?></td>
<td data-sort-value="<?= h((string) ($comet['ra_hours'] ?? '')) ?>"><?= h((string) $comet['ra_display']) ?></td>
<td data-sort-value="<?= h((string) ($comet['dec_deg'] ?? '')) ?>"><?= h((string) $comet['dec_display']) ?></td>
<td data-sort-value="<?= h((string) $perihelionTs) ?>"><?= h((string) $comet['perihelion_date_display']) ?></td>
<td data-sort-value="<?= h((string) ($comet['days_from_today'] ?? '')) ?>"><span class="comets-days <?= $daysClass ?>"><?= h($daysLabel) ?></span></td>
<td data-sort-value="<?= h((string) ($comet['estimated_magnitude'] ?? '')) ?>" title="r = <?= h((string) $comet['heliocentric_distance_display']) ?> AE, Δ = <?= h((string) $comet['geocentric_distance_display']) ?> AE"><?= h((string) $comet['estimated_magnitude_display']) ?></td>
<?php if ($loggedIn): ?>
<td class="action-cell">
<?php if ((int) $comet['is_tracked'] !== 1): ?>
<form method="post" action="comets.php">
<input type="hidden" name="action" value="add_comet">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-primary">Hinzufügen</button>
</form>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
</div>
<script>
(function () {
function getCellValue(row, colIndex) {
const cell = row.cells[colIndex];
if (!cell) return '';
const sv = cell.getAttribute('data-sort-value');
if (sv !== null && sv !== '') return sv;
return cell.textContent.trim();
}
function compareValues(a, b) {
const numA = parseFloat(a);
const numB = parseFloat(b);
if (!isNaN(numA) && !isNaN(numB)) return numA - numB;
return a.localeCompare(b, 'de', { sensitivity: 'base' });
}
document.querySelectorAll('.comets-table').forEach(function (table) {
const headers = table.querySelectorAll('th[data-sortable]');
headers.forEach(function (th, colIndex) {
th.addEventListener('click', function () {
const currentDir = th.getAttribute('data-sort-dir');
const ascending = currentDir !== 'asc';
headers.forEach(function (h) { h.removeAttribute('data-sort-dir'); });
th.setAttribute('data-sort-dir', ascending ? 'asc' : 'desc');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort(function (a, b) {
const av = getCellValue(a, colIndex);
const bv = getCellValue(b, colIndex);
const cmp = compareValues(av, bv);
return ascending ? cmp : -cmp;
});
rows.forEach(function (row) { tbody.appendChild(row); });
});
});
});
}());
</script>
<?php require __DIR__ . '/footer.php'; ?>