From 5652cc7e4077ed6d81a00bf01dec77e5c9cb8f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 14 Sep 2026 09:35:36 +0200 Subject: [PATCH] Neue Seite fuer Mond-Planeten-Bedeckungen mit Jahresberechnung hinzufuegen Berechnet Bedeckungen der Planeten Merkur bis Neptun durch den Mond fuer ein ganzes Jahr am Standardstandort, inklusive Detailkarte. Ereignisse unter dem Horizont werden weiterhin angezeigt statt gefiltert, dafuer mit eigenem Sichtbarkeits-Hinweis. Co-Authored-By: Claude Sonnet 5 --- BERECHNUNGSGRUNDLAGEN.md | 1 + public/auth_helpers.php | 1 + public/css/style.css | 6 + public/header.php | 1 + public/mondplanetenbedeckungen.php | 1284 ++++++++++++++++++++++++++++ public/py/api.py | 157 +++- 6 files changed, 1448 insertions(+), 2 deletions(-) create mode 100644 public/mondplanetenbedeckungen.php diff --git a/BERECHNUNGSGRUNDLAGEN.md b/BERECHNUNGSGRUNDLAGEN.md index f9e11de..2fc1279 100644 --- a/BERECHNUNGSGRUNDLAGEN.md +++ b/BERECHNUNGSGRUNDLAGEN.md @@ -49,6 +49,7 @@ Nicht jede Berechnung ist Skyfield oder Astronomy Engine: | `sonnenfinsternis.php` | **Astronomy Engine**, einschließlich lokaler Berechnung für den Standardstandort. | | `mondfinsternis.php` | **Astronomy Engine**. Der Zeitpunkt der Finsternis ist global; eine lokale Sichtbarkeitsbewertung ist davon getrennt. | | `sternbedeckungen.php` | **Astronomy Engine** über die lokale Python-API. Topozentrische Koordinaten und der Standardstandort werden berücksichtigt. | +| `mondplanetenbedeckungen.php` | **Astronomy Engine** über die lokale Python-API. Durchsucht ein ganzes Jahr nach Mond-Planeten-Bedeckungen (Merkur bis Neptun) für den Standardstandort; berücksichtigt die scheinbare Scheibengröße von Mond und Planet. | | `moonphase.php` | **Astronomy Engine** im Browser beziehungsweise über die lokale Python-API. | | `moonyear.php` | **Astronomy Engine** für Mondphasen und Mondpositionen. | | `satellitenhimmel.php` | **SGP4** für Satellitenbahnen; Astronomy Engine für astronomischen Kontext wie Sonne, Mond und Dämmerung. | diff --git a/public/auth_helpers.php b/public/auth_helpers.php index e20df23..011edd2 100644 --- a/public/auth_helpers.php +++ b/public/auth_helpers.php @@ -134,6 +134,7 @@ function manageablePublicPages(): array 'sonnenfinsternis.php' => 'Sonnenfinsternis', 'bedeckung.php' => 'Bedeckungsphänomene', 'sternbedeckungen.php' => 'Mond-Stern-Bedeckungen', + 'mondplanetenbedeckungen.php' => 'Mond-Planeten-Bedeckungen', 'monatsvorhersage.php' => 'Monatsvorhersage', 'moonyear.php' => 'Mondphasen-Jahresübersicht', 'wochenvorhersage.php' => 'Wochenvorhersage', diff --git a/public/css/style.css b/public/css/style.css index c771b43..ff7a58a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -4676,6 +4676,12 @@ body.solarsystem-page:not(.jupitersystem-page) .solarsystem-stage-wrap { box-shadow: inset 0 0 0 1px rgba(143, 230, 141, 0.22); } +.occult-visibility-below { + background: rgba(136, 148, 173, 0.16); + color: #9aa6c2; + box-shadow: inset 0 0 0 1px rgba(154, 166, 194, 0.22); +} + .occult-detail-card { overflow: hidden; } diff --git a/public/header.php b/public/header.php index 30d4528..020555b 100644 --- a/public/header.php +++ b/public/header.php @@ -146,6 +146,7 @@ $menuGroups = [ ['href' => 'sonnenfinsternis.php', 'label' => 'Sonnenfinsternis'], ['href' => 'bedeckung.php', 'label' => 'Bedeckungsphänomene'], ['href' => 'sternbedeckungen.php', 'label' => 'Mond-Stern-Bedeckungen'], + ['href' => 'mondplanetenbedeckungen.php', 'label' => 'Mond-Planeten-Bedeckungen'], ], ], [ diff --git a/public/mondplanetenbedeckungen.php b/public/mondplanetenbedeckungen.php new file mode 100644 index 0000000..d259273 --- /dev/null +++ b/public/mondplanetenbedeckungen.php @@ -0,0 +1,1284 @@ +&1'; + $output = []; + $resultCode = 0; + exec($command, $output, $resultCode); + + $joined = trim(implode("\n", $output)); + $debugOutput = $output; + + if ($resultCode !== 0) { + $lastError = $joined !== '' ? $joined : ('Fehlercode ' . $resultCode . ' bei ' . $pythonBinary); + continue; + } + + $decoded = json_decode($joined, true); + if (!is_array($decoded)) { + $lastError = $joined !== '' ? 'Python-Ausgabe war kein gültiges JSON.' : 'Python lieferte keine Ausgabe zurück.'; + continue; + } + + if (!($decoded['ok'] ?? false)) { + return [ + 'ok' => false, + 'error' => (string)($decoded['error'] ?? 'Unbekannter Python-Fehler'), + 'debug' => $joined, + ]; + } + + return ['ok' => true, 'data' => $decoded]; + } + + return [ + 'ok' => false, + 'error' => $lastError, + 'debug' => implode("\n", $debugOutput), + ]; +} + +$defaultLocation = null; +$pdo = null; +$pageError = null; +$pageLocation = null; +$detailMapStars = []; +$constellationLines = []; +$detailMapCatalogLimit = 8.0; +$isAjax = isset($_POST['ajax']) && (string)$_POST['ajax'] === '1'; + +try { + $config = require __DIR__ . '/../config/database.php'; + $dsn = sprintf( + 'mysql:host=%s;dbname=%s;charset=%s', + $config['host'], + $config['dbname'], + $config['charset'] ?? 'utf8mb4' + ); + + $pdo = new PDO( + $dsn, + $config['user'], + $config['pass'], + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] + ); + + if ($loggedIn && isset($_SESSION['user_id'])) { + $stmtLocation = $pdo->prepare( + 'SELECT id, name, latitude, longitude, elevation, timezone + FROM app_user_locations + WHERE user_id = ? AND is_default = 1 + LIMIT 1' + ); + $stmtLocation->execute([(int)$_SESSION['user_id']]); + $defaultLocation = $stmtLocation->fetch() ?: null; + } +} catch (Throwable $exception) { + $pageError = $exception->getMessage(); +} + +if (!empty($defaultLocation) && is_array($defaultLocation)) { + $pageLocation = [ + 'name' => (string)($defaultLocation['name'] ?? ''), + 'latitude' => isset($defaultLocation['latitude']) ? (float)$defaultLocation['latitude'] : null, + 'longitude' => isset($defaultLocation['longitude']) ? (float)$defaultLocation['longitude'] : null, + 'elevation' => isset($defaultLocation['elevation']) && $defaultLocation['elevation'] !== null ? (float)$defaultLocation['elevation'] : 0.0, + 'timezone' => (string)($defaultLocation['timezone'] ?? 'Europe/Berlin'), + ]; +} + +$timezoneName = $pageLocation['timezone'] ?? 'Europe/Berlin'; +try { + $timezone = new DateTimeZone($timezoneName); +} catch (Throwable $exception) { + $timezoneName = 'Europe/Berlin'; + $timezone = new DateTimeZone($timezoneName); +} + +$todayYear = (int)(new DateTimeImmutable('now', $timezone))->format('Y'); +$selectedYear = isset($_POST['year']) && preg_match('/^\d{4}$/', (string)$_POST['year']) + ? (int)$_POST['year'] + : $todayYear; +$selectedYear = max(1900, min(2100, $selectedYear)); + +if (!$isAjax && $pageError === null && $pdo instanceof PDO && $pageLocation !== null && $pageLocation['latitude'] !== null && $pageLocation['longitude'] !== null) { + try { + $stmtDetailMap = $pdo->query( + "SELECT `hip`, `proper`, `bf`, `con`, `mag`, `ra`, `dec`, `dist` + FROM `star_hipparcos` + WHERE `mag` IS NOT NULL + AND `ra` IS NOT NULL + AND `dec` IS NOT NULL + AND `mag` <= 8.0 + ORDER BY `mag` ASC, `proper` ASC, `bf` ASC, `hip` ASC" + ); + + foreach ($stmtDetailMap->fetchAll() as $row) { + $proper = trim((string)($row['proper'] ?? '')); + $bf = trim((string)($row['bf'] ?? '')); + $hip = isset($row['hip']) ? (int)$row['hip'] : 0; + $label = $proper !== '' ? $proper : ($bf !== '' ? $bf : ($hip > 0 ? 'HIP ' . $hip : 'Unbenannter Stern')); + + $detailMapStars[] = [ + 'hip' => $hip, + 'label' => $label, + 'constellation' => trim((string)($row['con'] ?? '')), + 'mag' => isset($row['mag']) ? (float)$row['mag'] : null, + 'ra' => isset($row['ra']) ? (float)$row['ra'] : null, + 'dec' => isset($row['dec']) ? (float)$row['dec'] : null, + 'distLy' => isset($row['dist']) && $row['dist'] !== null && (float)$row['dist'] > 0.0 + ? (float)$row['dist'] * 3.26156 + : 1000.0, + ]; + } + + $stmtLines = $pdo->query( + "SELECT + cl.`id` AS line_id, + clp.`point_index`, + sh.`ra` AS ra_hours, + sh.`dec` AS dec_deg + FROM `constellation_lines` cl + INNER JOIN `constellation_line_points` clp + ON clp.`line_id` = cl.`id` + INNER JOIN `star_hipparcos` sh + ON sh.`hip` = clp.`hip_number` + WHERE sh.`ra` IS NOT NULL + AND sh.`dec` IS NOT NULL + ORDER BY cl.`id`, clp.`point_index`" + ); + + foreach ($stmtLines->fetchAll() as $row) { + $lineId = (int)$row['line_id']; + $constellationLines[$lineId][] = [ + 'ra' => isset($row['ra_hours']) ? (float)$row['ra_hours'] : null, + 'dec' => isset($row['dec_deg']) ? (float)$row['dec_deg'] : null, + ]; + } + } catch (Throwable $exception) { + $pageError = $exception->getMessage(); + $detailMapStars = []; + $constellationLines = []; + } +} + +if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + + if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) { + echo json_encode([ + 'ok' => false, + 'error' => 'Kein Standard-Standort gesetzt. Bitte zuerst in den Einstellungen einen Standard-Standort hinterlegen.', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + if ($pageError !== null || !($pdo instanceof PDO)) { + echo json_encode([ + 'ok' => false, + 'error' => $pageError ?? 'Die Datenbank ist nicht verfuegbar.', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + try { + $pythonResult = runPythonApi( + __DIR__ . '/py/api.py', + 'moon_planet_occultations_for_year', + [ + (string)$pageLocation['latitude'], + (string)$pageLocation['longitude'], + (string)$pageLocation['elevation'], + (string)$selectedYear, + $timezoneName, + ] + ); + + if (!$pythonResult['ok']) { + echo json_encode([ + 'ok' => false, + 'error' => $pythonResult['error'] ?? 'Unbekannter Python-Fehler', + 'debug' => $pythonResult['debug'] ?? null, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + echo json_encode([ + 'ok' => true, + 'results' => $pythonResult['data']['results'] ?? [], + 'selected_year' => $selectedYear, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } catch (Throwable $exception) { + echo json_encode([ + 'ok' => false, + 'error' => $exception->getMessage(), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } +} + +require __DIR__ . '/header.php'; + +if (!empty($currentLocation) && is_array($currentLocation)) { + $pageLocation = [ + 'name' => (string)($currentLocation['name'] ?? ''), + 'latitude' => isset($currentLocation['latitude']) ? (float)$currentLocation['latitude'] : null, + 'longitude' => isset($currentLocation['longitude']) ? (float)$currentLocation['longitude'] : null, + 'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float)$currentLocation['elevation'] : 0.0, + 'timezone' => (string)($currentLocation['timezone'] ?? 'Europe/Berlin'), + ]; +} +?> + + + +
+ +
+

Kein Standard-Standort gesetzt

+

+ Fuer die Berechnung wird der Standard-Standort aus der Datenbank verwendet. + Bitte wähle in den Einstellungen einen Standard-Standort aus. +

+
+ +
+

Datenbank nicht verfuegbar

+

Die Sterndaten konnten nicht geladen werden.

+

Technischer Hinweis:

+
+ +
+
+
+ Standort + + ° / ° + +
+
+ Berechnung +
+ + + + + + + +
+
+
+ +
+ +
+
+

Bereit zur Berechnung

+

+ Wähle ein Jahr und starte die Auswertung mit dem Button. + Mond-Planeten-Bedeckungen sind an einem festen Standort selten + – es ist normal, wenn in manchen Jahren keine gefunden werden. +

+
+
+ + + +
+ + + + + + diff --git a/public/py/api.py b/public/py/api.py index 5f93493..b55f458 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -5844,6 +5844,154 @@ def action_moon_star_occultations(args: list[str]) -> dict: } +PLANET_OCCULTATION_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"), +] + + +def compute_moon_planet_occultations_for_range( + observer: astronomy.Observer, + utc_start: datetime, + utc_end: datetime, + tz: ZoneInfo, +) -> list[dict]: + scan_step = timedelta(hours=1) + coarse_threshold_deg = 3.0 + results: list[dict] = [] + + for label, body, key in PLANET_OCCULTATION_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_deg: + 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) + seen_ranges.append((left, right)) + + overlap_fraction = moon_planet_overlap_fraction(body, observer, min_time_utc) + if overlap_fraction <= 0.0: + continue + + time_value = dt_to_time(min_time_utc) + moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True) + planet_eq = astronomy.Equator(body, time_value, observer, True, True) + sun_eq = astronomy.Equator(astronomy.Body.Sun, time_value, observer, True, True) + moon_hor = astronomy.Horizon(time_value, observer, moon_eq.ra, moon_eq.dec, astronomy.Refraction.Normal) + planet_hor = astronomy.Horizon(time_value, observer, planet_eq.ra, planet_eq.dec, astronomy.Refraction.Normal) + sun_hor = astronomy.Horizon(time_value, observer, sun_eq.ra, sun_eq.dec, astronomy.Refraction.Normal) + + below_horizon = float(moon_hor.altitude) <= 0.0 or float(planet_hor.altitude) <= 0.0 + + ingress_time = refine_occultation_contact(body, observer, min_time_utc, -1) + egress_time = refine_occultation_contact(body, observer, min_time_utc, 1) + + duration_seconds = None + if ingress_time is not None and egress_time is not None: + duration_seconds = int(round((egress_time - ingress_time).total_seconds())) + + moon_radius_deg = moon_angular_radius_deg(float(moon_eq.dist)) + planet_radius_deg = planet_angular_radius_deg(body, float(planet_eq.dist)) + + results.append({ + "planet_key": key, + "planet_label": label, + "ingress": serialize_occultation_time(ingress_time, tz), + "maximum": serialize_occultation_time(min_time_utc, tz), + "egress": serialize_occultation_time(egress_time, tz), + "partial_start": ingress_time is None, + "partial_end": egress_time is None, + "duration_seconds": duration_seconds, + "separation_arcmin": float(min_sep) * 60.0, + "moon_radius_arcmin": moon_radius_deg * 60.0, + "planet_radius_arcsec": planet_radius_deg * 3600.0, + "overlap_fraction": float(overlap_fraction), + "is_total": overlap_fraction >= 0.999, + "moon_alt_deg": float(moon_hor.altitude), + "planet_alt_deg": float(planet_hor.altitude), + "sun_alt_deg": float(sun_hor.altitude), + "below_horizon": below_horizon, + "visibility": {"key": "below_horizon", "label": "Unter Horizont"} if below_horizon + else classify_visibility(float(sun_hor.altitude)), + }) + + results.sort( + key=lambda item: item["maximum"]["utc_iso"] if item["maximum"] else "9999-99-99T99:99:99Z" + ) + return results + + +def action_moon_planet_occultations_for_year(args: list[str]) -> dict: + if len(args) != 5: + fail( + "Aktion moon_planet_occultations_for_year erwartet 5 Argumente: latitude longitude elevation year timezone", + 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]) + except ValueError as exc: + fail("Jahr ist ungueltig.", extra={"details": str(exc), "argv": args}) + + timezone_name = args[4] + 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, 1, 1, 0, 0, 0, tzinfo=tz) + local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz) + utc_start = local_start.astimezone(timezone.utc) + utc_end = local_end.astimezone(timezone.utc) + + results = compute_moon_planet_occultations_for_range(observer, utc_start, utc_end, tz) + + return { + "ok": True, + "action": "moon_planet_occultations_for_year", + "selected": { + "year": year, + "timezone": timezone_name, + }, + "observer": { + "latitude": latitude, + "longitude": longitude, + "elevation": elevation, + }, + "results": results, + } + + # ── satellite_passes ────────────────────────────────────────────────────────── import math as _math @@ -6415,7 +6563,7 @@ def action_barycenter(args: list[str]) -> dict: def main() -> None: if len(sys.argv) < 2: - fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month"]}) + fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month", "moon_planet_occultations_for_year"]}) action = sys.argv[1] args = sys.argv[2:] @@ -6605,6 +6753,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return + if action == "moon_planet_occultations_for_year": + result = action_moon_planet_occultations_for_year(args) + print(json.dumps(result, ensure_ascii=True)) + return + if action == "lunar_eclipses_for_year": result = action_lunar_eclipses_for_year(args) print(json.dumps(result, ensure_ascii=True)) @@ -6615,7 +6768,7 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "twilight_chart", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "twilight_chart", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "golden_gate_of_ecliptic_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month", "moon_planet_occultations_for_year"]}) if __name__ == "__main__":