Ephemeriden eingbaut
This commit is contained in:
@@ -44,6 +44,10 @@
|
||||
"label": "Planetensichtbarkeit",
|
||||
"menu_group": "Himmel & Planeten"
|
||||
},
|
||||
"ephemeriden.php": {
|
||||
"label": "Ephemeriden",
|
||||
"menu_group": "Himmel & Planeten"
|
||||
},
|
||||
"comets.php": {
|
||||
"label": "Kometen",
|
||||
"menu_group": "Himmel & Planeten"
|
||||
@@ -120,6 +124,7 @@
|
||||
"links": [
|
||||
"moonphase.php",
|
||||
"planetensichtbarkeit.php",
|
||||
"ephemeriden.php",
|
||||
"comets.php",
|
||||
"astronomical_conversions.php",
|
||||
"tcrb_lightcurve.php",
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/auth_helpers.php';
|
||||
applyLocalDevLoginBypass();
|
||||
|
||||
$pageTitle = 'Ephemeriden - AstroTools';
|
||||
$headerIntroPre = 'Berechnungen';
|
||||
$headerIntroTitle = 'Ephemeriden';
|
||||
$headerIntroSub = '';
|
||||
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
$defaultLocation = null;
|
||||
|
||||
if ($loggedIn && isset($_SESSION['user_id'])) {
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
$stmtLoc = $pdo->prepare(
|
||||
'SELECT id, name, latitude, longitude, elevation, timezone
|
||||
FROM app_user_locations
|
||||
WHERE user_id = ? AND is_default = 1
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmtLoc->execute([(int) $_SESSION['user_id']]);
|
||||
$defaultLocation = $stmtLoc->fetch() ?: null;
|
||||
} catch (Throwable $e) {
|
||||
$defaultLocation = null;
|
||||
}
|
||||
}
|
||||
|
||||
$planetOptions = [
|
||||
'Sun' => 'Sonne',
|
||||
'Moon' => 'Mond',
|
||||
'Mercury' => 'Merkur',
|
||||
'Venus' => 'Venus',
|
||||
'Mars' => 'Mars',
|
||||
'Jupiter' => 'Jupiter',
|
||||
'Saturn' => 'Saturn',
|
||||
'Uranus' => 'Uranus',
|
||||
'Neptune' => 'Neptun',
|
||||
];
|
||||
|
||||
$intervalOptions = [
|
||||
'1' => '1 Minute',
|
||||
'10' => '10 Minuten',
|
||||
'30' => '30 Minuten',
|
||||
'60' => '1 Stunde',
|
||||
'360' => '6 Stunden',
|
||||
'720' => '12 Stunden',
|
||||
'1440' => '1 Tag',
|
||||
'10080' => '7 Tage',
|
||||
'20160' => '14 Tage',
|
||||
'43200' => '30 Tage',
|
||||
];
|
||||
|
||||
$rangeUnitOptions = [
|
||||
'minutes' => 'Minuten',
|
||||
'hours' => 'Stunden',
|
||||
'days' => 'Tage',
|
||||
'weeks' => 'Wochen',
|
||||
'months' => 'Monate',
|
||||
'years' => 'Jahre',
|
||||
];
|
||||
|
||||
$selectedBody = (string) ($_POST['body'] ?? 'Mercury');
|
||||
if (!isset($planetOptions[$selectedBody])) {
|
||||
$selectedBody = 'Mercury';
|
||||
}
|
||||
|
||||
$selectedInterval = (string) ($_POST['interval_minutes'] ?? '1440');
|
||||
if (!isset($intervalOptions[$selectedInterval])) {
|
||||
$selectedInterval = '1440';
|
||||
}
|
||||
|
||||
$selectedRangeUnit = (string) ($_POST['range_unit'] ?? 'days');
|
||||
if (!isset($rangeUnitOptions[$selectedRangeUnit])) {
|
||||
$selectedRangeUnit = 'days';
|
||||
}
|
||||
|
||||
$rangeValueInput = trim((string) ($_POST['range_value'] ?? '7'));
|
||||
if ($rangeValueInput === '') {
|
||||
$rangeValueInput = '7';
|
||||
}
|
||||
|
||||
function ephemeriden_h(mixed $value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function ephemeriden_slug(string $value): string
|
||||
{
|
||||
$value = strtolower(trim($value));
|
||||
$value = str_replace(
|
||||
['ä', 'ö', 'ü', 'ß', ' '],
|
||||
['ae', 'oe', 'ue', 'ss', '-'],
|
||||
$value
|
||||
);
|
||||
$value = preg_replace('/[^a-z0-9\-]+/', '-', $value) ?? $value;
|
||||
$value = trim($value, '-');
|
||||
return $value !== '' ? $value : 'ephemeriden';
|
||||
}
|
||||
|
||||
function runEphemeridenPythonApi(string $action, array $args): array
|
||||
{
|
||||
$scriptPath = __DIR__ . '/py/ephemeriden_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,
|
||||
];
|
||||
}
|
||||
|
||||
$pageLocation = null;
|
||||
if (is_array($defaultLocation) && $defaultLocation !== []) {
|
||||
$pageLocation = [
|
||||
'name' => trim((string) ($defaultLocation['name'] ?? '')),
|
||||
'latitude' => isset($defaultLocation['latitude']) ? (float) $defaultLocation['latitude'] : null,
|
||||
'longitude' => isset($defaultLocation['longitude']) ? (float) $defaultLocation['longitude'] : null,
|
||||
'elevation' => isset($defaultLocation['elevation']) && $defaultLocation['elevation'] !== null ? (float) $defaultLocation['elevation'] : 0.0,
|
||||
'timezone' => (string) ($defaultLocation['timezone'] ?? 'Europe/Berlin'),
|
||||
];
|
||||
}
|
||||
|
||||
$defaultLocationName = $pageLocation['name'] ?? '';
|
||||
$errorMessage = '';
|
||||
$ephemerisRows = [];
|
||||
$exportFormat = (string) ($_POST['export_format'] ?? '');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) {
|
||||
$errorMessage = 'Kein gültiger Standard-Standort verfügbar. Bitte zuerst in den Einstellungen einen Standard-Standort setzen.';
|
||||
} elseif (!ctype_digit($rangeValueInput) || (int) $rangeValueInput < 1 || (int) $rangeValueInput > 31) {
|
||||
$errorMessage = 'Der Zeitraum muss als ganze Zahl zwischen 1 und 31 angegeben werden.';
|
||||
} else {
|
||||
$pythonResult = runEphemeridenPythonApi('planet_ephemeris', [
|
||||
(string) $pageLocation['latitude'],
|
||||
(string) $pageLocation['longitude'],
|
||||
(string) $pageLocation['elevation'],
|
||||
(string) $pageLocation['timezone'],
|
||||
$selectedBody,
|
||||
$selectedInterval,
|
||||
$rangeValueInput,
|
||||
$selectedRangeUnit,
|
||||
]);
|
||||
|
||||
if (!$pythonResult['ok']) {
|
||||
$errorMessage = (string) (
|
||||
$pythonResult['data']['error']
|
||||
?? $pythonResult['error']
|
||||
?? 'Die Ephemeriden konnten nicht berechnet werden.'
|
||||
);
|
||||
} else {
|
||||
$ephemerisRows = $pythonResult['data']['rows'] ?? [];
|
||||
if (!is_array($ephemerisRows)) {
|
||||
$ephemerisRows = [];
|
||||
}
|
||||
|
||||
if ($exportFormat === 'csv' && $ephemerisRows !== []) {
|
||||
$filename = 'ephemeriden-' . ephemeriden_slug((string) ($planetOptions[$selectedBody] ?? 'objekt')) . '.csv';
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
|
||||
echo "\xEF\xBB\xBF";
|
||||
$handle = fopen('php://output', 'wb');
|
||||
if ($handle === false) {
|
||||
exit;
|
||||
}
|
||||
|
||||
fputcsv($handle, ['Name des Objekts', 'Datum', 'Uhrzeit', 'RA (J2000)', 'RA (J2000, dezimal h)', 'Dek (J2000)', 'Dek (J2000, dezimal °)', 'Aufgang', 'Untergang'], ';', '"', '\\');
|
||||
foreach ($ephemerisRows as $row) {
|
||||
fputcsv($handle, [
|
||||
(string) ($row['object_name'] ?? ''),
|
||||
(string) ($row['date_local'] ?? ''),
|
||||
(string) ($row['time_local'] ?? ''),
|
||||
(string) ($row['ra'] ?? ''),
|
||||
(string) ($row['ra_decimal_hours'] ?? ''),
|
||||
(string) ($row['dec'] ?? ''),
|
||||
(string) ($row['dec_decimal_deg'] ?? ''),
|
||||
(string) (($row['rise'] ?? null) ?? '—'),
|
||||
(string) (($row['set'] ?? null) ?? '—'),
|
||||
], ';', '"', '\\');
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require __DIR__ . '/header.php';
|
||||
|
||||
?>
|
||||
|
||||
<style>
|
||||
.ephemeriden-form {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.ephemeriden-fields-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||
gap: 0.8rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.ephemeriden-submit-group {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ephemeriden-results-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.ephemeriden-results-note {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.ephemeriden-export-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ephemeriden-message {
|
||||
margin-bottom: 0.9rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card2);
|
||||
}
|
||||
|
||||
.ephemeriden-message.is-error {
|
||||
border-color: rgba(190, 92, 92, 0.45);
|
||||
color: #ffd7d7;
|
||||
}
|
||||
|
||||
.ephemeriden-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ephemeriden-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.ephemeriden-table th,
|
||||
.ephemeriden-table td {
|
||||
padding: 0.75rem 0.85rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ephemeriden-table th {
|
||||
color: var(--gold-pale);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ephemeriden-table-empty td {
|
||||
color: var(--text-dim);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ephemeriden-fields-row {
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ephemeriden-fields-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="card">
|
||||
<form method="post" class="ephemeriden-form">
|
||||
<div class="ephemeriden-fields-row">
|
||||
<div class="form-group">
|
||||
<label for="ephemeridenBody">Planet</label>
|
||||
<select id="ephemeridenBody" name="body">
|
||||
<?php foreach ($planetOptions as $bodyValue => $bodyLabel): ?>
|
||||
<option value="<?= ephemeriden_h($bodyValue) ?>"<?= $bodyValue === $selectedBody ? ' selected' : '' ?>><?= ephemeriden_h($bodyLabel) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ephemeridenInterval">Intervall</label>
|
||||
<select id="ephemeridenInterval" name="interval_minutes">
|
||||
<?php foreach ($intervalOptions as $intervalValue => $intervalLabel): ?>
|
||||
<option value="<?= ephemeriden_h($intervalValue) ?>"<?= (string) $intervalValue === $selectedInterval ? ' selected' : '' ?>><?= ephemeriden_h($intervalLabel) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ephemeridenRangeValue">Zeitraum ab heute</label>
|
||||
<input
|
||||
type="number"
|
||||
id="ephemeridenRangeValue"
|
||||
name="range_value"
|
||||
min="1"
|
||||
max="31"
|
||||
step="1"
|
||||
value="<?= ephemeriden_h($rangeValueInput) ?>"
|
||||
inputmode="numeric"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ephemeridenRangeUnit">Einheit</label>
|
||||
<select id="ephemeridenRangeUnit" name="range_unit">
|
||||
<?php foreach ($rangeUnitOptions as $rangeUnitValue => $rangeUnitLabel): ?>
|
||||
<option value="<?= ephemeriden_h($rangeUnitValue) ?>"<?= $rangeUnitValue === $selectedRangeUnit ? ' selected' : '' ?>><?= ephemeriden_h($rangeUnitLabel) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="ephemeriden-submit-group">
|
||||
<button type="submit" class="btn btn-primary">Berechnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card mt-md">
|
||||
<div class="ephemeriden-results-head">
|
||||
<div>
|
||||
<h2>Ephemeriden</h2>
|
||||
<div class="ephemeriden-results-note">
|
||||
Standard-Standort:
|
||||
<strong><?= ephemeriden_h($defaultLocationName !== '' ? $defaultLocationName : 'nicht verfügbar') ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($ephemerisRows !== []): ?>
|
||||
<form method="post" class="ephemeriden-export-form">
|
||||
<input type="hidden" name="body" value="<?= ephemeriden_h($selectedBody) ?>">
|
||||
<input type="hidden" name="interval_minutes" value="<?= ephemeriden_h($selectedInterval) ?>">
|
||||
<input type="hidden" name="range_value" value="<?= ephemeriden_h($rangeValueInput) ?>">
|
||||
<input type="hidden" name="range_unit" value="<?= ephemeriden_h($selectedRangeUnit) ?>">
|
||||
<input type="hidden" name="export_format" value="csv">
|
||||
<button type="submit" class="btn btn-secondary">Export CSV</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($errorMessage !== ''): ?>
|
||||
<div class="ephemeriden-message is-error"><?= ephemeriden_h($errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ephemeriden-table-wrap">
|
||||
<table class="ephemeriden-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name des Objekts</th>
|
||||
<th>Datum</th>
|
||||
<th>Uhrzeit</th>
|
||||
<th>RA (J2000)</th>
|
||||
<th>Dek (J2000)</th>
|
||||
<th>Aufgang</th>
|
||||
<th>Untergang</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($ephemerisRows !== []): ?>
|
||||
<?php foreach ($ephemerisRows as $row): ?>
|
||||
<tr>
|
||||
<td><?= ephemeriden_h((string) ($row['object_name'] ?? '')) ?></td>
|
||||
<td><?= ephemeriden_h((string) ($row['date_local'] ?? '')) ?></td>
|
||||
<td><?= ephemeriden_h((string) ($row['time_local'] ?? '')) ?></td>
|
||||
<td><?= ephemeriden_h((string) ($row['ra'] ?? '')) ?></td>
|
||||
<td><?= ephemeriden_h((string) ($row['dec'] ?? '')) ?></td>
|
||||
<td><?= ephemeriden_h((string) (($row['rise'] ?? null) ?? '—')) ?></td>
|
||||
<td><?= ephemeriden_h((string) (($row['set'] ?? null) ?? '—')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && $errorMessage === ''): ?>
|
||||
<tr class="ephemeriden-table-empty">
|
||||
<td colspan="7">Für die gewählte Anfrage wurden keine Tabellenzeilen erzeugt.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<tr class="ephemeriden-table-empty">
|
||||
<td colspan="7">Noch keine Berechnung ausgeführt.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
@@ -143,6 +143,7 @@ $menuGroups = [
|
||||
['href' => 'moonphase.php', 'label' => 'Mondphase'],
|
||||
['href' => 'moonyear.php', 'label' => 'Mondphasen-Jahresübersicht'],
|
||||
['href' => 'planetensichtbarkeit.php', 'label' => 'Planetensichtbarkeit'],
|
||||
['href' => 'ephemeriden.php', 'label' => 'Ephemeriden'],
|
||||
['href' => 'comets.php', 'label' => 'Kometen'],
|
||||
['href' => 'astronomical_conversions.php', 'label' => 'Astronomical Conversions'],
|
||||
['href' => 'tcrb_lightcurve.php', 'label' => 'T CrB Lichtkurve'],
|
||||
|
||||
Binary file not shown.
+185
-2
@@ -414,6 +414,19 @@ PLANETS = [
|
||||
("Neptun", astronomy.Body.Neptune),
|
||||
]
|
||||
|
||||
EPHEMERIS_BODIES = {
|
||||
"Sun": ("Sonne", astronomy.Body.Sun),
|
||||
"Moon": ("Mond", astronomy.Body.Moon),
|
||||
"Mercury": ("Merkur", astronomy.Body.Mercury),
|
||||
"Venus": ("Venus", astronomy.Body.Venus),
|
||||
"Earth": ("Erde", astronomy.Body.Earth),
|
||||
"Mars": ("Mars", astronomy.Body.Mars),
|
||||
"Jupiter": ("Jupiter", astronomy.Body.Jupiter),
|
||||
"Saturn": ("Saturn", astronomy.Body.Saturn),
|
||||
"Uranus": ("Uranus", astronomy.Body.Uranus),
|
||||
"Neptune": ("Neptun", astronomy.Body.Neptune),
|
||||
}
|
||||
|
||||
PLANET_VISIBILITY_ROWS = [
|
||||
{
|
||||
"key": "mercury",
|
||||
@@ -820,6 +833,171 @@ def action_planet_rise_set(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def format_ra_hours(ra_hours: float) -> str:
|
||||
total_seconds = int(round(float(ra_hours) * 3600.0))
|
||||
total_seconds %= 24 * 3600
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def format_dec_deg(dec_deg: float) -> str:
|
||||
sign = "+" if dec_deg >= 0 else "-"
|
||||
total_seconds = int(round(abs(float(dec_deg)) * 3600.0))
|
||||
degrees = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{sign}{degrees:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def days_in_month(year: int, month: int) -> int:
|
||||
if month == 12:
|
||||
next_month = datetime(year + 1, 1, 1)
|
||||
else:
|
||||
next_month = datetime(year, month + 1, 1)
|
||||
this_month = datetime(year, month, 1)
|
||||
return (next_month - this_month).days
|
||||
|
||||
|
||||
def add_calendar_unit(base: datetime, amount: int, unit: str) -> datetime:
|
||||
if unit == "minutes":
|
||||
return base + timedelta(minutes=amount)
|
||||
if unit == "hours":
|
||||
return base + timedelta(hours=amount)
|
||||
if unit == "days":
|
||||
return base + timedelta(days=amount)
|
||||
if unit == "weeks":
|
||||
return base + timedelta(weeks=amount)
|
||||
if unit == "months":
|
||||
month_index = (base.month - 1) + amount
|
||||
year = base.year + (month_index // 12)
|
||||
month = (month_index % 12) + 1
|
||||
day = min(base.day, days_in_month(year, month))
|
||||
return base.replace(year=year, month=month, day=day)
|
||||
if unit == "years":
|
||||
year = base.year + amount
|
||||
day = base.day
|
||||
if base.month == 2 and base.day == 29:
|
||||
day = min(day, days_in_month(year, base.month))
|
||||
return base.replace(year=year, day=day)
|
||||
fail("Zeitraum-Einheit ist ungueltig.", extra={"unit": unit})
|
||||
|
||||
|
||||
def action_planet_ephemeris(args: list[str]) -> dict:
|
||||
if len(args) != 8:
|
||||
fail(
|
||||
"Aktion planet_ephemeris erwartet 8 Argumente: latitude longitude elevation timezone body intervalMinutes rangeValue rangeUnit",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
latitude = parse_float(args[0], "Latitude")
|
||||
longitude = parse_float(args[1], "Longitude")
|
||||
elevation = parse_float(args[2], "Elevation")
|
||||
timezone_name = args[3]
|
||||
body_name = str(args[4]).strip()
|
||||
|
||||
try:
|
||||
interval_minutes = int(args[5])
|
||||
range_value = int(args[6])
|
||||
except ValueError as exc:
|
||||
fail("Intervall oder Zeitraum ist ungueltig.", extra={"details": str(exc), "argv": args})
|
||||
|
||||
range_unit = str(args[7]).strip()
|
||||
|
||||
if interval_minutes <= 0:
|
||||
fail("Intervall muss groesser als 0 sein.", extra={"interval_minutes": interval_minutes})
|
||||
if range_value <= 0:
|
||||
fail("Zeitraum muss groesser als 0 sein.", extra={"range_value": range_value})
|
||||
if body_name not in EPHEMERIS_BODIES:
|
||||
fail("Planet ist ungueltig.", extra={"body": body_name})
|
||||
if range_unit not in {"minutes", "hours", "days", "weeks", "months", "years"}:
|
||||
fail("Zeitraum-Einheit ist ungueltig.", extra={"range_unit": range_unit})
|
||||
|
||||
try:
|
||||
tz = ZoneInfo(timezone_name)
|
||||
except Exception as exc:
|
||||
fail("Zeitzone ist ungueltig.", extra={"details": str(exc)})
|
||||
|
||||
label, body = EPHEMERIS_BODIES[body_name]
|
||||
if body == astronomy.Body.Earth:
|
||||
fail("Fuer die Erde kann von einem Standort auf der Erde keine sinnvolle geozentrische Ephemeride berechnet werden.")
|
||||
|
||||
observer = astronomy.Observer(latitude, longitude, elevation)
|
||||
local_start = datetime.now(tz).replace(second=0, microsecond=0)
|
||||
local_end = add_calendar_unit(local_start, range_value, range_unit)
|
||||
|
||||
rows = []
|
||||
current_local = local_start
|
||||
step = timedelta(minutes=interval_minutes)
|
||||
rise_set_cache: dict[str, tuple[str | None, str | None]] = {}
|
||||
max_rows = 5000
|
||||
|
||||
while current_local <= local_end:
|
||||
if len(rows) >= max_rows:
|
||||
fail(
|
||||
"Die Anfrage erzeugt zu viele Tabellenzeilen. Bitte Zeitraum verkuerzen oder groesseres Intervall waehlen.",
|
||||
extra={
|
||||
"max_rows": max_rows,
|
||||
"interval_minutes": interval_minutes,
|
||||
"range_value": range_value,
|
||||
"range_unit": range_unit,
|
||||
},
|
||||
)
|
||||
|
||||
current_utc = current_local.astimezone(timezone.utc)
|
||||
time_value = dt_to_time(current_utc)
|
||||
eq = astronomy.Equator(body, time_value, observer, True, True)
|
||||
|
||||
day_key = current_local.strftime("%Y-%m-%d")
|
||||
if day_key not in rise_set_cache:
|
||||
local_day_start = current_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
local_day_end = local_day_start + timedelta(days=1)
|
||||
day_start_time = dt_to_time(local_day_start.astimezone(timezone.utc))
|
||||
day_end_utc = local_day_end.astimezone(timezone.utc)
|
||||
|
||||
rise = search_event(body, astronomy.Direction.Rise, observer, day_start_time, day_end_utc)
|
||||
set_ = search_event(body, astronomy.Direction.Set, observer, day_start_time, day_end_utc)
|
||||
rise_label = serialize_event("Aufgang", rise, tz).get("local_time") if rise is not None else None
|
||||
set_label = serialize_event("Untergang", set_, tz).get("local_time") if set_ is not None else None
|
||||
rise_set_cache[day_key] = (rise_label, set_label)
|
||||
|
||||
rise_label, set_label = rise_set_cache[day_key]
|
||||
rows.append({
|
||||
"object_name": label,
|
||||
"datetime_local": current_local.strftime("%d.%m.%Y %H:%M"),
|
||||
"ra": format_ra_hours(float(eq.ra)),
|
||||
"dec": format_dec_deg(float(eq.dec)),
|
||||
"rise": rise_label,
|
||||
"set": set_label,
|
||||
})
|
||||
|
||||
current_local += step
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "planet_ephemeris",
|
||||
"observer": {
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"elevation": elevation,
|
||||
"timezone": timezone_name,
|
||||
},
|
||||
"object": {
|
||||
"key": body_name,
|
||||
"label": label,
|
||||
},
|
||||
"window": {
|
||||
"local_start": local_start.isoformat(),
|
||||
"local_end": local_end.isoformat(),
|
||||
"interval_minutes": interval_minutes,
|
||||
"range_value": range_value,
|
||||
"range_unit": range_unit,
|
||||
},
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
def body_altitude_deg(
|
||||
body: astronomy.Body,
|
||||
observer: astronomy.Observer,
|
||||
@@ -4932,7 +5110,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", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "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", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "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:]
|
||||
@@ -4997,6 +5175,11 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "planet_ephemeris":
|
||||
result = action_planet_ephemeris(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "planet_visibility_chart":
|
||||
result = action_planet_visibility_chart(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
@@ -5102,7 +5285,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", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_visibility_chart", "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", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "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,288 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
|
||||
import astronomy
|
||||
|
||||
|
||||
EPHEMERIS_BODIES = {
|
||||
"Sun": ("Sonne", astronomy.Body.Sun),
|
||||
"Moon": ("Mond", astronomy.Body.Moon),
|
||||
"Mercury": ("Merkur", astronomy.Body.Mercury),
|
||||
"Venus": ("Venus", astronomy.Body.Venus),
|
||||
"Mars": ("Mars", astronomy.Body.Mars),
|
||||
"Jupiter": ("Jupiter", astronomy.Body.Jupiter),
|
||||
"Saturn": ("Saturn", astronomy.Body.Saturn),
|
||||
"Uranus": ("Uranus", astronomy.Body.Uranus),
|
||||
"Neptune": ("Neptun", astronomy.Body.Neptune),
|
||||
}
|
||||
|
||||
|
||||
def fail(message: str, *, extra: dict | None = None, code: int = 1) -> None:
|
||||
payload = {"ok": False, "error": message}
|
||||
if extra:
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, ensure_ascii=True))
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def parse_float(value: str, label: str) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError as exc:
|
||||
fail(f"{label} ist ungueltig.", extra={"details": str(exc), "value": value})
|
||||
|
||||
|
||||
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 time_to_datetime(time_value: astronomy.Time) -> datetime:
|
||||
year, month, day, hour, minute, second = time_value.Calendar()
|
||||
second_int = int(second)
|
||||
microsecond = int(round((second - second_int) * 1_000_000))
|
||||
|
||||
if microsecond >= 1_000_000:
|
||||
second_int += 1
|
||||
microsecond -= 1_000_000
|
||||
|
||||
return datetime(year, month, day, hour, minute, second_int, microsecond, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def serialize_event(label: str, event_time: astronomy.Time | None, tz: ZoneInfo) -> dict:
|
||||
if event_time is None:
|
||||
return {"label": label, "found": False}
|
||||
|
||||
utc_dt = time_to_datetime(event_time)
|
||||
local_dt = utc_dt.astimezone(tz)
|
||||
|
||||
return {
|
||||
"label": label,
|
||||
"found": True,
|
||||
"utc_iso": utc_dt.isoformat().replace("+00:00", "Z"),
|
||||
"local_iso": local_dt.isoformat(),
|
||||
"local_time": local_dt.strftime("%H:%M"),
|
||||
"local_date": local_dt.strftime("%Y-%m-%d"),
|
||||
}
|
||||
|
||||
|
||||
def search_event(
|
||||
body: astronomy.Body,
|
||||
direction: astronomy.Direction,
|
||||
observer: astronomy.Observer,
|
||||
start_time: astronomy.Time,
|
||||
end_utc: datetime,
|
||||
) -> astronomy.Time | None:
|
||||
search_limit_days = (end_utc - time_to_datetime(start_time)).total_seconds() / 86400.0
|
||||
if search_limit_days <= 0:
|
||||
return None
|
||||
|
||||
result = astronomy.SearchRiseSet(body, observer, direction, start_time, search_limit_days)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
result_dt = time_to_datetime(result)
|
||||
if result_dt >= end_utc:
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_ra_hours(ra_hours: float) -> str:
|
||||
total_seconds = int(round(float(ra_hours) * 3600.0))
|
||||
total_seconds %= 24 * 3600
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def format_dec_deg(dec_deg: float) -> str:
|
||||
sign = "+" if dec_deg >= 0 else "-"
|
||||
total_seconds = int(round(abs(float(dec_deg)) * 3600.0))
|
||||
degrees = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{sign}{degrees:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def days_in_month(year: int, month: int) -> int:
|
||||
if month == 12:
|
||||
next_month = datetime(year + 1, 1, 1)
|
||||
else:
|
||||
next_month = datetime(year, month + 1, 1)
|
||||
this_month = datetime(year, month, 1)
|
||||
return (next_month - this_month).days
|
||||
|
||||
|
||||
def add_calendar_unit(base: datetime, amount: int, unit: str) -> datetime:
|
||||
if unit == "minutes":
|
||||
return base + timedelta(minutes=amount)
|
||||
if unit == "hours":
|
||||
return base + timedelta(hours=amount)
|
||||
if unit == "days":
|
||||
return base + timedelta(days=amount)
|
||||
if unit == "weeks":
|
||||
return base + timedelta(weeks=amount)
|
||||
if unit == "months":
|
||||
month_index = (base.month - 1) + amount
|
||||
year = base.year + (month_index // 12)
|
||||
month = (month_index % 12) + 1
|
||||
day = min(base.day, days_in_month(year, month))
|
||||
return base.replace(year=year, month=month, day=day)
|
||||
if unit == "years":
|
||||
year = base.year + amount
|
||||
day = base.day
|
||||
if base.month == 2 and base.day == 29:
|
||||
day = min(day, days_in_month(year, base.month))
|
||||
return base.replace(year=year, day=day)
|
||||
fail("Zeitraum-Einheit ist ungueltig.", extra={"unit": unit})
|
||||
|
||||
|
||||
def action_planet_ephemeris(args: list[str]) -> dict:
|
||||
if len(args) != 8:
|
||||
fail(
|
||||
"Aktion planet_ephemeris erwartet 8 Argumente: latitude longitude elevation timezone body intervalMinutes rangeValue rangeUnit",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
latitude = parse_float(args[0], "Latitude")
|
||||
longitude = parse_float(args[1], "Longitude")
|
||||
elevation = parse_float(args[2], "Elevation")
|
||||
timezone_name = args[3]
|
||||
body_name = str(args[4]).strip()
|
||||
|
||||
try:
|
||||
interval_minutes = int(args[5])
|
||||
range_value = int(args[6])
|
||||
except ValueError as exc:
|
||||
fail("Intervall oder Zeitraum ist ungueltig.", extra={"details": str(exc), "argv": args})
|
||||
|
||||
range_unit = str(args[7]).strip()
|
||||
|
||||
if interval_minutes <= 0:
|
||||
fail("Intervall muss groesser als 0 sein.", extra={"interval_minutes": interval_minutes})
|
||||
if range_value <= 0:
|
||||
fail("Zeitraum muss groesser als 0 sein.", extra={"range_value": range_value})
|
||||
if body_name not in EPHEMERIS_BODIES:
|
||||
fail("Objekt ist ungueltig.", extra={"body": body_name})
|
||||
if range_unit not in {"minutes", "hours", "days", "weeks", "months", "years"}:
|
||||
fail("Zeitraum-Einheit ist ungueltig.", extra={"range_unit": range_unit})
|
||||
|
||||
try:
|
||||
tz = ZoneInfo(timezone_name)
|
||||
except Exception as exc:
|
||||
fail("Zeitzone ist ungueltig.", extra={"details": str(exc)})
|
||||
|
||||
label, body = EPHEMERIS_BODIES[body_name]
|
||||
observer = astronomy.Observer(latitude, longitude, elevation)
|
||||
local_start = datetime.now(tz).replace(second=0, microsecond=0)
|
||||
local_end = add_calendar_unit(local_start, range_value, range_unit)
|
||||
|
||||
rows = []
|
||||
current_local = local_start
|
||||
step = timedelta(minutes=interval_minutes)
|
||||
rise_set_cache: dict[str, tuple[str | None, str | None]] = {}
|
||||
max_rows = 50000
|
||||
|
||||
while current_local <= local_end:
|
||||
if len(rows) >= max_rows:
|
||||
fail(
|
||||
"Die Anfrage erzeugt zu viele Tabellenzeilen. Bitte Zeitraum verkuerzen oder groesseres Intervall waehlen.",
|
||||
extra={
|
||||
"max_rows": max_rows,
|
||||
"interval_minutes": interval_minutes,
|
||||
"range_value": range_value,
|
||||
"range_unit": range_unit,
|
||||
},
|
||||
)
|
||||
|
||||
current_utc = current_local.astimezone(timezone.utc)
|
||||
time_value = dt_to_time(current_utc)
|
||||
eq = astronomy.Equator(body, time_value, observer, False, True)
|
||||
|
||||
day_key = current_local.strftime("%Y-%m-%d")
|
||||
if day_key not in rise_set_cache:
|
||||
local_day_start = current_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
local_day_end = local_day_start + timedelta(days=1)
|
||||
day_start_time = dt_to_time(local_day_start.astimezone(timezone.utc))
|
||||
day_end_utc = local_day_end.astimezone(timezone.utc)
|
||||
|
||||
rise = search_event(body, astronomy.Direction.Rise, observer, day_start_time, day_end_utc)
|
||||
set_ = search_event(body, astronomy.Direction.Set, observer, day_start_time, day_end_utc)
|
||||
rise_label = serialize_event("Aufgang", rise, tz).get("local_time") if rise is not None else None
|
||||
set_label = serialize_event("Untergang", set_, tz).get("local_time") if set_ is not None else None
|
||||
rise_set_cache[day_key] = (rise_label, set_label)
|
||||
|
||||
rise_label, set_label = rise_set_cache[day_key]
|
||||
rows.append({
|
||||
"object_name": label,
|
||||
"date_local": current_local.strftime("%d.%m.%Y"),
|
||||
"time_local": current_local.strftime("%H:%M"),
|
||||
"ra": format_ra_hours(float(eq.ra)),
|
||||
"ra_decimal_hours": round(float(eq.ra), 8),
|
||||
"dec": format_dec_deg(float(eq.dec)),
|
||||
"dec_decimal_deg": round(float(eq.dec), 8),
|
||||
"rise": rise_label,
|
||||
"set": set_label,
|
||||
})
|
||||
|
||||
current_local += step
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "planet_ephemeris",
|
||||
"observer": {
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"elevation": elevation,
|
||||
"timezone": timezone_name,
|
||||
},
|
||||
"object": {
|
||||
"key": body_name,
|
||||
"label": label,
|
||||
},
|
||||
"window": {
|
||||
"local_start": local_start.isoformat(),
|
||||
"local_end": local_end.isoformat(),
|
||||
"interval_minutes": interval_minutes,
|
||||
"range_value": range_value,
|
||||
"range_unit": range_unit,
|
||||
},
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["planet_ephemeris"]})
|
||||
|
||||
action = sys.argv[1]
|
||||
args = sys.argv[2:]
|
||||
|
||||
if action == "planet_ephemeris":
|
||||
result = action_planet_ephemeris(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["planet_ephemeris"]})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user