diff --git a/public/monatsvorhersage_old.php b/public/monatsvorhersage_old.php
index 26571b0..1015edf 100644
--- a/public/monatsvorhersage_old.php
+++ b/public/monatsvorhersage_old.php
@@ -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
Bereite Südhimmelkarte vor...
+
+
+
+
+
+
@@ -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;
}
diff --git a/public/py/api.py b/public/py/api.py
index c8eff1d..bcd331f 100644
--- a/public/py/api.py
+++ b/public/py/api.py
@@ -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__":