347 lines
9.9 KiB
PHP
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'; ?>
|