From 5bb05e69df5036550462ecabe56cdbd889edee6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 12 Sep 2026 11:44:49 +0200 Subject: [PATCH] =?UTF-8?q?Mondereignisse=20vollst=C3=A4ndig=20und=20kompa?= =?UTF-8?q?kt=20ausgeben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/monatsvorhersage.php | 22 +++++---- public/py/api.py | 96 +++++++++++++++++++++++++++++++++---- 2 files changed, 101 insertions(+), 17 deletions(-) diff --git a/public/monatsvorhersage.php b/public/monatsvorhersage.php index 31a1ef5..3057950 100644 --- a/public/monatsvorhersage.php +++ b/public/monatsvorhersage.php @@ -913,13 +913,16 @@ if (!function_exists('monthForecastBuildNarrativeText')) { )); $phaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'moon_phase')); - // Alle Mondphasen müssen erhalten bleiben. Die übrigen Mondereignisse - // werden weiterhin begrenzt, damit der Übersichtstext kompakt bleibt. - $nonPhaseEvents = array_slice( - array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') !== 'moon_phase')), - 0, - 10 - ); + // Alle Mondphasen und alle Mondereignisse werden vollständig übernommen. + $nonPhaseCandidates = array_values(array_filter( + $sortedMoonEvents, + static fn (array $e): bool => (string) ($e['type'] ?? '') !== 'moon_phase' + )); + $nonPhaseEvents = $nonPhaseCandidates; + usort($nonPhaseEvents, static fn (array $a, array $b): int => strcmp( + (string) ($a['local_iso'] ?? $a['date'] ?? ''), + (string) ($b['local_iso'] ?? $b['date'] ?? '') + )); $moonSentences = []; @@ -985,7 +988,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) { } if ($goldenHandle !== null) { - $moonSentences[] = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel: Ein Gebirgskamm am Mondrand leuchtet bereits im Sonnenlicht, während das Tal dahinter noch im Schatten liegt – ein lohnender Anblick schon im kleinen Teleskop.'; + $moonSentences[] = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel.'; } if ($apsis !== null) { @@ -1057,6 +1060,9 @@ if (!function_exists('monthForecastBuildNarrativeText')) { if (str_starts_with($apLabel, 'Mond nahe ')) { $target = substr($apLabel, strlen('Mond nahe ')); $moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.'; + } elseif (str_starts_with($apLabel, 'Mond streift ')) { + $target = substr($apLabel, strlen('Mond streift ')); + $moonSentences[] = 'Am ' . $apSd . ' streift der Mond ' . $target . '.'; } elseif (str_starts_with($apLabel, 'Mond bedeckt ')) { $target = substr($apLabel, strlen('Mond bedeckt ')); $moonSentences[] = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.'; diff --git a/public/py/api.py b/public/py/api.py index ca36030..6de7db1 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -2917,15 +2917,28 @@ def moon_planet_occultation_margin_deg( moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True) body_eq = astronomy.Equator(body, time_value, observer, True, True) moon_radius = moon_angular_radius_deg(float(moon_eq.dist)) - planet_radius_km = PLANET_RADIUS_KM[body] - planet_ratio = planet_radius_km / (float(body_eq.dist) * astronomy.KM_PER_AU) - planet_radius = math.degrees(math.asin(max(-1.0, min(1.0, planet_ratio)))) + planet_radius = planet_angular_radius_deg(body, float(body_eq.dist)) separation = spherical_separation_deg( float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec) ) return separation - moon_radius - planet_radius +def moon_planet_overlap_fraction( + 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 circle_overlap_fraction( + moon_angular_radius_deg(float(moon_eq.dist)), + planet_angular_radius_deg(body, float(body_eq.dist)), + moon_planet_separation_deg(body, observer, dt_utc), + ) + + def refine_occultation_contact( body: astronomy.Body, observer: astronomy.Observer, @@ -3383,8 +3396,8 @@ def action_moon_planet_approaches(args: list[str]) -> dict: continue seen_ranges.append((left, right)) - occultation_margin = moon_planet_occultation_margin_deg(body, observer, min_time_utc) - if occultation_margin <= 0.0: + overlap_fraction = moon_planet_overlap_fraction(body, observer, min_time_utc) + if overlap_fraction >= (1.0 / 3.0): contact_times = [ ("start", refine_occultation_contact(body, observer, min_time_utc, -1), "Beginn der Bedeckung"), ("maximum", min_time_utc, "Größte Bedeckung"), @@ -3407,6 +3420,19 @@ def action_moon_planet_approaches(args: list[str]) -> dict: }) continue + if overlap_fraction > 0.0: + approaches.append({ + "planet_key": key, + "planet_label": label, + "label": f"Mond streift {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"), + }) + continue + approaches.append({ "planet_key": key, "planet_label": label, @@ -3625,12 +3651,13 @@ def action_moon_deep_sky_approaches_for_month(args: list[str]) -> dict: coarse_threshold = max_sep_deg + 1.0 target_defs = [ - ("pleiades", "Plejaden", 3.7833, 24.1167), - ("praesepe", "Praesepe", 8.6667, 19.9833), + # Katalogisierte major_axis-Werte aus dso_objects, in Grad. + ("pleiades", "Plejaden", 3.7833, 24.1167, 60.0 / 60.0), + ("praesepe", "Praesepe", 8.6667, 19.9833, 108.6 / 60.0), ] approaches = [] - for key, label, ra_hours, dec_deg in target_defs: + for key, label, ra_hours, dec_deg, diameter_deg in target_defs: samples: list[tuple[datetime, float]] = [] current = utc_start while current <= utc_end: @@ -3664,10 +3691,22 @@ def action_moon_deep_sky_approaches_for_month(args: list[str]) -> dict: continue seen_ranges.append((left, right)) + moon_eq = astronomy.Equator(astronomy.Body.Moon, dt_to_time(min_time_utc), observer, True, True) + overlap_fraction = moon_disk_overlap_fraction( + diameter_deg / 2.0, + moon_angular_radius_deg(float(moon_eq.dist)), + min_sep, + ) + if overlap_fraction >= (1.0 / 3.0): + event_label = f"Mond bedeckt {label}" + elif overlap_fraction > 0.0: + event_label = f"Mond streift {label}" + else: + event_label = f"Mond nahe {label}" approaches.append({ "target_key": key, "target_label": label, - "label": f"Mond nahe {label}", + "label": event_label, "separation_deg": float(min_sep), "utc_iso": min_time_utc.isoformat().replace("+00:00", "Z"), "local_iso": local_dt.isoformat(), @@ -5133,6 +5172,45 @@ def moon_angular_radius_deg(distance_au: float) -> float: return math.degrees(math.asin(ratio)) +def planet_angular_radius_deg(body: astronomy.Body, distance_au: float) -> float: + if not math.isfinite(distance_au) or distance_au <= 0: + return 0.0 + ratio = PLANET_RADIUS_KM[body] / (distance_au * astronomy.KM_PER_AU) + ratio = max(-1.0, min(1.0, ratio)) + return math.degrees(math.asin(ratio)) + + +def circle_overlap_fraction(moon_radius_deg: float, target_radius_deg: float, separation_deg: float) -> float: + """Berechnet den Anteil der Zielscheibe, den der Mond überdeckt.""" + if moon_radius_deg <= 0.0 or target_radius_deg <= 0.0: + return 0.0 + if separation_deg >= moon_radius_deg + target_radius_deg: + return 0.0 + if separation_deg <= abs(moon_radius_deg - target_radius_deg): + overlap_area = math.pi * min(moon_radius_deg, target_radius_deg) ** 2 + else: + moon_r2 = moon_radius_deg ** 2 + target_r2 = target_radius_deg ** 2 + moon_angle = math.acos((separation_deg ** 2 + moon_r2 - target_r2) / (2.0 * separation_deg * moon_radius_deg)) + target_angle = math.acos((separation_deg ** 2 + target_r2 - moon_r2) / (2.0 * separation_deg * target_radius_deg)) + triangle = 0.5 * math.sqrt(max(0.0, ( + -separation_deg + moon_radius_deg + target_radius_deg + ) * ( + separation_deg + moon_radius_deg - target_radius_deg + ) * ( + separation_deg - moon_radius_deg + target_radius_deg + ) * ( + separation_deg + moon_radius_deg + target_radius_deg + ))) + overlap_area = moon_r2 * moon_angle + target_r2 * target_angle - triangle + return max(0.0, min(1.0, overlap_area / (math.pi * target_radius_deg ** 2))) + + +def moon_disk_overlap_fraction(target_radius_deg: float, moon_radius_deg: float, separation_deg: float) -> float: + """Berechnet bei Sternhaufen den überdeckten Anteil der Mondscheibe.""" + return circle_overlap_fraction(target_radius_deg, moon_radius_deg, separation_deg) + + def normalize_delta_ra_hours(delta_ra_hours: float) -> float: while delta_ra_hours > 12.0: delta_ra_hours -= 24.0