Compare commits

...
7 Commits
5 changed files with 369 additions and 33 deletions
+1
View File
@@ -4,3 +4,4 @@ php-server.err.log
php-server-error.log
__pycache__/
*.pyc
public/py/vendor_data/jup365-de421-range.bsp
@@ -161,7 +161,10 @@ try {
);
$authorId = (int) $authorStmt->fetchColumn();
if ($authorId <= 0) {
throw new RuntimeException('Kein Joomla-Autor für die Monatsvorschau vorhanden.');
// Kategorie 59 kann beim ersten Export noch leer sein.
// Monatsvorschauen werden dann mit dem festgelegten Joomla-
// Autor angelegt.
$authorId = 217;
}
$insertContentStmt = $pdo->prepare(
+113 -19
View File
@@ -66,12 +66,14 @@ $bodyClass = 'solarsystem-page jupitersystem-page';
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Datum</div>
<div class="solarsystem-label">Datum und Uhrzeit (Ortszeit)</div>
<div class="solarsystem-input-row">
<input type="date" id="jupiterDatePicker">
<input type="time" id="jupiterTimePicker" step="1">
<button type="button" class="btn btn-secondary" id="jupiterBtnToday">Heute</button>
</div>
<div class="solarsystem-current-date" id="jupiterCurrentDate">--</div>
<div class="hint mt-2xs">Die Monatsgrafik verwendet UT.</div>
</div>
<div class="solarsystem-sidebar-block">
@@ -192,16 +194,28 @@ $bodyClass = 'solarsystem-page jupitersystem-page';
<script type="module">
const AstronomyEngine = window.Astronomy;
const AU_KM = 149597870.7;
const LIGHT_TIME_DAYS_PER_AU = 499.004783836 / 86400;
const JUPITER_RADIUS_KM = AstronomyEngine.JUPITER_EQUATORIAL_RADIUS_KM;
const JUPITER_DISPLAY_RADIUS = 5.8;
// Positions from JupiterMoons() are in AU → convert to display units
// Die vier Galileischen Monde werden über Astronomy Engines Implementierung
// der semi-analytischen Stellarium-L1.2-Theorie berechnet. JupiterMoons() liefert
// jovizentrische EQJ-Vektoren in AU, die wir in Anzeigeeinheiten umrechnen.
// so that 1 Jupiter radius = JUPITER_DISPLAY_RADIUS display units
const MOON_DISTANCE_SCALE = (JUPITER_DISPLAY_RADIUS / JUPITER_RADIUS_KM) * AU_KM;
const KM_TO_DISPLAY = JUPITER_DISPLAY_RADIUS / JUPITER_RADIUS_KM;
// Jupiter System III (Referenz): 9h 55m 29.7s — System II (GRS): 9h 55m 40.632s
const GRS_SYSTEM_II_PERIOD_DAYS = 35740.632 / 86400;
// GRS-Kalibrierung: Referenzzeitpunkt und Rotationswinkel bei Transit
// GRS-Kalibrierung: Referenzzeitpunkt und Rotationswinkel bei Transit.
//
// WICHTIG: Die Transitzeiten von Sky & Telescope sind beobachtete/geozentrische
// UT-Zeitpunkte. Die Lichtlaufzeit Jupiter→Erde ist für den jeweiligen
// Referenzzeitpunkt daher bereits enthalten. Hier NICHT nochmals pauschal die
// Jupiter-Lichtlaufzeit abziehen, sonst würden wir doppelt korrigieren.
// Bei der Monatsprognose wird aktuell die GRS-Rotation ab diesem beobachteten
// Referenzzeitpunkt mit der System-II-Periode fortgeschrieben. Eine mögliche
// spätere Verbesserung wäre, zusätzlich die Änderung der Lichtlaufzeit zwischen
// Referenzzeitpunkt und Prognosezeitpunkt zu berücksichtigen.
let grsRefDate = new Date(0);
let grsRefAngle = 0;
@@ -226,6 +240,7 @@ const canvas = document.getElementById('jupiterCanvas');
const labelsLayer = document.getElementById('jupiterLabels');
const overlay = document.getElementById('jupiterOverlay');
const datePicker = document.getElementById('jupiterDatePicker');
const timePicker = document.getElementById('jupiterTimePicker');
const currentDateLabel = document.getElementById('jupiterCurrentDate');
const btnToday = document.getElementById('jupiterBtnToday');
const btnPlay = document.getElementById('jupiterBtnPlay');
@@ -266,7 +281,7 @@ let showOrbits = true;
let showLabels = false;
let moonScaleEnlarged = false;
let showInnerMoons = false;
let playing = true;
let playing = false;
let speedDaysPerSecond = 1 / 24;
let focusedObjectKey = null;
let simDate = new Date();
@@ -300,10 +315,18 @@ function formatDisplayDate(date) {
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
second: '2-digit',
timeZoneName: 'short'
});
}
function formatTimeInput(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
function normalizedSpeedText(daysPerSecond) {
const totalMinutes = daysPerSecond * 24 * 60;
if (Math.abs(totalMinutes) < 0.5) {
@@ -327,6 +350,7 @@ function updateSpeedFromSlider() {
function refreshCurrentDateUi() {
currentDateLabel.textContent = formatDisplayDate(simDate);
datePicker.value = formatDateInput(simDate);
timePicker.value = formatTimeInput(simDate);
}
function resizeRenderer() {
@@ -356,8 +380,27 @@ function getMoonMeta(key) {
return MOONS.find((moon) => moon.key === key) || null;
}
function getJupiterEmissionDate(observationDate) {
// Sicht von der Erde: iterativ den Zeitpunkt bestimmen, an dem das Licht
// das Jupiter-System verlassen hat.
const observationMs = observationDate.getTime();
let emissionMs = observationMs;
for (let i = 0; i < 2; i += 1) {
const geo = AstronomyEngine.GeoVector(
AstronomyEngine.Body.Jupiter,
new Date(emissionMs),
true
);
const distanceAu = Math.hypot(geo.x, geo.y, geo.z);
emissionMs = observationMs - (distanceAu * LIGHT_TIME_DAYS_PER_AU * 86400000);
}
return new Date(emissionMs);
}
function getJupiterMoonStates(date) {
return AstronomyEngine.JupiterMoons(date);
// Das ist die konkrete L1.2-Theorie, die auch Stellarium für die vier
// Galileischen Monde verwendet – nicht eine einfache Kepler-Näherung.
return AstronomyEngine.JupiterMoons(getJupiterEmissionDate(date));
}
function toDisplayPosition(state) {
@@ -670,7 +713,8 @@ function createJupiterSystemObjects() {
function updateSunDirection() {
// Jupiterposition heliozentrisch in EQJ → negiert = Richtung Jupiter→Sonne
const jupVec = AstronomyEngine.HelioVector(AstronomyEngine.Body.Jupiter, simDate);
const emissionDate = getJupiterEmissionDate(simDate);
const jupVec = AstronomyEngine.HelioVector(AstronomyEngine.Body.Jupiter, emissionDate);
const toSun = { x: -jupVec.x, y: -jupVec.y, z: -jupVec.z };
const ecl = AstronomyEngine.RotateVector(rotationEqjToEcl, toSun);
// Nur die Richtung verwenden (normieren), Licht auf feste Distanz von 800 setzen
@@ -896,7 +940,7 @@ function shiftGrsRotation(angleDeg) {
grsRefAngle = currentAngle + (angleDeg * Math.PI / 180);
}
function applyEarthView(targetRadius = 200) {
function applyEarthView(targetRadius = 280) {
const geoVec = AstronomyEngine.GeoVector(AstronomyEngine.Body.Jupiter, simDate, true);
const toEarth = { x: -geoVec.x, y: -geoVec.y, z: -geoVec.z };
const ecl = AstronomyEngine.RotateVector(rotationEqjToEcl, toEarth);
@@ -1102,7 +1146,13 @@ function attachUiHandlers() {
updateCamera();
});
btnEarth.addEventListener('click', () => applyEarthView());
btnEarth.addEventListener('click', () => {
// Im Erde-Blick sollen alle vier Galileischen Bahnen vollständig sichtbar sein.
showOrbits = true;
btnOrbits.classList.add('is-active');
moonOrbitLines.forEach((line) => { line.visible = true; });
applyEarthView(280);
});
btnOrbits.addEventListener('click', () => {
showOrbits = !showOrbits;
@@ -1176,16 +1226,19 @@ function attachUiHandlers() {
}
});
datePicker.addEventListener('change', () => {
function applyDateTimeInputs() {
if (!datePicker.value) {
return;
}
const [year, month, day] = datePicker.value.split('-').map(Number);
const current = new Date(simDate);
simDate = new Date(year, month - 1, day, current.getHours(), current.getMinutes(), current.getSeconds());
const [hours, minutes, seconds = 0] = (timePicker.value || '00:00:00').split(':').map(Number);
simDate = new Date(year, month - 1, day, hours, minutes, seconds);
refreshCurrentDateUi();
MOONS.forEach(rebuildMoonOrbitLine);
});
}
datePicker.addEventListener('change', applyDateTimeInputs);
timePicker.addEventListener('change', applyDateTimeInputs);
}
function animate() {
@@ -1305,8 +1358,8 @@ async function initJupiterSystem() {
attachPointerControls();
attachUiHandlers();
attachAdminHandlers();
btnPlay.textContent = 'Pause';
btnPlay.classList.add('is-active');
btnPlay.textContent = 'Play';
btnPlay.classList.remove('is-active');
updateSpeedFromSlider();
refreshCurrentDateUi();
resizeRenderer();
@@ -1381,7 +1434,8 @@ function generateEphemerisCanvas() {
// → Mond steht zwischen Sonne und Jupiter (auf der Sonnenseite)
// → Schatten des Mondes (entlang Sonne→Jupiter-Achse projiziert) trifft Jupiterscheibe
function getMoonsNearDisk(date, states) {
const jupHelio = AstronomyEngine.HelioVector(AstronomyEngine.Body.Jupiter, date);
const emissionDate = getJupiterEmissionDate(date);
const jupHelio = AstronomyEngine.HelioVector(AstronomyEngine.Body.Jupiter, emissionDate);
const jupHelioDist = Math.hypot(jupHelio.x, jupHelio.y, jupHelio.z);
// Einheitsvektor Sonne → Jupiter
const sx = jupHelio.x / jupHelioDist;
@@ -1403,13 +1457,43 @@ function generateEphemerisCanvas() {
return result;
}
// Scheinbare Ost-West-Projektion für die Monatsgrafik.
// Die Jupiter-Mondvektoren liegen in EQJ. Für die Ansicht von der Erde
// projizieren wir sie auf die lokale Himmels-Ost-West-Achse. Für diese
// Darstellung steht Westen links und Osten rechts.
function getApparentWestBasis(date) {
const emissionDate = getJupiterEmissionDate(date);
const geo = AstronomyEngine.GeoVector(
AstronomyEngine.Body.Jupiter,
emissionDate,
true
);
const equatorialLength = Math.hypot(geo.x, geo.y);
if (equatorialLength === 0) {
return { x: 1, y: 0, z: 0 };
}
// Richtung zunehmender Rektaszension = Himmels-Osten.
const east = {
x: -geo.y / equatorialLength,
y: geo.x / equatorialLength,
z: 0
};
// Westen wird links gezeichnet, daher ist rechts die positive Ostseite.
return east;
}
function getApparentWestOffset(state, westBasis) {
return state.x * westBasis.x + state.y * westBasis.y + state.z * westBasis.z;
}
const points = [];
for (let d = 1; d <= daysInMonth; d++) {
for (let s = 0; s < STEPS_PER_DAY; s++) {
const date = new Date(Date.UTC(year, month, d, s * STEP_HOURS, 0, 0));
const states = getJupiterMoonStates(date);
const westBasis = getApparentWestBasis(date);
const moonX = {};
MOON_KEYS.forEach(key => { moonX[key] = states[key].x; });
MOON_KEYS.forEach(key => { moonX[key] = getApparentWestOffset(states[key], westBasis); });
const nearDisk = getMoonsNearDisk(date, states);
points.push({ d, t: (d - 1) + s / STEPS_PER_DAY, date, moonX, nearDisk });
}
@@ -1445,6 +1529,8 @@ function generateEphemerisCanvas() {
});
// GRS-Transitzeiten: alle Transits im Monat berechnen
function getGrsTransitsForDay(d) {
// grsRefDate ist bereits eine scheinbare, von der Erde aus beobachtete
// Transitzeit. Deshalb keine erneute pauschale Lichtlaufzeit-Korrektur.
if (!grsRefDate || grsRefDate.getTime() === 0) return [];
const dayStart = new Date(Date.UTC(year, month, d, 0, 0, 0)).getTime();
const dayEnd = dayStart + 86400000;
@@ -1466,8 +1552,9 @@ function generateEphemerisCanvas() {
for (let d = 1; d <= daysInMonth; d++) {
const date = new Date(Date.UTC(year, month, d, 12, 0, 0));
const states = getJupiterMoonStates(date);
const westBasis = getApparentWestBasis(date);
const moonX = {};
MOON_KEYS.forEach(key => { moonX[key] = states[key].x; });
MOON_KEYS.forEach(key => { moonX[key] = getApparentWestOffset(states[key], westBasis); });
const grsTransits = getGrsTransitsForDay(d);
rows.push({ d, moonX, grsTransits });
}
@@ -1506,7 +1593,7 @@ function generateEphemerisCanvas() {
moonRim: 'rgba(0,0,0,0.2)',
legBg: 'rgba(240,240,245,0.9)', legBorder: 'rgba(100,80,20,0.3)',
legText: 'rgba(40,40,80,0.8)',
grsCol: '#c05000',
grsCol: '#d02020',
footText: 'rgba(60,60,100,0.5)',
titleLine: 'rgba(120,90,20,0.4)',
};
@@ -1533,6 +1620,13 @@ function generateEphemerisCanvas() {
c.textAlign = 'center';
c.fillText(`Jupitermonde — ${monthName}`, W / 2, 56);
c.fillStyle = COL.footText;
c.font = '18px monospace';
c.textAlign = 'left';
c.fillText('West', MARGIN_LEFT, MARGIN_TOP - 12);
c.textAlign = 'right';
c.fillText('Ost', MARGIN_LEFT + chartW, MARGIN_TOP - 12);
// Goldene Trennlinie unter Titel
const tl = c.createLinearGradient(MARGIN_LEFT, 0, MARGIN_LEFT + chartW, 0);
tl.addColorStop(0, 'transparent');
+48 -8
View File
@@ -896,11 +896,10 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
}
$moonEvents = $findEvents(
$moonEvents = array_values(array_filter(
$filteredEvents,
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['moon_phase', 'golden_handle', 'moon_planet', 'moon_planet_approach', 'moon_deep_sky', 'moon_deep_sky_approach', 'moon_occultation', 'moon_apsis'], true),
10
);
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['moon_phase', 'golden_handle', 'moon_planet', 'moon_planet_approach', 'moon_deep_sky', 'moon_deep_sky_approach', 'moon_occultation', 'moon_apsis'], true)
));
// Jahreszahl aus Datum entfernen (z. B. "19.04.2026" → "19.04.")
$shortDate = static function (string $d): string {
return (string) preg_replace('/\.\d{4}$/', '.', $d);
@@ -914,7 +913,16 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
));
$phaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'moon_phase'));
$nonPhaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') !== 'moon_phase'));
// 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 = [];
@@ -952,12 +960,22 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$apsis = null;
$approaches = [];
$goldenHandle = null;
$occultationTargets = [];
foreach ($dateEvents as $de) {
$type = (string) ($de['type'] ?? '');
if ($type === 'moon_apsis') {
$apsis = $de;
} elseif ($type === 'golden_handle') {
$goldenHandle = $de;
} elseif ($type === 'moon_occultation' && isset($de['event_kind'])) {
$occultationLabel = trim((string) ($de['event'] ?? ''));
if (str_starts_with($occultationLabel, 'Mond bedeckt ')) {
$target = substr($occultationLabel, strlen('Mond bedeckt '));
$target = trim(explode(' – ', $target, 2)[0]);
if ($target !== '') {
$occultationTargets[$target] = true;
}
}
} else {
$approaches[] = $de;
}
@@ -965,8 +983,12 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$sd = $shortDate($date);
foreach (array_keys($occultationTargets) as $target) {
$moonSentences[] = 'Am ' . $sd . ' bedeckt der Mond ' . $target . '.';
}
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) {
@@ -1022,9 +1044,25 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$sepStr = ' in nur ' . $sepMatch[1] . ' Abstand';
}
$apSd = $shortDate($apDate !== '' ? $apDate : $date);
$eventKind = (string) ($ap['event_kind'] ?? '');
if ($eventKind !== '' && str_starts_with($apLabel, 'Mond bedeckt ')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$apTime = trim((string) ($ap['time'] ?? ''));
$timeClause = $apTime !== '' ? ' um ' . $apTime . ' Uhr' : '';
$moonSentences[] = match ($eventKind) {
'start' => 'Am ' . $apSd . $timeClause . ' beginnt die Bedeckung von ' . $target . ' durch den Mond.',
'maximum' => 'Am ' . $apSd . $timeClause . ' erreicht die Bedeckung von ' . $target . ' durch den Mond ihr Maximum.',
'end' => 'Am ' . $apSd . $timeClause . ' endet die Bedeckung von ' . $target . ' durch den Mond.',
default => 'Am ' . $apSd . $timeClause . ' bedeckt der Mond ' . $target . '.',
};
continue;
}
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 . '.';
@@ -2451,7 +2489,8 @@ foreach ($moonPlanetApproaches as $approachEvent) {
continue;
}
$distanceText = isset($approachEvent['separation_deg'])
$eventKind = (string) ($approachEvent['event_kind'] ?? '');
$distanceText = $eventKind === '' && isset($approachEvent['separation_deg'])
? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . ' Grad'
: '';
@@ -2460,7 +2499,8 @@ foreach ($moonPlanetApproaches as $approachEvent) {
'date' => (string) ($approachEvent['local_date'] ?? ''),
'time' => (string) ($approachEvent['local_time'] ?? ''),
'local_iso' => (string) ($approachEvent['local_iso'] ?? ''),
'type' => 'moon_planet',
'event_kind' => $eventKind,
'type' => $eventKind !== '' ? 'moon_occultation' : 'moon_planet',
];
}
+203 -5
View File
@@ -18,6 +18,15 @@ from skyfield import almanac
MOON_RADIUS_KM = 1737.4
SYNODIC_MONTH = 29.530588853
PLANET_RADIUS_KM = {
astronomy.Body.Mercury: 2439.7,
astronomy.Body.Venus: 6051.8,
astronomy.Body.Mars: 3396.2,
astronomy.Body.Jupiter: astronomy.JUPITER_EQUATORIAL_RADIUS_KM,
astronomy.Body.Saturn: 60268.0,
astronomy.Body.Uranus: 25559.0,
astronomy.Body.Neptune: 24764.0,
}
STAR_BODIES = [
astronomy.Body.Star1,
astronomy.Body.Star2,
@@ -67,6 +76,30 @@ def dt_to_time(dt_utc: datetime) -> astronomy.Time:
)
def jupiter_emission_datetime(observation_dt: datetime) -> datetime:
"""Berechnet den Emissionszeitpunkt für eine Beobachtung von der Erde."""
observation_dt = observation_dt.astimezone(timezone.utc)
emission_dt = observation_dt
# Die Jupiterentfernung wird am zunächst geschätzten Emissionszeitpunkt
# neu bestimmt. Zwei Durchläufe reichen für die Lichtlaufzeit im
# Jupiter-System deutlich aus.
for _ in range(2):
geo = astronomy.GeoVector(
astronomy.Body.Jupiter,
dt_to_time(emission_dt),
True,
)
distance_au = math.sqrt(
(float(geo.x) * float(geo.x))
+ (float(geo.y) * float(geo.y))
+ (float(geo.z) * float(geo.z))
)
emission_dt = observation_dt - timedelta(days=distance_au / astronomy.C_AUDAY)
return emission_dt
def time_to_datetime(time_value: astronomy.Time) -> datetime:
year, month, day, hour, minute, second = time_value.Calendar()
second_int = int(second)
@@ -2874,6 +2907,78 @@ def moon_planet_separation_deg(
return spherical_separation_deg(float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec))
def moon_planet_occultation_margin_deg(
body: astronomy.Body,
observer: astronomy.Observer,
dt_utc: datetime,
) -> float:
"""Negativ bedeutet Überlappung der scheinbaren Mond- und Planetenscheiben."""
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)
moon_radius = moon_angular_radius_deg(float(moon_eq.dist))
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,
center_utc: datetime,
direction: int,
) -> datetime | None:
"""Findet den Scheibenkontakt vor oder nach der größten Bedeckung."""
step = timedelta(minutes=5)
inner = center_utc
inner_margin = moon_planet_occultation_margin_deg(body, observer, inner)
if inner_margin > 0.0:
return None
outer = inner
for _ in range(288): # maximal 24 Stunden vom Maximum entfernt suchen
outer = outer + (step if direction > 0 else -step)
outer_margin = moon_planet_occultation_margin_deg(body, observer, outer)
if outer_margin >= 0.0:
left, right = (inner, outer) if direction > 0 else (outer, inner)
for _ in range(40):
middle = left + (right - left) / 2
middle_margin = moon_planet_occultation_margin_deg(body, observer, middle)
if direction > 0:
if middle_margin < 0.0:
left = middle
else:
right = middle
else:
if middle_margin < 0.0:
right = middle
else:
left = middle
return right if direction > 0 else left
inner = outer
inner_margin = outer_margin
return None
def moon_fixed_equatorial_separation_deg(
ra_hours: float,
dec_deg: float,
@@ -3291,6 +3396,43 @@ def action_moon_planet_approaches(args: list[str]) -> dict:
continue
seen_ranges.append((left, right))
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"),
("end", refine_occultation_contact(body, observer, min_time_utc, 1), "Ende der Bedeckung"),
]
if all(contact_time is not None for _, contact_time, _ in contact_times):
for event_kind, contact_time, event_label in contact_times:
assert contact_time is not None
contact_local = contact_time.astimezone(tz)
approaches.append({
"planet_key": key,
"planet_label": label,
"label": f"Mond bedeckt {label} – {event_label}",
"event_kind": event_kind,
"separation_deg": float(min_sep),
"utc_iso": contact_time.isoformat().replace("+00:00", "Z"),
"local_iso": contact_local.isoformat(),
"local_date": contact_local.strftime("%d.%m.%Y"),
"local_time": contact_local.strftime("%H:%M"),
})
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,
@@ -3509,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:
@@ -3548,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(),
@@ -4644,7 +4799,11 @@ def action_jupiter_moons_one_side_for_month(args: list[str]) -> dict:
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)
# Die ausgegebenen Ereigniszeiten bleiben Beobachtungszeiten in UTC.
# Für die tatsächliche Stellung der Monde rechnen wir auf den
# Emissionszeitpunkt des Jupiter-Lichts zurück.
emission_dt = jupiter_emission_datetime(dt_utc)
time_value = dt_to_time(emission_dt)
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)
@@ -5013,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