Compare commits
8
Commits
59cb076640
...
291b5d973e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
291b5d973e | ||
|
|
71c8e832ba | ||
|
|
5652cc7e40 | ||
|
|
86fccdc773 | ||
|
|
61aa9cd89f | ||
|
|
f550875107 | ||
|
|
4c6fe6faeb | ||
|
|
efb2d8ed04 |
@@ -49,6 +49,7 @@ Nicht jede Berechnung ist Skyfield oder Astronomy Engine:
|
|||||||
| `sonnenfinsternis.php` | **Astronomy Engine**, einschließlich lokaler Berechnung für den Standardstandort. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `moonphase.php` | **Astronomy Engine** im Browser beziehungsweise über die lokale Python-API. |
|
||||||
| `moonyear.php` | **Astronomy Engine** für Mondphasen und Mondpositionen. |
|
| `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. |
|
| `satellitenhimmel.php` | **SGP4** für Satellitenbahnen; Astronomy Engine für astronomischen Kontext wie Sonne, Mond und Dämmerung. |
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ function manageablePublicPages(): array
|
|||||||
'sonnenfinsternis.php' => 'Sonnenfinsternis',
|
'sonnenfinsternis.php' => 'Sonnenfinsternis',
|
||||||
'bedeckung.php' => 'Bedeckungsphänomene',
|
'bedeckung.php' => 'Bedeckungsphänomene',
|
||||||
'sternbedeckungen.php' => 'Mond-Stern-Bedeckungen',
|
'sternbedeckungen.php' => 'Mond-Stern-Bedeckungen',
|
||||||
|
'mondplanetenbedeckungen.php' => 'Mond-Planeten-Bedeckungen',
|
||||||
'monatsvorhersage.php' => 'Monatsvorhersage',
|
'monatsvorhersage.php' => 'Monatsvorhersage',
|
||||||
'moonyear.php' => 'Mondphasen-Jahresübersicht',
|
'moonyear.php' => 'Mondphasen-Jahresübersicht',
|
||||||
'wochenvorhersage.php' => 'Wochenvorhersage',
|
'wochenvorhersage.php' => 'Wochenvorhersage',
|
||||||
|
|||||||
@@ -203,6 +203,69 @@ $pdo = new PDO(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$userId = isset($_SESSION['user_id']) ? (int) $_SESSION['user_id'] : 0;
|
$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'));
|
$today = new DateTimeImmutable('today', new DateTimeZone('UTC'));
|
||||||
$windowStart = $today->modify('-180 days');
|
$windowStart = $today->modify('-180 days');
|
||||||
$windowEnd = $today->modify('+365 days');
|
$windowEnd = $today->modify('+365 days');
|
||||||
@@ -632,6 +695,9 @@ arsort($orbitTypeCounts);
|
|||||||
<th data-sortable>Perihel</th>
|
<th data-sortable>Perihel</th>
|
||||||
<th data-sortable>Abstand</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>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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) $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['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>
|
<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>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -689,6 +768,9 @@ arsort($orbitTypeCounts);
|
|||||||
<th data-sortable>Perihel</th>
|
<th data-sortable>Perihel</th>
|
||||||
<th data-sortable>Abstand</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>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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) $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['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>
|
<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>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -2569,6 +2569,26 @@ body.moonphase-page.has-modal {
|
|||||||
gap: 1rem;
|
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 {
|
.control-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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);
|
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 {
|
.occult-detail-card {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ $menuGroups = [
|
|||||||
['href' => 'sonnenfinsternis.php', 'label' => 'Sonnenfinsternis'],
|
['href' => 'sonnenfinsternis.php', 'label' => 'Sonnenfinsternis'],
|
||||||
['href' => 'bedeckung.php', 'label' => 'Bedeckungsphänomene'],
|
['href' => 'bedeckung.php', 'label' => 'Bedeckungsphänomene'],
|
||||||
['href' => 'sternbedeckungen.php', 'label' => 'Mond-Stern-Bedeckungen'],
|
['href' => 'sternbedeckungen.php', 'label' => 'Mond-Stern-Bedeckungen'],
|
||||||
|
['href' => 'mondplanetenbedeckungen.php', 'label' => 'Mond-Planeten-Bedeckungen'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
+81
-15
@@ -285,6 +285,7 @@ $planetDataJson = '[]';
|
|||||||
$moonDataJson = 'null';
|
$moonDataJson = 'null';
|
||||||
$monthChartCometsJson = '[]';
|
$monthChartCometsJson = '[]';
|
||||||
$moonPhaseEventsJson = '[]';
|
$moonPhaseEventsJson = '[]';
|
||||||
|
$monthLunarEclipsesJson = '{}';
|
||||||
$monthEventListJson = '[]';
|
$monthEventListJson = '[]';
|
||||||
$starDataErrorJson = 'null';
|
$starDataErrorJson = 'null';
|
||||||
$observationTipCalendarIcsJson = '""';
|
$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),
|
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['favorite_comet_good_visibility', 'favorite_comet_perihelion', 'favorite_comet_geocentric_minimum'], true),
|
||||||
4
|
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([
|
$cometIntro = $seededPick([
|
||||||
'Bei den Kometen sind in diesem Monat besonders',
|
'Auch bei den Kometen gibt es in diesem Monat einiges zu entdecken.',
|
||||||
'Auch bei den Kometen lohnt sich in diesem Monat der Blick auf',
|
'Kometenfreunde kommen in diesem Monat ebenfalls auf ihre Kosten.',
|
||||||
'Unter den Kometen fallen in diesem Monat vor allem',
|
'Unter den Kometen gibt es in diesem Monat lohnende Ziele.',
|
||||||
'Kometenfreunde können sich in diesem Monat besonders für',
|
'Auch der Blick auf die Kometen lohnt sich in diesem Monat.',
|
||||||
], 'cometIntro', 'Bei den Kometen sind in diesem Monat besonders');
|
], 'cometIntro', 'Auch bei den Kometen gibt es in diesem Monat einiges zu entdecken.');
|
||||||
$cometOutro = $seededPick([
|
$addParagraph($cometIntro . ' ' . implode(' ', $cometSentences));
|
||||||
'interessant.',
|
|
||||||
'beobachtenswert.',
|
|
||||||
'bemerkenswert.',
|
|
||||||
'einen genaueren Blick wert.',
|
|
||||||
], 'cometOutro', 'interessant.');
|
|
||||||
$addParagraph($cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$eclipseEvents = $findEvents(
|
$eclipseEvents = $findEvents(
|
||||||
@@ -2992,6 +3004,7 @@ foreach ($favoriteCometEvents as $favoriteCometEvent) {
|
|||||||
'end_local_iso' => (string) ($favoriteCometEvent['end_local_iso'] ?? ''),
|
'end_local_iso' => (string) ($favoriteCometEvent['end_local_iso'] ?? ''),
|
||||||
'duration_minutes' => isset($favoriteCometEvent['duration_minutes']) ? (int) $favoriteCometEvent['duration_minutes'] : null,
|
'duration_minutes' => isset($favoriteCometEvent['duration_minutes']) ? (int) $favoriteCometEvent['duration_minutes'] : null,
|
||||||
'type' => $favoriteCometType,
|
'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);
|
$monthChartCometsJson = json_encode($monthChartComets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
$moonPhaseEventsJson = json_encode($moonPhaseEvents, 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);
|
$starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
$observationTipCalendarIcsJson = json_encode($observationTipCalendarIcs, 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);
|
$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_MOON = <?= $moonDataJson ?: 'null' ?>;
|
||||||
const MONTH_CHART_COMETS = <?= $monthChartCometsJson ?: '[]' ?>;
|
const MONTH_CHART_COMETS = <?= $monthChartCometsJson ?: '[]' ?>;
|
||||||
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
||||||
|
const MONTH_LUNAR_ECLIPSES = <?= $monthLunarEclipsesJson ?: '{}' ?>;
|
||||||
const MONTH_EVENT_LIST = <?= $monthEventListJson ?: '[]' ?>;
|
const MONTH_EVENT_LIST = <?= $monthEventListJson ?: '[]' ?>;
|
||||||
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
||||||
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
|
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
|
||||||
@@ -4355,7 +4393,7 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
renderPhaseCanvas(cssWidth, cssHeight, dpr, true);
|
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 phaseRad = phaseInfo.phaseAngleDeg * Math.PI / 180;
|
||||||
const cosPhase = Math.cos(phaseRad);
|
const cosPhase = Math.cos(phaseRad);
|
||||||
|
|
||||||
@@ -4408,6 +4446,33 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
ctx.drawImage(phaseImage, x - radius, y - radius, radius * 2, radius * 2);
|
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();
|
||||||
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 radius = Math.max(23 * layoutScale, Math.min(33 * layoutScale, spacing * 0.52));
|
||||||
const x = leftPad + spacing * (dayInRow - 0.5);
|
const x = leftPad + spacing * (dayInRow - 0.5);
|
||||||
const centerY = rowCenters[row];
|
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));
|
ctx.fillText(String(day), x, centerY + radius + (12 * layoutScale));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+286
-53
@@ -225,7 +225,51 @@ try {
|
|||||||
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="moon-visual">
|
<div class="moon-visual">
|
||||||
@@ -339,6 +383,17 @@ const moonExportTransparentCheckbox = document.getElementById('moonExportTranspa
|
|||||||
const moonExportStatus = document.getElementById('moonExportStatus');
|
const moonExportStatus = document.getElementById('moonExportStatus');
|
||||||
const moonExportCancelBtn = document.getElementById('moonExportCancelBtn');
|
const moonExportCancelBtn = document.getElementById('moonExportCancelBtn');
|
||||||
const moonExportConfirmBtn = document.getElementById('moonExportConfirmBtn');
|
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 moonSceneState = null;
|
||||||
let moonZoom = 1;
|
let moonZoom = 1;
|
||||||
let moonPanX = 0;
|
let moonPanX = 0;
|
||||||
@@ -379,6 +434,55 @@ const MOON_DISPLACEMENT_SCALE = 0.0095;
|
|||||||
const MOON_DISPLACEMENT_BIAS = -0.1;
|
const MOON_DISPLACEMENT_BIAS = -0.1;
|
||||||
const MOON_BUMP_SCALE = 2.0;
|
const MOON_BUMP_SCALE = 2.0;
|
||||||
const MOON_CAMERA_BASE_Z = 6.8;
|
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_MIN = 0.2;
|
||||||
const MOON_ZOOM_MAX = 4.1;
|
const MOON_ZOOM_MAX = 4.1;
|
||||||
const MOON_PAN_LIMIT = 1.8;
|
const MOON_PAN_LIMIT = 1.8;
|
||||||
@@ -492,18 +596,47 @@ function applyMoonZoom() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = MOON_CAMERA_BASE_Z / getMoonZoomFactor();
|
|
||||||
|
|
||||||
if (freeCameraMode) {
|
if (freeCameraMode) {
|
||||||
|
const r = MOON_CAMERA_BASE_Z / getMoonZoomFactor();
|
||||||
moonSceneState.camera.position.set(
|
moonSceneState.camera.position.set(
|
||||||
r * Math.sin(cameraOrbitPhi) * Math.sin(cameraOrbitTheta),
|
r * Math.sin(cameraOrbitPhi) * Math.sin(cameraOrbitTheta),
|
||||||
r * Math.cos(cameraOrbitPhi),
|
r * Math.cos(cameraOrbitPhi),
|
||||||
r * Math.sin(cameraOrbitPhi) * Math.cos(cameraOrbitTheta)
|
r * Math.sin(cameraOrbitPhi) * Math.cos(cameraOrbitTheta)
|
||||||
);
|
);
|
||||||
moonSceneState.camera.lookAt(0, 0, 0);
|
moonSceneState.camera.lookAt(0, 0, 0);
|
||||||
|
moonSceneState.camera.zoom = 1;
|
||||||
|
moonSceneState.camera.clearViewOffset();
|
||||||
} else {
|
} else {
|
||||||
moonSceneState.camera.position.set(moonPanX, moonPanY, r);
|
// Kamera bleibt IMMER exakt auf den Mond ausgerichtet UND auf fester
|
||||||
moonSceneState.camera.lookAt(moonPanX, moonPanY, 0);
|
// 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();
|
moonSceneState.camera.updateProjectionMatrix();
|
||||||
@@ -838,24 +971,29 @@ async function buildMoonSceneFromUrls(url1, url2) {
|
|||||||
powerPreference: 'high-performance'
|
powerPreference: 'high-performance'
|
||||||
});
|
});
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
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.setClearColor(0x010102, 1);
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 3));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 3));
|
||||||
renderer.shadowMap.enabled = true;
|
renderer.shadowMap.enabled = true;
|
||||||
renderer.shadowMap.type = THREE.PCFShadowMap;
|
renderer.shadowMap.type = MOON_SHADOW_MAP_TYPE;
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
|
const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
|
||||||
camera.position.set(0, 0, MOON_CAMERA_BASE_Z);
|
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);
|
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.castShadow = true;
|
||||||
sunLight.shadow.mapSize.width = 4096;
|
sunLight.shadow.mapSize.width = 4096;
|
||||||
sunLight.shadow.mapSize.height = 4096;
|
sunLight.shadow.mapSize.height = 4096;
|
||||||
sunLight.shadow.bias = -0.00015;
|
sunLight.shadow.bias = MOON_SHADOW_BIAS;
|
||||||
sunLight.shadow.normalBias = 0.02;
|
sunLight.shadow.normalBias = MOON_SHADOW_NORMAL_BIAS;
|
||||||
sunLight.shadow.camera.near = 1;
|
sunLight.shadow.camera.near = 1;
|
||||||
sunLight.shadow.camera.far = 20;
|
sunLight.shadow.camera.far = 20;
|
||||||
sunLight.shadow.camera.left = -3;
|
sunLight.shadow.camera.left = -3;
|
||||||
@@ -864,7 +1002,7 @@ async function buildMoonSceneFromUrls(url1, url2) {
|
|||||||
sunLight.shadow.camera.bottom = -3;
|
sunLight.shadow.camera.bottom = -3;
|
||||||
scene.add(sunLight);
|
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);
|
rimLight.position.set(-2.5, 1.2, 3.2);
|
||||||
scene.add(rimLight);
|
scene.add(rimLight);
|
||||||
|
|
||||||
@@ -1003,44 +1141,34 @@ async function renderMoon3D(phaseData, libration) {
|
|||||||
|
|
||||||
function applyMoon3DRenderState(state, phaseData, libration) {
|
function applyMoon3DRenderState(state, phaseData, libration) {
|
||||||
state.moonMesh.rotation.order = 'ZYX';
|
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.x = toRadians(libration.latitude);
|
||||||
state.moonMesh.rotation.z = toRadians(libration.positionAngle);
|
state.moonMesh.rotation.z = toRadians(libration.positionAngle);
|
||||||
|
|
||||||
const viewAlignedSunZ = phaseData.illumination * 2 - 1;
|
// Sonnenrichtung direkt aus der Geometrie: derselbe lokale Vektor wie bei
|
||||||
const northOnScreen = new THREE.Vector3(0, 1, 0).applyEuler(state.moonMesh.rotation).normalize();
|
// Kratern (selenographische Laenge/Breite -> mondfester Einheitsvektor),
|
||||||
const planeNorth = new THREE.Vector2(northOnScreen.x, northOnScreen.y);
|
// gedreht mit derselben Mesh-Rotation. Exakt statt aus der Beleuchtungs-
|
||||||
const planeNorthLength = planeNorth.length();
|
// Prozentzahl zurueckgerechnet, und dadurch garantiert konsistent mit
|
||||||
const horizontalMagnitude = Math.sqrt(Math.max(0, 1 - viewAlignedSunZ * viewAlignedSunZ));
|
// Kraterlage und Textur. Haengt nur von state.moonMesh.rotation ab (Datum/
|
||||||
const subsolarNorthDot = Math.sin(toRadians(libration.subsolarLatitude));
|
// 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;
|
state.sunLight.position.copy(sunDirectionWorld.multiplyScalar(8));
|
||||||
let yComponent;
|
// ambientLight/rimLight sind konstant (siehe MOON_AMBIENT_LIGHT_INTENSITY /
|
||||||
|
// MOON_RIM_LIGHT_INTENSITY oben) und werden schon bei der Szenen-Erstellung
|
||||||
if (planeNorthLength < 1e-6 || horizontalMagnitude < 1e-6) {
|
// gesetzt - hier bewusst kein erneutes Ueberschreiben mehr, damit beide
|
||||||
xComponent = phaseData.waxing ? horizontalMagnitude : -horizontalMagnitude;
|
// Stellen nicht auseinanderlaufen koennen.
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawMoonTexture(ctx, cx, cy, radius) {
|
function drawMoonTexture(ctx, cx, cy, radius) {
|
||||||
@@ -1191,10 +1319,12 @@ async function renderMoon3DToBlob(phaseData, libration, size, transparentBackgro
|
|||||||
powerPreference: 'high-performance'
|
powerPreference: 'high-performance'
|
||||||
});
|
});
|
||||||
exportRenderer.outputColorSpace = THREE.SRGBColorSpace;
|
exportRenderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
exportRenderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
exportRenderer.toneMappingExposure = MOON_TONE_MAPPING_EXPOSURE;
|
||||||
exportRenderer.setClearColor(0x010102, transparentBackground ? 0 : 1);
|
exportRenderer.setClearColor(0x010102, transparentBackground ? 0 : 1);
|
||||||
exportRenderer.setPixelRatio(1);
|
exportRenderer.setPixelRatio(1);
|
||||||
exportRenderer.shadowMap.enabled = true;
|
exportRenderer.shadowMap.enabled = true;
|
||||||
exportRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
exportRenderer.shadowMap.type = MOON_SHADOW_MAP_TYPE;
|
||||||
exportRenderer.setSize(size, size, false);
|
exportRenderer.setSize(size, size, false);
|
||||||
|
|
||||||
const previousAspect = state.camera.aspect;
|
const previousAspect = state.camera.aspect;
|
||||||
@@ -1318,19 +1448,32 @@ function normalizeSignedLongitude(degrees) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function craterToMoonLocalVector(crater) {
|
// Einheitsvektor im mondfesten (lokalen, noch unrotierten) Koordinatensystem
|
||||||
const latRad = toRadians(Number(crater.lat || 0) + craterLatitudeOffsetDeg);
|
// aus selenographischer Laenge/Breite (Ost-positiv, wie von api.py geliefert).
|
||||||
const lonRad = toRadians(-normalizeSignedLongitude(Number(crater.lon || 0) + craterLongitudeOffsetDeg));
|
// 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 cosLat = Math.cos(latRad);
|
||||||
const craterRadius = MOON_SPHERE_RADIUS * craterOverlayScale;
|
|
||||||
|
|
||||||
return new THREE.Vector3(
|
return new THREE.Vector3(
|
||||||
craterRadius * cosLat * Math.cos(lonRad),
|
cosLat * Math.cos(lonRad),
|
||||||
craterRadius * Math.sin(latRad),
|
Math.sin(latRad),
|
||||||
craterRadius * cosLat * Math.sin(lonRad)
|
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') {
|
function getCraterVisualStyle(crater, mode = 'all') {
|
||||||
if (mode === 'terminator') {
|
if (mode === 'terminator') {
|
||||||
return {
|
return {
|
||||||
@@ -1695,6 +1838,96 @@ timeSlider.addEventListener('wheel', event => {
|
|||||||
updateMoon();
|
updateMoon();
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { 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));
|
prevDayBtn.addEventListener('click', () => shiftDateByDays(-1));
|
||||||
nextDayBtn.addEventListener('click', () => shiftDateByDays(1));
|
nextDayBtn.addEventListener('click', () => shiftDateByDays(1));
|
||||||
nowBtn.addEventListener('click', () => {
|
nowBtn.addEventListener('click', () => {
|
||||||
|
|||||||
+238
-14
@@ -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]
|
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]:
|
def vector_normalize(v: tuple[float, float, float]) -> tuple[float, float, float]:
|
||||||
length = math.sqrt(vector_dot(v, v))
|
length = math.sqrt(vector_dot(v, v))
|
||||||
if length <= 0:
|
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
|
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:
|
def calculate_moon_position_angle(time_value: astronomy.Time) -> float:
|
||||||
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
|
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
|
||||||
moon_equator = astronomy.EquatorFromVector(moon_vector)
|
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)
|
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)
|
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
|
||||||
sun_vector = astronomy.GeoVector(astronomy.Body.Sun, 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)
|
moon_xyz = vector_from_astronomy(moon_vector)
|
||||||
sun_xyz = vector_from_astronomy(sun_vector)
|
sun_xyz = vector_from_astronomy(sun_vector)
|
||||||
|
|
||||||
earth_from_moon = vector_normalize((-moon_xyz[0], -moon_xyz[1], -moon_xyz[2]))
|
earth_from_moon = (-moon_xyz[0], -moon_xyz[1], -moon_xyz[2])
|
||||||
sun_from_moon = vector_normalize(vector_subtract(sun_xyz, moon_xyz))
|
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 {
|
return {
|
||||||
"subearth_latitude": math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(earth_from_moon, north))))),
|
"subearth_longitude": subearth_longitude,
|
||||||
"subsolar_latitude": math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(sun_from_moon, north))))),
|
"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
|
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)
|
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)
|
position_angle = calculate_moon_position_angle(time_value)
|
||||||
local_dt = dt_utc.astimezone(tz)
|
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),
|
"label": get_phase_label(age_days),
|
||||||
},
|
},
|
||||||
"libration": {
|
"libration": {
|
||||||
"longitude": -normalize_signed_degrees(float(libration.elon)),
|
"longitude": float(orientation["subearth_longitude"]),
|
||||||
"latitude": float(libration.elat),
|
"latitude": float(orientation["subearth_latitude"]),
|
||||||
"subsolarLatitude": float(axis_latitudes["subsolar_latitude"]),
|
"subsolarLongitude": float(orientation["subsolar_longitude"]),
|
||||||
"subearthLatitude": float(axis_latitudes["subearth_latitude"]),
|
"subsolarLatitude": float(orientation["subsolar_latitude"]),
|
||||||
|
"subearthLatitude": float(orientation["subearth_latitude"]),
|
||||||
"positionAngle": float(position_angle),
|
"positionAngle": float(position_angle),
|
||||||
"distKm": float(libration.dist_km),
|
"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 ──────────────────────────────────────────────────────────
|
# ── satellite_passes ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
import math as _math
|
import math as _math
|
||||||
@@ -6344,7 +6563,7 @@ def action_barycenter(args: list[str]) -> dict:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if len(sys.argv) < 2:
|
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]
|
action = sys.argv[1]
|
||||||
args = sys.argv[2:]
|
args = sys.argv[2:]
|
||||||
@@ -6534,6 +6753,11 @@ def main() -> None:
|
|||||||
print(json.dumps(result, ensure_ascii=True))
|
print(json.dumps(result, ensure_ascii=True))
|
||||||
return
|
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":
|
if action == "lunar_eclipses_for_year":
|
||||||
result = action_lunar_eclipses_for_year(args)
|
result = action_lunar_eclipses_for_year(args)
|
||||||
print(json.dumps(result, ensure_ascii=True))
|
print(json.dumps(result, ensure_ascii=True))
|
||||||
@@ -6544,7 +6768,7 @@ def main() -> None:
|
|||||||
print(json.dumps(result, ensure_ascii=True))
|
print(json.dumps(result, ensure_ascii=True))
|
||||||
return
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user