Compare commits
7
Commits
4f4a2ae7ef
...
5bb05e69df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bb05e69df | ||
|
|
d62879cce3 | ||
|
|
b5c7248a64 | ||
|
|
dd430bcfd1 | ||
|
|
d4c44c55d2 | ||
|
|
1725d9db27 | ||
|
|
7a7abdf25a |
@@ -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
@@ -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');
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user