Compare commits

...
8 Commits
Author SHA1 Message Date
Eskimue 291b5d973e Merge branch 'main' of https://git.daheim.eskimue.de/Eskimue/skyview.astronomiemuseum.de 2026-09-15 17:41:02 +02:00
EskimueandClaude Sonnet 5 71c8e832ba Detailkarte der Mond-Planeten-Bedeckungen: echte Mondtextur statt Flaechenfuellung
Verwendet dieselbe Mondgrafik (moon4calendar.png) wie Mondphasenansicht und
Mondjahr, korrekt zur Himmelsorientierung gedreht, mit Fallback auf die
alte Flachdarstellung solange die Textur noch nicht geladen ist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 10:34:12 +02:00
EskimueandClaude Sonnet 5 5652cc7e40 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 <noreply@anthropic.com>
2026-09-14 09:35:36 +02:00
EskimueandClaude Sonnet 5 86fccdc773 Kometen-Ereignisse im Monatsfließtext als vollständige Saetze formulieren
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 19:02:52 +02:00
EskimueandClaude Sonnet 5 61aa9cd89f Kometenseite: Hinzufuegen/Entfernen aus der eigenen Liste direkt ermoeglichen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 18:25:55 +02:00
EskimueandClaude Sonnet 5 f550875107 Mondfinsternis-Einfärbung in der Mondphasenleiste der Monatsvorhersage ergänzen
Analog zu moonyear.php wird die Mondscheibe je nach Finsternisart (total,
partiell, Halbschatten) eingefärbt, basierend auf den bereits geladenen
Mondfinsternis-Ereignissen des Monats.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 14:08:39 +02:00
EskimueandClaude Sonnet 5 4c6fe6faeb Mond-3D: Pan und Zoom ohne Blickwinkelveraenderung umsetzen
Verschieben nutzt jetzt camera.setViewOffset() statt Kameraposition/-ziel
zu bewegen, und Zoom erfolgt ueber camera.zoom (Brennweite) statt ueber
Annaeherung der Kamera. So bleibt der Blickwinkel auf den Mond in der
Standardansicht immer wie von der Erde aus - nur der freie Kamera-Modus
darf ihn aendern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 13:38:15 +02:00
EskimueandClaude Sonnet 5 efb2d8ed04 Mond-3D-Darstellung auf konsistente IAU-Rotationsachse umstellen und Beleuchtung ueberarbeiten
Sub-Erd- und Sub-Sonnen-Punkt (Laenge/Breite) werden jetzt aus der
offiziellen IAU-Rotationsachse des Mondes (RotationAxis: Pol-RA/Dec und
Rotationswinkel W) berechnet statt aus der aelteren, nur naeherungsweisen
Meeus-Optiklibration - gegen die oeffentlichen NASA-Dial-a-Moon-Daten
exakt validiert (Laenge/Breite und Positionswinkel stimmen auf drei
Nachkommastellen).

- Sonnenrichtung im 3D-Rendering direkt aus der Geometrie abgeleitet
  (derselbe lokale Vektor wie bei der Krater-Platzierung, dann mit der
  Mesh-Rotation gedreht), statt aus der Beleuchtungs-Prozentzahl
  zurueckgerechnet - exakt statt angenaehert, konsistent mit Kratern.
- Mesh-Rotation (Kugel-Ausrichtung inkl. Textur) laeuft jetzt durch
  dieselbe Laengen-Konvention wie Krater und Sonne, statt eine eigene,
  inkonsistente Berechnung zu nutzen (behebt u. a. eine falsch
  abgeschnittene Darstellung von Mare Crisium nahe dem Rand).
- Ost/West-Vorzeichen der selenographischen Laenge korrigiert.
- Tone-Mapping (ACESFilmicToneMapping) ergaenzt, damit duenne Mondsicheln
  und Vollmond gleichzeitig vernuenftig belichtet dargestellt werden
  koennen - eine feste Lichtstaerke allein konnte das nicht leisten.
- Schatten-Bias erhoeht gegen Shadow-Acne bei sehr flachem Lichteinfall;
  Schatten-Map-Typ vereinheitlicht (Live-Ansicht und PNG-Export nutzten
  bisher unterschiedliche Werte).
- Alle Licht-Parameter als benannte Konstanten mit Erklaerung gesammelt
  und dabei doppelte, auseinanderlaufende Einstellungen (u. a.
  toneMappingExposure, ambientLight) bereinigt.
- Neues, einklappbares (aktuell ausgeblendetes) Testregler-Panel fuer
  alle Licht-Parameter samt "Werte speichern"-Button, um kuenftige
  Feinabstimmung ohne Codesuche zu ermoeglichen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 13:08:54 +02:00
9 changed files with 2071 additions and 82 deletions
+1
View File
@@ -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. |
+1
View File
@@ -135,6 +135,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',
+93
View File
@@ -203,6 +203,69 @@ $pdo = new PDO(
);
$userId = isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $userId > 0) {
$action = $_POST['action'] ?? '';
$cometId = (int) ($_POST['comet_id'] ?? 0);
if ($action === 'add_comet' && $cometId > 0) {
$stmtCheck = $pdo->prepare("
SELECT `id`
FROM `comets_mpc`
WHERE `id` = :id
LIMIT 1
");
$stmtCheck->execute([':id' => $cometId]);
if ($stmtCheck->fetch()) {
$stmtInsert = $pdo->prepare("
INSERT INTO `app_user_comets` (
`user_id`,
`comet_id`,
`is_favorite`,
`created_at`
) VALUES (
:user_id,
:comet_id,
0,
NOW()
)
ON DUPLICATE KEY UPDATE
`comet_id` = VALUES(`comet_id`)
");
$stmtInsert->execute([
':user_id' => $userId,
':comet_id' => $cometId,
]);
}
}
if ($action === 'remove_comet' && $cometId > 0) {
$stmtCount = $pdo->prepare("
SELECT COUNT(*)
FROM `app_user_comets`
WHERE `user_id` = :user_id
");
$stmtCount->execute([':user_id' => $userId]);
$cometCount = (int) $stmtCount->fetchColumn();
if ($cometCount > 1) {
$stmtDelete = $pdo->prepare("
DELETE FROM `app_user_comets`
WHERE `user_id` = :user_id
AND `comet_id` = :comet_id
");
$stmtDelete->execute([
':user_id' => $userId,
':comet_id' => $cometId,
]);
}
}
header('Location: comets.php');
exit;
}
$today = new DateTimeImmutable('today', new DateTimeZone('UTC'));
$windowStart = $today->modify('-180 days');
$windowEnd = $today->modify('+365 days');
@@ -632,6 +695,9 @@ arsort($orbitTypeCounts);
<th data-sortable>Perihel</th>
<th data-sortable>Abstand</th>
<th data-sortable title="Grobe Helligkeitsschaetzung fuer heute nach H + 5 log(Delta) + 2,5 * n * log(r).">m grob</th>
<?php if ($loggedIn): ?>
<th></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
@@ -664,6 +730,19 @@ arsort($orbitTypeCounts);
<td data-sort-value="<?= h((string) $perihelionTs) ?>"><?= h((string) $comet['perihelion_date_display']) ?></td>
<td data-sort-value="<?= h((string) ($comet['days_from_today'] ?? '')) ?>"><span class="comets-days <?= $daysClass ?>"><?= h($daysLabel) ?></span></td>
<td data-sort-value="<?= h((string) ($comet['estimated_magnitude'] ?? '')) ?>" title="r = <?= h((string) $comet['heliocentric_distance_display']) ?> AE, Δ = <?= h((string) $comet['geocentric_distance_display']) ?> AE"><?= h((string) $comet['estimated_magnitude_display']) ?></td>
<?php if ($loggedIn): ?>
<td class="action-cell">
<?php if (count($trackedComets) > 1): ?>
<form method="post" action="comets.php" onsubmit="return confirm('Komet wirklich aus deiner Liste entfernen?');">
<input type="hidden" name="action" value="remove_comet">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">Entfernen</button>
</form>
<?php else: ?>
<span class="hint">Letzter Komet</span>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
@@ -689,6 +768,9 @@ arsort($orbitTypeCounts);
<th data-sortable>Perihel</th>
<th data-sortable>Abstand</th>
<th data-sortable title="Grobe Helligkeitsschaetzung fuer heute nach H + 5 log(Delta) + 2,5 * n * log(r).">m grob</th>
<?php if ($loggedIn): ?>
<th></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
@@ -722,6 +804,17 @@ arsort($orbitTypeCounts);
<td data-sort-value="<?= h((string) $perihelionTs) ?>"><?= h((string) $comet['perihelion_date_display']) ?></td>
<td data-sort-value="<?= h((string) ($comet['days_from_today'] ?? '')) ?>"><span class="comets-days <?= $daysClass ?>"><?= h($daysLabel) ?></span></td>
<td data-sort-value="<?= h((string) ($comet['estimated_magnitude'] ?? '')) ?>" title="r = <?= h((string) $comet['heliocentric_distance_display']) ?> AE, Δ = <?= h((string) $comet['geocentric_distance_display']) ?> AE"><?= h((string) $comet['estimated_magnitude_display']) ?></td>
<?php if ($loggedIn): ?>
<td class="action-cell">
<?php if ((int) $comet['is_tracked'] !== 1): ?>
<form method="post" action="comets.php">
<input type="hidden" name="action" value="add_comet">
<input type="hidden" name="comet_id" value="<?= (int) $comet['id'] ?>">
<button type="submit" class="btn btn-sm btn-primary">Hinzufügen</button>
</form>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
+26
View File
@@ -2569,6 +2569,26 @@ body.moonphase-page.has-modal {
gap: 1rem;
}
/* Licht-Testregler (moonphase.php): als Flex-Kind von .moon-side erbt dieser
Block sonst "overflow: hidden" von .moon-side > * und wuerde bei geoeffnetem
<details> zusammengedrueckt, sodass untere Regler verdeckt sind. Eigener,
in sich scrollbarer Bereich behebt das, unabhaengig vom verfuegbaren Platz. */
#moonLightDebugPanel summary {
cursor: pointer;
}
#moonLightDebugPanel[open] {
overflow-y: auto;
max-height: 320px;
}
/* .moon-controls setzt display:flex und wuerde damit das hidden-Attribut
(Browser-Standard: display:none) ausser Kraft setzen - Autoren-CSS gewinnt
sonst immer gegen UA-Stile. Explizit erzwingen, solange [hidden] gesetzt ist. */
#moonLightDebugPanel[hidden] {
display: none;
}
.control-head {
display: flex;
align-items: center;
@@ -4656,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;
}
+1
View File
@@ -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'],
],
],
[
+81 -15
View File
@@ -285,6 +285,7 @@ $planetDataJson = '[]';
$moonDataJson = 'null';
$monthChartCometsJson = '[]';
$moonPhaseEventsJson = '[]';
$monthLunarEclipsesJson = '{}';
$monthEventListJson = '[]';
$starDataErrorJson = 'null';
$observationTipCalendarIcsJson = '""';
@@ -1613,20 +1614,31 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['favorite_comet_good_visibility', 'favorite_comet_perihelion', 'favorite_comet_geocentric_minimum'], true),
4
);
if ($cometEvents !== []) {
$describeCometEvent = static function (array $event): ?string {
$name = trim((string) ($event['comet_name'] ?? ''));
$date = trim((string) ($event['date'] ?? ''));
if ($name === '' || $date === '') {
return null;
}
return match ((string) ($event['type'] ?? '')) {
'favorite_comet_perihelion' => "Der Komet {$name} erreicht am {$date} seinen sonnennächsten Bahnpunkt, das Perihel.",
'favorite_comet_geocentric_minimum' => "Der Komet {$name} kommt der Erde am {$date} am nächsten.",
'favorite_comet_good_visibility' => "Der Komet {$name} ist ab dem {$date} besonders gut zu beobachten.",
default => null,
};
};
$cometSentences = array_values(array_filter(array_map($describeCometEvent, $cometEvents)));
if ($cometSentences !== []) {
$cometIntro = $seededPick([
'Bei den Kometen sind in diesem Monat besonders',
'Auch bei den Kometen lohnt sich in diesem Monat der Blick auf',
'Unter den Kometen fallen in diesem Monat vor allem',
'Kometenfreunde können sich in diesem Monat besonders für',
], 'cometIntro', 'Bei den Kometen sind in diesem Monat besonders');
$cometOutro = $seededPick([
'interessant.',
'beobachtenswert.',
'bemerkenswert.',
'einen genaueren Blick wert.',
], 'cometOutro', 'interessant.');
$addParagraph($cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro);
'Auch bei den Kometen gibt es in diesem Monat einiges zu entdecken.',
'Kometenfreunde kommen in diesem Monat ebenfalls auf ihre Kosten.',
'Unter den Kometen gibt es in diesem Monat lohnende Ziele.',
'Auch der Blick auf die Kometen lohnt sich in diesem Monat.',
], 'cometIntro', 'Auch bei den Kometen gibt es in diesem Monat einiges zu entdecken.');
$addParagraph($cometIntro . ' ' . implode(' ', $cometSentences));
}
$eclipseEvents = $findEvents(
@@ -2992,6 +3004,7 @@ foreach ($favoriteCometEvents as $favoriteCometEvent) {
'end_local_iso' => (string) ($favoriteCometEvent['end_local_iso'] ?? ''),
'duration_minutes' => isset($favoriteCometEvent['duration_minutes']) ? (int) $favoriteCometEvent['duration_minutes'] : null,
'type' => $favoriteCometType,
'comet_name' => trim((string) ($favoriteCometEvent['comet_name'] ?? '')),
];
}
@@ -3197,6 +3210,30 @@ $monthChartComets = array_map(
);
$monthChartCometsJson = json_encode($monthChartComets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$moonPhaseEventsJson = json_encode($moonPhaseEvents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Mondfinsternisse dieses Monats, nach Tag gruppiert (Peak-Ereignis reicht für
// die Einfärbung der Mondscheibe in der Mondphasenleiste, analog zu moonyear.php)
$monthLunarEclipsesByDay = [];
foreach ($eclipseEvents as $eclipseEvent) {
if (!is_array($eclipseEvent)) {
continue;
}
if (($eclipseEvent['category'] ?? '') !== 'lunar_eclipse' || ($eclipseEvent['stage'] ?? '') !== 'peak') {
continue;
}
$eclipseDateParts = explode('.', (string) ($eclipseEvent['local_date'] ?? ''));
$eclipseDay = isset($eclipseDateParts[0]) ? (int) $eclipseDateParts[0] : 0;
if ($eclipseDay < 1) {
continue;
}
$monthLunarEclipsesByDay[$eclipseDay] = [
'kind' => (string) ($eclipseEvent['kind'] ?? ''),
'time' => (string) ($eclipseEvent['local_time'] ?? ''),
];
}
$monthLunarEclipsesJson = json_encode($monthLunarEclipsesByDay, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$observationTipCalendarIcsJson = json_encode($observationTipCalendarIcs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatsereignisse-%04d-%02d.ics', $selectedYear, $selectedMonth), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -3831,6 +3868,7 @@ const MONTH_CHART_PLANETS = <?= $planetDataJson ?: '[]' ?>;
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
const MONTH_CHART_COMETS = <?= $monthChartCometsJson ?: '[]' ?>;
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
const MONTH_LUNAR_ECLIPSES = <?= $monthLunarEclipsesJson ?: '{}' ?>;
const MONTH_EVENT_LIST = <?= $monthEventListJson ?: '[]' ?>;
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
@@ -4355,7 +4393,7 @@ function monthChartCometRaDec(comet, date) {
renderPhaseCanvas(cssWidth, cssHeight, dpr, true);
}
function drawMoonDisc(x, y, radius, phaseInfo) {
function drawMoonDisc(x, y, radius, phaseInfo, eclipseKind) {
const phaseRad = phaseInfo.phaseAngleDeg * Math.PI / 180;
const cosPhase = Math.cos(phaseRad);
@@ -4408,6 +4446,33 @@ function monthChartCometRaDec(comet, date) {
ctx.drawImage(phaseImage, x - radius, y - radius, radius * 2, radius * 2);
}
// Finsternis-Einfärbung als Overlay (analog zu moonyear.php)
if (eclipseKind === 'total') {
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = 'rgba(180, 50, 10, 0.55)';
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
ctx.restore();
} else if (eclipseKind === 'partial') {
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = 'rgba(160, 60, 10, 0.35)';
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
ctx.restore();
} else if (eclipseKind === 'penumbral') {
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = 'rgba(80, 80, 100, 0.25)';
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
ctx.restore();
}
ctx.restore();
ctx.restore();
@@ -4466,7 +4531,8 @@ function monthChartCometRaDec(comet, date) {
const radius = Math.max(23 * layoutScale, Math.min(33 * layoutScale, spacing * 0.52));
const x = leftPad + spacing * (dayInRow - 0.5);
const centerY = rowCenters[row];
drawMoonDisc(x, centerY, radius, phaseInfoForDay(year, month, day));
const eclipseInfo = MONTH_LUNAR_ECLIPSES ? MONTH_LUNAR_ECLIPSES[day] : null;
drawMoonDisc(x, centerY, radius, phaseInfoForDay(year, month, day), eclipseInfo ? eclipseInfo.kind : null);
ctx.fillText(String(day), x, centerY + radius + (12 * layoutScale));
}
File diff suppressed because it is too large Load Diff
+285 -52
View File
@@ -225,7 +225,51 @@ try {
</div>
<details class="moon-controls" id="moonLightDebugPanel" hidden>
<summary class="control-label">Licht (Testregler)</summary>
<div class="control-card">
<div class="control-head">
<label class="control-label" for="lightSunIntensity">Sonnenlicht</label>
<span class="control-value" id="lightSunIntensityValue">-</span>
</div>
<input type="range" id="lightSunIntensity" min="0" max="10" step="0.1">
</div>
<div class="control-card">
<div class="control-head">
<label class="control-label" for="lightExposure">Belichtung</label>
<span class="control-value" id="lightExposureValue">-</span>
</div>
<input type="range" id="lightExposure" min="0.1" max="3" step="0.05">
</div>
<div class="control-card">
<div class="control-head">
<label class="control-label" for="lightAmbient">Umgebungslicht</label>
<span class="control-value" id="lightAmbientValue">-</span>
</div>
<input type="range" id="lightAmbient" min="0" max="1" step="0.01">
</div>
<div class="control-card">
<div class="control-head">
<label class="control-label" for="lightRim">Randlicht</label>
<span class="control-value" id="lightRimValue">-</span>
</div>
<input type="range" id="lightRim" min="0" max="0.2" step="0.005">
</div>
<div class="control-card">
<div class="control-head">
<label class="control-label" for="lightShadowNormalBias">Schatten-Bias</label>
<span class="control-value" id="lightShadowNormalBiasValue">-</span>
</div>
<input type="range" id="lightShadowNormalBias" min="0" max="0.3" step="0.005">
</div>
<button type="button" class="date-shift-btn moon-action-btn" id="lightSaveValuesBtn">Werte speichern</button>
</details>
</div>
<div class="moon-visual">
@@ -339,6 +383,17 @@ const moonExportTransparentCheckbox = document.getElementById('moonExportTranspa
const moonExportStatus = document.getElementById('moonExportStatus');
const moonExportCancelBtn = document.getElementById('moonExportCancelBtn');
const moonExportConfirmBtn = document.getElementById('moonExportConfirmBtn');
const lightSunIntensity = document.getElementById('lightSunIntensity');
const lightSunIntensityValue = document.getElementById('lightSunIntensityValue');
const lightExposure = document.getElementById('lightExposure');
const lightExposureValue = document.getElementById('lightExposureValue');
const lightAmbient = document.getElementById('lightAmbient');
const lightAmbientValue = document.getElementById('lightAmbientValue');
const lightRim = document.getElementById('lightRim');
const lightRimValue = document.getElementById('lightRimValue');
const lightShadowNormalBias = document.getElementById('lightShadowNormalBias');
const lightShadowNormalBiasValue = document.getElementById('lightShadowNormalBiasValue');
const lightSaveValuesBtn = document.getElementById('lightSaveValuesBtn');
let moonSceneState = null;
let moonZoom = 1;
let moonPanX = 0;
@@ -379,6 +434,55 @@ const MOON_DISPLACEMENT_SCALE = 0.0095;
const MOON_DISPLACEMENT_BIAS = -0.1;
const MOON_BUMP_SCALE = 2.0;
const MOON_CAMERA_BASE_Z = 6.8;
// ---- Beleuchtung des 3D-Mondmodells ------------------------------------
// Alle Licht-Werte gesammelt an einer Stelle, damit man beim Justieren nicht
// im Code suchen muss. Reines JavaScript - nach einer Aenderung reicht ein
// Neuladen der Seite, kein Server-Neustart.
// Haupt-"Sonnenlicht" (gerichtetes Licht). Bestimmt, wie hell die beleuchtete
// Seite grundsaetzlich ist, gleichmaessig ueber alle Mondphasen hinweg.
// Hoeher = insgesamt heller (auch bei Vollmond, das dann eher ueberstrahlt).
let MOON_SUN_LIGHT_INTENSITY = 4.7;
// Wirkt wie eine Kamera-Belichtung, NACH der eigentlichen Lichtberechnung
// (Three.js ACESFilmicToneMapping). Im Gegensatz zu MOON_SUN_LIGHT_INTENSITY
// komprimiert sie helle Bereiche automatisch mit (ueberstrahlt also nicht
// gleich), waehrend sie dunkle/Streiflicht-Bereiche (duenne Sichel) sichtbarer
// macht. Faustregel: Wirkt eine duenne Sichel zu dunkel, aber Vollmond okay ->
// diesen Wert erhoehen statt MOON_SUN_LIGHT_INTENSITY. Gilt fuer Live-Ansicht
// UND PNG-Export gleichermassen (beide nutzen dieselbe Konstante).
let MOON_TONE_MAPPING_EXPOSURE = 1.15;
// Gleichmaessige Grundaufhellung der GESAMTEN Kugel, auch der unbeleuchteten
// Nachtseite, unabhaengig vom Lichtwinkel. Niedrig halten - sonst wirkt die
// Nachtseite grau statt schwarz und der Tag/Nacht-Kontrast (Kraterschatten)
// geht verloren.
const MOON_AMBIENT_LIGHT_COLOR = 0xffffff;
let MOON_AMBIENT_LIGHT_INTENSITY = 0.01;
// Schwaches zusaetzliches Licht von der Seite fuer einen dezenten Rand-/
// Tiefeneffekt auf der Nachtseite (kein echtes Erdlicht, rein optisch).
// Leicht blaeulich fuer einen kuehlen "Weltraum"-Farbton.
const MOON_RIM_LIGHT_COLOR = 0x89a8ff;
let MOON_RIM_LIGHT_INTENSITY = 0.095;
// Verhindert falsche Selbstverschattung ("Shadow Acne") der Krater-Shadow-Map
// bei sehr flachem Lichteinfall (duenne Sichel = fast ueberall Streiflicht).
// Die Tag/Nacht-Grenze selbst kommt schon aus der normalen Beleuchtungs-
// rechnung; die Shadow-Map ist nur fuer feine Krater-Schlagschatten noetig.
const MOON_SHADOW_BIAS = 0;
let MOON_SHADOW_NORMAL_BIAS = 0.005;
// Kantenschaerfe der Krater-Schlagschatten (nicht die Tag/Nacht-Grenze selbst -
// die kommt aus der normalen Beleuchtung und hat immer einen weichen, runden
// Verlauf, weil eine Kugel nie hart in Licht/Schatten uebergeht). Optionen von
// weich zu hart: THREE.PCFSoftShadowMap (weichste Kanten) < THREE.PCFShadowMap
// (Mittelweg) < THREE.BasicShadowMap (haerteste, scharfe Kanten, keine
// Weichzeichnung - kann bei feinen Kraterdetails leicht kantig/treppig wirken).
const MOON_SHADOW_MAP_TYPE = THREE.BasicShadowMap;
// -------------------------------------------------------------------------
const MOON_ZOOM_MIN = 0.2;
const MOON_ZOOM_MAX = 4.1;
const MOON_PAN_LIMIT = 1.8;
@@ -492,18 +596,47 @@ function applyMoonZoom() {
return;
}
const r = MOON_CAMERA_BASE_Z / getMoonZoomFactor();
if (freeCameraMode) {
const r = MOON_CAMERA_BASE_Z / getMoonZoomFactor();
moonSceneState.camera.position.set(
r * Math.sin(cameraOrbitPhi) * Math.sin(cameraOrbitTheta),
r * Math.cos(cameraOrbitPhi),
r * Math.sin(cameraOrbitPhi) * Math.cos(cameraOrbitTheta)
);
moonSceneState.camera.lookAt(0, 0, 0);
moonSceneState.camera.zoom = 1;
moonSceneState.camera.clearViewOffset();
} else {
moonSceneState.camera.position.set(moonPanX, moonPanY, r);
moonSceneState.camera.lookAt(moonPanX, moonPanY, 0);
// Kamera bleibt IMMER exakt auf den Mond ausgerichtet UND auf fester
// Distanz ("Blickwinkel wie von der Erde aus" darf sich weder beim
// Verschieben noch beim Zoomen aendern - genau wie ein Teleskop, das den
// Mond vergroessert, ohne dass sich die Erde-Mond-Distanz aendert). Zoom
// wird deshalb NICHT durch Annaehern der Kamera simuliert (das wuerde
// durch die perspektivische Projektion echte Parallaxe/Verzerrung
// erzeugen, die bei einer realen Fernbeobachtung so nicht auftritt),
// sondern rein optisch ueber camera.zoom (Brennweiten-Vergroesserung
// ohne Positionsaenderung). Das Verschieben passiert ebenfalls ohne
// Positionsaenderung, rein als Bildausschnitt-Versatz (Shift-Objektiv-
// Prinzip via camera.setViewOffset).
moonSceneState.camera.position.set(0, 0, MOON_CAMERA_BASE_Z);
moonSceneState.camera.lookAt(0, 0, 0);
moonSceneState.camera.zoom = getMoonZoomFactor();
const fullWidth = Math.max(1, moonCanvas.clientWidth || 900);
const fullHeight = Math.max(1, moonCanvas.clientHeight || 900);
// Der Bildwinkel-Versatz muss mit dem Zoom mitwachsen: die scheinbare
// Mondgroesse auf dem Bildschirm skaliert mit dem Zoomfaktor, sonst reicht
// der Pan-Bereich bei starkem Zoom nicht mehr aus, um den Mond bis an den
// Rand zu ziehen.
const panPixelScale = Math.min(fullWidth, fullHeight) * 0.18 * getMoonZoomFactor();
const offsetX = moonPanX * panPixelScale;
const offsetY = -moonPanY * panPixelScale;
if (offsetX === 0 && offsetY === 0) {
moonSceneState.camera.clearViewOffset();
} else {
moonSceneState.camera.setViewOffset(fullWidth, fullHeight, offsetX, offsetY, fullWidth, fullHeight);
}
}
moonSceneState.camera.updateProjectionMatrix();
@@ -838,24 +971,29 @@ async function buildMoonSceneFromUrls(url1, url2) {
powerPreference: 'high-performance'
});
renderer.outputColorSpace = THREE.SRGBColorSpace;
// Tone-Mapping statt reinem Clamping: komprimiert helle Bereiche (Vollmond),
// ohne dunkle/Streiflicht-Bereiche (duenne Sichel) zu verlieren - eine feste
// Lichtstaerke allein kann sonst nicht beides gleichzeitig darstellen.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = MOON_TONE_MAPPING_EXPOSURE;
renderer.setClearColor(0x010102, 1);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 3));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
renderer.shadowMap.type = MOON_SHADOW_MAP_TYPE;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
camera.position.set(0, 0, MOON_CAMERA_BASE_Z);
const ambientLight = new THREE.AmbientLight(0x1b2230, 0.004);
const ambientLight = new THREE.AmbientLight(MOON_AMBIENT_LIGHT_COLOR, MOON_AMBIENT_LIGHT_INTENSITY);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 2.9);
const sunLight = new THREE.DirectionalLight(0xffffff, MOON_SUN_LIGHT_INTENSITY);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 4096;
sunLight.shadow.mapSize.height = 4096;
sunLight.shadow.bias = -0.00015;
sunLight.shadow.normalBias = 0.02;
sunLight.shadow.bias = MOON_SHADOW_BIAS;
sunLight.shadow.normalBias = MOON_SHADOW_NORMAL_BIAS;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 20;
sunLight.shadow.camera.left = -3;
@@ -864,7 +1002,7 @@ async function buildMoonSceneFromUrls(url1, url2) {
sunLight.shadow.camera.bottom = -3;
scene.add(sunLight);
const rimLight = new THREE.DirectionalLight(0x89a8ff, 0.015);
const rimLight = new THREE.DirectionalLight(MOON_RIM_LIGHT_COLOR, MOON_RIM_LIGHT_INTENSITY);
rimLight.position.set(-2.5, 1.2, 3.2);
scene.add(rimLight);
@@ -1003,44 +1141,34 @@ async function renderMoon3D(phaseData, libration) {
function applyMoon3DRenderState(state, phaseData, libration) {
state.moonMesh.rotation.order = 'ZYX';
state.moonMesh.rotation.y = MOON_TEXTURE_BASE_ROTATION_Y + toRadians(libration.longitude);
// Dieselbe Laengen-Konvention wie bei Kratern/Sonne (moonLocalDirectionFromLonLat)
// verwenden, statt libration.longitude direkt zu nehmen - sonst rotiert die
// Textur/Kugel selbst in einem anderen Bezugssystem als Krater und Sonne,
// obwohl alle von derselben (gegen NASA validierten) Sub-Erd-Laenge ausgehen.
state.moonMesh.rotation.y = MOON_TEXTURE_BASE_ROTATION_Y + toRadians(-normalizeSignedLongitude(libration.longitude));
state.moonMesh.rotation.x = toRadians(libration.latitude);
state.moonMesh.rotation.z = toRadians(libration.positionAngle);
const viewAlignedSunZ = phaseData.illumination * 2 - 1;
const northOnScreen = new THREE.Vector3(0, 1, 0).applyEuler(state.moonMesh.rotation).normalize();
const planeNorth = new THREE.Vector2(northOnScreen.x, northOnScreen.y);
const planeNorthLength = planeNorth.length();
const horizontalMagnitude = Math.sqrt(Math.max(0, 1 - viewAlignedSunZ * viewAlignedSunZ));
const subsolarNorthDot = Math.sin(toRadians(libration.subsolarLatitude));
// Sonnenrichtung direkt aus der Geometrie: derselbe lokale Vektor wie bei
// Kratern (selenographische Laenge/Breite -> mondfester Einheitsvektor),
// gedreht mit derselben Mesh-Rotation. Exakt statt aus der Beleuchtungs-
// Prozentzahl zurueckgerechnet, und dadurch garantiert konsistent mit
// Kraterlage und Textur. Haengt nur von state.moonMesh.rotation ab (Datum/
// Uhrzeit), nicht von Kamera-Pan - "wie von der Erde aus zu sehen" bleibt
// beim Verschieben also unveraendert; nur der freie Kamera-Modus rotiert
// die Kamera selbst und veraendert damit den Blickwinkel auf Mond+Licht.
const sunDirectionWorld = moonLocalDirectionFromLonLat(
libration.subsolarLongitude,
libration.subsolarLatitude,
craterLongitudeOffsetDeg,
craterLatitudeOffsetDeg
).applyEuler(state.moonMesh.rotation);
let xComponent;
let yComponent;
if (planeNorthLength < 1e-6 || horizontalMagnitude < 1e-6) {
xComponent = phaseData.waxing ? horizontalMagnitude : -horizontalMagnitude;
yComponent = 0;
} else {
const northUnit = planeNorth.normalize();
const eastUnit = new THREE.Vector2(-northUnit.y, northUnit.x);
const targetAlongNorth = THREE.MathUtils.clamp(
(subsolarNorthDot - northOnScreen.z * viewAlignedSunZ) / planeNorthLength,
-horizontalMagnitude,
horizontalMagnitude
);
const sideMagnitude = Math.sqrt(Math.max(0, horizontalMagnitude * horizontalMagnitude - targetAlongNorth * targetAlongNorth));
const sideSign = phaseData.waxing ? -1 : 1;
const screenVector =
northUnit.multiplyScalar(targetAlongNorth)
.add(eastUnit.multiplyScalar(sideSign * sideMagnitude));
xComponent = screenVector.x;
yComponent = screenVector.y;
}
state.sunLight.position.set(xComponent * 8, yComponent * 8, viewAlignedSunZ * 8);
state.ambientLight.intensity = 0.004;
state.rimLight.intensity = 0.006;
state.sunLight.position.copy(sunDirectionWorld.multiplyScalar(8));
// ambientLight/rimLight sind konstant (siehe MOON_AMBIENT_LIGHT_INTENSITY /
// MOON_RIM_LIGHT_INTENSITY oben) und werden schon bei der Szenen-Erstellung
// gesetzt - hier bewusst kein erneutes Ueberschreiben mehr, damit beide
// Stellen nicht auseinanderlaufen koennen.
}
function drawMoonTexture(ctx, cx, cy, radius) {
@@ -1191,10 +1319,12 @@ async function renderMoon3DToBlob(phaseData, libration, size, transparentBackgro
powerPreference: 'high-performance'
});
exportRenderer.outputColorSpace = THREE.SRGBColorSpace;
exportRenderer.toneMapping = THREE.ACESFilmicToneMapping;
exportRenderer.toneMappingExposure = MOON_TONE_MAPPING_EXPOSURE;
exportRenderer.setClearColor(0x010102, transparentBackground ? 0 : 1);
exportRenderer.setPixelRatio(1);
exportRenderer.shadowMap.enabled = true;
exportRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
exportRenderer.shadowMap.type = MOON_SHADOW_MAP_TYPE;
exportRenderer.setSize(size, size, false);
const previousAspect = state.camera.aspect;
@@ -1318,19 +1448,32 @@ function normalizeSignedLongitude(degrees) {
return value;
}
function craterToMoonLocalVector(crater) {
const latRad = toRadians(Number(crater.lat || 0) + craterLatitudeOffsetDeg);
const lonRad = toRadians(-normalizeSignedLongitude(Number(crater.lon || 0) + craterLongitudeOffsetDeg));
// Einheitsvektor im mondfesten (lokalen, noch unrotierten) Koordinatensystem
// aus selenographischer Laenge/Breite (Ost-positiv, wie von api.py geliefert).
// Gemeinsame Grundlage fuer Krater UND Sonnenrichtung, damit beide nach
// Anwendung derselben state.moonMesh-Rotation garantiert zueinander passen.
function moonLocalDirectionFromLonLat(lonDeg, latDeg, lonOffsetDeg = 0, latOffsetDeg = 0) {
const latRad = toRadians(Number(latDeg || 0) + latOffsetDeg);
const lonRad = toRadians(-normalizeSignedLongitude(Number(lonDeg || 0) + lonOffsetDeg));
const cosLat = Math.cos(latRad);
const craterRadius = MOON_SPHERE_RADIUS * craterOverlayScale;
return new THREE.Vector3(
craterRadius * cosLat * Math.cos(lonRad),
craterRadius * Math.sin(latRad),
craterRadius * cosLat * Math.sin(lonRad)
cosLat * Math.cos(lonRad),
Math.sin(latRad),
cosLat * Math.sin(lonRad)
);
}
function craterToMoonLocalVector(crater) {
const craterRadius = MOON_SPHERE_RADIUS * craterOverlayScale;
return moonLocalDirectionFromLonLat(
Number(crater.lon || 0),
Number(crater.lat || 0),
craterLongitudeOffsetDeg,
craterLatitudeOffsetDeg
).multiplyScalar(craterRadius);
}
function getCraterVisualStyle(crater, mode = 'all') {
if (mode === 'terminator') {
return {
@@ -1695,6 +1838,96 @@ timeSlider.addEventListener('wheel', event => {
updateMoon();
}
}, { passive: false });
// Testregler fuer die Licht-Konstanten (siehe MOON_SUN_LIGHT_INTENSITY etc.
// oben): Schieberegler + Mausrad, wirkt sofort ohne Neuladen der Seite.
const lightSliderConfigs = [
{
input: lightSunIntensity, label: lightSunIntensityValue, initial: MOON_SUN_LIGHT_INTENSITY,
apply: value => {
MOON_SUN_LIGHT_INTENSITY = value;
if (moonSceneState) moonSceneState.sunLight.intensity = value;
}
},
{
input: lightExposure, label: lightExposureValue, initial: MOON_TONE_MAPPING_EXPOSURE,
apply: value => {
MOON_TONE_MAPPING_EXPOSURE = value;
if (moonSceneState) moonSceneState.renderer.toneMappingExposure = value;
}
},
{
input: lightAmbient, label: lightAmbientValue, initial: MOON_AMBIENT_LIGHT_INTENSITY,
apply: value => {
MOON_AMBIENT_LIGHT_INTENSITY = value;
if (moonSceneState) moonSceneState.ambientLight.intensity = value;
}
},
{
input: lightRim, label: lightRimValue, initial: MOON_RIM_LIGHT_INTENSITY,
apply: value => {
MOON_RIM_LIGHT_INTENSITY = value;
if (moonSceneState) moonSceneState.rimLight.intensity = value;
}
},
{
input: lightShadowNormalBias, label: lightShadowNormalBiasValue, initial: MOON_SHADOW_NORMAL_BIAS,
apply: value => {
MOON_SHADOW_NORMAL_BIAS = value;
if (moonSceneState) moonSceneState.sunLight.shadow.normalBias = value;
}
}
];
function setupLightSlider({ input, label, initial, apply }) {
input.value = String(initial);
label.textContent = initial.toFixed(3);
const commit = () => {
const value = Number(input.value);
apply(value);
label.textContent = value.toFixed(3);
if (moonSceneState) {
moonSceneState.renderer.render(moonSceneState.scene, moonSceneState.camera);
}
};
input.addEventListener('input', commit);
input.addEventListener('wheel', event => {
event.preventDefault();
const step = Number(input.step) || 0.01;
const min = Number(input.min) || 0;
const max = Number(input.max) || 1;
const direction = event.deltaY > 0 ? 1 : -1;
const nextValue = Math.min(max, Math.max(min, Number(input.value) + direction * step));
input.value = String(nextValue);
commit();
}, { passive: false });
}
lightSliderConfigs.forEach(setupLightSlider);
// Aktuelle Reglerwerte als Textdatei speichern - fertig zum Einsetzen als
// neue Standardwerte oben bei den MOON_*-Konstanten.
lightSaveValuesBtn.addEventListener('click', () => {
const lines = [
`MOON_SUN_LIGHT_INTENSITY = ${MOON_SUN_LIGHT_INTENSITY}`,
`MOON_TONE_MAPPING_EXPOSURE = ${MOON_TONE_MAPPING_EXPOSURE}`,
`MOON_AMBIENT_LIGHT_INTENSITY = ${MOON_AMBIENT_LIGHT_INTENSITY}`,
`MOON_RIM_LIGHT_INTENSITY = ${MOON_RIM_LIGHT_INTENSITY}`,
`MOON_SHADOW_NORMAL_BIAS = ${MOON_SHADOW_NORMAL_BIAS}`
];
const blob = new Blob([lines.join('\n') + '\n'], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'mondlicht-werte.txt';
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
});
prevDayBtn.addEventListener('click', () => shiftDateByDays(-1));
nextDayBtn.addEventListener('click', () => shiftDateByDays(1));
nowBtn.addEventListener('click', () => {
+238 -14
View File
@@ -5435,6 +5435,14 @@ def vector_dot(a: tuple[float, float, float], b: tuple[float, float, float]) ->
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def vector_cross(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]:
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def vector_normalize(v: tuple[float, float, float]) -> tuple[float, float, float]:
length = math.sqrt(vector_dot(v, v))
if length <= 0:
@@ -5442,6 +5450,21 @@ def vector_normalize(v: tuple[float, float, float]) -> tuple[float, float, float
return v[0] / length, v[1] / length, v[2] / length
def rotate_vector_around_axis(
v: tuple[float, float, float], axis: tuple[float, float, float], angle_rad: float
) -> tuple[float, float, float]:
"""Rodrigues-Rotationsformel: dreht v um die (Einheits-)Achse axis."""
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
axis_cross_v = vector_cross(axis, v)
axis_dot_v = vector_dot(axis, v)
return (
v[0] * cos_a + axis_cross_v[0] * sin_a + axis[0] * axis_dot_v * (1.0 - cos_a),
v[1] * cos_a + axis_cross_v[1] * sin_a + axis[1] * axis_dot_v * (1.0 - cos_a),
v[2] * cos_a + axis_cross_v[2] * sin_a + axis[2] * axis_dot_v * (1.0 - cos_a),
)
def calculate_moon_position_angle(time_value: astronomy.Time) -> float:
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
moon_equator = astronomy.EquatorFromVector(moon_vector)
@@ -5461,21 +5484,64 @@ def calculate_moon_position_angle(time_value: astronomy.Time) -> float:
)
def calculate_moon_axis_latitudes(time_value: astronomy.Time) -> dict:
def moon_body_frame(time_value: astronomy.Time) -> tuple[
tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]
]:
"""Baut eine orthonormale Basis (body_x = Nullmeridian-Richtung, body_y,
north = Rotationsachse) des mondfesten Koordinatensystems in EQJ, nach dem
offiziellen IAU-Rotationsachsen-Standard (RotationAxis: Pol-RA/Dec und
Rotationswinkel W, siehe IAU WGCCRE 2015)."""
axis = astronomy.RotationAxis(astronomy.Body.Moon, time_value)
north = vector_normalize(vector_from_astronomy(axis.north))
ra_rad = math.radians(axis.ra * 15.0)
# Aufsteigender Knoten der Mondaequatorebene auf der EQJ-Ebene (RA+90 Grad, Dec=0).
ascending_node = (-math.sin(ra_rad), math.cos(ra_rad), 0.0)
body_x = vector_normalize(rotate_vector_around_axis(ascending_node, north, math.radians(axis.spin)))
body_y = vector_normalize(vector_cross(north, body_x))
return body_x, body_y, north
def selenographic_lonlat(
vector: tuple[float, float, float],
body_x: tuple[float, float, float],
body_y: tuple[float, float, float],
north: tuple[float, float, float],
) -> tuple[float, float]:
"""Selenographische Laenge/Breite eines geozentrischen Richtungsvektors,
Ost-positiv (kalibriert gegen die IAU-Kraterdatenbank, z. B. Mare Crisium
bei +59,1 Grad Ost) und gegen astronomy.Libration() validiert.
HINWEIS: Vorzeichen wird gerade per Browsertest verifiziert (Ost/West-Problem)."""
v = vector_normalize(vector)
longitude = math.degrees(math.atan2(vector_dot(v, body_y), vector_dot(v, body_x)))
latitude = math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(v, north)))))
return longitude, latitude
def calculate_moon_orientation(time_value: astronomy.Time) -> dict:
"""Sub-Erd- und Sub-Sonnen-Punkt (selenographische Laenge/Breite) aus der
offiziellen IAU-Rotationsachse des Mondes - eine einzige, in sich
konsistente Grundlage fuer Mondausrichtung (Libration) UND Sonnenrichtung,
anstelle der frueheren, nur die Breite abdeckenden Berechnung."""
body_x, body_y, north = moon_body_frame(time_value)
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
sun_vector = astronomy.GeoVector(astronomy.Body.Sun, time_value, True)
north = vector_normalize(vector_from_astronomy(axis.north))
moon_xyz = vector_from_astronomy(moon_vector)
sun_xyz = vector_from_astronomy(sun_vector)
earth_from_moon = vector_normalize((-moon_xyz[0], -moon_xyz[1], -moon_xyz[2]))
sun_from_moon = vector_normalize(vector_subtract(sun_xyz, moon_xyz))
earth_from_moon = (-moon_xyz[0], -moon_xyz[1], -moon_xyz[2])
sun_from_moon = vector_subtract(sun_xyz, moon_xyz)
subearth_longitude, subearth_latitude = selenographic_lonlat(earth_from_moon, body_x, body_y, north)
subsolar_longitude, subsolar_latitude = selenographic_lonlat(sun_from_moon, body_x, body_y, north)
return {
"subearth_latitude": math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(earth_from_moon, north))))),
"subsolar_latitude": math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(sun_from_moon, north))))),
"subearth_longitude": subearth_longitude,
"subearth_latitude": subearth_latitude,
"subsolar_longitude": subsolar_longitude,
"subsolar_latitude": subsolar_latitude,
}
@@ -5510,8 +5576,12 @@ def action_moon_phase_details(args: list[str]) -> dict:
else phase * SYNODIC_MONTH
)
# Mondausrichtung (Libration) und Sonnenrichtung aus einer einzigen,
# konsistenten Grundlage: der offiziellen IAU-Rotationsachse des Mondes.
# distKm bleibt bewusst von astronomy.Libration() (bereits gegen PyEphem
# validiert), da hierfuer kein Wechsel noetig ist.
libration = astronomy.Libration(time_value)
axis_latitudes = calculate_moon_axis_latitudes(time_value)
orientation = calculate_moon_orientation(time_value)
position_angle = calculate_moon_position_angle(time_value)
local_dt = dt_utc.astimezone(tz)
@@ -5531,10 +5601,11 @@ def action_moon_phase_details(args: list[str]) -> dict:
"label": get_phase_label(age_days),
},
"libration": {
"longitude": -normalize_signed_degrees(float(libration.elon)),
"latitude": float(libration.elat),
"subsolarLatitude": float(axis_latitudes["subsolar_latitude"]),
"subearthLatitude": float(axis_latitudes["subearth_latitude"]),
"longitude": float(orientation["subearth_longitude"]),
"latitude": float(orientation["subearth_latitude"]),
"subsolarLongitude": float(orientation["subsolar_longitude"]),
"subsolarLatitude": float(orientation["subsolar_latitude"]),
"subearthLatitude": float(orientation["subearth_latitude"]),
"positionAngle": float(position_angle),
"distKm": float(libration.dist_km),
},
@@ -5773,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
@@ -6344,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:]
@@ -6534,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))
@@ -6544,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__":