Compare commits

...
3 Commits
Author SHA1 Message Date
EskimueandClaude Sonnet 5 2396cac6db Monatsvorhersage-Fließtext überarbeiten: Struktur, Grammatik, viertes Textset
Textbaustein-System für die Monatsübersicht deutlich robuster und
natürlicher gemacht:

- Zwischenüberschriften ("Der Mond im ...", "Die Planeten im ...") durch
  strukturierte Absatz-Rückgabe statt reinem String; Live-Seite und
  HTML-/Joomla-Export rendern jetzt korrekt h3 -> h4 -> p.
- Deterministische statt zufällige Textbaustein-Auswahl (seeded statt
  random_int), damit derselbe Monat/Jahr bei erneutem Aufruf denselben
  Text liefert.
- Übergangsphrasen zwischen Ereignissätzen ("Anschließend", "Kurz
  darauf" etc.) berücksichtigen jetzt den tatsächlichen Tagesabstand
  und wiederholen sich nie zwei Mal hintereinander; Ereignisse am
  selben Tag werden zu einem Satz zusammengefasst statt das Datum
  doppelt zu nennen.
- Bugfix: Merkur-Elongationen und Venus-Konjunktionen landeten wegen
  eines Typo im Typvergleich im generischen Fallback-Satz.
- Venus korrekt feminin dekliniert (sie/ihre) statt durchgängig "er".
- Morgen-/Abenddämmerung wird bei Merkur-Sichtbarkeit und Venus-
  Helligkeit jetzt benannt (Venus-Sichtbarkeitsrichtung neu in
  api.py berechnet).
- Meteorstrom- und Bedeckungs-Label für Fließtext aufbereitet statt
  Rohdaten-Format ("Maximum X - CODE - ZHR ca. Y", "- Größte
  Bedeckung") direkt auszugeben.
- Aufhänger-Satz: Priorität um Mondbedeckungen erweitert, Planeten-
  parade entfernt; nur im Export fett hervorgehoben.
- Viertes Sky-Intro-Textset (V4) ergänzt, Rotation von year%3 auf
  year%4 umgestellt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 09:27:05 +02:00
EskimueandClaude Sonnet 5 39a0882291 Bezugssystem-Fehler in Skyfield-Ephemeridenrechnung beheben
apparent.radec(epoch=timescale.J2000) lieferte faelschlich das wahre
Aequinoktium/Aequator von J2000.0 statt ICRF-Koordinaten und rechnete
damit die Nutation von J2000.0 (~14,5") in die Merkur/Planeten-Ausgabe
ein, obwohl die CSV-Spalten "RA (J2000)"/"Dek (J2000)" ICRF-Koordinaten
meinen sollen. radec() ohne epoch-Argument liefert das korrekte
Bezugssystem.

Regressionstest ergaenzt, der bei erneutem epoch=-Argument fehlschlaegt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 19:45:35 +02:00
Eskimue 0357d957c0 Ephemeriden mit Skyfield optimieren und dokumentieren 2026-09-12 19:35:15 +02:00
6 changed files with 717 additions and 104 deletions
+79
View File
@@ -0,0 +1,79 @@
# Berechnungsgrundlagen
Stand: 12.09.2026
Diese Datei dokumentiert, welche Berechnungsbibliothek beziehungsweise welche
Datenquelle die einzelnen Seiten verwenden.
## Die drei wichtigsten Grundlagen
### Astronomy Engine
Die lokale Python-Version liegt in `public/py/astronomy.py`. Im Browser wird
`public/js/astronomy.browser.min.js` verwendet.
Astronomy Engine liefert auf vielen Seiten die Positionen von Sonne, Mond und
Planeten sowie Ereigniszeiten wie Mondphasen, Finsternisse, Bedeckungen und
Konjunktionen.
### Skyfield mit `de421.bsp`
Skyfield liegt lokal in `public/py/vendor` beziehungsweise
`public/py/vendor_linux`. Die verwendete JPL-Ephemeride ist:
`public/py/vendor_data/de421.bsp`
Die zentrale Einrichtung erfolgt in `public/py/astronomical_conversions.py`.
`de421.bsp` wird derzeit für die Planet-Ephemeriden in `ephemeriden_api.py`
und für die Dämmerungsgrafik verwendet.
Die Datei enthält Jupiter bis Neptun als Systemschwerpunkte. Die lokale
`jup365`-Datei wird von den hier dokumentierten Seiten derzeit nicht verwendet.
### Weitere Berechnungen
Nicht jede Berechnung ist Skyfield oder Astronomy Engine:
- Satellitenbahnen werden mit SGP4 aus TLE-Daten berechnet.
- Kometen und Kleinplaneten werden derzeit aus ihren Bahnelementen mit eigenen
Kepler-Berechnungen bestimmt.
## Seitenübersicht
| Seite | Berechnungsgrundlage |
|---|---|
| `ephemeriden.php` | **Skyfield + `de421.bsp`** für Sonne, Mond und Planeten. Auf- und Untergang sowie Horizontkoordinaten werden ebenfalls mit Skyfield berechnet. Kometen und Kleinplaneten bleiben bei den eigenen Bahnelement-Berechnungen. Die interaktive Karte nutzt im Browser zusätzlich Astronomy Engine für Horizontberechnungen und Sterne. |
| `monatsvorhersage.php` | Überwiegend **Astronomy Engine** über `public/py/api.py`. Satellitenereignisse verwenden SGP4. Die Dämmerungsgrafik verwendet Skyfield + `de421.bsp`. Jupiter- und Jupitermond-Ereignisse verwenden aktuell Astronomy Engine. |
| `wochenvorhersage.php` | **Astronomy Engine** über `public/py/api.py`; je nach Bereich zusätzlich SGP4 für Satelliten. |
| `jupitersystem.php` | **Astronomy Engine**. Die Darstellung im Browser und die Galileischen Monde werden über Astronomy Engine berechnet. `jup365.bsp` wird hier aktuell nicht geladen. |
| `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. |
| `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. |
| `drehbare_sternkarte.php` | **Astronomy Engine** im Browser für Horizont- und Koordinatenberechnungen. |
| `sternenhimmel.php` | **Astronomy Engine** im Browser beziehungsweise über die lokale API. |
| `tagbogen.php` | **Astronomy Engine** für Sonnen-, Mond- und Planetenbahnen. |
| `solarsystem.php` | **Astronomy Engine** im Browser für das Sonnensystemmodell. |
| `geocron3d.php` | **Astronomy Engine** im Browser. |
| `geocron/index.php` | **Astronomy Engine** über das Geocron-Plugin. |
## Zentrale Python-Dateien
| Datei | Aufgabe |
|---|---|
| `public/py/api.py` | Allgemeine astronomische Ereignisse; überwiegend Astronomy Engine. Einzelne Dämmerungsberechnungen verwenden Skyfield. |
| `public/py/ephemeriden_api.py` | Planet-Ephemeriden mit Skyfield + `de421.bsp`; Kleinplaneten und Kometen mit eigenen Bahnelement-Berechnungen. |
| `public/py/astronomical_conversions.py` | Gemeinsame Skyfield-Konfiguration und Zugriff auf `de421.bsp`. |
| `public/py/astronomy.py` | Lokale Python-Version der Astronomy Engine. |
## Standort und Lichtlaufzeit
Wo eine Seite einen Standort benötigt, wird grundsätzlich der persönliche
Standardstandort des angemeldeten Benutzers verwendet. Fehlt dieser, greift
der konfigurierte Fallback-Standort.
Die Verwendung von Skyfield oder Astronomy Engine sagt allein noch nicht, ob
eine Berechnung topozentrisch, geozentrisch oder mit Lichtlaufzeit erfolgt.
Das ist je Ereignis in der jeweiligen Funktion festgelegt.
+1
View File
@@ -819,6 +819,7 @@ require __DIR__ . '/header.php';
<input type="hidden" name="range_value" value="<?= ephemeriden_h($rangeValueInput) ?>">
<input type="hidden" name="range_unit" value="<?= ephemeriden_h($selectedRangeUnit) ?>">
<input type="hidden" name="start_dt" value="<?= ephemeriden_h($startDtInput) ?>">
<input type="hidden" name="end_dt" value="<?= ephemeriden_h($endDtInput) ?>">
<input type="hidden" name="export_format" value="csv">
<button type="submit" class="btn btn-secondary">Export CSV</button>
</form>
+479 -89
View File
@@ -269,7 +269,7 @@ $seasonChanges = [];
$timeChanges = [];
$monthEventList = [];
$starDataError = null;
$monthNarrativeText = '';
$monthNarrativeText = [];
$observationTipInsertSql = '';
$observationTipCalendarIcs = '';
$observationTipInsertPayloadJson = '[]';
@@ -761,7 +761,15 @@ if (!function_exists('buildObservationTipCalendarIcs')) {
}
if (!function_exists('monthForecastBuildNarrativeText')) {
function monthForecastBuildNarrativeText(array $monthEventList, int $monthNumber, string $monthName, int $year): string
/**
* @return list<array{heading: ?string, text: string, emphasize?: bool}> Abschnitte des
* Monatstexts; `heading` ist gesetzt, wenn der Abschnitt eine eigene
* Zwischenueberschrift beginnt (z. B. "Der Mond im September 2026"), sonst null.
* `emphasize` markiert den kurzen Aufhaenger-Satz am Anfang (falls vorhanden) -
* wird auf der Seite selbst nicht hervorgehoben, nur beim HTML-/Joomla-Export
* als <strong> ausgegeben.
*/
function monthForecastBuildNarrativeText(array $monthEventList, int $monthNumber, string $monthName, int $year): array
{
$filteredEvents = array_values(array_filter(
$monthEventList,
@@ -814,20 +822,47 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
12 => "Lange Nächte und ein glanzvoller Winterhimmel – das ist der Dezember. Die Sommersternbilder ziehen sich in den frühen Abendstunden zurück; Orion, Stier, Zwillinge und Fuhrmann haben längst ihren Platz eingenommen. Sirius erscheint früh über dem Südosthorizont. In der Neujahrsnacht erreicht er gegen Mitternacht seinen Tageshöchstpunkt und steht dann von Deutschland aus fast genau im Süden.\n\nIm Südwesten verbleiben Reste des Herbsthimmels: Wassermann, Fische und Walfisch unterhalb des Herbstvierecks, lichtschwach und weitläufig. Zwischen Walfisch und Orion windet sich der Eridanus – seine Sterne sind unauffällig, doch sein geschwungener Verlauf lädt bei klarer Nacht zu einer kleinen Himmelsreise ein.",
];
$skyIntroByMonth = match ($year % 3) {
$skyIntroByMonthV4 = [
1 => "Hoch im Süden dominiert im Januar eine Sternfigur, die selbst Laien sofort wiedererkennen: der Orion mit seinen sieben hellen Hauptsternen. Die drei Gürtelsterne in der Mitte sind sein Markenzeichen; direkt darunter lässt sich schon im Fernglas der neblige Schleier des Orionnebels aufspüren. Die rechte untere Ecke der Figur markiert Rigel, einer der hellsten Sterne des gesamten Himmels.\n\nRigel ist gleichzeitig Eckpunkt einer noch größeren Figur: des Wintersechsecks. Im Uhrzeigersinn reihen sich daran Sirius im Großen Hund, Prokyon im Kleinen Hund, Castor in den Zwillingen, Capella im Fuhrmann und Aldebaran im Stier. Durch genau diesen Bereich zieht sich auch die Wintermilchstraße, von Großem Hund über Einhorn, Zwillinge, Stier, Fuhrmann und Perseus bis zur Kassiopeia – nur ist sie deutlich blasser als ihr Sommerpendant, weil unser Blick jetzt eher zum Rand der Galaxis geht.",
2 => "Selten zeigt sich der Winterhimmel so sternenreich wie an einem klaren Februarabend. Um 22 Uhr, auf diese Zeit bezieht sich die Sternkarte, haben die Sternbilder rund um den Orion den Meridian bereits überquert und ziehen langsam nach Westen. Unter dem Orion funkelt Sirius, der hellste Fixstern des Nachthimmels überhaupt. Rechts oben leuchtet der Stier mit Aldebaran, darüber der Fuhrmann mit der gelblichen Capella, links die Zwillinge mit Castor und Pollux; unterhalb von ihnen sitzt Prokyon, der Hauptstern des Kleinen Hundes.\n\nIm Osten kündigt sich derweil schon der Frühling an: Der Löwe mit Regulus ist komplett aufgegangen, und tief darunter blitzt Alphard als einsamer heller Punkt in der Wasserschlange. Bis die Jungfrau vollständig über dem Horizont steht, dauert es dagegen noch bis nach Mitternacht.",
3 => "Im März treffen zwei Jahreszeiten am Himmel aufeinander. Am frühen Abend sind die Wintersternbilder noch da – Orion, Großer Hund, Kleiner Hund, Zwillinge, Fuhrmann, Stier –, doch im Lauf der Nacht wandern sie unaufhaltsam Richtung Westen, während im Osten schon die Frühlingssternbilder nachrücken. Zur Mitternachtsstunde gehört der Himmel bereits den Sternbildern rund um den Löwen.\n\nZwischen den Zwillingen und dem Löwen duckt sich der unscheinbare Krebs – wer ihn unter dunklem Himmel aufsucht, wird mit dem offenen Sternhaufen Praesepe belohnt, liebevoll „Krippe\" genannt und als milchiger Fleck erkennbar. Fast im Zenit steht jetzt der Große Wagen, so hoch wie kaum sonst im Jahr. Im Südosten geht Spica auf, im Nordosten leuchtet der orangefarbene Arktur. Tief am Südhimmel reckt sich die Wasserschlange empor, meist nur zu erahnen am einsamen Alphard, dessen Name „der Einsame\" bezeichnenderweise passt. Und ganz knapp über dem Horizont wartet noch der kompakte Rabe.",
4 => "Drei Sterne prägen den Aprilhimmel wie ein Wegweiser: Regulus im Löwen, Spica in der Jungfrau und der orangefarbene Arktur im Bärenhüter – gemeinsam bilden sie das Frühlingsdreieck. Mittendrin steht der Löwe hoch im Süden, dessen Sternfigur bei etwas Fantasie tatsächlich an eine liegende Raubkatze erinnert.\n\nFast senkrecht über uns steht jetzt der Große Wagen. Zwischen ihm und dem Löwen liegen nur unscheinbare Sternbilder – Kleiner Löwe, Jagdhunde, Haar der Berenike –, ebenso sternarm wie der Streifen zwischen Löwe und Horizont, wo sich die langgestreckte Wasserschlange entlangzieht. Etwas leichter zu finden sind dagegen Becher und Rabe, deren kompakte Formen trotz mäßiger Helligkeit auffallen.",
5 => "Wenn ein Stern den Maiabend beherrscht, dann Arktur: orange, hell und hoch im Süden, als Hauptstern des Bärenhüters kaum zu übersehen. Tiefer im Süden schimmert Spica in der Jungfrau, im Südwesten hält Regulus im Löwen die Stellung – zusammen spannen die drei Sterne das Frühlingsdreieck auf. Etwas blasser liegt darüber das Sternbild Haar der Berenike.\n\nAm Südosthorizont deutet sich mit dem tief stehenden Skorpion und seinem Hauptstern Antares bereits der Sommer an. Im Westen sinken die letzten Wintersterne, während im Osten schon Wega in der Leier aufsteigt. Zwischen Wega und Arktur liegen das eher blasse Herkules und der elegante Halbkreis der Nördlichen Krone.",
6 => "Der Skorpion bleibt im Juni ein Horizontgast: Sein rötlicher Hauptstern Antares zeigt sich zwar tief im Südosten, steigt von unseren Breiten aus aber nie besonders hoch. Weit darüber thront der Große Wagen, das auffälligste Stück des Großen Bären – seine geschwungene Deichsel weist zuverlässig erst zu Arktur, dann weiter zu Spica. Im Südwesten neigt sich der Löwe bereits dem Untergang zu.\n\nGleich links über Arktur öffnet sich ein zarter Halbkreis: die Nördliche Krone, deren hellster Stern Gemma zu Recht „Edelstein\" heißt. Darunter breiten sich Schlangenträger und die zweigeteilte Schlange aus – lichtschwach, aber unter dunklem Himmel gut zu verfolgen. Den eigentlichen Höhepunkt des Juniabends liefert jedoch der Osten: Dort erhebt sich das Sommerdreieck aus Wega in der Leier, Deneb im Schwan und Altair im Adler – ein Trio, das den Himmel die kommenden Monate begleiten wird.",
7 => "Wirklich hoch steigen Skorpion und Schütze im Juli nur für Beobachter im Süden Europas – von Deutschland aus bleiben beide Tierkreissternbilder flach über dem Südhorizont hängen. Darüber reihen sich Schlangenträger, Schlange und Herkules, allesamt eher unauffällig, aber typisch für den sommerlichen Sternhimmel.\n\nDeutlich mehr Aufmerksamkeit verdienen Arktur, Wega, Deneb und Altair, die vier hellen Fixpunkte dieser Jahreszeit. Wega, Deneb und Altair formen dabei das Sommerdreieck. Zwar zieht sich auch die Milchstraße schon als Band über den Himmel, bleibt in den noch nicht ganz dunklen Julinächten aber oft blass – ihre beste Zeit kommt erst im August. Der Große Bär ist inzwischen weiter in den Nordwesten gewandert.",
8 => "Kaum ein Monat lohnt sich für Sterngucker mehr als der August. Nach 22 Uhr wird es endlich wieder richtig dunkel, und ein Stern nach dem anderen erscheint am Himmel. Um 23 Uhr hat sich das Sommerdreieck bereits im Zenit eingerichtet: Wega fast senkrecht über uns, Deneb im Schwan links davon, Altair im Adler deutlich tiefer – ein unverwechselbares Muster.\n\nMit den länger werdenden Dunkelphasen kehrt auch die Milchstraße zurück: Als schimmerndes Band zieht sie mitten durch das Sommerdreieck, am schönsten fernab jeder Lichtquelle. Im Osten deuten Pegasus und Andromeda schon den nahenden Herbst an. Und um den 12. August erreicht der Meteorstrom der Perseiden sein Maximum – Staubkörner des Kometen Swift-Tuttle verglühen dann zahlreich als Sternschnuppen in der Atmosphäre.",
9 => "Der September bringt die Dunkelheit zurück, und mit ihr wieder richtige Sternennächte. Gegen 21.30 Uhr ist der Himmel schon gut gefüllt, allen voran mit Wega, die hoch über uns als Erste ins Auge fällt. Wer genauer hinschaut, entdeckt um sie herum die kleine Raute der Leier. Zusammen mit Altair im Adler und Deneb im Schwan bildet Wega weiterhin das große Sommerdreieck, das jetzt schon bis in den Herbst hineinreicht.\n\nVersteckt im und am Rand des Sommerdreiecks liegen zwei kleine, aber einprägsame Sternbilder: Pfeil und Delfin – nicht hell, dafür durch ihre kompakte Form überraschend leicht zu finden. Auch die Milchstraße zeigt sich wieder deutlicher als fahles Band. Während im Westen die letzten Sommersternbilder verschwinden, tauchen im Osten die ersten Herbstgestalten auf – dazwischen liegt ein eher sternarmer Himmelsabschnitt.",
10 => "Seit die Sonne Ende September den Himmelsäquator nach Süden überquert hat, sind die Nächte länger als die Tage – ein untrügliches Zeichen für den Herbst. Am frühen Oktoberabend hält sich noch das vertraute Sommerdreieck aus Wega, Deneb und Altair, doch im Lauf der Nacht übernehmen zunehmend die Herbststernbilder das Kommando.\n\nGegen 23 Uhr Sommerzeit steht das große Herbstviereck hoch am Himmel: drei seiner Ecken gehören zu Pegasus, die vierte zur Sternenkette der Andromeda. Darunter liegen die eher lichtschwachen Tierkreissternbilder Steinbock, Wassermann und Fische, die nur unter dunklem Himmel richtig zur Geltung kommen. Diphda im Walfisch gehört zu den wenigen helleren Sternen dieser Himmelsregion, und tief im Süden lässt sich zudem Fomalhaut im Südlichen Fisch erspähen.",
11 => "Im November gehört die Himmelsbühne dem Herbstviereck – auch Pegasus-Quadrat genannt, mit drei Ecken aus dem Sternbild Pegasus und einer aus der Andromeda-Kette. Genau in der Andromeda lohnt sich ein genauerer Blick: Dort liegt die Andromedagalaxie, mit bloßem Auge das mit Abstand fernste sichtbare Objekt – unter dunklem Himmel als zarter, nebliger Fleck erkennbar.\n\nRundherum, im Südwesten und Süden, ziehen sich die eher blassen Sternbilder Wassermann, Walfisch und Fische entlang. Im Walfisch versteckt sich zudem Mira, einer der bekanntesten veränderlichen Sterne überhaupt. Im Osten dagegen kündigt sich bereits der Winter an: Die Kassiopeia zeigt ihr markantes W, Perseus steigt mit dem veränderlichen Algol empor, und im Stier fallen die Plejaden und der rötliche Aldebaran auf. Ganz am östlichen Horizont lässt sich schließlich schon Orion blicken.",
12 => "Der Dezember zeigt den Winterhimmel in seiner vollen Pracht, und das schon früh am Abend. Die Sommersternbilder Schwan, Leier und Adler sind längst verschwunden; Orion, Stier, Zwillinge und Fuhrmann haben ihren festen Platz eingenommen. Auch Sirius, der hellste Stern des gesamten Nachthimmels, taucht schon früh über dem Südosthorizont auf. Ein besonderer Moment: In der Silvesternacht erreicht er gegen Mitternacht seinen höchsten Stand und steht dann von Deutschland aus fast exakt im Süden.\n\nIm Südwesten hält sich noch ein letzter Rest des Herbsthimmels – Wassermann, Fische und Walfisch, allesamt ausgedehnt, aber lichtschwach, unterhalb des Herbstvierecks. Zwischen Walfisch und Orion schlängelt sich außerdem der Eridanus: unauffällige Sterne, aber ein Verlauf, dem in einer klaren Nacht nachzuspüren sich lohnt.",
];
// Echte Abwechslung entsteht ueber die Rotation der vier Varianten-Sets
// (year % 4); pro Jahr gibt es genau einen Text je Monat.
$skyIntroByMonth = match ($year % 4) {
1 => $skyIntroByMonthV2,
2 => $skyIntroByMonthV3,
3 => $skyIntroByMonthV4,
default => $skyIntroByMonthV1,
};
foreach ($skyIntroByMonth as $introMonthNumber => $introText) {
$skyIntroByMonth[$introMonthNumber] = [$introText];
}
$paragraphs = [];
$monthIntroVariants = $skyIntroByMonth[$monthNumber] ?? [($monthName . ' ' . $year . ' bietet einen abwechslungsreichen Himmel zwischen den jahreszeitlichen Sternbildern.')];
$monthIntroIndex = (($year * 100) + $monthNumber) % count($monthIntroVariants);
$paragraphs[] = $monthIntroVariants[$monthIntroIndex];
// Fuegt einen Textabschnitt hinzu, optional mit eigener Zwischenueberschrift
// (z. B. "Der Mond im September 2026"), die beim Rendern als <h4> erscheint.
$addParagraph = static function (string $text, ?string $heading = null) use (&$paragraphs): void {
if (trim($text) === '') {
return;
}
$paragraphs[] = ['heading' => $heading, 'text' => $text];
};
$addParagraph(
$skyIntroByMonth[$monthNumber]
?? ($monthName . ' ' . $year . ' bietet einen abwechslungsreichen Himmel zwischen den jahreszeitlichen Sternbildern.')
);
// Die Planeten-Ueberschrift soll nur einmal erscheinen, auch wenn Beobachtungs-
// und Bewegungsereignisse als getrennte Absaetze ausgegeben werden.
$planetsHeadingUsed = false;
$findEvents = static function (array $events, callable $filter, int $limit = 3): array {
$matches = [];
@@ -842,7 +877,88 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
return $matches;
};
$describeEvents = static function (array $events): string {
// Deterministisch statt zufaellig: gleiche Formulierung bei jedem Aufruf
// fuer denselben Monat/Jahr (reproduzierbar bei erneutem Seitenaufruf),
// aber Abwechslung von Monat zu Monat und Jahr zu Jahr.
$seededPick = static function (array $options, string $seedKey, string $fallback = '') use ($year, $monthNumber): string {
$options = array_values(array_filter($options, static fn ($option): bool => trim((string) $option) !== ''));
if ($options === []) {
return $fallback;
}
$hash = crc32($year . '|' . $monthNumber . '|' . $seedKey);
return (string) $options[$hash % count($options)];
};
// Natuerliche Aufzaehlung "A, B und C" statt "A sowie B sowie C".
$naturalJoin = static function (array $parts, string $conjunction = 'und'): string {
$parts = array_values(array_filter($parts, static fn ($part): bool => trim((string) $part) !== ''));
if ($parts === []) {
return '';
}
if (count($parts) === 1) {
return $parts[0];
}
$last = array_pop($parts);
return implode(', ', $parts) . ' ' . $conjunction . ' ' . $last;
};
// Ermittelt den Tag im Monat aus "dd.mm." oder "dd.mm.yyyy", um den
// zeitlichen Abstand zweier Ereignisse abzuschaetzen.
$dayNumberOf = static function (string $dateStr): ?int {
return preg_match('/^(\d{1,2})\./', trim($dateStr), $m) ? (int) $m[1] : null;
};
// Variiert den Satzanfang ("Am {Datum} ..." bzw. "Ab dem {Datum} ...")
// bei aufeinanderfolgenden Ereignissen, damit nicht jeder Satz eines
// Absatzes gleich beginnt. index 0 bleibt immer unveraendert. Dieselbe
// Uebergangsphrase wird nie zweimal direkt hintereinander verwendet, und
// Phrasen wie "Nur wenige Tage spaeter" werden nur bei tatsaechlich
// kurzem Abstand angeboten (sonst waeren sie schlicht falsch).
$lastLeadInLabel = null;
$varyLeadIn = static function (string $sentence, string $originalPrefix, string $datePhraseLower, int $index, string $seedKey, ?int $dayGap = null) use ($seededPick, &$lastLeadInLabel): string {
if ($index === 0 || !str_starts_with($sentence, $originalPrefix)) {
return $sentence;
}
$rest = substr($sentence, strlen($originalPrefix));
$plain = $originalPrefix . $rest;
if ($dayGap !== null && $dayGap <= 4) {
$variantMap = [
'plain' => $plain,
'nurWenigeTage' => 'Nur wenige Tage später, ' . $datePhraseLower . ', ' . $rest,
'kurzDarauf' => 'Kurz darauf, ' . $datePhraseLower . ', ' . $rest,
'wenigSpaeter' => 'Wenig später, ' . $datePhraseLower . ', ' . $rest,
'anschliessend' => 'Anschließend, ' . $datePhraseLower . ', ' . $rest,
];
} elseif ($dayGap !== null && $dayGap <= 10) {
$variantMap = [
'plain' => $plain,
'wenigSpaeter' => 'Wenig später, ' . $datePhraseLower . ', ' . $rest,
'etwasSpaeter' => 'Etwas später, ' . $datePhraseLower . ', ' . $rest,
'anschliessend' => 'Anschließend, ' . $datePhraseLower . ', ' . $rest,
];
} else {
// Groesserer oder unbekannter Abstand: keine "bald danach"-Phrasen.
$variantMap = [
'plain' => $plain,
'spaeterImMonat' => 'Später im Monat, ' . $datePhraseLower . ', ' . $rest,
'weitererVerlauf' => 'Im weiteren Monatsverlauf, ' . $datePhraseLower . ', ' . $rest,
];
}
$labels = array_keys($variantMap);
if ($lastLeadInLabel !== null) {
$withoutLast = array_values(array_filter($labels, static fn (string $label): bool => $label !== $lastLeadInLabel));
if ($withoutLast !== []) {
$labels = $withoutLast;
}
}
$chosenLabel = $seededPick($labels, $seedKey . '#' . $index, $labels[0]);
$lastLeadInLabel = $chosenLabel;
return $variantMap[$chosenLabel];
};
$describeEvents = static function (array $events) use ($naturalJoin): string {
$parts = [];
foreach ($events as $event) {
$label = trim((string) ($event['event'] ?? ''));
@@ -852,20 +968,42 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
$parts[] = $label . ' am ' . $date;
}
return implode(', ', $parts);
return $naturalJoin($parts);
};
$pickRandomText = static function (array $options, string $fallback): string {
$options = array_values(array_filter($options, static fn ($option): bool => trim((string) $option) !== ''));
if ($options === []) {
return $fallback;
// Wandelt das Tabellen-Label "Maximum Draconiden - DRA - ZHR ca. 20-700"
// (fuer CSV/Tabelle/Kalender gedacht) in Fliesstext um: "das Maximum der
// Draconiden (DRA, ZHR ca. 20-700)". Das zugrunde liegende Datenformat
// bleibt dabei unveraendert.
$formatMeteorLabel = static function (string $label): string {
$segments = array_map('trim', explode(' - ', $label));
if ($segments === [] || !str_starts_with($segments[0], 'Maximum ')) {
return $label;
}
$designation = substr($segments[0], strlen('Maximum '));
$extra = implode(', ', array_slice($segments, 1));
$formatted = $designation === 'Meteorstrom'
? 'das Maximum des Meteorstroms'
: 'das Maximum der ' . $designation;
return $formatted . ($extra !== '' ? ' (' . $extra . ')' : '');
};
try {
return (string) $options[random_int(0, count($options) - 1)];
} catch (Throwable) {
return (string) $options[array_rand($options)];
$describeMeteorEvents = static function (array $events) use ($naturalJoin, $formatMeteorLabel): string {
$parts = [];
foreach ($events as $event) {
$label = trim((string) ($event['event'] ?? ''));
$date = trim((string) ($event['date'] ?? ''));
if ($label === '' || $date === '') {
continue;
}
$segments = array_map('trim', explode(' - ', $label));
if ($segments !== [] && str_starts_with($segments[0], 'Maximum ')) {
$parts[] = $formatMeteorLabel($label) . ' am ' . $date;
continue;
}
$parts[] = $label . ' am ' . $date;
}
return $naturalJoin($parts);
};
$buildSeasonNarrative = static function (array $event): ?string {
@@ -892,7 +1030,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
if ($seasonEvents !== []) {
$seasonNarrative = $buildSeasonNarrative($seasonEvents[0]);
if ($seasonNarrative !== null) {
$paragraphs[] = $seasonNarrative;
$addParagraph($seasonNarrative);
}
}
@@ -925,6 +1063,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
));
$moonSentences = [];
$moonEventIndex = 0;
// 1. Mondphasen in einem natürlichen Satz zusammenfassen
if ($phaseEvents !== []) {
@@ -942,7 +1081,8 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
if ($phaseParts !== []) {
$lastPhase = array_pop($phaseParts);
$phaseList = $phaseParts !== [] ? implode(', ', $phaseParts) . ' und ' . $lastPhase : $lastPhase;
$moonSentences[] = 'Die Mondphasen im Überblick: ' . $phaseList . '.';
// rtrim gegen doppelten Punkt, da shortDate() z. B. "12.04." liefert
$moonSentences[] = 'Die Mondphasen im Überblick: ' . rtrim($phaseList, '.') . '.';
}
}
@@ -956,6 +1096,31 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$byDate[$date][] = $ne;
}
// Fasst Saetze desselben Tages ohne Datumswiederholung an ("Am selben Tag ...")
// statt mit einer Sequenz-Phrase wie "Anschliessend", die faelschlich Zeitfolge suggeriert.
$previousMoonSentenceDay = null;
$pushMoonSentence = static function (string $dayKey, string $sentence, string $originalPrefix, string $datePhraseLower) use (&$moonSentences, &$moonEventIndex, &$previousMoonSentenceDay, $varyLeadIn, $seededPick, $dayNumberOf): void {
if ($moonEventIndex > 0 && $dayKey === $previousMoonSentenceDay && str_starts_with($sentence, $originalPrefix)) {
$rest = substr($sentence, strlen($originalPrefix));
$sameDayVariants = [
'Am selben Tag ' . $rest,
'Zugleich ' . $rest,
'Außerdem ' . $rest,
];
$moonSentences[] = $seededPick($sameDayVariants, 'moonSameDay#' . $moonEventIndex, $sentence);
} else {
$dayGap = null;
$currentDayNumber = $dayNumberOf($dayKey);
$previousDayNumber = $previousMoonSentenceDay !== null ? $dayNumberOf($previousMoonSentenceDay) : null;
if ($currentDayNumber !== null && $previousDayNumber !== null) {
$dayGap = abs($currentDayNumber - $previousDayNumber);
}
$moonSentences[] = $varyLeadIn($sentence, $originalPrefix, $datePhraseLower, $moonEventIndex, 'moonSentence', $dayGap);
}
$previousMoonSentenceDay = $dayKey;
$moonEventIndex++;
};
foreach ($byDate as $date => $dateEvents) {
$apsis = null;
$approaches = [];
@@ -983,12 +1148,16 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$sd = $shortDate($date);
foreach (array_keys($occultationTargets) as $target) {
$moonSentences[] = 'Am ' . $sd . ' bedeckt der Mond ' . $target . '.';
if ($occultationTargets !== []) {
// Mehrere Bedeckungen am selben Tag zu einem Satz zusammenfassen,
// statt das Datum in getrennten Saetzen zu wiederholen.
$sentence = 'Am ' . $sd . ' bedeckt der Mond ' . $naturalJoin(array_keys($occultationTargets)) . '.';
$pushMoonSentence($sd, $sentence, 'Am ' . $sd . ' ', 'am ' . $sd);
}
if ($goldenHandle !== null) {
$moonSentences[] = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel.';
$sentence = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel.';
$pushMoonSentence($sd, $sentence, 'Am ' . $sd . ' ', 'am ' . $sd);
}
if ($apsis !== null) {
@@ -1006,7 +1175,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$apsisText = 'Am ' . $sd . ' erreicht der Mond sein Apogäum' . $distanceStr . ' – er steht der Erde in diesem Monat am fernsten';
}
if ($approaches !== []) {
// Begegnungen natürlich formulieren: "und zieht dabei in nur X Grad Abstand an Y vorbei"
// Begegnungen natürlich formulieren, z. B. "und zieht dabei in nur X Grad Abstand an Y vorbei"
$approachFragments = [];
foreach ($approaches as $ap) {
$apLabel = trim((string) ($ap['event'] ?? ''));
@@ -1018,21 +1187,21 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
if (str_starts_with($apLabel, 'Mond nahe ')) {
$target = substr($apLabel, strlen('Mond nahe '));
$approachFragments[] = $sepStr . ' an ' . $target . ' vorbei';
$approachFragments[] = 'zieht dabei' . $sepStr . ' an ' . $target . ' vorbei';
} elseif (str_starts_with($apLabel, 'Mond bedeckt ')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$approachFragments[] = ' und bedeckt dabei ' . $target;
$approachFragments[] = 'bedeckt dabei ' . $target;
} else {
$approachFragments[] = $sepStr !== '' ? $sepStr . ' bei ' . $apLabel : ' bei ' . $apLabel;
$approachFragments[] = $sepStr !== '' ? 'steht' . $sepStr . ' bei ' . $apLabel : 'steht bei ' . $apLabel;
}
}
$approachClause = count($approachFragments) === 1
? ' und zieht dabei' . $approachFragments[0]
: ' und zieht dabei' . implode(' sowie', $approachFragments);
$moonSentences[] = $apsisText . $approachClause . '.';
// Natuerliche Aufzaehlung ("A und B", "A, B und C") statt "A sowie B sowie C".
$approachClause = ' und ' . $naturalJoin($approachFragments);
$sentence = $apsisText . $approachClause . '.';
} else {
$moonSentences[] = $apsisText . '.';
$sentence = $apsisText . '.';
}
$pushMoonSentence($sd, $sentence, 'Am ' . $sd . ' ', 'am ' . $sd);
} elseif ($approaches !== []) {
foreach ($approaches as $ap) {
$apLabel = trim((string) ($ap['event'] ?? ''));
@@ -1049,37 +1218,51 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$apTime = trim((string) ($ap['time'] ?? ''));
$timeClause = $apTime !== '' ? ' um ' . $apTime . ' Uhr' : '';
$moonSentences[] = match ($eventKind) {
$sentence = match ($eventKind) {
'start' => 'Am ' . $apSd . $timeClause . ' beginnt die Bedeckung von ' . $target . ' durch den Mond.',
'maximum' => 'Am ' . $apSd . $timeClause . ' erreicht die Bedeckung von ' . $target . ' durch den Mond ihr Maximum.',
'end' => 'Am ' . $apSd . $timeClause . ' endet die Bedeckung von ' . $target . ' durch den Mond.',
default => 'Am ' . $apSd . $timeClause . ' bedeckt der Mond ' . $target . '.',
};
$pushMoonSentence($apSd, $sentence, 'Am ' . $apSd . $timeClause . ' ', 'am ' . $apSd . $timeClause);
continue;
}
if (str_starts_with($apLabel, 'Mond nahe ')) {
$target = substr($apLabel, strlen('Mond nahe '));
$moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.';
$sentence = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.';
} elseif (str_starts_with($apLabel, 'Mond streift ')) {
$target = substr($apLabel, strlen('Mond streift '));
$moonSentences[] = 'Am ' . $apSd . ' streift der Mond ' . $target . '.';
$sentence = 'Am ' . $apSd . ' streift der Mond ' . $target . '.';
} elseif (str_starts_with($apLabel, 'Mond bedeckt ')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$moonSentences[] = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.';
$sentence = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.';
} else {
$moonSentences[] = $apLabel . ' am ' . $apSd . ($sepStr !== '' ? ' – ' . ltrim($sepStr) : '') . '.';
$sentence = $apLabel . ' am ' . $apSd . ($sepStr !== '' ? ' – ' . ltrim($sepStr) : '') . '.';
}
$pushMoonSentence($apSd, $sentence, 'Am ' . $apSd . ' ', 'am ' . $apSd);
}
}
}
if ($moonSentences !== []) {
$paragraphs[] = implode("\n", $moonSentences);
// Fliesstext statt Zeilenliste, konsistent mit den anderen Absaetzen.
$addParagraph(implode(' ', $moonSentences), 'Der Mond im ' . $monthName . ' ' . $year);
}
}
// Sprachliches Geschlecht: im Deutschen sind alle Planeten maskulin ("er"),
// nur die Venus ist feminin ("sie"). Betrifft auch das Possessivpronomen,
// da die hier vorkommenden Bezugswoerter (Bewegung, Beobachtung, Elongation
// etc.) alle feminin sind ("seine" -> "ihre").
$planetPronoun = static function (string $planetName): string {
return trim($planetName) === 'Venus' ? 'sie' : 'er';
};
$planetPossessive = static function (string $planetName): string {
return trim($planetName) === 'Venus' ? 'ihre' : 'seine';
};
// Hilfsfunktion: natürlichen Satz aus einem Planeten-Event bauen
$buildPlanetSentence = static function (array $event) use ($shortDate): ?string {
$buildPlanetSentence = static function (array $event) use ($shortDate, $planetPronoun): ?string {
$type = (string) ($event['type'] ?? '');
$label = trim((string) ($event['event'] ?? ''));
$date = trim((string) ($event['date'] ?? ''));
@@ -1113,20 +1296,34 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
}
if ($type === 'inner_planet_elongation') {
$sepMatch = [];
$sepStr = '';
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) {
$sepStr = ' (' . $sepMatch[1] . ')';
$label = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $label));
}
$isWest = str_contains($label, 'westlich') || str_contains($label, 'West');
if ($type === 'inner_planet_event') {
$planet = '';
if (preg_match('/^(\S+)\s+/', $label, $m)) {
$planet = $m[1];
}
$visibility = $isWest ? 'als Morgenstern am besten zu beobachten' : 'als Abendstern am besten zu beobachten';
return 'Am ' . $sd . ' erreicht ' . ($planet ?: 'der Planet') . ' seine größte Elongation' . $sepStr . ' und ist dann ' . $visibility . '.';
$planetOrFallback = $planet ?: 'der Planet';
$pronoun = $planetPronoun($planet);
if (str_contains($label, 'Elongation')) {
$sepMatch = [];
$sepStr = '';
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) {
$sepStr = ' (' . $sepMatch[1] . ')';
}
$isWest = str_contains($label, 'westlich') || str_contains($label, 'West');
$visibility = $isWest ? 'als Morgenstern' : 'als Abendstern';
// Bewusst laienverstaendlich statt des Fachbegriffs "Elongation":
// der Wert beschreibt den maximalen Winkelabstand von der Sonne.
return 'Am ' . $sd . ' steht ' . $planetOrFallback . ' so weit wie in diesem Monat möglich von der Sonne entfernt' . $sepStr . ' und ist deshalb ' . $visibility . ' am besten zu beobachten.';
}
if (str_contains($label, 'unterer Konjunktion')) {
return 'Am ' . $sd . ' steht ' . $planetOrFallback . ' in unterer Konjunktion – ' . $pronoun . ' befindet sich dabei zwischen Erde und Sonne und ist deshalb einige Zeit lang nicht zu beobachten.';
}
if (str_contains($label, 'oberer Konjunktion')) {
return 'Am ' . $sd . ' steht ' . $planetOrFallback . ' in oberer Konjunktion – von der Erde aus gesehen steht ' . $pronoun . ' dann hinter der Sonne und ist ebenfalls nicht zu sehen.';
}
}
if ($type === 'venus_peak_magnitude') {
@@ -1135,7 +1332,11 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
if (preg_match('/\(([^)]+)\)/', $label, $magMatch)) {
$magStr = ' mit ' . $magMatch[1];
}
return 'Am ' . $sd . ' erreicht Venus ihre größte Helligkeit' . $magStr . ' – sie ist dann selbst in der Dämmerung kaum zu übersehen.';
$visibility = (string) ($event['visibility'] ?? '');
$twilight = $visibility === 'morning'
? 'in der Morgendämmerung'
: ($visibility === 'evening' ? 'in der Abenddämmerung' : 'in der Dämmerung');
return 'Am ' . $sd . ' erreicht Venus ihre größte Helligkeit' . $magStr . ' – sie ist dann selbst ' . $twilight . ' kaum zu übersehen.';
}
if ($type === 'outer_planet_event') {
@@ -1155,18 +1356,26 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
if ($type === 'mercury_good_visibility') {
return 'Um den ' . $sd . ' zeigt sich Merkur günstig am Himmel und lässt sich in der Dämmerung tief über dem Horizont aufspüren.';
$period = (string) ($event['period'] ?? '');
$twilight = $period === 'morning'
? 'in der Morgendämmerung'
: ($period === 'evening' ? 'in der Abenddämmerung' : 'in der Dämmerung');
return 'Um den ' . $sd . ' zeigt sich Merkur günstig am Himmel und lässt sich ' . $twilight . ' tief über dem Horizont aufspüren.';
}
if ($type === 'planet_parade') {
return 'Am ' . $sd . ' sind mehrere Planeten gleichzeitig am Himmel zu sehen – eine seltene Planetenparade, die einen Blick Richtung Süden lohnt.';
return 'Am ' . $sd . ' sind mehrere Planeten gleichzeitig am Himmel zu sehen – eine Planetenparade, bei der sich ein Blick in den Himmel lohnt.';
}
// Fallback
return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.';
return 'Am ' . $sd . ' zeigt sich zudem: ' . $label . '.';
};
$buildPlanetMotionSentence = static function (array $event) use ($shortDate): ?string {
// Verhindert, dass zwei rückläufige (oder zwei wieder rechtläufige) Planeten
// im selben Monat wortgleich beschrieben werden.
$lastRetrogradeVariant = null;
$lastProgradeVariant = null;
$buildPlanetMotionSentence = static function (array $event) use ($shortDate, $planetPronoun, $planetPossessive, $seededPick, &$lastRetrogradeVariant, &$lastProgradeVariant): ?string {
$type = (string) ($event['type'] ?? '');
$label = trim((string) ($event['event'] ?? ''));
$date = trim((string) ($event['date'] ?? ''));
@@ -1182,13 +1391,44 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
if (preg_match('/^(\S+)\s+/', $label, $m)) {
$planet = $m[1];
}
$planetOrFallback = $planet ?: 'der Planet';
$pronoun = $planetPronoun($planet);
$possessive = $planetPossessive($planet);
if ($isRetrograde) {
return 'Ab dem ' . $sd . ' läuft ' . ($planet ?: 'der Planet') . ' rückläufig – er bewegt sich scheinbar rückwärts vor dem Sternhintergrund, was seine Beobachtung besonders interessant macht.';
$variants = [
'bewegung' => 'Ab dem ' . $sd . ' läuft ' . $planetOrFallback . ' rückläufig – ' . $pronoun . ' bewegt sich scheinbar rückwärts vor dem Sternhintergrund, was ' . $possessive . ' Beobachtung besonders interessant macht.',
'ueberholt' => 'Ab dem ' . $sd . ' bewegt sich ' . $planetOrFallback . ' scheinbar rückwärts vor dem Sternhintergrund – die Erde überholt ' . $planetOrFallback . ' dabei auf ihrer schnelleren Umlaufbahn.',
'schleife' => 'Ab dem ' . $sd . ' zieht ' . $planetOrFallback . ' für einige Wochen eine kleine Schleife vor dem Sternhintergrund, gut zu verfolgen von Nacht zu Nacht.',
];
$labels = array_keys($variants);
if ($lastRetrogradeVariant !== null) {
$withoutLast = array_values(array_filter($labels, static fn (string $l): bool => $l !== $lastRetrogradeVariant));
if ($withoutLast !== []) {
$labels = $withoutLast;
}
}
$chosen = $seededPick($labels, 'retrogradeVariant#' . $sd . '#' . $planetOrFallback, $labels[0]);
$lastRetrogradeVariant = $chosen;
return $variants[$chosen];
}
if ($isPrograde) {
return 'Ab dem ' . $sd . ' setzt ' . ($planet ?: 'der Planet') . ' seine normale Vorwärtsbewegung am Himmel wieder fort.';
$variants = [
'fortsetzung' => 'Ab dem ' . $sd . ' setzt ' . $planetOrFallback . ' ' . $possessive . ' normale Vorwärtsbewegung am Himmel wieder fort.',
'gewohnteRichtung' => 'Ab dem ' . $sd . ' wandert ' . $planetOrFallback . ' wieder in die gewohnte Richtung vor dem Sternhintergrund.',
'endeSchleife' => 'Ab dem ' . $sd . ' beendet ' . $planetOrFallback . ' ' . $possessive . ' Rückwärtsschleife und zieht wieder normal durch die Sternbilder.',
];
$labels = array_keys($variants);
if ($lastProgradeVariant !== null) {
$withoutLast = array_values(array_filter($labels, static fn (string $l): bool => $l !== $lastProgradeVariant));
if ($withoutLast !== []) {
$labels = $withoutLast;
}
}
$chosen = $seededPick($labels, 'progradeVariant#' . $sd . '#' . $planetOrFallback, $labels[0]);
$lastProgradeVariant = $chosen;
return $variants[$chosen];
}
return 'Am ' . $sd . ' wird ' . ($planet ?: 'der Planet') . ' stationär und ändert seine Bewegungsrichtung am Himmel.';
return 'Am ' . $sd . ' wird ' . $planetOrFallback . ' stationär und ändert ' . $possessive . ' Bewegungsrichtung am Himmel.';
}
if ($type === 'planet_constellation_change') {
@@ -1213,7 +1453,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
}
return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.';
return 'Am ' . $sd . ' zeigt sich zudem: ' . $label . '.';
};
$planetObservationEvents = $findEvents(
@@ -1221,16 +1461,35 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['inner_planet_event', 'venus_peak_magnitude', 'outer_planet_event', 'planet_conjunction', 'planet_parade', 'mercury_good_visibility', 'golden_gate_of_ecliptic'], true),
5
);
// Chronologisch sortieren, damit Uebergangsphrasen wie "Anschliessend"
// tatsaechlich die zeitliche Reihenfolge widerspiegeln.
usort($planetObservationEvents, static fn (array $a, array $b): int => strcmp(
(string) ($a['date'] ?? ''),
(string) ($b['date'] ?? '')
));
if ($planetObservationEvents !== []) {
$planetSentences = [];
$planetSentenceIndex = 0;
$previousPlanetSentenceDayNumber = null;
foreach ($planetObservationEvents as $pe) {
$sentence = $buildPlanetSentence($pe);
if ($sentence !== null) {
$planetSentences[] = $sentence;
if ($sentence === null) {
continue;
}
$rawDate = trim((string) ($pe['date'] ?? ''));
$sd = $shortDate($rawDate);
$currentDayNumber = $dayNumberOf($rawDate);
$dayGap = ($currentDayNumber !== null && $previousPlanetSentenceDayNumber !== null)
? abs($currentDayNumber - $previousPlanetSentenceDayNumber)
: null;
$sentence = $varyLeadIn($sentence, 'Am ' . $sd . ' ', 'am ' . $sd, $planetSentenceIndex, 'planetSentence', $dayGap);
$planetSentences[] = $sentence;
$planetSentenceIndex++;
$previousPlanetSentenceDayNumber = $currentDayNumber;
}
if ($planetSentences !== []) {
$paragraphs[] = implode(' ', $planetSentences);
$addParagraph(implode(' ', $planetSentences), 'Die Planeten im ' . $monthName . ' ' . $year);
$planetsHeadingUsed = true;
}
}
@@ -1247,7 +1506,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
'Waage' => 'die Waage', 'Skorpion' => 'den Skorpion', 'Schütze' => 'den Schützen',
'Steinbock' => 'den Steinbock', 'Wassermann' => 'den Wassermann', 'Fische' => 'die Fische',
'Walfisch' => 'den Walfisch', 'Orion' => 'den Orion', 'Fuhrmann' => 'den Fuhrmann',
'Stier' => 'den Stier', 'Kassiopeia' => 'die Kassiopeia', 'Andromeda' => 'die Andromeda',
'Kassiopeia' => 'die Kassiopeia', 'Andromeda' => 'die Andromeda',
'Perseus' => 'den Perseus', 'Pegasus' => 'den Pegasus', 'Leier' => 'die Leier',
'Schwan' => 'den Schwan', 'Adler' => 'den Adler', 'Schlange' => 'die Schlange',
'Schlangenträger' => 'den Schlangenträger', 'Herkules' => 'den Herkules',
@@ -1267,15 +1526,34 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$stationEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'outer_planet_station'));
$changeEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'planet_constellation_change'));
// Chronologisch sortieren, damit Uebergangsphrasen wie "Anschliessend"
// tatsaechlich die zeitliche Reihenfolge widerspiegeln.
usort($stationEvents, static fn (array $a, array $b): int => strcmp(
(string) ($a['date'] ?? ''),
(string) ($b['date'] ?? '')
));
$motionSentences = [];
// Rückläufigkeits-Ereignisse einzeln als Satz
// Rückläufigkeits-Ereignisse einzeln als Satz, mit variierten Satzanfaengen
$stationSentenceIndex = 0;
$previousStationSentenceDayNumber = null;
foreach ($stationEvents as $se) {
$sentence = $buildPlanetMotionSentence($se);
if ($sentence !== null) {
$motionSentences[] = $sentence;
if ($sentence === null) {
continue;
}
$rawDate = trim((string) ($se['date'] ?? ''));
$sd = $shortDate($rawDate);
$currentDayNumber = $dayNumberOf($rawDate);
$dayGap = ($currentDayNumber !== null && $previousStationSentenceDayNumber !== null)
? abs($currentDayNumber - $previousStationSentenceDayNumber)
: null;
$sentence = $varyLeadIn($sentence, 'Ab dem ' . $sd . ' ', 'ab dem ' . $sd, $stationSentenceIndex, 'stationSentence', $dayGap);
$sentence = $varyLeadIn($sentence, 'Am ' . $sd . ' ', 'am ' . $sd, $stationSentenceIndex, 'stationSentence', $dayGap);
$motionSentences[] = $sentence;
$stationSentenceIndex++;
$previousStationSentenceDayNumber = $currentDayNumber;
}
// Sternbild-Wechsel: nach Planet gruppieren, kompakt als eine Aussage
@@ -1322,7 +1600,11 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
if ($motionSentences !== []) {
$paragraphs[] = implode(' ', $motionSentences);
$addParagraph(
implode(' ', $motionSentences),
$planetsHeadingUsed ? null : 'Die Planeten im ' . $monthName . ' ' . $year
);
$planetsHeadingUsed = true;
}
}
@@ -1332,19 +1614,19 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
4
);
if ($cometEvents !== []) {
$cometIntro = $pickRandomText([
$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',
], 'Bei den Kometen sind in diesem Monat besonders');
$cometOutro = $pickRandomText([
], 'cometIntro', 'Bei den Kometen sind in diesem Monat besonders');
$cometOutro = $seededPick([
'interessant.',
'beobachtenswert.',
'bemerkenswert.',
'einen genaueren Blick wert.',
], 'interessant.');
$paragraphs[] = $cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro;
], 'cometOutro', 'interessant.');
$addParagraph($cometIntro . ' ' . $describeEvents($cometEvents) . ' ' . $cometOutro);
}
$eclipseEvents = $findEvents(
@@ -1353,13 +1635,13 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
3
);
if ($eclipseEvents !== []) {
$eclipseIntro = $pickRandomText([
$eclipseIntro = $seededPick([
'Besondere Aufmerksamkeit verdienen außerdem die Finsternistermine.',
'Auch die Finsternisse setzen in diesem Monat markante Akzente.',
'Ein weiterer Höhepunkt können in diesem Monat die Finsternistermine sein.',
'Nicht zuletzt lohnt sich in diesem Monat auch ein Blick auf mögliche Finsternisse.',
], 'Besondere Aufmerksamkeit verdienen ausserdem die Finsternistermine.');
$paragraphs[] = $eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.';
], 'eclipseIntro', 'Besondere Aufmerksamkeit verdienen außerdem die Finsternistermine.');
$addParagraph($eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.');
}
$meteorCandidates = array_values(array_filter(
@@ -1394,27 +1676,83 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
}
if ($meteorEvents !== []) {
$meteorIntro = $pickRandomText([
$meteorIntro = $seededPick([
'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.',
'Auch die Meteorströme verdienen in diesem Monat Aufmerksamkeit.',
'Wer nach Sternschnuppen Ausschau halten möchte, sollte auch die Meteorströme beachten.',
'Ein Blick auf die Meteorströme kann sich in diesem Monat ebenfalls lohnen.',
], 'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.');
$paragraphs[] = $meteorIntro . ' Erwähnenswert sind besonders ' . $describeEvents($meteorEvents) . '.';
], 'meteorIntro', 'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.');
$addParagraph($meteorIntro . ' Erwähnenswert sind besonders ' . $describeMeteorEvents($meteorEvents) . '.');
}
if (count($paragraphs) === 1 && $filteredEvents !== []) {
$highlights = array_slice($filteredEvents, 0, 4);
$highlightIntro = $pickRandomText([
$highlightIntro = $seededPick([
'Zu den auffälligen Terminen des Monats gehören',
'Unter den markanten Terminen dieses Monats finden sich',
'Im Monatsverlauf fallen vor allem',
'Besonders bemerkenswert im Monatskalender sind',
], 'Zu den auffaelligen Terminen des Monats gehoeren');
$paragraphs[] = $highlightIntro . ' ' . $describeEvents($highlights) . '.';
], 'highlightIntro', 'Zu den auffälligen Terminen des Monats gehören');
$addParagraph($highlightIntro . ' ' . $describeEvents($highlights) . '.');
}
return implode("\n\n", $paragraphs);
// Aufhaenger: das markanteste Ereignis des Monats vorab kurz ankuendigen,
// statt immer nur mit dem generischen Himmelsueberblick zu beginnen.
$headlineCandidate = null;
if ($eclipseEvents !== []) {
$headlineCandidate = $eclipseEvents[0];
} elseif (($matches = array_values(array_filter($filteredEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'moon_occultation' && !in_array((string) ($e['event_kind'] ?? ''), ['start', 'end'], true)))) !== []) {
$headlineCandidate = $matches[0];
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'outer_planet_event' && str_contains((string) ($e['event'] ?? ''), 'Opposition')))) !== []) {
$headlineCandidate = $matches[0];
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'golden_gate_of_ecliptic'))) !== []) {
$headlineCandidate = $matches[0];
} elseif ($meteorEvents !== [] && max((int) ($meteorEvents[0]['zhr_min_estimate'] ?? 0), (int) ($meteorEvents[0]['zhr_max_estimate'] ?? 0)) >= 50) {
$headlineCandidate = $meteorEvents[0];
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'planet_conjunction'))) !== []) {
$headlineCandidate = $matches[0];
} elseif (($matches = array_values(array_filter($planetObservationEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'venus_peak_magnitude'))) !== []) {
$headlineCandidate = $matches[0];
}
if ($headlineCandidate !== null) {
$headlineDate = trim((string) ($headlineCandidate['date'] ?? ''));
$headlineLabel = trim((string) ($headlineCandidate['event'] ?? ''));
// Fuer Meteorstroeme das rohe Tabellen-Label ("Maximum X - CODE - ZHR ca. Y")
// in Fliesstext umwandeln, wie im Meteor-Absatz.
if ((string) ($headlineCandidate['type'] ?? '') === 'meteor_shower_peak') {
$headlineLabel = $formatMeteorLabel($headlineLabel);
}
// Bedeckungen mit Kontaktphase ("Mond bedeckt Jupiter – Groesste Bedeckung")
// sind ein einmaliges Ereignis an einem Tag, kein Ereignis mit eigenem
// Maximum ueber mehrere Tage - den Kontaktphasen-Zusatz daher weglassen.
if ((string) ($headlineCandidate['type'] ?? '') === 'moon_occultation') {
$headlineLabel = trim(explode(' – ', $headlineLabel, 2)[0]);
}
$headlineText = $headlineLabel . ' am ' . rtrim($shortDate($headlineDate), '.');
if ($headlineLabel !== '' && $headlineDate !== '') {
$headlineLeadIn = $seededPick([
'Der Höhepunkt des Monats',
'Am meisten Aufmerksamkeit verdient in diesem Monat',
'Nicht verpassen',
'Besonders hervorzuheben in diesem Monat',
], 'headlineLeadIn', 'Der Höhepunkt des Monats');
array_unshift(
$paragraphs,
['heading' => null, 'text' => $headlineLeadIn . ': ' . $headlineText . '.', 'emphasize' => true]
);
}
}
$closingSentence = $seededPick([
'Ein Blick zum Himmel lohnt sich in diesem Monat gleich mehrfach – denn das Wesentliche spielt sich dort oben ab.',
'Es gibt also einige gute Gründe, in diesem Monat öfter nach oben zu schauen – dort oben ist einiges los.',
'Wer sich Zeit für den Nachthimmel nimmt, wird in diesem Monat mehrfach belohnt – denn das Wichtigste findet dort oben statt.',
'Für Sternfreunde bietet dieser Monat damit reichlich Gelegenheit zum Beobachten – schließlich passiert das Wesentliche am Himmel.',
], 'closingSentence', 'Ein Blick zum Himmel lohnt sich in diesem Monat gleich mehrfach – denn das Wesentliche spielt sich dort oben ab.');
$addParagraph($closingSentence);
return $paragraphs;
}
}
@@ -2426,6 +2764,7 @@ foreach ($venusPeakMagnitudeEvents as $venusPeakMagnitudeEvent) {
'date' => (string) ($venusPeakMagnitudeEvent['local_date'] ?? ''),
'time' => (string) ($venusPeakMagnitudeEvent['local_time'] ?? ''),
'local_iso' => (string) ($venusPeakMagnitudeEvent['local_iso'] ?? ''),
'visibility' => (string) ($venusPeakMagnitudeEvent['visibility'] ?? ''),
'type' => 'venus_peak_magnitude',
];
}
@@ -2584,6 +2923,7 @@ foreach ($mercuryGoodVisibilityEvents as $visibilityEvent) {
'local_iso' => (string) ($visibilityEvent['local_iso'] ?? ''),
'end_local_iso' => (string) ($visibilityEvent['end_local_iso'] ?? ''),
'duration_minutes' => isset($visibilityEvent['duration_minutes']) ? (int) $visibilityEvent['duration_minutes'] : null,
'period' => (string) ($visibilityEvent['period'] ?? ''),
'type' => 'mercury_good_visibility',
];
}
@@ -3031,7 +3371,21 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
</div>
</div>
<div class="monatskarte-footer">
<p class="monatsnarrativ-text"><?= htmlspecialchars($monthNarrativeText, ENT_QUOTES, 'UTF-8') ?></p>
<div class="monatsnarrativ-text">
<?php foreach ($monthNarrativeText as $narrativeBlock): ?>
<?php if (!empty($narrativeBlock['heading'])): ?>
<h4><?= htmlspecialchars((string) $narrativeBlock['heading'], ENT_QUOTES, 'UTF-8') ?></h4>
<?php endif; ?>
<?php foreach (explode("\n\n", (string) ($narrativeBlock['text'] ?? '')) as $narrativeSubParagraph): ?>
<?php $narrativeSubParagraph = trim($narrativeSubParagraph); ?>
<?php if ($narrativeSubParagraph !== ''): ?>
<?php // "highlight"-Marker nur fuer den HTML-/Joomla-Export (siehe narrativeSectionsHtml() im JS);
// auf der Seite selbst bleibt der Absatz optisch normal. ?>
<p<?= !empty($narrativeBlock['emphasize']) ? ' class="monatsnarrativ-highlight"' : '' ?>><?= htmlspecialchars($narrativeSubParagraph, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
</div>
</section>
@@ -3128,7 +3482,9 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
</div>
</div>
<div class="monatskarte-footer">
<p class="monatsnarrativ-text">Bitte zuerst Jahr und Monat auswählen und dann auf &bdquo;Monatsvorhersage berechnen&ldquo; klicken.</p>
<div class="monatsnarrativ-text">
<p>Bitte zuerst Jahr und Monat auswählen und dann auf &bdquo;Monatsvorhersage berechnen&ldquo; klicken.</p>
</div>
</div>
</section>
<?php endif; ?>
@@ -3384,7 +3740,24 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
.monatsnarrativ-text {
font-size: 1.02rem;
white-space: pre-line;
}
.monatsnarrativ-text p {
margin: 0 0 12px;
color: rgba(222, 232, 255, 0.86);
line-height: 1.7;
}
.monatsnarrativ-text p:last-child {
margin-bottom: 0;
}
.monatsnarrativ-text h4 {
margin: 20px 0 8px;
}
.monatsnarrativ-text h4:first-child {
margin-top: 0;
}
.monatskarte-stage {
@@ -6329,10 +6702,27 @@ function monthChartCometRaDec(comet, date) {
}
}
function narrativeSectionsHtml() {
const container = overviewEl.querySelector('.monatsnarrativ-text');
if (!container) {
return '';
}
return Array.from(container.children).map(function (el) {
const text = (el.textContent || '').trim();
if (text === '') {
return '';
}
const tag = el.tagName === 'H4' ? 'h4' : 'p';
// Der Aufhaenger-Satz wird nur im Export fett hervorgehoben, nicht auf der Seite selbst.
const inner = el.classList.contains('monatsnarrativ-highlight')
? '<strong>' + escapeHtml(text) + '</strong>'
: escapeHtml(text);
return '<' + tag + '>' + inner + '</' + tag + '>\n';
}).join('');
}
async function overviewHtml(images) {
const monthLabel = (overviewEl.querySelector('.monatskarte-header p')?.textContent || '').trim();
const narrative = (overviewEl.querySelector('.monatsnarrativ-text')?.textContent || '').trim();
const escapedNarrative = escapeHtml(narrative).replace(/\r?\n/g, '<br>\n');
const imageMarkup = await Promise.all(images.map(async function (image) {
const scaledImage = await scaleImageBlob(image.content, htmlImageMaxWidth);
const imageDataUrl = await blobToDataUrl(scaledImage);
@@ -6341,7 +6731,7 @@ function monthChartCometRaDec(comet, date) {
}));
return '<h3>' + escapeHtml(monthLabel) + '</h3>\n'
+ '<p>' + escapedNarrative + '</p>\n'
+ narrativeSectionsHtml()
+ imageMarkup.join('')
+ eventTableHtml()
+ '<p>Alle Zeiten gelten für MEZ/MESZ.</p>\n'
+2
View File
@@ -4509,12 +4509,14 @@ def action_venus_peak_magnitude_for_month(args: list[str]) -> dict:
break
if local_dt >= local_start:
elongation_info = astronomy.Elongation(astronomy.Body.Venus, event.time)
events.append({
"planet_key": "venus",
"planet_label": "Venus",
"kind": "peak_magnitude",
"label": "Venus in größter Helligkeit",
"mag": float(event.mag),
"visibility": "morning" if elongation_info.visibility == astronomy.Visibility.Morning else "evening",
"utc_iso": dt_utc.isoformat().replace("+00:00", "Z"),
"local_iso": local_dt.isoformat(),
"local_date": local_dt.strftime("%d.%m.%Y"),
+80 -15
View File
@@ -12,6 +12,8 @@ if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
import astronomy
from skyfield import almanac as skyfield_almanac
from astronomical_conversions import skyfield_context, skyfield_wgs84
EPHEMERIS_BODIES = {
@@ -26,6 +28,21 @@ EPHEMERIS_BODIES = {
"Neptune": ("Neptun", astronomy.Body.Neptune),
}
# DE421 enthält die äußeren Planeten als Systemschwerpunkte, nicht als
# separate Planetenzentren. Für Ephemeriden ist das die vorgesehene
# Zielkörperdefinition dieser Datei.
SKYFIELD_EPHEMERIS_TARGETS = {
"Sun": "sun",
"Moon": "moon",
"Mercury": "mercury",
"Venus": "venus",
"Mars": "mars",
"Jupiter": 5,
"Saturn": 6,
"Uranus": 7,
"Neptune": 8,
}
J2000_OBLIQUITY_DEG = 23.439279444444445
GAUSSIAN_GRAVITATIONAL_CONSTANT = 0.01720209895
PARABOLIC_ECCENTRICITY_TOLERANCE = 1.0e-6
@@ -118,6 +135,45 @@ def search_event(
return result
def skyfield_observer(latitude: float, longitude: float, elevation: float):
_timescale, planets = skyfield_context()
return planets["earth"] + skyfield_wgs84.latlon(latitude, longitude, elevation_m=elevation)
def skyfield_planet_state(body_name: str, observer, dt_utc: datetime, timescale=None, planets=None):
if timescale is None or planets is None:
timescale, planets = skyfield_context()
time_value = timescale.from_datetime(dt_utc.astimezone(timezone.utc))
apparent = observer.at(time_value).observe(planets[SKYFIELD_EPHEMERIS_TARGETS[body_name]]).apparent()
ra, dec, _distance = apparent.radec()
altitude, azimuth, _distance = apparent.altaz("standard")
return float(ra.hours), float(dec.degrees), float(azimuth.degrees), float(altitude.degrees)
def search_skyfield_rise_set(observer, body_name: str, local_day_start: datetime, timescale, planets) -> tuple[datetime | None, datetime | None]:
"""Sucht Auf- und Untergang gemeinsam über Skyfields diskrete Ereignissuche."""
target = planets[SKYFIELD_EPHEMERIS_TARGETS[body_name]]
event_function = skyfield_almanac.risings_and_settings(
planets,
target,
observer - planets["earth"],
)
local_day_end = local_day_start + timedelta(days=1)
start_time = timescale.from_datetime(local_day_start.astimezone(timezone.utc))
end_time = timescale.from_datetime(local_day_end.astimezone(timezone.utc))
event_times, event_states = skyfield_almanac.find_discrete(start_time, end_time, event_function)
rise = None
set_ = None
for event_time, state in zip(event_times.utc_datetime(), event_states):
event_dt = event_time.astimezone(timezone.utc)
if bool(state) and rise is None:
rise = event_dt
elif not bool(state) and set_ is None:
set_ = event_dt
return rise, set_
def format_ra_hours(ra_hours: float) -> str:
total_seconds = int(round(float(ra_hours) * 3600.0))
total_seconds %= 24 * 3600
@@ -522,21 +578,28 @@ def action_planet_ephemeris(args: list[str]) -> dict:
label, body = EPHEMERIS_BODIES[body_name]
observer = astronomy.Observer(latitude, longitude, elevation)
skyfield_timescale, skyfield_planets = skyfield_context()
skyfield_topos = skyfield_observer(latitude, longitude, elevation)
def row_builder(current_local: datetime, time_value: astronomy.Time, rise_label: str | None, set_label: str | None) -> dict:
eq = astronomy.Equator(body, time_value, observer, False, True)
horizontal = astronomy.Horizon(time_value, observer, float(eq.ra), float(eq.dec), astronomy.Refraction.Normal)
ra_hours, dec_deg, azimuth_deg, altitude_deg = skyfield_planet_state(
body_name,
skyfield_topos,
current_local.astimezone(timezone.utc),
skyfield_timescale,
skyfield_planets,
)
return {
"object_name": label,
"date_local": current_local.strftime("%d.%m.%Y"),
"time_local": current_local.strftime("%H:%M"),
"local_iso": current_local.isoformat(),
"ra": format_ra_hours(float(eq.ra)),
"ra_decimal_hours": round(float(eq.ra), 8),
"dec": format_dec_deg(float(eq.dec)),
"dec_decimal_deg": round(float(eq.dec), 8),
"azimuth_deg": round(float(horizontal.azimuth), 6),
"altitude_deg": round(float(horizontal.altitude), 6),
"ra": format_ra_hours(ra_hours),
"ra_decimal_hours": round(ra_hours, 8),
"dec": format_dec_deg(dec_deg),
"dec_decimal_deg": round(dec_deg, 8),
"azimuth_deg": round(azimuth_deg, 6),
"altitude_deg": round(altitude_deg, 6),
"rise": rise_label,
"set": set_label,
}
@@ -547,13 +610,15 @@ def action_planet_ephemeris(args: list[str]) -> dict:
except Exception as exc:
fail("Zeitzone ist ungueltig.", extra={"details": str(exc)})
local_day_end = local_day_start + timedelta(days=1)
day_start_time = dt_to_time(local_day_start.astimezone(timezone.utc))
day_end_utc = local_day_end.astimezone(timezone.utc)
rise = search_event(body, astronomy.Direction.Rise, observer, day_start_time, day_end_utc)
set_ = search_event(body, astronomy.Direction.Set, observer, day_start_time, day_end_utc)
rise_label = serialize_event("Aufgang", rise, tz).get("local_time") if rise is not None else None
set_label = serialize_event("Untergang", set_, tz).get("local_time") if set_ is not None else None
rise, set_ = search_skyfield_rise_set(
skyfield_topos,
body_name,
local_day_start,
skyfield_timescale,
skyfield_planets,
)
rise_label = rise.astimezone(tz).strftime("%H:%M") if rise is not None else None
set_label = set_.astimezone(tz).strftime("%H:%M") if set_ is not None else None
return rise_label, set_label
return build_rows_response(
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Regressionstest fuer den Bezugssystem-Fehler in skyfield_planet_state().
apparent.radec() muss ohne epoch-Argument aufgerufen werden, damit die
Ausgabe echte ICRF/J2000-Koordinaten liefert. Ein wieder eingefuehrtes
epoch=timescale.J2000 wuerde die Nutation von J2000.0 (~14,5" bei diesem
Merkur-Test) mit einrechnen und diesen Test zum Scheitern bringen.
Aufruf: python test_ephemeriden_api.py
"""
import math
import os
import sys
import unittest
from datetime import datetime, timezone
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
import ephemeriden_api as api
from astronomical_conversions import skyfield_wgs84
# Sternwarte Sonneberg
TEST_LATITUDE = 50.3773963
TEST_LONGITUDE = 11.1898541
TEST_ELEVATION_M = 640.0
TEST_INSTANT_UTC = datetime(2026, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
# Erwartete topozentrische, scheinbare ICRF-Koordinaten (radec() ohne epoch=),
# berechnet mit DE421 fuer obigen Ort/Zeitpunkt. Geocentric-astrometrisches
# Gegenstueck stimmt mit einer unabhaengigen VSOP87-Rechnung (PyEphem) auf
# rund 0,3" ueberein.
EXPECTED_RA_HOURS = 7.348643358806561
EXPECTED_DEC_DEG = 23.20555540423118
TOLERANCE_ARCSEC = 1.0
def angular_separation_arcsec(ra1_hours: float, dec1_deg: float, ra2_hours: float, dec2_deg: float) -> float:
ra1 = math.radians(ra1_hours * 15.0)
ra2 = math.radians(ra2_hours * 15.0)
dec1 = math.radians(dec1_deg)
dec2 = math.radians(dec2_deg)
cos_sep = (
math.sin(dec1) * math.sin(dec2)
+ math.cos(dec1) * math.cos(dec2) * math.cos(ra1 - ra2)
)
cos_sep = max(-1.0, min(1.0, cos_sep))
return math.degrees(math.acos(cos_sep)) * 3600.0
class SkyfieldReferenceFrameTest(unittest.TestCase):
def test_mercury_position_matches_icrf_reference(self):
timescale, planets = api.skyfield_context()
observer = planets["earth"] + skyfield_wgs84.latlon(
TEST_LATITUDE, TEST_LONGITUDE, elevation_m=TEST_ELEVATION_M
)
ra_hours, dec_deg, _azimuth_deg, _altitude_deg = api.skyfield_planet_state(
"Mercury", observer, TEST_INSTANT_UTC, timescale, planets
)
separation_arcsec = angular_separation_arcsec(
ra_hours, dec_deg, EXPECTED_RA_HOURS, EXPECTED_DEC_DEG
)
self.assertLess(
separation_arcsec,
TOLERANCE_ARCSEC,
f"Position weicht um {separation_arcsec:.3f}\" vom Sollwert ab "
f"(Toleranz {TOLERANCE_ARCSEC}\"). Wurde epoch= bei radec() wieder "
"eingefuehrt?",
)
if __name__ == "__main__":
unittest.main()