Files
skyview.astronomiemuseum.de/public/meteorshowers.php
T

483 lines
15 KiB
PHP

<?php
declare(strict_types=1);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Meteorströme - AstroTools';
$bodyClass = 'admin-page meteorshowers-page';
$publicBasePath = './';
$headerIntroPre = 'Beobachtung';
$headerIntroTitle = 'Meteorströme';
$headerIntroSub = 'Liste der importierten Meteorströme aus der Datenbank mit Angaben in solarer Länge.';
$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,
]
);
$searchQuery = trim((string) ($_GET['q'] ?? ''));
$showMode = trim((string) ($_GET['mode'] ?? 'all'));
$allowedModes = ['all', 'active'];
if (!in_array($showMode, $allowedModes, true)) {
$showMode = 'all';
}
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function normalizeSolarLongitude(float $value): float
{
$normalized = fmod($value, 360.0);
if ($normalized < 0.0) {
$normalized += 360.0;
}
return $normalized;
}
function currentSolarLongitude(DateTimeImmutable $dateTime): float
{
$julianDay = ((float) $dateTime->format('U')) / 86400.0 + 2440587.5;
$daysSinceJ2000 = $julianDay - 2451545.0;
$meanLongitude = normalizeSolarLongitude(280.46 + 0.9856474 * $daysSinceJ2000);
$meanAnomalyRad = deg2rad(normalizeSolarLongitude(357.528 + 0.9856003 * $daysSinceJ2000));
return normalizeSolarLongitude(
$meanLongitude
+ 1.915 * sin($meanAnomalyRad)
+ 0.020 * sin(2.0 * $meanAnomalyRad)
);
}
function fetchCurrentSolarLongitude(DateTimeZone $displayTimezone): ?float
{
$pythonResult = runPythonApi('current_solar_longitude', [$displayTimezone->getName()]);
if (!$pythonResult['ok']) {
return null;
}
$solarLongitude = $pythonResult['data']['result']['solar_longitude_deg'] ?? null;
if (!is_numeric($solarLongitude)) {
return null;
}
return normalizeSolarLongitude((float) $solarLongitude);
}
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 isSolarLongitudeInRange(float $current, ?float $start, ?float $finish): bool
{
if ($start === null || $finish === null) {
return false;
}
$current = normalizeSolarLongitude($current);
$start = normalizeSolarLongitude($start);
$finish = normalizeSolarLongitude($finish);
if ($start <= $finish) {
return $current >= $start && $current <= $finish;
}
return $current >= $start || $current <= $finish;
}
function solarLongitudeToUtcDate(?float $targetLongitude, int $year, DateTimeZone $displayTimezone): ?DateTimeImmutable
{
if ($targetLongitude === null) {
return null;
}
static $cache = [];
$cacheKey = $year . '|' . $displayTimezone->getName() . '|' . number_format((float) $targetLongitude, 6, '.', '');
if (array_key_exists($cacheKey, $cache)) {
return $cache[$cacheKey];
}
$pythonResult = runPythonApi('solar_longitude_to_datetime', [
(string) $targetLongitude,
(string) $year,
$displayTimezone->getName(),
]);
if (!$pythonResult['ok']) {
$cache[$cacheKey] = null;
return null;
}
$resultData = $pythonResult['data'];
$utcIso = trim((string) ($resultData['result']['utc_iso'] ?? ''));
if ($utcIso === '') {
$cache[$cacheKey] = null;
return null;
}
try {
$cache[$cacheKey] = new DateTimeImmutable($utcIso);
} catch (Throwable $e) {
$cache[$cacheKey] = null;
return null;
}
return $cache[$cacheKey];
}
function warmSolarLongitudeDateCache(array $showers, int $year, DateTimeZone $displayTimezone): void
{
$uniqueLongitudes = [];
foreach ($showers as $shower) {
foreach (['active_start_sol', 'active_finish_sol', 'peak_sol'] as $field) {
if (!isset($shower[$field]) || $shower[$field] === null || $shower[$field] === '') {
continue;
}
$key = number_format((float) $shower[$field], 6, '.', '');
$uniqueLongitudes[$key] = (float) $shower[$field];
}
}
foreach ($uniqueLongitudes as $solarLongitude) {
solarLongitudeToUtcDate($solarLongitude, $year, $displayTimezone);
}
}
function formatNullableNumber(mixed $value, int $decimals = 1, string $suffix = ''): string
{
if ($value === null || $value === '') {
return '-';
}
return number_format((float) $value, $decimals, ',', '.') . $suffix;
}
function formatSolarLongitudeWithDate(?float $solarLongitude, int $year, DateTimeZone $displayTimezone): string
{
if ($solarLongitude === null) {
return '-';
}
$label = number_format((float) $solarLongitude, 2, ',', '.') . '&deg;';
$date = solarLongitudeToUtcDate($solarLongitude, $year, $displayTimezone);
if (!$date instanceof DateTimeImmutable) {
return $label;
}
return $label . '<br><span class="hint">' . htmlspecialchars($date->setTimezone($displayTimezone)->format('d.m.Y'), ENT_QUOTES, 'UTF-8') . '</span>';
}
$displayTimezone = new DateTimeZone(date_default_timezone_get() ?: 'Europe/Berlin');
$displayYear = (int) (new DateTimeImmutable('now', $displayTimezone))->format('Y');
$todaySolarLongitude = fetchCurrentSolarLongitude($displayTimezone)
?? currentSolarLongitude(new DateTimeImmutable('now', new DateTimeZone('UTC')));
$sql = "
SELECT
`id`,
`shower_code`,
`iau_no`,
`designation`,
`speed_kms`,
`radiant_alpha_deg`,
`radiant_delta_deg`,
`parent_object`,
`population_index`,
`active_start_sol`,
`active_finish_sol`,
`peak_sol`,
`zhr`,
`zhr_variable`,
`has_yearly_variants`
FROM `meteorshowers`
WHERE 1 = 1
";
$params = [];
if ($searchQuery !== '') {
$sql .= "
AND (
`shower_code` LIKE :query
OR `designation` LIKE :query
OR `parent_object` LIKE :query
)
";
$params[':query'] = '%' . $searchQuery . '%';
}
$sql .= "
ORDER BY
COALESCE(`zhr`, 0) DESC,
`peak_sol` IS NULL ASC,
`peak_sol` ASC,
`designation` ASC
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$allShowers = $stmt->fetchAll();
$showers = [];
$activeCount = 0;
foreach ($allShowers as $shower) {
$isActive = isSolarLongitudeInRange(
$todaySolarLongitude,
isset($shower['active_start_sol']) ? (float) $shower['active_start_sol'] : null,
isset($shower['active_finish_sol']) ? (float) $shower['active_finish_sol'] : null
);
if ($isActive) {
$activeCount++;
}
$shower['is_active_now'] = $isActive;
if ($showMode === 'active' && !$isActive) {
continue;
}
$showers[] = $shower;
}
warmSolarLongitudeDateCache($showers, $displayYear, $displayTimezone);
$topZhr = null;
foreach ($allShowers as $shower) {
if ($shower['zhr'] !== null) {
$topZhr = (int) $shower['zhr'];
break;
}
}
require __DIR__ . '/header.php';
?>
<?php
$statsCards = [
['label' => 'Importierte Meteorströme', 'value' => (string) count($allShowers)],
['label' => 'Davon aktuell aktiv', 'value' => (string) $activeCount],
['label' => 'Höchste ZHR', 'value' => $topZhr !== null ? (string) $topZhr : '-'],
['label' => 'Aktuelle solare Länge', 'value' => number_format($todaySolarLongitude, 2, ',', '.') . '°'],
['label' => 'Datumsumrechnung', 'value' => (string) $displayYear],
];
?>
<section class="card">
<div class="stats-grid">
<?php foreach ($statsCards as $card): ?>
<div class="stats-card">
<div class="stats-label"><?= h($card['label']) ?></div>
<div class="stats-value"><?= h($card['value']) ?></div>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="card">
<form method="get" action="" class="form-stack">
<div class="grid-2 gap-sm">
<div class="form-group">
<label for="q">Suche</label>
<input
type="text"
id="q"
name="q"
value="<?= h($searchQuery) ?>"
placeholder="Code, Bezeichnung oder Mutterkörper"
>
</div>
<div class="form-group">
<label for="mode">Anzeige</label>
<select id="mode" name="mode">
<option value="all" <?= $showMode === 'all' ? 'selected' : '' ?>>Alle Meteorströme</option>
<option value="active" <?= $showMode === 'active' ? 'selected' : '' ?>>Nur aktuell aktive</option>
</select>
</div>
</div>
<div class="input-row">
<button type="submit" class="btn btn-primary">Anzeigen</button>
<a href="meteorshowers.php" class="btn btn-secondary">Zurücksetzen</a>
</div>
</form>
</section>
<section class="card">
<h2>Liste der Meteorströme</h2>
<p class="hint">Die Gradangaben in Aktivität und Maximum sind Werte der solaren Länge. Zusätzlich wird dafür ein Kalenderdatum im Jahr <?= h((string) $displayYear) ?> angezeigt.</p>
<?php if ($showers === []): ?>
<p class="hint">Keine Meteorströme für diese Auswahl gefunden.</p>
<?php else: ?>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Status</th>
<th>Code</th>
<th>IAU</th>
<th>Bezeichnung</th>
<th>ZHR</th>
<th>Aktivität (solare Länge)</th>
<th>Maximum (solare Länge)</th>
<th>Radiant</th>
<th>v<sub>g</sub></th>
<th>Mutterkörper</th>
</tr>
</thead>
<tbody>
<?php foreach ($showers as $shower): ?>
<tr>
<td>
<?php if ($shower['is_active_now']): ?>
<span class="pill pill-success">Aktiv</span>
<?php else: ?>
<span class="pill pill-muted">Inaktiv</span>
<?php endif; ?>
</td>
<td><strong><?= h((string) $shower['shower_code']) ?></strong></td>
<td><?= h($shower['iau_no'] !== null ? (string) $shower['iau_no'] : '-') ?></td>
<td>
<?= h((string) $shower['designation']) ?>
<?php if ((int) ($shower['has_yearly_variants'] ?? 0) === 1): ?>
<br><span class="hint">mit Jahresvarianten</span>
<?php endif; ?>
</td>
<td>
<?=
h(
$shower['zhr_variable'] !== null && trim((string) $shower['zhr_variable']) !== ''
? (string) $shower['zhr_variable']
: ($shower['zhr'] !== null ? (string) $shower['zhr'] : '-')
)
?>
</td>
<td>
<?= formatSolarLongitudeWithDate(isset($shower['active_start_sol']) ? (float) $shower['active_start_sol'] : null, $displayYear, $displayTimezone) ?>
bis
<?= formatSolarLongitudeWithDate(isset($shower['active_finish_sol']) ? (float) $shower['active_finish_sol'] : null, $displayYear, $displayTimezone) ?>
</td>
<td><?= formatSolarLongitudeWithDate(isset($shower['peak_sol']) ? (float) $shower['peak_sol'] : null, $displayYear, $displayTimezone) ?></td>
<td>
RA <?= h(formatNullableNumber($shower['radiant_alpha_deg'], 1, '°')) ?><br>
Dec <?= h(formatNullableNumber($shower['radiant_delta_deg'], 1, '°')) ?>
</td>
<td><?= h(formatNullableNumber($shower['speed_kms'], 1, ' km/s')) ?></td>
<td><?= h($shower['parent_object'] !== null && trim((string) $shower['parent_object']) !== '' ? (string) $shower['parent_object'] : '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}
.stats-card {
padding: 1rem 1.1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
background: rgba(255, 255, 255, 0.04);
}
.stats-label {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
}
.stats-value {
margin-top: 0.35rem;
font-size: 1.5rem;
font-weight: 700;
}
.table-wrap {
overflow-x: auto;
}
.pill {
display: inline-block;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.pill-success {
color: #123b1d;
background: #7fe09a;
}
.pill-muted {
color: #dce4f5;
background: rgba(220, 228, 245, 0.16);
}
</style>
<?php require __DIR__ . '/footer.php'; ?>