Jupitermonde in MV eingabuet

This commit is contained in:
2026-04-03 20:10:29 +02:00
parent 81941d803d
commit d147c9cda8
4 changed files with 3605 additions and 298 deletions
+3240 -296
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -67,6 +67,7 @@ $innerPlanetElongationEvents = [];
$venusPeakMagnitudeEvents = [];
$outerPlanetEvents = [];
$outerPlanetStationEvents = [];
$jupiterMoonSideEvents = [];
$moonOccultations = [];
$meteorShowerPeaks = [];
$seasonChanges = [];
@@ -616,6 +617,23 @@ if (is_array($outerPlanetStationDecoded) && ($outerPlanetStationDecoded['ok'] ??
$outerPlanetStationEvents = is_array($outerPlanetStationDecoded['events'] ?? null) ? $outerPlanetStationDecoded['events'] : [];
}
$jupiterMoonSideOutput = [];
$jupiterMoonSideExitCode = 0;
$jupiterMoonSideArgs = [
$pythonScriptPath,
'jupiter_moons_one_side_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$jupiterMoonSideCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $jupiterMoonSideArgs)) . ' 2>&1';
exec($jupiterMoonSideCommand, $jupiterMoonSideOutput, $jupiterMoonSideExitCode);
$jupiterMoonSideRaw = trim(implode("\n", $jupiterMoonSideOutput));
$jupiterMoonSideDecoded = json_decode($jupiterMoonSideRaw, true);
if (is_array($jupiterMoonSideDecoded) && ($jupiterMoonSideDecoded['ok'] ?? false)) {
$jupiterMoonSideEvents = is_array($jupiterMoonSideDecoded['events'] ?? null) ? $jupiterMoonSideDecoded['events'] : [];
}
$moonPlanetOutput = [];
$moonPlanetExitCode = 0;
$moonPlanetArgs = [
@@ -930,6 +948,26 @@ foreach ($outerPlanetStationEvents as $outerPlanetStationEvent) {
];
}
foreach ($jupiterMoonSideEvents as $jupiterMoonSideEvent) {
if (!is_array($jupiterMoonSideEvent)) {
continue;
}
$durationLabel = trim((string) ($jupiterMoonSideEvent['duration_label'] ?? ''));
$eventLabel = trim((string) ($jupiterMoonSideEvent['label'] ?? 'Alle 4 Jupitermonde auf einer Seite von Jupiter'));
if ($durationLabel !== '') {
$eventLabel .= ' (' . $durationLabel . ')';
}
$monthEventList[] = [
'event' => $eventLabel,
'date' => (string) ($jupiterMoonSideEvent['local_date'] ?? ''),
'time' => (string) ($jupiterMoonSideEvent['local_time'] ?? ''),
'local_iso' => (string) ($jupiterMoonSideEvent['local_iso'] ?? ''),
'type' => 'jupiter_moons_one_side',
];
}
foreach ($moonPlanetApproaches as $approachEvent) {
if (!is_array($approachEvent)) {
continue;
Binary file not shown.
+327 -2
View File
@@ -1759,6 +1759,326 @@ def action_outer_planet_stations_for_month(args: list[str]) -> dict:
}
def action_jupiter_moons_one_side_for_month(args: list[str]) -> dict:
if len(args) != 3:
fail(
"Aktion jupiter_moons_one_side_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)
utc_start = local_start.astimezone(timezone.utc)
utc_end = local_end.astimezone(timezone.utc)
scan_step = timedelta(minutes=10)
x_tolerance_au = 1.0e-6
jupiter_radius_au = astronomy.JUPITER_EQUATORIAL_RADIUS_KM / astronomy.KM_PER_AU
moon_keys = ["io", "europa", "ganymede", "callisto"]
def classify_side(dt_utc: datetime) -> tuple[str | None, str | None, dict[str, float], dict[str, float]]:
time_value = dt_to_time(dt_utc)
rotation_eqj_to_ecl = astronomy.Rotation_EQJ_ECL()
jupiter_geo = astronomy.GeoVector(astronomy.Body.Jupiter, time_value, True)
jupiter_helio = astronomy.HelioVector(astronomy.Body.Jupiter, time_value)
moon_states = astronomy.JupiterMoons(time_value)
to_earth_eqj = astronomy.Vector(
-float(jupiter_geo.x),
-float(jupiter_geo.y),
-float(jupiter_geo.z),
time_value,
)
to_earth_ecl = astronomy.RotateVector(rotation_eqj_to_ecl, to_earth_eqj)
camera_x = float(to_earth_ecl.x)
camera_y = float(to_earth_ecl.z)
camera_z = -float(to_earth_ecl.y)
camera_len = math.sqrt(
(camera_x * camera_x) + (camera_y * camera_y) + (camera_z * camera_z)
)
if camera_len <= 1.0e-12:
return None, None, {}, {}
# Dieselbe Welt-/Kameraorientierung wie in jupitersystem.php:
# Display-Koordinaten sind (x, z, -y), die Kamera schaut aus der Richtung "toEarth".
forward_x = -camera_x / camera_len
forward_y = -camera_y / camera_len
forward_z = -camera_z / camera_len
up_x = 0.0
up_y = 1.0
up_z = 0.0
right_x = (up_y * forward_z) - (up_z * forward_y)
right_y = (up_z * forward_x) - (up_x * forward_z)
right_z = (up_x * forward_y) - (up_y * forward_x)
right_len = math.sqrt(
(right_x * right_x) + (right_y * right_y) + (right_z * right_z)
)
if right_len <= 1.0e-12:
return None, None, {}, {}
right_x /= right_len
right_y /= right_len
right_z /= right_len
# Himmlische Ostrichtung am Jupiter-Ort fuer die textliche Richtung oestlich/westlich.
jx = float(jupiter_geo.x)
jy = float(jupiter_geo.y)
east_eqj_x = -jy
east_eqj_y = jx
east_eqj_z = 0.0
east_eqj_len = math.sqrt(
(east_eqj_x * east_eqj_x) + (east_eqj_y * east_eqj_y) + (east_eqj_z * east_eqj_z)
)
if east_eqj_len <= 1.0e-12:
return None, None, {}, {}
east_eqj_x /= east_eqj_len
east_eqj_y /= east_eqj_len
east_eqj_z /= east_eqj_len
east_ecl = astronomy.RotateVector(
rotation_eqj_to_ecl,
astronomy.Vector(east_eqj_x, east_eqj_y, east_eqj_z, time_value),
)
east_display_x = float(east_ecl.x)
east_display_y = float(east_ecl.z)
east_display_z = -float(east_ecl.y)
east_screen_x = (
(east_display_x * right_x)
+ (east_display_y * right_y)
+ (east_display_z * right_z)
)
sun_to_jupiter_x = float(jupiter_helio.x)
sun_to_jupiter_y = float(jupiter_helio.y)
sun_to_jupiter_z = float(jupiter_helio.z)
sun_to_jupiter_len = math.sqrt(
(sun_to_jupiter_x * sun_to_jupiter_x)
+ (sun_to_jupiter_y * sun_to_jupiter_y)
+ (sun_to_jupiter_z * sun_to_jupiter_z)
)
if sun_to_jupiter_len <= 1.0e-12:
return None, None, {}, {}
sun_to_jupiter_x /= sun_to_jupiter_len
sun_to_jupiter_y /= sun_to_jupiter_len
sun_to_jupiter_z /= sun_to_jupiter_len
projected_x_positions = {}
east_offsets = {}
shadow_flags = {}
for key in moon_keys:
moon_state = getattr(moon_states, key)
moon_ecl = astronomy.RotateVector(rotation_eqj_to_ecl, moon_state)
moon_display_x = float(moon_ecl.x)
moon_display_y = float(moon_ecl.z)
moon_display_z = -float(moon_ecl.y)
projected_x = (
(moon_display_x * right_x)
+ (moon_display_y * right_y)
+ (moon_display_z * right_z)
)
projected_x_positions[key] = projected_x
east_offsets[key] = projected_x * east_screen_x
moon_x = float(moon_state.x)
moon_y = float(moon_state.y)
moon_z = float(moon_state.z)
shadow_axis_distance = (
(moon_x * sun_to_jupiter_x)
+ (moon_y * sun_to_jupiter_y)
+ (moon_z * sun_to_jupiter_z)
)
perp_x = moon_x - (shadow_axis_distance * sun_to_jupiter_x)
perp_y = moon_y - (shadow_axis_distance * sun_to_jupiter_y)
perp_z = moon_z - (shadow_axis_distance * sun_to_jupiter_z)
shadow_flags[key] = (
shadow_axis_distance > 0.0
and math.sqrt((perp_x * perp_x) + (perp_y * perp_y) + (perp_z * perp_z)) < jupiter_radius_au
)
projected_side_kind = None
minimum_positive_x = jupiter_radius_au + x_tolerance_au
if (
all(x_value > minimum_positive_x for x_value in projected_x_positions.values())
and not any(shadow_flags.values())
):
projected_side_kind = "right"
elif (
all(x_value < -minimum_positive_x for x_value in projected_x_positions.values())
and not any(shadow_flags.values())
):
projected_side_kind = "left"
east_west_kind = None
if all(offset > x_tolerance_au for offset in east_offsets.values()):
east_west_kind = "east"
elif all(offset < -x_tolerance_au for offset in east_offsets.values()):
east_west_kind = "west"
return projected_side_kind, east_west_kind, projected_x_positions, east_offsets
def refine_transition(
left_utc: datetime,
right_utc: datetime,
target_kind: str,
*,
find_start: bool,
) -> datetime:
left = left_utc
right = right_utc
for _ in range(32):
mid = left + (right - left) / 2
mid_kind, _, _, _ = classify_side(mid)
if find_start:
if mid_kind == target_kind:
right = mid
else:
left = mid
else:
if mid_kind == target_kind:
left = mid
else:
right = mid
return left + (right - left) / 2
def format_duration_label(duration_minutes: int) -> str:
hours, minutes = divmod(max(0, duration_minutes), 60)
if hours > 0 and minutes > 0:
return f"{hours} h {minutes:02d} min"
if hours > 0:
return f"{hours} h"
return f"{minutes} min"
def direction_text_from_kinds(raw_kind: str | None, east_west_kind: str | None) -> str:
if east_west_kind == "east":
return "oestlich"
if east_west_kind == "west":
return "westlich"
return "westlich" if raw_kind == "left" else "oestlich"
events: list[dict] = []
current = utc_start
active_kind, active_east_west_kind, _, _ = classify_side(current)
active_start_utc = utc_start if active_kind is not None else None
previous_time = current
previous_kind = active_kind
current += scan_step
while current <= utc_end:
current_kind, current_east_west_kind, _, _ = classify_side(current)
if active_kind is None and previous_kind != current_kind and current_kind is not None:
active_kind = current_kind
active_east_west_kind = current_east_west_kind
active_start_utc = refine_transition(previous_time, current, current_kind, find_start=True)
elif active_kind is not None and current_kind != active_kind:
active_end_utc = refine_transition(previous_time, current, active_kind, find_start=False)
clamped_start_utc = max(active_start_utc or utc_start, utc_start)
clamped_end_utc = min(active_end_utc, utc_end)
if clamped_end_utc > clamped_start_utc:
start_local = clamped_start_utc.astimezone(tz)
end_local = clamped_end_utc.astimezone(tz)
duration_minutes = max(
1,
int(round((clamped_end_utc - clamped_start_utc).total_seconds() / 60.0)),
)
side_text = direction_text_from_kinds(active_kind, active_east_west_kind)
events.append({
"kind": active_kind,
"direction": active_east_west_kind,
"label": f"Alle 4 Jupitermonde {side_text} von Jupiter",
"duration_label": format_duration_label(duration_minutes),
"duration_minutes": duration_minutes,
"utc_iso": clamped_start_utc.isoformat().replace("+00:00", "Z"),
"local_iso": start_local.isoformat(),
"local_date": start_local.strftime("%d.%m.%Y"),
"local_time": start_local.strftime("%H:%M"),
"end_utc_iso": clamped_end_utc.isoformat().replace("+00:00", "Z"),
"end_local_iso": end_local.isoformat(),
"end_local_date": end_local.strftime("%d.%m.%Y"),
"end_local_time": end_local.strftime("%H:%M"),
})
active_kind = None
active_east_west_kind = None
active_start_utc = None
if current_kind is not None:
active_kind = current_kind
active_east_west_kind = current_east_west_kind
active_start_utc = refine_transition(previous_time, current, current_kind, find_start=True)
previous_time = current
previous_kind = current_kind
current += scan_step
if active_kind is not None and active_start_utc is not None:
clamped_start_utc = max(active_start_utc, utc_start)
clamped_end_utc = utc_end
if clamped_end_utc > clamped_start_utc:
start_local = clamped_start_utc.astimezone(tz)
end_local = clamped_end_utc.astimezone(tz)
duration_minutes = max(
1,
int(round((clamped_end_utc - clamped_start_utc).total_seconds() / 60.0)),
)
side_text = direction_text_from_kinds(active_kind, active_east_west_kind)
events.append({
"kind": active_kind,
"direction": active_east_west_kind,
"label": f"Alle 4 Jupitermonde {side_text} von Jupiter",
"duration_label": format_duration_label(duration_minutes),
"duration_minutes": duration_minutes,
"utc_iso": clamped_start_utc.isoformat().replace("+00:00", "Z"),
"local_iso": start_local.isoformat(),
"local_date": start_local.strftime("%d.%m.%Y"),
"local_time": start_local.strftime("%H:%M"),
"end_utc_iso": clamped_end_utc.isoformat().replace("+00:00", "Z"),
"end_local_iso": end_local.isoformat(),
"end_local_date": end_local.strftime("%d.%m.%Y"),
"end_local_time": end_local.strftime("%H:%M"),
})
events.sort(key=lambda item: item["local_iso"])
return {
"ok": True,
"action": "jupiter_moons_one_side_for_month",
"selected": {
"year": year,
"month": month,
"timezone": timezone_name,
},
"events": events,
}
def action_moon_star_occultations_for_month(args: list[str]) -> dict:
if len(args) != 7:
fail(
@@ -2815,7 +3135,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", "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", "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", "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", "moon_star_occultations_for_month"]})
action = sys.argv[1]
args = sys.argv[2:]
@@ -2915,12 +3235,17 @@ def main() -> None:
print(json.dumps(result, ensure_ascii=False))
return
if action == "jupiter_moons_one_side_for_month":
result = action_jupiter_moons_one_side_for_month(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", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "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", "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", "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", "moon_star_occultations_for_month"]})
if __name__ == "__main__":