Kometenseite erstmal fertig
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# Lokaler Dev-Login
|
||||
|
||||
Diese Projektkopie hat einen kleinen Entwicklungs-Bypass fuer den Login, damit geschuetzte Seiten auf `localhost` einfacher getestet werden koennen.
|
||||
|
||||
## Zweck
|
||||
|
||||
Der Dev-Login ist nur fuer die lokale Entwicklung gedacht.
|
||||
Er hilft beim Testen von Seiten wie:
|
||||
|
||||
- `public/comets.php`
|
||||
- `public/my_favorites.php`
|
||||
- andere nicht oeffentlich freigegebene Seiten
|
||||
|
||||
## Aktivierung
|
||||
|
||||
Im lokalen Browser einfach eine geschuetzte Seite mit `?dev_login=1` aufrufen, zum Beispiel:
|
||||
|
||||
```text
|
||||
http://localhost:8000/public/comets.php?dev_login=1
|
||||
```
|
||||
|
||||
Dann wird automatisch der erste aktive Benutzer mit Rolle `master` oder `admin` in die Session eingeloggt.
|
||||
|
||||
Danach koennen auch weitere geschuetzte Seiten im selben Browser/Fenster normal aufgerufen werden.
|
||||
|
||||
## Abmelden
|
||||
|
||||
Zum lokalen Dev-Logout:
|
||||
|
||||
```text
|
||||
http://localhost:8000/public/comets.php?dev_logout=1
|
||||
```
|
||||
|
||||
Das entfernt die Dev-Session wieder.
|
||||
|
||||
## Sicherheitsgrenzen
|
||||
|
||||
Der Dev-Login greift nur bei lokalen Requests:
|
||||
|
||||
- Host muss `localhost`, `127.0.0.1` oder `::1` sein
|
||||
- der Request muss lokal kommen
|
||||
|
||||
Auf dem echten Server oder unter normalen externen Domains hat dieser Mechanismus keine Wirkung.
|
||||
|
||||
## Technische Umsetzung
|
||||
|
||||
Die Erkennung und Session-Initialisierung liegt in:
|
||||
|
||||
- [public/auth_helpers.php](/abs/path/c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/auth_helpers.php:4)
|
||||
|
||||
Die Aktivierung beim Seitenaufbau liegt in:
|
||||
|
||||
- [public/header.php](/abs/path/c:/Users/HP/source/repos/skyview.astronomiemuseum.de/public/header.php:3)
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Der Dev-Login ist bewusst nicht fuer Produktion gedacht.
|
||||
- Wenn lokal kein aktiver `master`- oder `admin`-Benutzer vorhanden ist, greift der Bypass nicht.
|
||||
- Fuer reproduzierbare Tests am besten dieselbe Browser-Session weiterverwenden, statt staendig neu zu starten.
|
||||
@@ -1,6 +1,62 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function isLocalDevelopmentRequest(): bool
|
||||
{
|
||||
$host = strtolower((string) ($_SERVER['HTTP_HOST'] ?? ''));
|
||||
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
|
||||
$remoteAddr = strtolower((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
|
||||
$localHosts = ['localhost', '127.0.0.1', '::1'];
|
||||
$localAddresses = ['127.0.0.1', '::1', ''];
|
||||
|
||||
return in_array($host, $localHosts, true) && in_array($remoteAddr, $localAddresses, true);
|
||||
}
|
||||
|
||||
function applyLocalDevLoginBypass(): void
|
||||
{
|
||||
if (!isLocalDevelopmentRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((string) ($_GET['dev_logout'] ?? '') === '1') {
|
||||
unset($_SESSION['user_id'], $_SESSION['username'], $_SESSION['role']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id']) || (string) ($_GET['dev_login'] ?? '') !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$cfg = require __DIR__ . '/../config/database.php';
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=%s', $cfg['host'], $cfg['dbname'], $cfg['charset'] ?? 'utf8mb4'),
|
||||
$cfg['user'],
|
||||
$cfg['pass'],
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
|
||||
$stmt = $pdo->query("
|
||||
SELECT `id`, `username`, `role`
|
||||
FROM `app_users`
|
||||
WHERE `is_active` = 1
|
||||
AND `role` IN ('master', 'admin')
|
||||
ORDER BY CASE WHEN `role` = 'master' THEN 0 ELSE 1 END, `id` ASC
|
||||
LIMIT 1
|
||||
");
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (is_array($user)) {
|
||||
$_SESSION['user_id'] = (int) $user['id'];
|
||||
$_SESSION['username'] = (string) $user['username'];
|
||||
$_SESSION['role'] = (string) $user['role'];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function currentUserRole(): string
|
||||
{
|
||||
return trim((string) ($_SESSION['role'] ?? ''));
|
||||
|
||||
+235
-38
@@ -16,6 +16,81 @@ 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)) {
|
||||
@@ -25,6 +100,51 @@ function formatDecimalGerman(?float $value, int $decimals = 3): string
|
||||
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;
|
||||
@@ -64,20 +184,6 @@ function classifyOrbitType(string $orbitType): string
|
||||
};
|
||||
}
|
||||
|
||||
function estimatePerihelMagnitude(?float $absoluteMagnitudeH, ?float $perihelionDistanceAu): ?float
|
||||
{
|
||||
if ($absoluteMagnitudeH === null || !is_finite($absoluteMagnitudeH) || $perihelionDistanceAu === null || !is_finite($perihelionDistanceAu) || $perihelionDistanceAu <= 0.0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$heliocentricDistance = max(0.05, $perihelionDistanceAu);
|
||||
$geocentricDistance = max(0.2, abs(1.0 - $perihelionDistanceAu));
|
||||
|
||||
return $absoluteMagnitudeH
|
||||
+ (5.0 * log10($geocentricDistance))
|
||||
+ (10.0 * log10($heliocentricDistance));
|
||||
}
|
||||
|
||||
$config = require __DIR__ . '/../config/database.php';
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
@@ -121,36 +227,29 @@ $stmtComets->execute([
|
||||
':year_min' => $queryYearMin,
|
||||
':year_max' => $queryYearMax,
|
||||
]);
|
||||
$cometRows = $stmtComets->fetchAll();
|
||||
|
||||
$allComets = [];
|
||||
$trackedComets = [];
|
||||
$interestingComets = [];
|
||||
|
||||
foreach ($stmtComets as $row) {
|
||||
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->setTimezone(new DateTimeZone('Europe/Berlin'))->format('d.m.Y H:i') . ' UTC' : '-';
|
||||
$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['slope_parameter_display'] = isset($row['slope_parameter_g']) && $row['slope_parameter_g'] !== null
|
||||
$row['photometric_index_display'] = isset($row['slope_parameter_g']) && $row['slope_parameter_g'] !== null
|
||||
? formatDecimalGerman((float) $row['slope_parameter_g'], 1)
|
||||
: '-';
|
||||
$row['perihelion_dist_display'] = formatDecimalGerman(isset($row['perihelion_dist_au']) ? (float) $row['perihelion_dist_au'] : null, 3);
|
||||
$row['eccentricity_display'] = formatDecimalGerman(isset($row['eccentricity']) ? (float) $row['eccentricity'] : null, 4);
|
||||
$row['inclination_display'] = formatDecimalGerman(isset($row['inclination_deg']) ? (float) $row['inclination_deg'] : null, 2);
|
||||
$estimatedPerihelMagnitude = estimatePerihelMagnitude(
|
||||
isset($row['absolute_magnitude_h']) && $row['absolute_magnitude_h'] !== null ? (float) $row['absolute_magnitude_h'] : null,
|
||||
isset($row['perihelion_dist_au']) && $row['perihelion_dist_au'] !== null ? (float) $row['perihelion_dist_au'] : null
|
||||
);
|
||||
$row['estimated_perihel_magnitude'] = $estimatedPerihelMagnitude;
|
||||
$row['estimated_perihel_magnitude_display'] = $estimatedPerihelMagnitude !== null
|
||||
? formatDecimalGerman($estimatedPerihelMagnitude, 1)
|
||||
: '-';
|
||||
$row['estimated_magnitude'] = null;
|
||||
$row['estimated_magnitude_display'] = '-';
|
||||
$row['brightness_error'] = null;
|
||||
|
||||
$allComets[] = $row;
|
||||
|
||||
@@ -183,6 +282,94 @@ usort($interestingComets, static function (array $left, array $right): int {
|
||||
});
|
||||
|
||||
$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'] ?? '?');
|
||||
@@ -365,9 +552,15 @@ arsort($orbitTypeCounts);
|
||||
Nutzer-Favoriten aus <code>app_user_comets</code> werden zusaetzlich separat gezeigt.
|
||||
</p>
|
||||
<p class="hint">
|
||||
Die Spalte <strong>m grob</strong> ist eine sehr einfache Helligkeitsschaetzung nahe dem Perihel
|
||||
auf Basis von MPC-<code>H</code> und <code>q</code>. Sie ist nur als Orientierung gedacht.
|
||||
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): ?>
|
||||
@@ -410,9 +603,11 @@ arsort($orbitTypeCounts);
|
||||
<tr>
|
||||
<th>Komet</th>
|
||||
<th>Typ</th>
|
||||
<th>RA</th>
|
||||
<th>Dec</th>
|
||||
<th>Perihel</th>
|
||||
<th>Abstand</th>
|
||||
<th title="Sehr grobe Helligkeitsschaetzung nahe dem Perihel.">m grob</th>
|
||||
<th title="Grobe Helligkeitsschaetzung fuer heute nach H + 5 log(Delta) + 2,5 * n * log(r).">m grob</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -439,9 +634,11 @@ arsort($orbitTypeCounts);
|
||||
</div>
|
||||
</td>
|
||||
<td><?= h((string) $comet['orbit_type_label']) ?></td>
|
||||
<td><?= h((string) $comet['ra_display']) ?></td>
|
||||
<td><?= h((string) $comet['dec_display']) ?></td>
|
||||
<td><?= h((string) $comet['perihelion_date_display']) ?></td>
|
||||
<td><span class="comets-days <?= $daysClass ?>"><?= h($daysLabel) ?></span></td>
|
||||
<td title="Sehr grobe Helligkeitsschaetzung nahe dem Perihel."><?= h((string) $comet['estimated_perihel_magnitude_display']) ?></td>
|
||||
<td title="r = <?= h((string) $comet['heliocentric_distance_display']) ?> AE, Δ = <?= h((string) $comet['geocentric_distance_display']) ?> AE"><?= h((string) $comet['estimated_magnitude_display']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -462,11 +659,11 @@ arsort($orbitTypeCounts);
|
||||
<tr>
|
||||
<th>Komet</th>
|
||||
<th>Typ</th>
|
||||
<th>RA</th>
|
||||
<th>Dec</th>
|
||||
<th>Perihel</th>
|
||||
<th>Abstand</th>
|
||||
<th title="Sehr grobe Helligkeitsschaetzung nahe dem Perihel.">m grob</th>
|
||||
<th>H</th>
|
||||
<th>G</th>
|
||||
<th title="Grobe Helligkeitsschaetzung fuer heute nach H + 5 log(Delta) + 2,5 * n * log(r).">m grob</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -494,11 +691,11 @@ arsort($orbitTypeCounts);
|
||||
</div>
|
||||
</td>
|
||||
<td><?= h((string) $comet['orbit_type_label']) ?></td>
|
||||
<td><?= h((string) $comet['ra_display']) ?></td>
|
||||
<td><?= h((string) $comet['dec_display']) ?></td>
|
||||
<td><?= h((string) $comet['perihelion_date_display']) ?></td>
|
||||
<td><span class="comets-days <?= $daysClass ?>"><?= h($daysLabel) ?></span></td>
|
||||
<td title="Sehr grobe Helligkeitsschaetzung nahe dem Perihel."><?= h((string) $comet['estimated_perihel_magnitude_display']) ?></td>
|
||||
<td><?= h((string) $comet['absolute_magnitude_display']) ?></td>
|
||||
<td><?= h((string) $comet['slope_parameter_display']) ?></td>
|
||||
<td title="r = <?= h((string) $comet['heliocentric_distance_display']) ?> AE, Δ = <?= h((string) $comet['geocentric_distance_display']) ?> AE"><?= h((string) $comet['estimated_magnitude_display']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth_helpers.php';
|
||||
if (!isset($loggedIn)) {
|
||||
applyLocalDevLoginBypass();
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
$title = isset($pageTitle) && is_string($pageTitle) && $pageTitle !== ''
|
||||
? $pageTitle
|
||||
|
||||
Binary file not shown.
+33
-2
@@ -13,6 +13,7 @@ if SCRIPT_DIR not in sys.path:
|
||||
|
||||
import astronomy
|
||||
import astronomical_conversions
|
||||
import comets
|
||||
|
||||
MOON_RADIUS_KM = 1737.4
|
||||
SYNODIC_MONTH = 29.530588853
|
||||
@@ -252,6 +253,31 @@ def action_astronomical_conversions(args: list[str]) -> dict:
|
||||
fail(str(exc), extra={"kind": kind})
|
||||
|
||||
|
||||
def action_comet_brightnesses(args: list[str]) -> dict:
|
||||
if len(args) != 1:
|
||||
fail(
|
||||
"Aktion comet_brightnesses erwartet 1 Argument: payloadBase64 oder @payloadDatei",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
try:
|
||||
payload_arg = args[0]
|
||||
if payload_arg.startswith("@"):
|
||||
payload_path = payload_arg[1:]
|
||||
with open(payload_path, "r", encoding="utf-8") as handle:
|
||||
payload_text = handle.read()
|
||||
else:
|
||||
payload_text = base64.urlsafe_b64decode(payload_arg.encode("ascii")).decode("utf-8")
|
||||
payload = json.loads(payload_text)
|
||||
except Exception as exc:
|
||||
fail("Payload fuer comet_brightnesses ist ungueltig.", extra={"details": str(exc)})
|
||||
|
||||
try:
|
||||
return comets.handle_request(payload)
|
||||
except ValueError as exc:
|
||||
fail(str(exc), extra={"action": "comet_brightnesses"})
|
||||
|
||||
|
||||
def search_event(
|
||||
body: astronomy.Body,
|
||||
direction: astronomy.Direction,
|
||||
@@ -4173,7 +4199,7 @@ def action_satellite_passes(args: list[str]) -> dict:
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
|
||||
action = sys.argv[1]
|
||||
args = sys.argv[2:]
|
||||
@@ -4198,6 +4224,11 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "comet_brightnesses":
|
||||
result = action_comet_brightnesses(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "moon_star_occultations":
|
||||
result = action_moon_star_occultations(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
@@ -4323,7 +4354,7 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import astronomy
|
||||
|
||||
GAUSSIAN_GRAVITATIONAL_CONSTANT = 0.01720209895
|
||||
J2000_OBLIQUITY_DEG = 23.439279444444445
|
||||
PARABOLIC_ECCENTRICITY_TOLERANCE = 1.0e-6
|
||||
|
||||
|
||||
def parse_float(value) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
text = str(value).strip()
|
||||
if text == "":
|
||||
return None
|
||||
|
||||
text = text.replace(",", ".")
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_int(value) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
text = str(value).strip()
|
||||
if text == "":
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def dt_to_time(dt_utc: datetime) -> astronomy.Time:
|
||||
dt_utc = dt_utc.astimezone(timezone.utc)
|
||||
return astronomy.Time.Make(
|
||||
dt_utc.year,
|
||||
dt_utc.month,
|
||||
dt_utc.day,
|
||||
dt_utc.hour,
|
||||
dt_utc.minute,
|
||||
dt_utc.second + (dt_utc.microsecond / 1_000_000.0),
|
||||
)
|
||||
|
||||
|
||||
def build_perihelion_datetime(comet: dict) -> datetime | None:
|
||||
year = parse_int(comet.get("year_of_perihelion"))
|
||||
month = parse_int(comet.get("month_of_perihelion"))
|
||||
day_value = parse_float(comet.get("day_of_perihelion"))
|
||||
|
||||
if year is None or month is None or day_value is None:
|
||||
return None
|
||||
|
||||
if month < 1 or month > 12 or day_value <= 0.0:
|
||||
return None
|
||||
|
||||
day = int(math.floor(day_value))
|
||||
if day < 1 or day > 31:
|
||||
return None
|
||||
|
||||
fractional_day = day_value - day
|
||||
seconds = int(round(fractional_day * 86400.0))
|
||||
|
||||
try:
|
||||
perihelion_dt = datetime(year, month, day, 0, 0, 0, tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return perihelion_dt + timedelta(seconds=seconds)
|
||||
|
||||
|
||||
def solve_elliptic_anomaly(mean_anomaly: float, eccentricity: float) -> float:
|
||||
anomaly = mean_anomaly if eccentricity < 0.8 else (math.pi if mean_anomaly >= 0.0 else -math.pi)
|
||||
for _ in range(30):
|
||||
delta = (anomaly - eccentricity * math.sin(anomaly) - mean_anomaly) / (1.0 - eccentricity * math.cos(anomaly))
|
||||
anomaly -= delta
|
||||
if abs(delta) < 1.0e-12:
|
||||
break
|
||||
return anomaly
|
||||
|
||||
|
||||
def solve_hyperbolic_anomaly(mean_anomaly: float, eccentricity: float) -> float:
|
||||
if mean_anomaly == 0.0:
|
||||
anomaly = 0.0
|
||||
else:
|
||||
anomaly = math.asinh(mean_anomaly / eccentricity)
|
||||
|
||||
for _ in range(40):
|
||||
sinh_value = math.sinh(anomaly)
|
||||
cosh_value = math.cosh(anomaly)
|
||||
delta = (eccentricity * sinh_value - anomaly - mean_anomaly) / (eccentricity * cosh_value - 1.0)
|
||||
anomaly -= delta
|
||||
if abs(delta) < 1.0e-12:
|
||||
break
|
||||
return anomaly
|
||||
|
||||
|
||||
def solve_parabolic_parameter(delta_days: float, perihelion_distance_au: float) -> float:
|
||||
scale = GAUSSIAN_GRAVITATIONAL_CONSTANT * delta_days / math.sqrt(2.0 * perihelion_distance_au**3)
|
||||
parameter = scale
|
||||
|
||||
for _ in range(40):
|
||||
numerator = parameter + (parameter**3) / 3.0 - scale
|
||||
denominator = 1.0 + parameter**2
|
||||
delta = numerator / denominator
|
||||
parameter -= delta
|
||||
if abs(delta) < 1.0e-12:
|
||||
break
|
||||
|
||||
return parameter
|
||||
|
||||
|
||||
def true_anomaly_and_radius(delta_days: float, perihelion_distance_au: float, eccentricity: float) -> tuple[float, float]:
|
||||
if perihelion_distance_au <= 0.0:
|
||||
raise ValueError("Periheldistanz muss positiv sein.")
|
||||
|
||||
if eccentricity < 1.0 - PARABOLIC_ECCENTRICITY_TOLERANCE:
|
||||
semi_major_axis = perihelion_distance_au / (1.0 - eccentricity)
|
||||
mean_motion = GAUSSIAN_GRAVITATIONAL_CONSTANT / (semi_major_axis ** 1.5)
|
||||
mean_anomaly = math.fmod(mean_motion * delta_days, 2.0 * math.pi)
|
||||
eccentric_anomaly = solve_elliptic_anomaly(mean_anomaly, eccentricity)
|
||||
radius = semi_major_axis * (1.0 - eccentricity * math.cos(eccentric_anomaly))
|
||||
true_anomaly = 2.0 * math.atan2(
|
||||
math.sqrt(1.0 + eccentricity) * math.sin(eccentric_anomaly / 2.0),
|
||||
math.sqrt(1.0 - eccentricity) * math.cos(eccentric_anomaly / 2.0),
|
||||
)
|
||||
return true_anomaly, radius
|
||||
|
||||
if eccentricity > 1.0 + PARABOLIC_ECCENTRICITY_TOLERANCE:
|
||||
semi_major_axis_abs = perihelion_distance_au / (eccentricity - 1.0)
|
||||
mean_anomaly = GAUSSIAN_GRAVITATIONAL_CONSTANT * delta_days / (semi_major_axis_abs ** 1.5)
|
||||
hyperbolic_anomaly = solve_hyperbolic_anomaly(mean_anomaly, eccentricity)
|
||||
radius = semi_major_axis_abs * (eccentricity * math.cosh(hyperbolic_anomaly) - 1.0)
|
||||
true_anomaly = 2.0 * math.atan2(
|
||||
math.sqrt(eccentricity + 1.0) * math.sinh(hyperbolic_anomaly / 2.0),
|
||||
math.sqrt(eccentricity - 1.0) * math.cosh(hyperbolic_anomaly / 2.0),
|
||||
)
|
||||
return true_anomaly, radius
|
||||
|
||||
parabolic_parameter = solve_parabolic_parameter(delta_days, perihelion_distance_au)
|
||||
true_anomaly = 2.0 * math.atan(parabolic_parameter)
|
||||
radius = perihelion_distance_au * (1.0 + parabolic_parameter**2)
|
||||
return true_anomaly, radius
|
||||
|
||||
|
||||
def ecliptic_to_equatorial(x_ecl: float, y_ecl: float, z_ecl: float) -> tuple[float, float, float]:
|
||||
epsilon = math.radians(J2000_OBLIQUITY_DEG)
|
||||
cos_epsilon = math.cos(epsilon)
|
||||
sin_epsilon = math.sin(epsilon)
|
||||
return (
|
||||
x_ecl,
|
||||
y_ecl * cos_epsilon - z_ecl * sin_epsilon,
|
||||
y_ecl * sin_epsilon + z_ecl * cos_epsilon,
|
||||
)
|
||||
|
||||
|
||||
def comet_heliocentric_vector(comet: dict, dt_utc: datetime) -> tuple[float, float, float] | None:
|
||||
perihelion_distance_au = parse_float(comet.get("perihelion_dist_au"))
|
||||
eccentricity = parse_float(comet.get("eccentricity"))
|
||||
arg_perihelion_deg = parse_float(comet.get("arg_perihelion_deg"))
|
||||
ascending_node_deg = parse_float(comet.get("ascending_node_deg"))
|
||||
inclination_deg = parse_float(comet.get("inclination_deg"))
|
||||
perihelion_dt = build_perihelion_datetime(comet)
|
||||
|
||||
if None in (
|
||||
perihelion_distance_au,
|
||||
eccentricity,
|
||||
arg_perihelion_deg,
|
||||
ascending_node_deg,
|
||||
inclination_deg,
|
||||
perihelion_dt,
|
||||
):
|
||||
return None
|
||||
|
||||
delta_days = (dt_utc - perihelion_dt).total_seconds() / 86400.0
|
||||
true_anomaly, radius = true_anomaly_and_radius(delta_days, perihelion_distance_au, eccentricity)
|
||||
|
||||
arg_perihelion = math.radians(arg_perihelion_deg)
|
||||
ascending_node = math.radians(ascending_node_deg)
|
||||
inclination = math.radians(inclination_deg)
|
||||
argument_of_latitude = arg_perihelion + true_anomaly
|
||||
|
||||
cos_node = math.cos(ascending_node)
|
||||
sin_node = math.sin(ascending_node)
|
||||
cos_inclination = math.cos(inclination)
|
||||
sin_inclination = math.sin(inclination)
|
||||
cos_argument = math.cos(argument_of_latitude)
|
||||
sin_argument = math.sin(argument_of_latitude)
|
||||
|
||||
x_ecl = radius * (cos_node * cos_argument - sin_node * sin_argument * cos_inclination)
|
||||
y_ecl = radius * (sin_node * cos_argument + cos_node * sin_argument * cos_inclination)
|
||||
z_ecl = radius * (sin_argument * sin_inclination)
|
||||
|
||||
return ecliptic_to_equatorial(x_ecl, y_ecl, z_ecl)
|
||||
|
||||
|
||||
def estimate_magnitude(absolute_magnitude_h: float | None, slope_parameter_g: float | None, heliocentric_distance_au: float, geocentric_distance_au: float) -> float | None:
|
||||
if absolute_magnitude_h is None or slope_parameter_g is None:
|
||||
return None
|
||||
|
||||
if heliocentric_distance_au <= 0.0 or geocentric_distance_au <= 0.0:
|
||||
return None
|
||||
|
||||
return absolute_magnitude_h + (5.0 * math.log10(geocentric_distance_au)) + (2.5 * slope_parameter_g * math.log10(heliocentric_distance_au))
|
||||
|
||||
|
||||
def equatorial_coordinates_from_vector(x: float, y: float, z: float) -> tuple[float, float]:
|
||||
distance = math.sqrt(x**2 + y**2 + z**2)
|
||||
if distance <= 0.0:
|
||||
raise ValueError("Geozentrischer Vektor ist null.")
|
||||
|
||||
ra_hours = math.degrees(math.atan2(y, x)) / 15.0
|
||||
if ra_hours < 0.0:
|
||||
ra_hours += 24.0
|
||||
|
||||
dec_deg = math.degrees(math.asin(z / distance))
|
||||
return ra_hours, dec_deg
|
||||
|
||||
|
||||
def calculate_brightness(comet: dict, dt_utc: datetime) -> dict:
|
||||
comet_id = parse_int(comet.get("id"))
|
||||
result = {
|
||||
"id": comet_id,
|
||||
"heliocentric_distance_au": None,
|
||||
"geocentric_distance_au": None,
|
||||
"ra_hours": None,
|
||||
"dec_deg": None,
|
||||
"estimated_magnitude": None,
|
||||
"model": "stellarium_like",
|
||||
}
|
||||
|
||||
heliocentric_position = comet_heliocentric_vector(comet, dt_utc)
|
||||
if heliocentric_position is None:
|
||||
result["error"] = "Bahnelemente unvollstaendig."
|
||||
return result
|
||||
|
||||
comet_x, comet_y, comet_z = heliocentric_position
|
||||
heliocentric_distance_au = math.sqrt(comet_x**2 + comet_y**2 + comet_z**2)
|
||||
earth_vector = astronomy.HelioVector(astronomy.Body.Earth, dt_to_time(dt_utc))
|
||||
geo_x = comet_x - earth_vector.x
|
||||
geo_y = comet_y - earth_vector.y
|
||||
geo_z = comet_z - earth_vector.z
|
||||
geocentric_distance_au = math.sqrt(geo_x**2 + geo_y**2 + geo_z**2)
|
||||
ra_hours, dec_deg = equatorial_coordinates_from_vector(geo_x, geo_y, geo_z)
|
||||
|
||||
absolute_magnitude_h = parse_float(comet.get("absolute_magnitude_h"))
|
||||
slope_parameter_g = parse_float(comet.get("slope_parameter_g"))
|
||||
estimated_magnitude = estimate_magnitude(
|
||||
absolute_magnitude_h,
|
||||
slope_parameter_g,
|
||||
heliocentric_distance_au,
|
||||
geocentric_distance_au,
|
||||
)
|
||||
|
||||
result["heliocentric_distance_au"] = heliocentric_distance_au
|
||||
result["geocentric_distance_au"] = geocentric_distance_au
|
||||
result["ra_hours"] = ra_hours
|
||||
result["dec_deg"] = dec_deg
|
||||
result["estimated_magnitude"] = estimated_magnitude
|
||||
return result
|
||||
|
||||
|
||||
def handle_request(payload: dict) -> dict:
|
||||
date_text = str(payload.get("date") or "").strip()
|
||||
if date_text == "":
|
||||
dt_utc = datetime.now(timezone.utc)
|
||||
else:
|
||||
normalized = date_text.replace("Z", "+00:00")
|
||||
dt_utc = datetime.fromisoformat(normalized)
|
||||
if dt_utc.tzinfo is None:
|
||||
dt_utc = dt_utc.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
dt_utc = dt_utc.astimezone(timezone.utc)
|
||||
|
||||
comet_items = payload.get("comets")
|
||||
if not isinstance(comet_items, list):
|
||||
raise ValueError("Kometenliste fehlt oder ist ungueltig.")
|
||||
|
||||
results = [calculate_brightness(comet, dt_utc) for comet in comet_items]
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "comet_brightnesses",
|
||||
"date_utc": dt_utc.isoformat().replace("+00:00", "Z"),
|
||||
"results": results,
|
||||
}
|
||||
Reference in New Issue
Block a user