diff --git a/public/monatsvorhersage_old.php b/public/monatsvorhersage_old.php index 927dc6c..0e50702 100644 --- a/public/monatsvorhersage_old.php +++ b/public/monatsvorhersage_old.php @@ -18,6 +18,9 @@ $messierDatabaseMagnitudeLimit = 4.0; $messierDisplayMagnitudeLimit = 1.0; $moonPlanetMaxSeparationDeg = 1.5; $moonDeepSkyMaxSeparationDeg = 1.0; +$planetBrightStarMaxSeparationDeg = 1.0; +// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und +// ausgewaehlten hellen Sternen wie Regulus oder Spica. $planetConjunctionMaxSeparationDeg = 1.0; $moonOccultationMaxMag = 2.0; // Gute Merkur-Sichtbarkeit wird nicht nur über Elongation, sondern über echte @@ -78,6 +81,7 @@ $moonPhaseEvents = []; $goldenHandleEvents = []; $moonPlanetApproaches = []; $moonDeepSkyApproaches = []; +$planetBrightStarApproaches = []; $mercuryGoodVisibilityEvents = []; $planetConjunctions = []; $eclipseEvents = []; @@ -696,6 +700,27 @@ if (is_array($moonDeepSkyDecoded) && ($moonDeepSkyDecoded['ok'] ?? false)) { $moonDeepSkyApproaches = is_array($moonDeepSkyDecoded['approaches'] ?? null) ? $moonDeepSkyDecoded['approaches'] : []; } +$planetBrightStarOutput = []; +$planetBrightStarExitCode = 0; +$planetBrightStarArgs = [ + $pythonScriptPath, + 'planet_bright_star_approaches_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, + (string) $planetBrightStarMaxSeparationDeg, +]; +$planetBrightStarCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetBrightStarArgs)) . ' 2>&1'; +exec($planetBrightStarCommand, $planetBrightStarOutput, $planetBrightStarExitCode); +$planetBrightStarRaw = trim(implode("\n", $planetBrightStarOutput)); +$planetBrightStarDecoded = json_decode($planetBrightStarRaw, true); +if (is_array($planetBrightStarDecoded) && ($planetBrightStarDecoded['ok'] ?? false)) { + $planetBrightStarApproaches = is_array($planetBrightStarDecoded['approaches'] ?? null) ? $planetBrightStarDecoded['approaches'] : []; +} + $planetConjunctionOutput = []; $planetConjunctionExitCode = 0; $planetConjunctionArgs = [ @@ -1069,6 +1094,24 @@ foreach ($moonDeepSkyApproaches as $approachEvent) { ]; } +foreach ($planetBrightStarApproaches as $approachEvent) { + if (!is_array($approachEvent)) { + continue; + } + + $distanceText = isset($approachEvent['separation_deg']) + ? number_format((float) ($approachEvent['separation_deg']), 2, ',', '') . ' Grad' + : ''; + + $monthEventList[] = [ + 'event' => trim((string) ($approachEvent['label'] ?? 'Planet nahe hellem Stern') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), + 'date' => (string) ($approachEvent['local_date'] ?? ''), + 'time' => (string) ($approachEvent['local_time'] ?? ''), + 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), + 'type' => 'planet_bright_star', + ]; +} + foreach ($planetConjunctions as $conjunctionEvent) { if (!is_array($conjunctionEvent)) { continue; diff --git a/public/py/__pycache__/api.cpython-312.pyc b/public/py/__pycache__/api.cpython-312.pyc index f132f92..5ebe79b 100644 Binary files a/public/py/__pycache__/api.cpython-312.pyc and b/public/py/__pycache__/api.cpython-312.pyc differ diff --git a/public/py/api.py b/public/py/api.py index 265c9ba..5d265d5 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -950,6 +950,18 @@ def planet_pair_separation_deg( return spherical_separation_deg(float(eq_a.ra), float(eq_a.dec), float(eq_b.ra), float(eq_b.dec)) +def planet_fixed_equatorial_separation_deg( + body: astronomy.Body, + ra_hours: float, + dec_deg: float, + observer: astronomy.Observer, + dt_utc: datetime, +) -> float: + time_value = dt_to_time(dt_utc) + body_eq = astronomy.Equator(body, time_value, observer, True, True) + return spherical_separation_deg(float(body_eq.ra), float(body_eq.dec), float(ra_hours), float(dec_deg)) + + def refine_minimum_separation( body: astronomy.Body, observer: astronomy.Observer, @@ -1024,6 +1036,32 @@ def refine_fixed_target_minimum_separation( return best, moon_fixed_equatorial_separation_deg(ra_hours, dec_deg, observer, best) +def refine_planet_fixed_target_minimum_separation( + body: astronomy.Body, + ra_hours: float, + dec_deg: float, + 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 = planet_fixed_equatorial_separation_deg(body, ra_hours, dec_deg, observer, m1) + f2 = planet_fixed_equatorial_separation_deg(body, ra_hours, dec_deg, observer, m2) + if f1 <= f2: + right = m2 + else: + left = m1 + + best = left + (right - left) / 2 + return best, planet_fixed_equatorial_separation_deg(body, ra_hours, dec_deg, observer, best) + + def action_moon_planet_approaches(args: list[str]) -> dict: if len(args) != 7: fail( @@ -1135,6 +1173,157 @@ def action_moon_planet_approaches(args: list[str]) -> dict: } +def action_planet_bright_star_approaches_for_month(args: list[str]) -> dict: + if len(args) != 7: + fail( + "Aktion planet_bright_star_approaches_for_month 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"), + ] + + # Helle, auffaellige Sterne nahe der Ekliptik, die regelmaessig + # attraktive Begegnungen mit den hellen Planeten liefern. + star_defs = [ + ("Alrescha", 2.0341, 2.7638), + ("Hamal", 2.1196, 23.4624), + ("Menkar", 3.0380, 4.0897), + ("Aldebaran", 4.5987, 16.5093), + ("Elnath", 5.4382, 28.6075), + ("Pollux", 7.7553, 28.0262), + ("Alphard", 9.4598, -8.6586), + ("Regulus", 10.1395, 11.9672), + ("Denebola", 11.8177, 14.5721), + ("Spica", 13.4199, -11.1613), + ("Zubenelgenubi", 14.8479, -16.0418), + ("Antares", 16.4901, -26.4319), + ] + + approaches = [] + for planet_label, planet_body, planet_key in planet_defs: + for star_label, ra_hours, dec_deg in star_defs: + samples: list[tuple[datetime, float]] = [] + current = utc_start + while current <= utc_end: + samples.append((current, planet_fixed_equatorial_separation_deg(planet_body, ra_hours, dec_deg, observer, current))) + current += scan_step + if samples[-1][0] < utc_end: + samples.append((utc_end, planet_fixed_equatorial_separation_deg(planet_body, ra_hours, dec_deg, observer, utc_end))) + + seen_ranges: list[tuple[datetime, datetime]] = [] + for index in range(1, len(samples) - 1): + curr_t, curr_sep = samples[index] + prev_sep = samples[index - 1][1] + next_sep = samples[index + 1][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_planet_fixed_target_minimum_separation( + planet_body, + ra_hours, + dec_deg, + 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": planet_key, + "planet_label": planet_label, + "star_label": star_label, + "label": f"{planet_label} nahe {star_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"]) + + deduplicated_approaches: list[dict] = [] + for approach in approaches: + if deduplicated_approaches: + previous = deduplicated_approaches[-1] + same_pair = ( + previous["planet_key"] == approach["planet_key"] + and previous["star_label"] == approach["star_label"] + ) + previous_local = datetime.fromisoformat(previous["local_iso"]) + current_local = datetime.fromisoformat(approach["local_iso"]) + if same_pair and abs((current_local - previous_local).total_seconds()) <= 36 * 3600: + if float(approach["separation_deg"]) < float(previous["separation_deg"]): + deduplicated_approaches[-1] = approach + continue + deduplicated_approaches.append(approach) + + return { + "ok": True, + "action": "planet_bright_star_approaches_for_month", + "selected": { + "year": year, + "month": month, + "timezone": timezone_name, + "max_separation_deg": max_sep_deg, + }, + "approaches": deduplicated_approaches, + } + + def action_moon_deep_sky_approaches_for_month(args: list[str]) -> dict: if len(args) != 7: fail( @@ -3430,7 +3619,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", "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_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", "moon_star_occultations_for_month"]}) + 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", "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", "moon_star_occultations_for_month"]}) action = sys.argv[1] args = sys.argv[2:] @@ -3495,6 +3684,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=False)) return + if action == "planet_bright_star_approaches_for_month": + result = action_planet_bright_star_approaches_for_month(args) + print(json.dumps(result, ensure_ascii=False)) + return + if action == "planet_conjunctions_for_month": result = action_planet_conjunctions_for_month(args) print(json.dumps(result, ensure_ascii=False)) @@ -3550,7 +3744,7 @@ 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", "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_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", "moon_star_occultations_for_month"]}) + 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", "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", "moon_star_occultations_for_month"]}) if __name__ == "__main__":