483 lines
15 KiB
PHP
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, ',', '.') . '°';
|
|
$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'; ?>
|