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

347 lines
9.9 KiB
PHP

<?php
declare(strict_types=1);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Meteorstroeme - AstroTools';
$bodyClass = 'meteorshowers-page';
$publicBasePath = './';
$locationIsGlobal = true;
$pdo = null;
$defaultLocation = [
'name' => 'Sternwarte Sonneberg',
'latitude' => 50.3547,
'longitude' => 11.1900,
'elevation' => 640.0,
'timezone' => 'Europe/Berlin',
];
$meteorRows = [];
$pageError = null;
function normalizeMeteorDegree(float $value): float
{
$normalized = fmod($value, 360.0);
if ($normalized < 0.0) {
$normalized += 360.0;
}
return $normalized;
}
function meteorSolarLongitude(DateTimeImmutable $dateTime): float
{
$julianDay = ((float) $dateTime->format('U')) / 86400.0 + 2440587.5;
$daysSinceJ2000 = $julianDay - 2451545.0;
$meanLongitude = normalizeMeteorDegree(280.46 + 0.9856474 * $daysSinceJ2000);
$meanAnomalyRad = deg2rad(normalizeMeteorDegree(357.528 + 0.9856003 * $daysSinceJ2000));
return normalizeMeteorDegree(
$meanLongitude
+ 1.915 * sin($meanAnomalyRad)
+ 0.020 * sin(2.0 * $meanAnomalyRad)
);
}
function meteorForwardDegreeDistance(float $from, float $to): float
{
return normalizeMeteorDegree($to - $from);
}
function meteorRadiantIsVisible(float $latitudeDeg, ?float $declinationDeg): bool
{
if ($declinationDeg === null) {
return false;
}
return abs($latitudeDeg - $declinationDeg) < 90.0;
}
function findMeteorSolarLongitudeCrossing(
DateTimeImmutable $rangeStartLocal,
DateTimeImmutable $rangeEndLocal,
float $targetLongitude,
DateTimeZone $displayTimezone
): ?DateTimeImmutable {
$utc = new DateTimeZone('UTC');
$rangeStartUtc = $rangeStartLocal->setTimezone($utc);
$rangeEndUtc = $rangeEndLocal->setTimezone($utc);
$startLongitude = meteorSolarLongitude($rangeStartUtc);
$endLongitude = meteorSolarLongitude($rangeEndUtc);
$targetDelta = meteorForwardDegreeDistance($startLongitude, $targetLongitude);
$rangeDelta = meteorForwardDegreeDistance($startLongitude, $endLongitude);
if ($targetDelta > $rangeDelta + 0.0001) {
return null;
}
$low = (int) $rangeStartUtc->format('U');
$high = (int) $rangeEndUtc->format('U');
for ($i = 0; $i < 40; $i++) {
$mid = (int) floor(($low + $high) / 2);
$midLongitude = meteorSolarLongitude(new DateTimeImmutable('@' . $mid));
$midDelta = meteorForwardDegreeDistance($startLongitude, $midLongitude);
if ($midDelta < $targetDelta) {
$low = $mid;
} else {
$high = $mid;
}
}
return (new DateTimeImmutable('@' . $high))->setTimezone($displayTimezone);
}
try {
$dbConfig = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$dbConfig['host'],
$dbConfig['dbname'],
$dbConfig['charset'] ?? 'utf8mb4'
);
$pdo = new PDO(
$dsn,
$dbConfig['user'],
$dbConfig['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
if ($loggedIn && isset($_SESSION['user_id'])) {
$stmtLocation = $pdo->prepare(
'SELECT id, name, latitude, longitude, elevation, timezone
FROM app_user_locations
WHERE user_id = ? AND is_default = 1
LIMIT 1'
);
$stmtLocation->execute([(int) $_SESSION['user_id']]);
$userLocation = $stmtLocation->fetch();
if (is_array($userLocation)) {
$defaultLocation = $userLocation;
$locationIsGlobal = false;
}
}
$stmt = $pdo->query(
"SELECT designation, shower_code, radiant_delta_deg, peak_sol, zhr, zhr_variable
FROM meteorshowers
WHERE peak_sol IS NOT NULL
ORDER BY peak_sol ASC, designation ASC"
);
$meteorRows = $stmt->fetchAll();
} catch (Throwable $e) {
$pageError = $e->getMessage();
}
require __DIR__ . '/header.php';
$displayTimezoneName = (string) (($currentLocation['timezone'] ?? null) ?: ($defaultLocation['timezone'] ?? 'Europe/Berlin'));
$displayTimezone = new DateTimeZone($displayTimezoneName);
$displayLocationName = (string) (($currentLocation['name'] ?? null) ?: ($defaultLocation['name'] ?? 'Sternwarte Sonneberg'));
$displayLatitude = isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : (float) $defaultLocation['latitude'];
$currentYear = (int) (new DateTimeImmutable('now', $displayTimezone))->format('Y');
$yearStart = new DateTimeImmutable(sprintf('%04d-01-01 00:00:00', $currentYear), $displayTimezone);
$yearEnd = $yearStart->modify('+1 year');
$meteorEvents = [];
foreach ($meteorRows as $row) {
if (!is_array($row) || !isset($row['peak_sol'])) {
continue;
}
$peakDateTime = findMeteorSolarLongitudeCrossing(
$yearStart,
$yearEnd,
(float) $row['peak_sol'],
$displayTimezone
);
if (!$peakDateTime instanceof DateTimeImmutable) {
continue;
}
$visible = meteorRadiantIsVisible(
$displayLatitude,
$row['radiant_delta_deg'] !== null ? (float) $row['radiant_delta_deg'] : null
);
$zhrText = '-';
if ($row['zhr'] !== null && (int) $row['zhr'] >= 0) {
$zhrText = 'ca. ' . (int) $row['zhr'];
} elseif (trim((string) ($row['zhr_variable'] ?? '')) !== '') {
$zhrText = 'ca. ' . trim((string) $row['zhr_variable']);
}
$meteorEvents[] = [
'designation' => trim((string) ($row['designation'] ?? '')),
'shower_code' => trim((string) ($row['shower_code'] ?? '')),
'peak' => $peakDateTime,
'visible' => $visible,
'visible_label' => $visible ? 'vom Standort sichtbar' : 'vom Standort nicht sichtbar',
'zhr_text' => $zhrText,
];
}
usort(
$meteorEvents,
static fn (array $a, array $b): int => $a['peak'] <=> $b['peak']
);
?>
<section class="card meteorshowers-card">
<div class="meteorshowers-shell">
<div class="meteorshowers-header">
<h2>Meteorstroeme <?= htmlspecialchars((string) $currentYear, ENT_QUOTES, 'UTF-8') ?></h2>
<p>
Einfache Jahresliste der Meteorstrom-Maxima fuer
<strong><?= htmlspecialchars($displayLocationName, ENT_QUOTES, 'UTF-8') ?></strong>
in <?= htmlspecialchars($displayTimezoneName, ENT_QUOTES, 'UTF-8') ?>.
</p>
</div>
<?php if ($pageError !== null): ?>
<div class="meteorshowers-empty meteorshowers-empty-error">
Daten konnten nicht geladen werden: <?= htmlspecialchars($pageError, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php elseif ($meteorEvents === []): ?>
<div class="meteorshowers-empty">
Fuer das aktuelle Jahr wurden keine Meteorstrom-Daten gefunden.
</div>
<?php else: ?>
<div class="meteorshowers-table-wrap">
<table class="meteorshowers-table">
<thead>
<tr>
<th>Datum</th>
<th>Uhrzeit</th>
<th>Meteorstrom</th>
<th>Code</th>
<th>ZHR</th>
<th>Sichtbarkeit</th>
</tr>
</thead>
<tbody>
<?php foreach ($meteorEvents as $event): ?>
<tr class="<?= $event['visible'] ? 'is-visible' : 'is-hidden' ?>">
<td><?= htmlspecialchars($event['peak']->format('d.m.Y'), ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($event['peak']->format('H:i'), ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($event['designation'] !== '' ? $event['designation'] : 'Unbenannter Meteorstrom', ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($event['shower_code'] !== '' ? $event['shower_code'] : '-', ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($event['zhr_text'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($event['visible_label'], ENT_QUOTES, 'UTF-8') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</section>
<style>
.meteorshowers-page main > .container {
max-width: min(1180px, calc(100% - 28px));
}
.meteorshowers-card {
padding: 18px;
}
.meteorshowers-shell {
display: grid;
gap: 18px;
}
.meteorshowers-header,
.meteorshowers-table-wrap,
.meteorshowers-empty {
border: 1px solid rgba(201, 168, 76, 0.14);
border-radius: calc(var(--radius) + 4px);
background:
radial-gradient(circle at top left, rgba(74, 158, 255, 0.1), transparent 34%),
linear-gradient(180deg, rgba(10, 15, 30, 0.96) 0%, rgba(7, 11, 22, 0.98) 100%);
}
.meteorshowers-header {
padding: 22px 20px;
}
.meteorshowers-header h2 {
margin: 0 0 8px;
}
.meteorshowers-header p,
.meteorshowers-empty {
margin: 0;
color: rgba(222, 232, 255, 0.86);
line-height: 1.7;
}
.meteorshowers-empty {
padding: 18px 20px;
}
.meteorshowers-empty-error {
border-color: rgba(200, 90, 90, 0.36);
color: rgba(255, 214, 214, 0.92);
}
.meteorshowers-table-wrap {
overflow: hidden;
}
.meteorshowers-table {
width: 100%;
border-collapse: collapse;
}
.meteorshowers-table th,
.meteorshowers-table td {
padding: 12px 14px;
text-align: left;
border-bottom: 1px solid rgba(201, 168, 76, 0.08);
}
.meteorshowers-table th {
color: rgba(235, 242, 255, 0.95);
font-size: 0.92rem;
letter-spacing: 0.04em;
}
.meteorshowers-table td {
color: rgba(232, 238, 250, 0.92);
}
.meteorshowers-table tbody tr.is-visible {
background: rgba(70, 150, 95, 0.22);
}
.meteorshowers-table tbody tr.is-hidden {
background: rgba(190, 168, 54, 0.22);
}
.meteorshowers-table tbody tr:hover {
background-color: rgba(255, 255, 255, 0.06);
}
.meteorshowers-table tbody tr:last-child td {
border-bottom: 0;
}
@media (max-width: 900px) {
.meteorshowers-table-wrap {
overflow-x: auto;
}
.meteorshowers-table {
min-width: 760px;
}
}
</style>
<?php require __DIR__ . '/footer.php'; ?>