Monatsvorschau weiter gebaut

This commit is contained in:
2026-04-03 12:18:13 +02:00
parent dfd65b16e3
commit 3d98fef789
2 changed files with 1136 additions and 4 deletions
+836 -2
View File
@@ -12,10 +12,12 @@ $currentYear = (int) date('Y');
$selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear;
$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n');
$starDatabaseMagnitudeLimit = 6.0;
$starDisplayMagnitudeLimit = 4.6;
$starDisplayMagnitudeLimit = 4.4;
$starLabelMagnitudeLimit = 2.0;
$messierDatabaseMagnitudeLimit = 4.0;
$messierDisplayMagnitudeLimit = 4.0;
$messierDisplayMagnitudeLimit = 1.0;
$moonPlanetMaxSeparationDeg = 1.5;
$moonOccultationMaxMag = 2.0;
$years = range($currentYear - 2, $currentYear + 5);
$months = [
@@ -51,8 +53,33 @@ $starData = [];
$lineStarData = [];
$constellationLines = [];
$messierData = [];
$planetData = [];
$moonData = null;
$moonPhaseEvents = [];
$moonPlanetApproaches = [];
$moonOccultations = [];
$monthEventList = [];
$starDataError = null;
if (!function_exists('buildMonthOccultationStarLabel')) {
function buildMonthOccultationStarLabel(array $row): string
{
$proper = trim((string) ($row['proper'] ?? ''));
if ($proper !== '') {
return $proper;
}
$bayer = trim((string) ($row['bayer'] ?? ''));
$con = trim((string) ($row['con'] ?? ''));
if ($bayer !== '') {
return $con !== '' ? $bayer . ' ' . $con : $bayer;
}
$hip = isset($row['hip']) ? (int) $row['hip'] : 0;
return $hip > 0 ? 'HIP ' . $hip : 'Unbenannter Stern';
}
}
try {
$dbConfig = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
@@ -230,6 +257,221 @@ $chartLocalTime = new DateTimeImmutable(
);
$chartUtcTime = $chartLocalTime->setTimezone(new DateTimeZone('UTC'));
$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python';
$pythonScriptPath = __DIR__ . '/py/api.py';
$pythonOutput = [];
$pythonExitCode = 0;
$planetErrorText = null;
$pythonArgs = [
$pythonScriptPath,
'month_sky_context',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
(string) $monthTime['hour'],
(string) $monthTime['minute'],
$chartTimezone,
];
$pythonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1';
exec($pythonCommand, $pythonOutput, $pythonExitCode);
$pythonRaw = trim(implode("\n", $pythonOutput));
$pythonDecoded = json_decode($pythonRaw, true);
if (is_array($pythonDecoded) && ($pythonDecoded['ok'] ?? false)) {
if (is_array($pythonDecoded['bodies']['moon'] ?? null)) {
$moon = $pythonDecoded['bodies']['moon'];
$moonData = [
'label' => (string) ($moon['label'] ?? 'Mond'),
'az' => isset($moon['azimuth_deg']) ? (float) $moon['azimuth_deg'] : null,
'alt' => isset($moon['altitude_deg']) ? (float) $moon['altitude_deg'] : null,
'visible' => !empty($moon['visible']),
'phaseFraction' => isset($moon['phase_fraction']) ? (float) $moon['phase_fraction'] : null,
'waxing' => isset($moon['waxing']) ? (bool) $moon['waxing'] : null,
];
}
foreach (($pythonDecoded['bodies']['planets'] ?? []) as $planet) {
if (!is_array($planet)) {
continue;
}
$planetData[] = [
'key' => (string) ($planet['key'] ?? ''),
'label' => (string) ($planet['label'] ?? ''),
'az' => isset($planet['azimuth_deg']) ? (float) $planet['azimuth_deg'] : null,
'alt' => isset($planet['altitude_deg']) ? (float) $planet['altitude_deg'] : null,
'mag' => isset($planet['magnitude']) ? (float) $planet['magnitude'] : null,
'visible' => !empty($planet['visible']),
];
}
} elseif ($pythonRaw !== '') {
$planetErrorText = $pythonRaw;
}
$moonPhaseOutput = [];
$moonPhaseExitCode = 0;
$moonPhaseArgs = [
$pythonScriptPath,
'moon_phases_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$moonPhaseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPhaseArgs)) . ' 2>&1';
exec($moonPhaseCommand, $moonPhaseOutput, $moonPhaseExitCode);
$moonPhaseRaw = trim(implode("\n", $moonPhaseOutput));
$moonPhaseDecoded = json_decode($moonPhaseRaw, true);
if (is_array($moonPhaseDecoded) && ($moonPhaseDecoded['ok'] ?? false)) {
$moonPhaseEvents = is_array($moonPhaseDecoded['phases'] ?? null) ? $moonPhaseDecoded['phases'] : [];
}
$moonPlanetOutput = [];
$moonPlanetExitCode = 0;
$moonPlanetArgs = [
$pythonScriptPath,
'moon_planet_approaches',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
(string) $moonPlanetMaxSeparationDeg,
];
$moonPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPlanetArgs)) . ' 2>&1';
exec($moonPlanetCommand, $moonPlanetOutput, $moonPlanetExitCode);
$moonPlanetRaw = trim(implode("\n", $moonPlanetOutput));
$moonPlanetDecoded = json_decode($moonPlanetRaw, true);
if (is_array($moonPlanetDecoded) && ($moonPlanetDecoded['ok'] ?? false)) {
$moonPlanetApproaches = is_array($moonPlanetDecoded['approaches'] ?? null) ? $moonPlanetDecoded['approaches'] : [];
}
$moonOccultationStars = [];
try {
if (isset($pdo)) {
$stmtOccultStars = $pdo->prepare(
"SELECT hip, proper, bayer, con, mag, rarad, decrad, dist
FROM star_hipparcos
WHERE rarad IS NOT NULL
AND decrad IS NOT NULL
AND mag IS NOT NULL
AND mag <= :max_mag
ORDER BY mag ASC, proper ASC, bayer ASC, hip ASC"
);
$stmtOccultStars->execute([':max_mag' => $moonOccultationMaxMag]);
foreach ($stmtOccultStars->fetchAll() as $row) {
$moonOccultationStars[] = [
'hip' => isset($row['hip']) ? (int) $row['hip'] : 0,
'label' => buildMonthOccultationStarLabel($row),
'constellation' => trim((string) ($row['con'] ?? '')),
'mag' => isset($row['mag']) ? (float) $row['mag'] : 99.0,
'ra' => isset($row['rarad']) ? (float) $row['rarad'] * (12 / M_PI) : 0.0,
'dec' => isset($row['decrad']) ? (float) $row['decrad'] * (180 / M_PI) : 0.0,
'distLy' => isset($row['dist']) && $row['dist'] !== null && (float) $row['dist'] > 0.0
? (float) $row['dist'] * 3.26156
: 1000.0,
];
}
}
} catch (Throwable $e) {
}
if ($moonOccultationStars) {
$tempOccultationFile = tempnam(sys_get_temp_dir(), 'month_occult_');
if ($tempOccultationFile !== false) {
try {
file_put_contents($tempOccultationFile, json_encode($moonOccultationStars, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
$moonOccultOutput = [];
$moonOccultExitCode = 0;
$moonOccultArgs = [
$pythonScriptPath,
'moon_star_occultations_for_month',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
$tempOccultationFile,
];
$moonOccultCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonOccultArgs)) . ' 2>&1';
exec($moonOccultCommand, $moonOccultOutput, $moonOccultExitCode);
$moonOccultRaw = trim(implode("\n", $moonOccultOutput));
$moonOccultDecoded = json_decode($moonOccultRaw, true);
if (is_array($moonOccultDecoded) && ($moonOccultDecoded['ok'] ?? false)) {
$moonOccultations = is_array($moonOccultDecoded['results'] ?? null) ? $moonOccultDecoded['results'] : [];
}
} catch (Throwable $e) {
} finally {
if (is_file($tempOccultationFile)) {
@unlink($tempOccultationFile);
}
}
}
}
foreach ($moonPhaseEvents as $phaseEvent) {
if (!is_array($phaseEvent)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($phaseEvent['label'] ?? ''),
'date' => (string) ($phaseEvent['local_date'] ?? ''),
'time' => (string) ($phaseEvent['local_time'] ?? ''),
'local_iso' => (string) ($phaseEvent['local_iso'] ?? ''),
'type' => 'moon_phase',
];
}
foreach ($moonPlanetApproaches as $approachEvent) {
if (!is_array($approachEvent)) {
continue;
}
$distanceText = isset($approachEvent['separation_deg'])
? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . '°'
: '';
$monthEventList[] = [
'event' => trim((string) ($approachEvent['label'] ?? 'Mond nahe Planet') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($approachEvent['local_date'] ?? ''),
'time' => (string) ($approachEvent['local_time'] ?? ''),
'local_iso' => (string) ($approachEvent['local_iso'] ?? ''),
'type' => 'moon_planet',
];
}
foreach ($moonOccultations as $occultationEvent) {
if (!is_array($occultationEvent)) {
continue;
}
$maximum = is_array($occultationEvent['maximum'] ?? null) ? $occultationEvent['maximum'] : null;
$star = is_array($occultationEvent['star'] ?? null) ? $occultationEvent['star'] : null;
if ($maximum === null || $star === null) {
continue;
}
$starLabel = (string) ($star['label'] ?? 'Stern');
$starMag = isset($star['mag']) ? number_format((float) $star['mag'], 1, ',', '') : '';
$monthEventList[] = [
'event' => 'Mond bedeckt ' . $starLabel . ($starMag !== '' ? ' (mag ' . $starMag . ')' : ''),
'date' => (string) ($maximum['local_date'] ?? ''),
'time' => (string) ($maximum['local_time'] ?? ''),
'local_iso' => (string) ($maximum['local_iso'] ?? ''),
'type' => 'moon_occultation',
];
}
usort(
$monthEventList,
static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? ''))
);
$chartConfig = [
'monthName' => $months[$selectedMonth],
'year' => $selectedYear,
@@ -253,6 +495,8 @@ $starDataJson = json_encode($starData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_S
$lineStarDataJson = json_encode($lineStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$constellationLinesJson = json_encode($constellationLines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$messierDataJson = json_encode($messierData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$planetDataJson = json_encode($planetData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$moonDataJson = json_encode($moonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
@@ -330,6 +574,90 @@ $starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_U
<p id="monatskarteSouthStatus">Bereite Südhimmelkarte vor...</p>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Mondphasen <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
<p>Chronologisch für <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?></p>
</div>
</div>
<div class="monatskarte-footer">
<?php if ($monthEventList): ?>
<div class="riseset-grid" style="grid-template-columns: 1.3fr 1fr 1fr; row-gap: 10px;">
<span class="riseset-label riseset-dim">Ereignis</span>
<span class="riseset-label riseset-dim">Datum</span>
<span class="riseset-label riseset-dim">Uhrzeit</span>
<?php foreach ($monthEventList as $eventItem): ?>
<span class="riseset-val"><?= htmlspecialchars((string) ($eventItem['event'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($eventItem['date'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($eventItem['time'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Keine Mondphasen für diesen Monat gefunden.</p>
<?php endif; ?>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Mond nahe Planeten</h3>
<p>Maximalabstand <?= htmlspecialchars(number_format($moonPlanetMaxSeparationDeg, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?>°</p>
</div>
</div>
<div class="monatskarte-footer">
<?php if ($moonPlanetApproaches): ?>
<div class="riseset-grid" style="grid-template-columns: 1.5fr 1fr 1fr 1fr; row-gap: 10px;">
<span class="riseset-label riseset-dim">Ereignis</span>
<span class="riseset-label riseset-dim">Datum</span>
<span class="riseset-label riseset-dim">Uhrzeit</span>
<span class="riseset-label riseset-dim">Abstand</span>
<?php foreach ($moonPlanetApproaches as $approachEvent): ?>
<span class="riseset-val"><?= htmlspecialchars((string) ($approachEvent['label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($approachEvent['local_date'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($approachEvent['local_time'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars(number_format((float) ($approachEvent['separation_deg'] ?? 0), 2, ',', ''), ENT_QUOTES, 'UTF-8') ?>°</span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Keine Mond-Planet-Nähen innerhalb von <?= htmlspecialchars(number_format($moonPlanetMaxSeparationDeg, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?>° gefunden.</p>
<?php endif; ?>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Sternbedeckungen</h3>
<p>Bis Grenzgr&ouml;&szlig;e <?= htmlspecialchars(number_format($moonOccultationMaxMag, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?></p>
</div>
</div>
<div class="monatskarte-footer">
<?php if ($moonOccultations): ?>
<div class="riseset-grid" style="grid-template-columns: 1.6fr 1fr 1fr 0.9fr; row-gap: 10px;">
<span class="riseset-label riseset-dim">Stern</span>
<span class="riseset-label riseset-dim">Datum</span>
<span class="riseset-label riseset-dim">Uhrzeit</span>
<span class="riseset-label riseset-dim">Mag</span>
<?php foreach ($moonOccultations as $occultationEvent): ?>
<?php $star = is_array($occultationEvent['star'] ?? null) ? $occultationEvent['star'] : []; ?>
<?php $maximum = is_array($occultationEvent['maximum'] ?? null) ? $occultationEvent['maximum'] : []; ?>
<span class="riseset-val"><?= htmlspecialchars((string) ($star['label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($maximum['local_date'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($maximum['local_time'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars(number_format((float) ($star['mag'] ?? 0), 1, ',', ''), ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Keine Sternbedeckungen bis Magnitude <?= htmlspecialchars(number_format($moonOccultationMaxMag, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?> gefunden.</p>
<?php endif; ?>
</div>
</section>
</div>
</section>
@@ -471,6 +799,8 @@ const MONTH_CHART_STARS = <?= $starDataJson ?: '[]' ?>;
const MONTH_CHART_LINE_STARS = <?= $lineStarDataJson ?: '[]' ?>;
const MONTH_CHART_CONSTELLATIONS = <?= $constellationLinesJson ?: '[]' ?>;
const MONTH_CHART_MESSIER = <?= $messierDataJson ?: '[]' ?>;
const MONTH_CHART_PLANETS = <?= $planetDataJson ?: '[]' ?>;
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
(function () {
@@ -636,6 +966,162 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
};
}
function placePlanetLabel(text, x, y, radius) {
const attempts = [
{ x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' },
{ x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' },
{ x, y: y - radius - 7, align: 'center', baseline: 'bottom' },
{ x, y: y + radius + 7, align: 'center', baseline: 'top' },
{ x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' },
{ x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' },
{ x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' },
{ x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' },
];
function drawLabelBackdrop(drawX, drawY, align, baseline) {
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
const padX = 5;
const padY = 3;
let boxX = drawX;
if (align === 'center') boxX = drawX - textWidth / 2;
else if (align === 'right') boxX = drawX - textWidth;
let boxY = drawY - ascent;
if (baseline === 'middle') boxY = drawY - (ascent + descent) / 2;
else if (baseline === 'bottom' || baseline === 'alphabetic') boxY = drawY - ascent - descent;
const rectX = boxX - padX;
const rectY = boxY - padY;
const rectW = textWidth + padX * 2;
const rectH = ascent + descent + padY * 2;
const radiusPx = 6;
ctx.fillStyle = 'rgba(3, 6, 12, 0.52)';
ctx.beginPath();
ctx.moveTo(rectX + radiusPx, rectY);
ctx.lineTo(rectX + rectW - radiusPx, rectY);
ctx.quadraticCurveTo(rectX + rectW, rectY, rectX + rectW, rectY + radiusPx);
ctx.lineTo(rectX + rectW, rectY + rectH - radiusPx);
ctx.quadraticCurveTo(rectX + rectW, rectY + rectH, rectX + rectW - radiusPx, rectY + rectH);
ctx.lineTo(rectX + radiusPx, rectY + rectH);
ctx.quadraticCurveTo(rectX, rectY + rectH, rectX, rectY + rectH - radiusPx);
ctx.lineTo(rectX, rectY + radiusPx);
ctx.quadraticCurveTo(rectX, rectY, rectX + radiusPx, rectY);
ctx.closePath();
ctx.fill();
ctx.restore();
}
for (const attempt of attempts) {
if (labelPlacer.drawText({
text,
x: attempt.x,
y: attempt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(246, 235, 214, 0.84)',
align: attempt.align,
baseline: attempt.baseline,
padding: 2,
})) {
drawLabelBackdrop(attempt.x, attempt.y, attempt.align, attempt.baseline);
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.textAlign = attempt.align;
ctx.textBaseline = attempt.baseline;
ctx.fillStyle = 'rgba(246, 235, 214, 0.84)';
ctx.fillText(text, attempt.x, attempt.y);
ctx.restore();
return true;
}
}
return false;
}
function placePlanetLabel(text, x, y, radius) {
const attempts = [
{ x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' },
{ x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' },
{ x, y: y - radius - 7, align: 'center', baseline: 'bottom' },
{ x, y: y + radius + 7, align: 'center', baseline: 'top' },
{ x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' },
{ x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' },
{ x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' },
{ x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' },
];
function drawLabelBackdrop(drawX, drawY, align, baseline) {
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
const padX = 5;
const padY = 3;
let boxX = drawX;
if (align === 'center') boxX = drawX - textWidth / 2;
else if (align === 'right') boxX = drawX - textWidth;
let boxY = drawY - ascent;
if (baseline === 'middle') boxY = drawY - (ascent + descent) / 2;
else if (baseline === 'bottom' || baseline === 'alphabetic') boxY = drawY - ascent - descent;
const rectX = boxX - padX;
const rectY = boxY - padY;
const rectW = textWidth + padX * 2;
const rectH = ascent + descent + padY * 2;
const radiusPx = 6;
ctx.fillStyle = 'rgba(3, 6, 12, 0.52)';
ctx.beginPath();
ctx.moveTo(rectX + radiusPx, rectY);
ctx.lineTo(rectX + rectW - radiusPx, rectY);
ctx.quadraticCurveTo(rectX + rectW, rectY, rectX + rectW, rectY + radiusPx);
ctx.lineTo(rectX + rectW, rectY + rectH - radiusPx);
ctx.quadraticCurveTo(rectX + rectW, rectY + rectH, rectX + rectW - radiusPx, rectY + rectH);
ctx.lineTo(rectX + radiusPx, rectY + rectH);
ctx.quadraticCurveTo(rectX, rectY + rectH, rectX, rectY + rectH - radiusPx);
ctx.lineTo(rectX, rectY + radiusPx);
ctx.quadraticCurveTo(rectX, rectY, rectX + radiusPx, rectY);
ctx.closePath();
ctx.fill();
ctx.restore();
}
for (const attempt of attempts) {
if (labelPlacer.drawText({
text,
x: attempt.x,
y: attempt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(246, 235, 214, 0.84)',
align: attempt.align,
baseline: attempt.baseline,
padding: 2,
})) {
drawLabelBackdrop(attempt.x, attempt.y, attempt.align, attempt.baseline);
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.textAlign = attempt.align;
ctx.textBaseline = attempt.baseline;
ctx.fillStyle = 'rgba(246, 235, 214, 0.84)';
ctx.fillText(text, attempt.x, attempt.y);
ctx.restore();
return true;
}
}
return false;
}
function resizeCanvas() {
const cssSize = Math.max(320, Math.min(canvas.parentElement.clientWidth, 900));
const ratio = Math.min(window.devicePixelRatio || 1, 2);
@@ -933,6 +1419,168 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
return name !== '' ? `M${messierId} ${name}` : `M${messierId}`;
}
function getPlanetStyle(planet) {
switch (String(planet.key || '')) {
case 'mercury':
return { fill: '#c9b9a7', glow: 'rgba(201, 185, 167, 0.34)' };
case 'venus':
return { fill: '#f4ddb2', glow: 'rgba(244, 221, 178, 0.4)' };
case 'mars':
return { fill: '#d7724c', glow: 'rgba(215, 114, 76, 0.34)' };
case 'jupiter':
return { fill: '#d9b07e', glow: 'rgba(217, 176, 126, 0.36)' };
case 'saturn':
return { fill: '#d8ca8b', glow: 'rgba(216, 202, 139, 0.34)' };
case 'uranus':
return { fill: '#8fd3df', glow: 'rgba(143, 211, 223, 0.3)' };
case 'neptune':
return { fill: '#5a78d6', glow: 'rgba(90, 120, 214, 0.3)' };
default:
return { fill: '#d8dde8', glow: 'rgba(216, 221, 232, 0.28)' };
}
}
function drawMoon(cx, cy, radius) {
if (!MONTH_CHART_MOON) {
return;
}
const alt = Number(MONTH_CHART_MOON.alt);
const az = Number(MONTH_CHART_MOON.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) {
return;
}
const pt = project(alt, az, radius, cx, cy);
if (!pt) {
return;
}
const phaseFraction = clamp01(Number(MONTH_CHART_MOON.phaseFraction ?? 0.5));
const waxing = Boolean(MONTH_CHART_MOON.waxing);
const moonRadius = 10.5;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, moonRadius * 2.1);
glow.addColorStop(0, 'rgba(220, 226, 238, 0.28)');
glow.addColorStop(0.5, 'rgba(220, 226, 238, 0.08)');
glow.addColorStop(1, 'rgba(220, 226, 238, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius * 2.1, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#d6d9df';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
const phaseScale = 1 - 2 * phaseFraction;
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.ellipse((waxing ? -1 : 1) * moonRadius * phaseScale, 0, moonRadius, moonRadius, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius, 0, Math.PI * 2);
ctx.stroke();
}
function drawPlanets(cx, cy, radius) {
for (const planet of MONTH_CHART_PLANETS) {
const alt = Number(planet.alt);
const az = Number(planet.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) {
continue;
}
const pt = project(alt, az, radius, cx, cy);
if (!pt) {
continue;
}
const style = getPlanetStyle(planet);
const mag = Number(planet.mag);
const baseRadius = Number.isFinite(mag)
? Math.max(3.8, Math.min(8.6, 6.7 - mag * 0.72))
: 5.4;
const glowRadius = baseRadius * 2.2;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, style.glow);
glow.addColorStop(0.45, style.glow.replace(/0\.\d+\)$/, '0.12)'));
glow.addColorStop(1, style.glow.replace(/0\.\d+\)$/, '0)'));
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = style.fill;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.stroke();
if (planet.key === 'jupiter') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, baseRadius - 0.3, 0, Math.PI * 2);
ctx.clip();
ctx.strokeStyle = 'rgba(116, 84, 54, 0.78)';
ctx.lineWidth = Math.max(1, baseRadius * 0.34);
ctx.beginPath();
ctx.moveTo(-baseRadius * 0.95, -baseRadius * 0.34);
ctx.lineTo(baseRadius * 0.95, -baseRadius * 0.34);
ctx.moveTo(-baseRadius * 0.95, baseRadius * 0.2);
ctx.lineTo(baseRadius * 0.95, baseRadius * 0.2);
ctx.stroke();
ctx.fillStyle = 'rgba(194, 92, 68, 0.95)';
ctx.beginPath();
ctx.ellipse(baseRadius * 0.42, baseRadius * 0.06, baseRadius * 0.3, baseRadius * 0.17, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
if (planet.key === 'saturn') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.rotate(-0.35);
ctx.strokeStyle = 'rgba(232, 221, 170, 0.72)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(0, 0, baseRadius * 1.7, baseRadius * 0.72, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
const label = String(planet.label || '').trim();
if (label !== '') {
placePlanetLabel(label, pt.x, pt.y, baseRadius);
}
}
}
function drawMessier(cx, cy, radius) {
for (const obj of MONTH_CHART_MESSIER) {
if (Number(obj.mag) > Number(MONTH_CHART_CONFIG.messierLimit)) {
@@ -1099,6 +1747,8 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
drawConstellations(cx, cy, radius);
drawConstellationLabels(cx, cy, radius);
drawMessier(cx, cy, radius);
drawMoon(cx, cy, radius);
drawPlanets(cx, cy, radius);
drawStars(cx, cy, radius);
statusEl.textContent = 'Sternkarte fuer ' + MONTH_CHART_CONFIG.localTimeLabel;
@@ -1248,6 +1898,36 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
};
}
function placePlanetLabel(text, x, y, radius) {
const attempts = [
{ x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' },
{ x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' },
{ x, y: y - radius - 7, align: 'center', baseline: 'bottom' },
{ x, y: y + radius + 7, align: 'center', baseline: 'top' },
{ x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' },
{ x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' },
{ x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' },
{ x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' },
];
for (const attempt of attempts) {
if (labelPlacer.drawText({
text,
x: attempt.x,
y: attempt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(246, 235, 214, 0.84)',
align: attempt.align,
baseline: attempt.baseline,
padding: 2,
})) {
return true;
}
}
return false;
}
function resizeCanvas() {
const cssWidth = Math.max(420, Math.min(canvas.parentElement.clientWidth, 1100));
const cssHeight = Math.round(cssWidth * 0.6);
@@ -1414,6 +2094,158 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
return name !== '' ? `M${messierId} ${name}` : `M${messierId}`;
}
function getPlanetStyle(planet) {
switch (String(planet.key || '')) {
case 'mercury':
return { fill: '#c9b9a7', glow: 'rgba(201, 185, 167, 0.34)' };
case 'venus':
return { fill: '#f4ddb2', glow: 'rgba(244, 221, 178, 0.4)' };
case 'mars':
return { fill: '#d7724c', glow: 'rgba(215, 114, 76, 0.34)' };
case 'jupiter':
return { fill: '#d9b07e', glow: 'rgba(217, 176, 126, 0.36)' };
case 'saturn':
return { fill: '#d8ca8b', glow: 'rgba(216, 202, 139, 0.34)' };
case 'uranus':
return { fill: '#8fd3df', glow: 'rgba(143, 211, 223, 0.3)' };
case 'neptune':
return { fill: '#5a78d6', glow: 'rgba(90, 120, 214, 0.3)' };
default:
return { fill: '#d8dde8', glow: 'rgba(216, 221, 232, 0.28)' };
}
}
function drawMoon(width, height) {
if (!MONTH_CHART_MOON) return;
const alt = Number(MONTH_CHART_MOON.alt);
const az = Number(MONTH_CHART_MOON.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) return;
const pt = southProject(alt, az);
if (!pt) return;
const phaseFraction = clamp01(Number(MONTH_CHART_MOON.phaseFraction ?? 0.5));
const waxing = Boolean(MONTH_CHART_MOON.waxing);
const moonRadius = 10.5;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, moonRadius * 2.1);
glow.addColorStop(0, 'rgba(220, 226, 238, 0.28)');
glow.addColorStop(0.5, 'rgba(220, 226, 238, 0.08)');
glow.addColorStop(1, 'rgba(220, 226, 238, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius * 2.1, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#d6d9df';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
const phaseScale = 1 - 2 * phaseFraction;
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.ellipse((waxing ? -1 : 1) * moonRadius * phaseScale, 0, moonRadius, moonRadius, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius, 0, Math.PI * 2);
ctx.stroke();
}
function drawPlanets(width, height) {
for (const planet of MONTH_CHART_PLANETS) {
const alt = Number(planet.alt);
const az = Number(planet.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) continue;
const pt = southProject(alt, az);
if (!pt) continue;
const style = getPlanetStyle(planet);
const mag = Number(planet.mag);
const baseRadius = Number.isFinite(mag)
? Math.max(3.8, Math.min(8.6, 6.7 - mag * 0.72))
: 5.4;
const glowRadius = baseRadius * 2.2;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, style.glow);
glow.addColorStop(0.45, style.glow.replace(/0\.\d+\)$/, '0.12)'));
glow.addColorStop(1, style.glow.replace(/0\.\d+\)$/, '0)'));
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = style.fill;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.stroke();
if (planet.key === 'jupiter') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, baseRadius - 0.3, 0, Math.PI * 2);
ctx.clip();
ctx.strokeStyle = 'rgba(116, 84, 54, 0.78)';
ctx.lineWidth = Math.max(1, baseRadius * 0.34);
ctx.beginPath();
ctx.moveTo(-baseRadius * 0.95, -baseRadius * 0.34);
ctx.lineTo(baseRadius * 0.95, -baseRadius * 0.34);
ctx.moveTo(-baseRadius * 0.95, baseRadius * 0.2);
ctx.lineTo(baseRadius * 0.95, baseRadius * 0.2);
ctx.stroke();
ctx.fillStyle = 'rgba(194, 92, 68, 0.95)';
ctx.beginPath();
ctx.ellipse(baseRadius * 0.42, baseRadius * 0.06, baseRadius * 0.3, baseRadius * 0.17, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
if (planet.key === 'saturn') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.rotate(-0.35);
ctx.strokeStyle = 'rgba(232, 221, 170, 0.72)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(0, 0, baseRadius * 1.7, baseRadius * 0.72, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
const label = String(planet.label || '').trim();
if (label !== '') {
placePlanetLabel(label, pt.x, pt.y, baseRadius);
}
}
}
function drawBackground(width, height) {
const zenitPt = southProject(88, 180) || { x: width / 2, y: height * 0.18 };
const horizonPt = southProject(0, 180) || { x: width / 2, y: height * 0.88 };
@@ -1734,6 +2566,8 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
drawConstellations(width, height);
drawConstellationLabels(width, height);
drawMessier(width, height);
drawMoon(width, height);
drawPlanets(width, height);
drawStars(width, height);
statusEl.textContent = 'Südhimmel für ' + MONTH_CHART_CONFIG.localTimeLabel;
}
+300 -2
View File
@@ -384,6 +384,289 @@ def action_month_sky_context(args: list[str]) -> dict:
}
def action_moon_phases_for_month(args: list[str]) -> dict:
if len(args) != 3:
fail(
"Aktion moon_phases_for_month erwartet 3 Argumente: year month timezone",
extra={"argv": args},
)
try:
year = int(args[0])
month = int(args[1])
except ValueError as exc:
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
if month < 1 or month > 12:
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
timezone_name = args[2]
try:
tz = ZoneInfo(timezone_name)
except Exception as exc:
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
if month == 12:
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
else:
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
search_start = dt_to_time(local_start.astimezone(timezone.utc) - timedelta(days=3))
search_limit_days = ((local_end - local_start).total_seconds() / 86400.0) + 10.0
phase_defs = [
(0.0, "Neumond"),
(90.0, "Erstes Viertel"),
(180.0, "Vollmond"),
(270.0, "Letztes Viertel"),
]
phases = []
for target_lon, label in phase_defs:
probe = search_start
found = None
for _ in range(3):
result = astronomy.SearchMoonPhase(target_lon, probe, search_limit_days)
if result is None:
break
dt_utc = time_to_datetime(result)
local_dt = dt_utc.astimezone(tz)
if local_dt.year == year and local_dt.month == month:
found = {
"label": label,
"utc_iso": dt_utc.isoformat().replace("+00:00", "Z"),
"local_iso": local_dt.isoformat(),
"local_date": local_dt.strftime("%d.%m.%Y"),
"local_time": local_dt.strftime("%H:%M"),
"sort_iso": local_dt.isoformat(),
}
break
probe = dt_to_time(dt_utc + timedelta(days=1))
if found is not None:
phases.append(found)
phases.sort(key=lambda item: item["sort_iso"])
for item in phases:
item.pop("sort_iso", None)
return {
"ok": True,
"action": "moon_phases_for_month",
"selected": {
"year": year,
"month": month,
"timezone": timezone_name,
},
"phases": phases,
}
def moon_planet_separation_deg(
body: astronomy.Body,
observer: astronomy.Observer,
dt_utc: datetime,
) -> float:
time_value = dt_to_time(dt_utc)
moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True)
body_eq = astronomy.Equator(body, time_value, observer, True, True)
return spherical_separation_deg(float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec))
def refine_minimum_separation(
body: astronomy.Body,
observer: astronomy.Observer,
left_utc: datetime,
right_utc: datetime,
) -> tuple[datetime, float]:
left = left_utc
right = right_utc
for _ in range(32):
span = (right - left) / 3
m1 = left + span
m2 = right - span
f1 = moon_planet_separation_deg(body, observer, m1)
f2 = moon_planet_separation_deg(body, observer, m2)
if f1 <= f2:
right = m2
else:
left = m1
best = left + (right - left) / 2
return best, moon_planet_separation_deg(body, observer, best)
def action_moon_planet_approaches(args: list[str]) -> dict:
if len(args) != 7:
fail(
"Aktion moon_planet_approaches erwartet 7 Argumente: latitude longitude elevation year month timezone max_sep_deg",
extra={"argv": args},
)
latitude = parse_float(args[0], "Latitude")
longitude = parse_float(args[1], "Longitude")
elevation = parse_float(args[2], "Elevation")
try:
year = int(args[3])
month = int(args[4])
except ValueError as exc:
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
if month < 1 or month > 12:
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
timezone_name = args[5]
max_sep_deg = parse_float(args[6], "Maximalabstand")
try:
tz = ZoneInfo(timezone_name)
except Exception as exc:
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
observer = astronomy.Observer(latitude, longitude, elevation)
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
if month == 12:
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
else:
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
utc_start = local_start.astimezone(timezone.utc)
utc_end = local_end.astimezone(timezone.utc)
scan_step = timedelta(hours=1)
coarse_threshold = max_sep_deg + 1.0
planet_defs = [
("Merkur", astronomy.Body.Mercury, "mercury"),
("Venus", astronomy.Body.Venus, "venus"),
("Mars", astronomy.Body.Mars, "mars"),
("Jupiter", astronomy.Body.Jupiter, "jupiter"),
("Saturn", astronomy.Body.Saturn, "saturn"),
("Uranus", astronomy.Body.Uranus, "uranus"),
("Neptun", astronomy.Body.Neptune, "neptune"),
]
approaches = []
for label, body, key in planet_defs:
samples: list[tuple[datetime, float]] = []
current = utc_start
while current <= utc_end:
samples.append((current, moon_planet_separation_deg(body, observer, current)))
current += scan_step
if samples[-1][0] < utc_end:
samples.append((utc_end, moon_planet_separation_deg(body, observer, utc_end)))
seen_ranges: list[tuple[datetime, datetime]] = []
for index in range(1, len(samples) - 1):
prev_t, prev_sep = samples[index - 1]
curr_t, curr_sep = samples[index]
next_t, next_sep = samples[index + 1]
if curr_sep > coarse_threshold:
continue
if curr_sep > prev_sep or curr_sep > next_sep:
continue
left = max(utc_start, curr_t - scan_step)
right = min(utc_end, curr_t + scan_step)
if any(not (right <= seen_left or left >= seen_right) for seen_left, seen_right in seen_ranges):
continue
min_time_utc, min_sep = refine_minimum_separation(body, observer, left, right)
local_dt = min_time_utc.astimezone(tz)
if local_dt.year != year or local_dt.month != month:
continue
if min_sep > max_sep_deg:
continue
seen_ranges.append((left, right))
approaches.append({
"planet_key": key,
"planet_label": label,
"label": f"Mond nahe {label}",
"separation_deg": float(min_sep),
"utc_iso": min_time_utc.isoformat().replace("+00:00", "Z"),
"local_iso": local_dt.isoformat(),
"local_date": local_dt.strftime("%d.%m.%Y"),
"local_time": local_dt.strftime("%H:%M"),
})
approaches.sort(key=lambda item: item["local_iso"])
return {
"ok": True,
"action": "moon_planet_approaches",
"selected": {
"year": year,
"month": month,
"timezone": timezone_name,
"max_separation_deg": max_sep_deg,
},
"approaches": approaches,
}
def action_moon_star_occultations_for_month(args: list[str]) -> dict:
if len(args) != 7:
fail(
"Aktion moon_star_occultations_for_month erwartet 7 Argumente: latitude longitude elevation year month timezone star_file",
extra={"argv": args},
)
latitude = parse_float(args[0], "Latitude")
longitude = parse_float(args[1], "Longitude")
elevation = parse_float(args[2], "Elevation")
try:
year = int(args[3])
month = int(args[4])
except ValueError as exc:
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
if month < 1 or month > 12:
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
timezone_name = args[5]
star_file_path = args[6]
if not star_file_path:
fail("Sterndatei fehlt.")
try:
tz = ZoneInfo(timezone_name)
except Exception as exc:
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
observer = astronomy.Observer(latitude, longitude, elevation)
star_rows = load_star_file(star_file_path)
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
if month == 12:
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
else:
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
results, debug_profile = compute_star_occultation(star_rows, observer, local_start, local_end, tz)
return {
"ok": True,
"action": "moon_star_occultations_for_month",
"selected": {
"year": year,
"month": month,
"timezone": timezone_name,
},
"star_count": len(star_rows),
"debug": debug_profile,
"results": results,
}
def classify_visibility(sun_alt_deg: float) -> dict:
if sun_alt_deg > -5:
return {"key": "day", "label": "Tag"}
@@ -1385,7 +1668,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", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context"]})
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "moon_planet_approaches", "moon_star_occultations_for_month"]})
action = sys.argv[1]
args = sys.argv[2:]
@@ -1420,7 +1703,22 @@ def main() -> None:
print(json.dumps(result, ensure_ascii=False))
return
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context"]})
if action == "moon_phases_for_month":
result = action_moon_phases_for_month(args)
print(json.dumps(result, ensure_ascii=False))
return
if action == "moon_planet_approaches":
result = action_moon_planet_approaches(args)
print(json.dumps(result, ensure_ascii=False))
return
if action == "moon_star_occultations_for_month":
result = action_moon_star_occultations_for_month(args)
print(json.dumps(result, ensure_ascii=False))
return
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "moon_planet_approaches", "moon_star_occultations_for_month"]})
if __name__ == "__main__":