Planeten Wechselt das Sternbild eingebaut
This commit is contained in:
+3839
-233
File diff suppressed because it is too large
Load Diff
+453
-15
@@ -11,35 +11,99 @@ $publicBasePath = './';
|
|||||||
$currentYear = (int) date('Y');
|
$currentYear = (int) date('Y');
|
||||||
$selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear;
|
$selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear;
|
||||||
$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n');
|
$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n');
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Grenzwerte fuer Sternkarte und Objektanzeige
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Maximale Helligkeit (Magnitude) der Sterne, die aus der Datenbank geladen
|
||||||
|
// werden. Hoehere Werte laden mehr, aber auch schwaechere Sterne.
|
||||||
$starDatabaseMagnitudeLimit = 6.0;
|
$starDatabaseMagnitudeLimit = 6.0;
|
||||||
|
|
||||||
|
// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar
|
||||||
|
// gezeichnet werden sollen.
|
||||||
$starDisplayMagnitudeLimit = 4.4;
|
$starDisplayMagnitudeLimit = 4.4;
|
||||||
|
|
||||||
|
// Sterne bis zu dieser Helligkeit erhalten auf der Sternkarte einen Namen.
|
||||||
$starLabelMagnitudeLimit = 2.0;
|
$starLabelMagnitudeLimit = 2.0;
|
||||||
$messierDatabaseMagnitudeLimit = 4.0;
|
|
||||||
|
// Maximale Helligkeit der Messier-Objekte, die aus der Datenbank geladen
|
||||||
|
// werden.
|
||||||
|
$messierDatabaseMagnitudeLimit = 2.0;
|
||||||
|
|
||||||
|
// Messier-Objekte bis zu dieser Helligkeit werden auf der Monatskarte
|
||||||
|
// eingeblendet.
|
||||||
$messierDisplayMagnitudeLimit = 1.0;
|
$messierDisplayMagnitudeLimit = 1.0;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Grenzwerte fuer Monatsereignisse
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und Planeten.
|
||||||
$moonPlanetMaxSeparationDeg = 1.5;
|
$moonPlanetMaxSeparationDeg = 1.5;
|
||||||
|
|
||||||
|
// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und ausgewaehlten
|
||||||
|
// Sternhaufen wie Plejaden oder Praesepe.
|
||||||
$moonDeepSkyMaxSeparationDeg = 1.0;
|
$moonDeepSkyMaxSeparationDeg = 1.0;
|
||||||
$planetBrightStarMaxSeparationDeg = 1.0;
|
|
||||||
// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und
|
// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und
|
||||||
// ausgewaehlten hellen Sternen wie Regulus oder Spica.
|
// ausgewaehlten hellen Sternen wie Regulus oder Spica.
|
||||||
|
$planetBrightStarMaxSeparationDeg = 1.0;
|
||||||
|
|
||||||
|
// Maximaler Winkelabstand fuer Planet-Planet-Konjunktionen.
|
||||||
$planetConjunctionMaxSeparationDeg = 1.0;
|
$planetConjunctionMaxSeparationDeg = 1.0;
|
||||||
|
|
||||||
|
// Nur Sterne bis zu dieser Helligkeit werden fuer Mondbedeckungen
|
||||||
|
// beruecksichtigt.
|
||||||
$moonOccultationMaxMag = 2.0;
|
$moonOccultationMaxMag = 2.0;
|
||||||
// Gute Merkur-Sichtbarkeit wird nicht nur über Elongation, sondern über echte
|
|
||||||
// Beobachtbarkeit am Himmel definiert: Wir prüfen pro Tag zu einem festen
|
// -------------------------------------------------------------------------
|
||||||
// Zeitpunkt relativ zu Sonnenauf- oder -untergang, ob Merkur hoch genug steht
|
// Schwellen fuer "Gute Merkur-Sichtbarkeit"
|
||||||
// und die Sonne bereits tief genug unter dem Horizont ist.
|
// -------------------------------------------------------------------------
|
||||||
|
// Gute Merkur-Sichtbarkeit wird nicht nur ueber Elongation definiert, sondern
|
||||||
|
// ueber echte Beobachtbarkeit in der Daemmerung.
|
||||||
|
// Geprueft wird pro Tag:
|
||||||
|
// morgens eine feste Zahl Minuten VOR Sonnenaufgang,
|
||||||
|
// abends eine feste Zahl Minuten NACH Sonnenuntergang.
|
||||||
$mercuryGoodVisibilitySampleOffsetMinutes = 45;
|
$mercuryGoodVisibilitySampleOffsetMinutes = 45;
|
||||||
// Morgens: so viele Minuten VOR Sonnenaufgang prüfen.
|
|
||||||
// Abends: so viele Minuten NACH Sonnenuntergang prüfen.
|
// Merkur muss zum Pruefzeitpunkt mindestens diese Hoehe ueber dem Horizont
|
||||||
|
// erreichen, damit der Tag als gut sichtbar gilt.
|
||||||
$mercuryGoodVisibilityMinAltitudeDeg = 7.0;
|
$mercuryGoodVisibilityMinAltitudeDeg = 7.0;
|
||||||
// Merkur muss bei diesem Prüfzeitpunkt mindestens so hoch über dem Horizont
|
|
||||||
// stehen, damit wir die Sichtbarkeit als "gut" einstufen.
|
// Gleichzeitig muss die Sonne ausreichend tief unter dem Horizont stehen.
|
||||||
|
// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar waere, aber
|
||||||
|
// noch zu stark in der hellen Daemmerung untergeht.
|
||||||
$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0;
|
$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0;
|
||||||
// Die Sonne muss gleichzeitig mindestens 4 Grad unter dem Horizont stehen.
|
|
||||||
// So vermeiden wir Treffer, bei denen Merkur zwar formal da ist, aber noch zu
|
// Erst ab dieser Anzahl aufeinanderfolgender guter Tage erzeugen wir ein
|
||||||
// stark in der hellen Dämmerung untergeht.
|
// Monatsereignis. Einzelne Ausreisser-Tage werden dadurch herausgefiltert.
|
||||||
$mercuryGoodVisibilityMinConsecutiveDays = 3;
|
$mercuryGoodVisibilityMinConsecutiveDays = 3;
|
||||||
// Einzelne gute Tage sind oft zu zufällig. Erst ab dieser Anzahl
|
|
||||||
// aufeinanderfolgender Tage erzeugen wir ein Monatsereignis.
|
// -------------------------------------------------------------------------
|
||||||
|
// Schwellen fuer "Planetenparade"
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Geprueft wird morgens eine feste Zahl Minuten VOR Sonnenaufgang und abends
|
||||||
|
// eine feste Zahl Minuten NACH Sonnenuntergang.
|
||||||
|
$planetParadeSampleOffsetMinutes = 45;
|
||||||
|
|
||||||
|
// Ein Planet zaehlt fuer die Parade nur dann mit, wenn er zum Pruefzeitpunkt
|
||||||
|
// mindestens diese Hoehe ueber dem Horizont erreicht.
|
||||||
|
$planetParadeMinAltitudeDeg = 7.0;
|
||||||
|
|
||||||
|
// Gleichzeitig muss die Sonne bereits tief genug unter dem Horizont stehen,
|
||||||
|
// damit die Parade nicht nur rechnerisch, sondern auch praktisch sichtbar ist.
|
||||||
|
$planetParadeMaxSunAltitudeDeg = -4.0;
|
||||||
|
|
||||||
|
// Die beteiligten Planeten muessen innerhalb dieses Azimutbogens liegen.
|
||||||
|
// So vermeiden wir Treffer, bei denen die Planeten zu weit ueber den Himmel
|
||||||
|
// verteilt sind und nicht mehr als zusammenhaengende Parade wirken.
|
||||||
|
$planetParadeMaxAzimuthSpanDeg = 120.0;
|
||||||
|
|
||||||
|
// Mindestens so viele helle Planeten muessen gleichzeitig sichtbar sein,
|
||||||
|
// damit wir von einer Planetenparade sprechen.
|
||||||
|
$planetParadeMinPlanetCount = 4;
|
||||||
|
|
||||||
|
// Erst ab dieser Anzahl aufeinanderfolgender Tage erzeugen wir ein Ereignis.
|
||||||
|
// Das filtert kurzlebige Ein-Tages-Treffer heraus.
|
||||||
|
$planetParadeMinConsecutiveDays = 2;
|
||||||
|
|
||||||
$years = range($currentYear - 2, $currentYear + 5);
|
$years = range($currentYear - 2, $currentYear + 5);
|
||||||
$months = [
|
$months = [
|
||||||
@@ -83,6 +147,8 @@ $moonPlanetApproaches = [];
|
|||||||
$moonDeepSkyApproaches = [];
|
$moonDeepSkyApproaches = [];
|
||||||
$planetBrightStarApproaches = [];
|
$planetBrightStarApproaches = [];
|
||||||
$mercuryGoodVisibilityEvents = [];
|
$mercuryGoodVisibilityEvents = [];
|
||||||
|
$planetParadeEvents = [];
|
||||||
|
$planetConstellationChangeEvents = [];
|
||||||
$planetConjunctions = [];
|
$planetConjunctions = [];
|
||||||
$eclipseEvents = [];
|
$eclipseEvents = [];
|
||||||
$moonApsisEvents = [];
|
$moonApsisEvents = [];
|
||||||
@@ -203,6 +269,171 @@ if (!function_exists('findMonthForecastSolarLongitudeCrossing')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('buildObservationTipInsertSql')) {
|
||||||
|
function buildObservationTipInsertSql(array $events, DateTimeZone $timezone): string
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
if (!is_array($event)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tip = trim((string) ($event['event'] ?? ''));
|
||||||
|
if ($tip === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moment = null;
|
||||||
|
$localIso = trim((string) ($event['local_iso'] ?? ''));
|
||||||
|
if ($localIso !== '') {
|
||||||
|
try {
|
||||||
|
$moment = new DateTimeImmutable($localIso);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$moment = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$moment instanceof DateTimeImmutable) {
|
||||||
|
$date = trim((string) ($event['date'] ?? ''));
|
||||||
|
$time = trim((string) ($event['time'] ?? ''));
|
||||||
|
if ($date !== '' && $time !== '') {
|
||||||
|
$moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$moment instanceof DateTimeImmutable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = sprintf(
|
||||||
|
"('%s', '%s')",
|
||||||
|
$moment->setTimezone($timezone)->format('Y-m-d H:i:s'),
|
||||||
|
str_replace("'", "''", $tip)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rows === []) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return "INSERT INTO `astro_sonobs_observationtips` (`moment`, `tip`)\nVALUES\n "
|
||||||
|
. implode(",\n ", $rows)
|
||||||
|
. ";\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('escapeIcalText')) {
|
||||||
|
function escapeIcalText(string $value): string
|
||||||
|
{
|
||||||
|
return str_replace(
|
||||||
|
["\\", ";", ",", "\r\n", "\n", "\r"],
|
||||||
|
["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"],
|
||||||
|
$value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('foldIcalLine')) {
|
||||||
|
function foldIcalLine(string $line): string
|
||||||
|
{
|
||||||
|
$chunks = preg_split('/(.{1,73})/u', $line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||||
|
if ($chunks === false || $chunks === []) {
|
||||||
|
return $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstChunk = array_shift($chunks);
|
||||||
|
return $firstChunk . ($chunks ? "\r\n " . implode("\r\n ", $chunks) : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('buildObservationTipCalendarIcs')) {
|
||||||
|
function buildObservationTipCalendarIcs(array $events, DateTimeZone $timezone): string
|
||||||
|
{
|
||||||
|
$lines = [
|
||||||
|
'BEGIN:VCALENDAR',
|
||||||
|
'VERSION:2.0',
|
||||||
|
'PRODID:-//AstroTools//Monatsvorhersage//DE',
|
||||||
|
'CALSCALE:GREGORIAN',
|
||||||
|
'METHOD:PUBLISH',
|
||||||
|
'X-WR-CALNAME:AstroTools Monatsereignisse',
|
||||||
|
];
|
||||||
|
|
||||||
|
$createdUtc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Ymd\THis\Z');
|
||||||
|
|
||||||
|
foreach ($events as $index => $event) {
|
||||||
|
if (!is_array($event)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = trim((string) ($event['event'] ?? ''));
|
||||||
|
if ($summary === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moment = null;
|
||||||
|
$localIso = trim((string) ($event['local_iso'] ?? ''));
|
||||||
|
if ($localIso !== '') {
|
||||||
|
try {
|
||||||
|
$moment = new DateTimeImmutable($localIso);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$moment = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$moment instanceof DateTimeImmutable) {
|
||||||
|
$date = trim((string) ($event['date'] ?? ''));
|
||||||
|
$time = trim((string) ($event['time'] ?? ''));
|
||||||
|
if ($date !== '' && $time !== '') {
|
||||||
|
$moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$moment instanceof DateTimeImmutable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startUtc = $moment->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
$endMoment = null;
|
||||||
|
$endLocalIso = trim((string) ($event['end_local_iso'] ?? ''));
|
||||||
|
if ($endLocalIso !== '') {
|
||||||
|
try {
|
||||||
|
$endMoment = new DateTimeImmutable($endLocalIso);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$endMoment = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$endMoment instanceof DateTimeImmutable) {
|
||||||
|
$durationMinutes = isset($event['duration_minutes']) ? max(1, (int) $event['duration_minutes']) : 15;
|
||||||
|
$endMoment = $moment->modify('+' . $durationMinutes . ' minutes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$endUtc = $endMoment->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
$uidBase = sha1($startUtc->format(DateTimeInterface::ATOM) . '|' . $summary . '|' . (string) $index);
|
||||||
|
|
||||||
|
$eventLines = [
|
||||||
|
'BEGIN:VEVENT',
|
||||||
|
'UID:' . $uidBase . '@astrotools.local',
|
||||||
|
'DTSTAMP:' . $createdUtc,
|
||||||
|
'DTSTART:' . $startUtc->format('Ymd\THis\Z'),
|
||||||
|
'DTEND:' . $endUtc->format('Ymd\THis\Z'),
|
||||||
|
'SUMMARY:' . escapeIcalText($summary),
|
||||||
|
'DESCRIPTION:' . escapeIcalText($summary),
|
||||||
|
'END:VEVENT',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($eventLines as $eventLine) {
|
||||||
|
$lines[] = foldIcalLine($eventLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = 'END:VCALENDAR';
|
||||||
|
|
||||||
|
return implode("\r\n", $lines) . "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$dbConfig = require __DIR__ . '/../config/database.php';
|
$dbConfig = require __DIR__ . '/../config/database.php';
|
||||||
$dsn = sprintf(
|
$dsn = sprintf(
|
||||||
@@ -766,6 +997,49 @@ if (is_array($mercuryGoodVisibilityDecoded) && ($mercuryGoodVisibilityDecoded['o
|
|||||||
$mercuryGoodVisibilityEvents = is_array($mercuryGoodVisibilityDecoded['events'] ?? null) ? $mercuryGoodVisibilityDecoded['events'] : [];
|
$mercuryGoodVisibilityEvents = is_array($mercuryGoodVisibilityDecoded['events'] ?? null) ? $mercuryGoodVisibilityDecoded['events'] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$planetParadeOutput = [];
|
||||||
|
$planetParadeExitCode = 0;
|
||||||
|
$planetParadeArgs = [
|
||||||
|
$pythonScriptPath,
|
||||||
|
'planet_parades_for_month',
|
||||||
|
(string) $chartLatitude,
|
||||||
|
(string) $chartLongitude,
|
||||||
|
(string) $chartElevation,
|
||||||
|
(string) $selectedYear,
|
||||||
|
(string) $selectedMonth,
|
||||||
|
$chartTimezone,
|
||||||
|
(string) $planetParadeSampleOffsetMinutes,
|
||||||
|
(string) $planetParadeMinAltitudeDeg,
|
||||||
|
(string) $planetParadeMaxSunAltitudeDeg,
|
||||||
|
(string) $planetParadeMaxAzimuthSpanDeg,
|
||||||
|
(string) $planetParadeMinPlanetCount,
|
||||||
|
(string) $planetParadeMinConsecutiveDays,
|
||||||
|
];
|
||||||
|
$planetParadeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetParadeArgs)) . ' 2>&1';
|
||||||
|
exec($planetParadeCommand, $planetParadeOutput, $planetParadeExitCode);
|
||||||
|
$planetParadeRaw = trim(implode("\n", $planetParadeOutput));
|
||||||
|
$planetParadeDecoded = json_decode($planetParadeRaw, true);
|
||||||
|
if (is_array($planetParadeDecoded) && ($planetParadeDecoded['ok'] ?? false)) {
|
||||||
|
$planetParadeEvents = is_array($planetParadeDecoded['events'] ?? null) ? $planetParadeDecoded['events'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$planetConstellationChangeOutput = [];
|
||||||
|
$planetConstellationChangeExitCode = 0;
|
||||||
|
$planetConstellationChangeArgs = [
|
||||||
|
$pythonScriptPath,
|
||||||
|
'planet_constellation_changes_for_month',
|
||||||
|
(string) $selectedYear,
|
||||||
|
(string) $selectedMonth,
|
||||||
|
$chartTimezone,
|
||||||
|
];
|
||||||
|
$planetConstellationChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConstellationChangeArgs)) . ' 2>&1';
|
||||||
|
exec($planetConstellationChangeCommand, $planetConstellationChangeOutput, $planetConstellationChangeExitCode);
|
||||||
|
$planetConstellationChangeRaw = trim(implode("\n", $planetConstellationChangeOutput));
|
||||||
|
$planetConstellationChangeDecoded = json_decode($planetConstellationChangeRaw, true);
|
||||||
|
if (is_array($planetConstellationChangeDecoded) && ($planetConstellationChangeDecoded['ok'] ?? false)) {
|
||||||
|
$planetConstellationChangeEvents = is_array($planetConstellationChangeDecoded['events'] ?? null) ? $planetConstellationChangeDecoded['events'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
$moonOccultationStars = [];
|
$moonOccultationStars = [];
|
||||||
try {
|
try {
|
||||||
if (isset($pdo)) {
|
if (isset($pdo)) {
|
||||||
@@ -1054,6 +1328,8 @@ foreach ($jupiterMoonSideEvents as $jupiterMoonSideEvent) {
|
|||||||
'date' => (string) ($jupiterMoonSideEvent['local_date'] ?? ''),
|
'date' => (string) ($jupiterMoonSideEvent['local_date'] ?? ''),
|
||||||
'time' => (string) ($jupiterMoonSideEvent['local_time'] ?? ''),
|
'time' => (string) ($jupiterMoonSideEvent['local_time'] ?? ''),
|
||||||
'local_iso' => (string) ($jupiterMoonSideEvent['local_iso'] ?? ''),
|
'local_iso' => (string) ($jupiterMoonSideEvent['local_iso'] ?? ''),
|
||||||
|
'end_local_iso' => (string) ($jupiterMoonSideEvent['end_local_iso'] ?? ''),
|
||||||
|
'duration_minutes' => isset($jupiterMoonSideEvent['duration_minutes']) ? (int) $jupiterMoonSideEvent['duration_minutes'] : null,
|
||||||
'type' => 'jupiter_moons_one_side',
|
'type' => 'jupiter_moons_one_side',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -1140,10 +1416,42 @@ foreach ($mercuryGoodVisibilityEvents as $visibilityEvent) {
|
|||||||
'date' => (string) ($visibilityEvent['local_date'] ?? ''),
|
'date' => (string) ($visibilityEvent['local_date'] ?? ''),
|
||||||
'time' => (string) ($visibilityEvent['local_time'] ?? ''),
|
'time' => (string) ($visibilityEvent['local_time'] ?? ''),
|
||||||
'local_iso' => (string) ($visibilityEvent['local_iso'] ?? ''),
|
'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,
|
||||||
'type' => 'mercury_good_visibility',
|
'type' => 'mercury_good_visibility',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($planetParadeEvents as $paradeEvent) {
|
||||||
|
if (!is_array($paradeEvent)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthEventList[] = [
|
||||||
|
'event' => (string) ($paradeEvent['label'] ?? 'Planetenparade'),
|
||||||
|
'date' => (string) ($paradeEvent['local_date'] ?? ''),
|
||||||
|
'time' => (string) ($paradeEvent['local_time'] ?? ''),
|
||||||
|
'local_iso' => (string) ($paradeEvent['local_iso'] ?? ''),
|
||||||
|
'end_local_iso' => (string) ($paradeEvent['end_local_iso'] ?? ''),
|
||||||
|
'duration_minutes' => isset($paradeEvent['duration_minutes']) ? (int) $paradeEvent['duration_minutes'] : null,
|
||||||
|
'type' => 'planet_parade',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($planetConstellationChangeEvents as $changeEvent) {
|
||||||
|
if (!is_array($changeEvent)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthEventList[] = [
|
||||||
|
'event' => (string) ($changeEvent['label'] ?? 'Planet wechselt Sternbild'),
|
||||||
|
'date' => (string) ($changeEvent['local_date'] ?? ''),
|
||||||
|
'time' => (string) ($changeEvent['local_time'] ?? ''),
|
||||||
|
'local_iso' => (string) ($changeEvent['local_iso'] ?? ''),
|
||||||
|
'type' => 'planet_constellation_change',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($moonOccultations as $occultationEvent) {
|
foreach ($moonOccultations as $occultationEvent) {
|
||||||
if (!is_array($occultationEvent)) {
|
if (!is_array($occultationEvent)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1227,6 +1535,9 @@ usort(
|
|||||||
static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? ''))
|
static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? ''))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$observationTipInsertSql = buildObservationTipInsertSql($monthEventList, new DateTimeZone($chartTimezone));
|
||||||
|
$observationTipCalendarIcs = buildObservationTipCalendarIcs($monthEventList, new DateTimeZone($chartTimezone));
|
||||||
|
|
||||||
$chartConfig = [
|
$chartConfig = [
|
||||||
'monthName' => $months[$selectedMonth],
|
'monthName' => $months[$selectedMonth],
|
||||||
'year' => $selectedYear,
|
'year' => $selectedYear,
|
||||||
@@ -1256,6 +1567,8 @@ $planetDataJson = json_encode($planetData, JSON_UNESCAPED_UNICODE | JSON_UNESCAP
|
|||||||
$moonDataJson = json_encode($moonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$moonDataJson = json_encode($moonData, 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);
|
||||||
$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);
|
||||||
|
$observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatsereignisse-%04d-%02d.ics', $selectedYear, $selectedMonth), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<section class="card monatsvorhersage-card">
|
<section class="card monatsvorhersage-card">
|
||||||
@@ -1358,9 +1671,30 @@ $starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_U
|
|||||||
<h3>Ereignisse <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
|
<h3>Ereignisse <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
|
||||||
<p>Chronologische Monatsliste fuer <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?></p>
|
<p>Chronologische Monatsliste fuer <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="monatskarte-export">
|
||||||
|
<?php if ($observationTipInsertSql !== ''): ?>
|
||||||
|
<button type="button" class="monatskarte-export-button" id="toggleObservationTipSql">
|
||||||
|
SQL-Insert anzeigen
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($observationTipCalendarIcs !== ''): ?>
|
||||||
|
<button type="button" class="monatskarte-export-button" id="downloadObservationTipIcal">
|
||||||
|
iCal herunterladen
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="monatskarte-footer">
|
<div class="monatskarte-footer">
|
||||||
|
<?php if ($observationTipInsertSql !== ''): ?>
|
||||||
|
<div class="monatskarte-sql-export" id="observationTipSqlPanel" hidden>
|
||||||
|
<label class="monatskarte-sql-label" for="observationTipSqlOutput">SQL fuer `astro_sonobs_observationtips`</label>
|
||||||
|
<textarea id="observationTipSqlOutput" readonly><?= htmlspecialchars($observationTipInsertSql, ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||||
|
<div class="monatskarte-sql-actions">
|
||||||
|
<button type="button" class="monatskarte-export-button" id="copyObservationTipSql">SQL kopieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($monthEventList): ?>
|
<?php if ($monthEventList): ?>
|
||||||
<div class="riseset-grid" style="grid-template-columns: 1.5fr 1fr 1fr; row-gap: 10px;">
|
<div class="riseset-grid" style="grid-template-columns: 1.5fr 1fr 1fr; row-gap: 10px;">
|
||||||
<span class="riseset-label riseset-dim">Ereignis</span>
|
<span class="riseset-label riseset-dim">Ereignis</span>
|
||||||
@@ -1478,6 +1812,54 @@ $starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_U
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monatskarte-export {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatskarte-export-button {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: 1px solid rgba(150, 190, 255, 0.72);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(24, 43, 83, 0.92);
|
||||||
|
color: rgba(245, 248, 255, 0.96);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatskarte-sql-export {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(108, 145, 220, 0.22);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(6, 11, 22, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatskarte-sql-label {
|
||||||
|
color: rgba(235, 242, 255, 0.95);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatskarte-sql-export textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid rgba(108, 145, 220, 0.28);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(3, 7, 14, 0.96);
|
||||||
|
color: rgba(233, 239, 250, 0.94);
|
||||||
|
font: 13px/1.5 Consolas, Monaco, monospace;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatskarte-sql-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.monatskarte-stage {
|
.monatskarte-stage {
|
||||||
padding: 0 20px 20px;
|
padding: 0 20px 20px;
|
||||||
}
|
}
|
||||||
@@ -1546,6 +1928,62 @@ const MONTH_CHART_PLANETS = <?= $planetDataJson ?: '[]' ?>;
|
|||||||
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
|
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
|
||||||
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
||||||
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
||||||
|
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
|
||||||
|
const OBSERVATION_TIP_ICAL_FILENAME = <?= $observationTipCalendarFilenameJson ?: '"astrotools-monatsereignisse.ics"' ?>;
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const toggleButton = document.getElementById('toggleObservationTipSql');
|
||||||
|
const copyButton = document.getElementById('copyObservationTipSql');
|
||||||
|
const icalButton = document.getElementById('downloadObservationTipIcal');
|
||||||
|
const panel = document.getElementById('observationTipSqlPanel');
|
||||||
|
const output = document.getElementById('observationTipSqlOutput');
|
||||||
|
|
||||||
|
if (toggleButton && panel) {
|
||||||
|
toggleButton.addEventListener('click', function () {
|
||||||
|
const isHidden = panel.hasAttribute('hidden');
|
||||||
|
if (isHidden) {
|
||||||
|
panel.removeAttribute('hidden');
|
||||||
|
toggleButton.textContent = 'SQL-Insert ausblenden';
|
||||||
|
} else {
|
||||||
|
panel.setAttribute('hidden', '');
|
||||||
|
toggleButton.textContent = 'SQL-Insert anzeigen';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyButton && output && navigator.clipboard) {
|
||||||
|
copyButton.addEventListener('click', async function () {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(output.value);
|
||||||
|
copyButton.textContent = 'SQL kopiert';
|
||||||
|
window.setTimeout(function () {
|
||||||
|
copyButton.textContent = 'SQL kopieren';
|
||||||
|
}, 1800);
|
||||||
|
} catch (error) {
|
||||||
|
copyButton.textContent = 'Kopieren fehlgeschlagen';
|
||||||
|
window.setTimeout(function () {
|
||||||
|
copyButton.textContent = 'SQL kopieren';
|
||||||
|
}, 1800);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icalButton && OBSERVATION_TIP_ICAL) {
|
||||||
|
icalButton.addEventListener('click', function () {
|
||||||
|
const blob = new Blob([OBSERVATION_TIP_ICAL], { type: 'text/calendar;charset=utf-8' });
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = objectUrl;
|
||||||
|
link.download = OBSERVATION_TIP_ICAL_FILENAME || 'astrotools-monatsereignisse.ics';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.setTimeout(function () {
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const imageEl = document.getElementById('monatsphasenImage');
|
const imageEl = document.getElementById('monatsphasenImage');
|
||||||
|
|||||||
Binary file not shown.
+400
-2
@@ -273,6 +273,26 @@ def body_altitude_deg(
|
|||||||
return float(hor.altitude)
|
return float(hor.altitude)
|
||||||
|
|
||||||
|
|
||||||
|
def body_horizontal_coords(
|
||||||
|
body: astronomy.Body,
|
||||||
|
observer: astronomy.Observer,
|
||||||
|
dt_utc: datetime,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
time_value = dt_to_time(dt_utc)
|
||||||
|
eq = astronomy.Equator(body, time_value, observer, True, True)
|
||||||
|
hor = astronomy.Horizon(time_value, observer, eq.ra, eq.dec, astronomy.Refraction.Normal)
|
||||||
|
return float(hor.altitude), float(hor.azimuth)
|
||||||
|
|
||||||
|
|
||||||
|
def body_constellation_info(
|
||||||
|
body: astronomy.Body,
|
||||||
|
dt_utc: datetime,
|
||||||
|
) -> astronomy.ConstellationInfo:
|
||||||
|
time_value = dt_to_time(dt_utc)
|
||||||
|
equ = astronomy.EquatorFromVector(astronomy.GeoVector(body, time_value, True))
|
||||||
|
return astronomy.Constellation(equ.ra, equ.dec)
|
||||||
|
|
||||||
|
|
||||||
def serialize_body_position(
|
def serialize_body_position(
|
||||||
key: str,
|
key: str,
|
||||||
label: str,
|
label: str,
|
||||||
@@ -419,7 +439,13 @@ def action_mercury_good_visibility_for_month(args: list[str]) -> dict:
|
|||||||
"local_iso": start_local.isoformat(),
|
"local_iso": start_local.isoformat(),
|
||||||
"local_date": start_local.strftime("%d.%m.%Y"),
|
"local_date": start_local.strftime("%d.%m.%Y"),
|
||||||
"local_time": start_local.strftime("%H:%M"),
|
"local_time": start_local.strftime("%H:%M"),
|
||||||
|
"end_local_iso": end_day[f"{period_key}_local"].isoformat() if end_day[f"{period_key}_local"] is not None else start_local.isoformat(),
|
||||||
|
"end_local_time": end_day[f"{period_key}_local"].strftime("%H:%M") if end_day[f"{period_key}_local"] is not None else start_local.strftime("%H:%M"),
|
||||||
"end_local_date": end_day["date"],
|
"end_local_date": end_day["date"],
|
||||||
|
"duration_minutes": max(
|
||||||
|
1,
|
||||||
|
int(round((((end_day[f"{period_key}_local"] or start_local) - start_local).total_seconds()) / 60.0)),
|
||||||
|
),
|
||||||
"duration_days": duration_days,
|
"duration_days": duration_days,
|
||||||
})
|
})
|
||||||
start_index = None
|
start_index = None
|
||||||
@@ -446,6 +472,368 @@ def action_mercury_good_visibility_for_month(args: list[str]) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def action_planet_parades_for_month(args: list[str]) -> dict:
|
||||||
|
if len(args) != 12:
|
||||||
|
fail(
|
||||||
|
"Aktion planet_parades_for_month erwartet 12 Argumente: latitude longitude elevation year month timezone sample_offset_minutes min_planet_alt_deg max_sun_alt_deg max_azimuth_span_deg min_planet_count min_consecutive_days",
|
||||||
|
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])
|
||||||
|
month = int(args[4])
|
||||||
|
sample_offset_minutes = int(args[6])
|
||||||
|
min_planet_count = int(args[10])
|
||||||
|
min_consecutive_days = int(args[11])
|
||||||
|
except ValueError as exc:
|
||||||
|
fail("Jahr, Monat oder Parade-Parameter sind ungueltig.", extra={"details": str(exc), "argv": args})
|
||||||
|
|
||||||
|
if month < 1 or month > 12:
|
||||||
|
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
|
||||||
|
|
||||||
|
timezone_name = args[5]
|
||||||
|
min_planet_alt_deg = parse_float(args[7], "Minimale Planetenhoehe")
|
||||||
|
max_sun_alt_deg = parse_float(args[8], "Maximale Sonnenhoehe")
|
||||||
|
max_azimuth_span_deg = parse_float(args[9], "Maximale Azimutspanne")
|
||||||
|
|
||||||
|
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, month, 1, 0, 0, 0, tzinfo=tz)
|
||||||
|
if month == 12:
|
||||||
|
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
|
||||||
|
else:
|
||||||
|
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
|
||||||
|
|
||||||
|
planet_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"),
|
||||||
|
]
|
||||||
|
|
||||||
|
daily_checks: list[dict] = []
|
||||||
|
current_local = local_start
|
||||||
|
while current_local < local_end:
|
||||||
|
day_start_utc = current_local.astimezone(timezone.utc)
|
||||||
|
next_day_utc = (current_local + timedelta(days=1)).astimezone(timezone.utc)
|
||||||
|
start_time = dt_to_time(day_start_utc)
|
||||||
|
|
||||||
|
sunrise = search_event(astronomy.Body.Sun, astronomy.Direction.Rise, observer, start_time, next_day_utc)
|
||||||
|
sunset = search_event(astronomy.Body.Sun, astronomy.Direction.Set, observer, start_time, next_day_utc)
|
||||||
|
|
||||||
|
def build_period(period_key: str, sample_local: datetime | None) -> dict:
|
||||||
|
if sample_local is None:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"sample_local": None,
|
||||||
|
"sun_alt_deg": None,
|
||||||
|
"visible_planets": [],
|
||||||
|
"azimuth_span_deg": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
sample_utc = sample_local.astimezone(timezone.utc)
|
||||||
|
sun_alt_deg = body_altitude_deg(astronomy.Body.Sun, observer, sample_utc)
|
||||||
|
visible_planets = []
|
||||||
|
for label, body, key in planet_defs:
|
||||||
|
planet_alt_deg, planet_az_deg = body_horizontal_coords(body, observer, sample_utc)
|
||||||
|
if planet_alt_deg >= min_planet_alt_deg:
|
||||||
|
visible_planets.append({
|
||||||
|
"key": key,
|
||||||
|
"label": label,
|
||||||
|
"altitude_deg": planet_alt_deg,
|
||||||
|
"azimuth_deg": planet_az_deg,
|
||||||
|
})
|
||||||
|
|
||||||
|
azimuth_span_deg = None
|
||||||
|
if visible_planets:
|
||||||
|
azimuths = sorted(planet["azimuth_deg"] for planet in visible_planets)
|
||||||
|
if len(azimuths) == 1:
|
||||||
|
azimuth_span_deg = 0.0
|
||||||
|
else:
|
||||||
|
wrap_gaps = [
|
||||||
|
azimuths[index + 1] - azimuths[index]
|
||||||
|
for index in range(len(azimuths) - 1)
|
||||||
|
]
|
||||||
|
wrap_gaps.append((azimuths[0] + 360.0) - azimuths[-1])
|
||||||
|
azimuth_span_deg = 360.0 - max(wrap_gaps)
|
||||||
|
|
||||||
|
is_ok = (
|
||||||
|
sun_alt_deg <= max_sun_alt_deg
|
||||||
|
and len(visible_planets) >= min_planet_count
|
||||||
|
and azimuth_span_deg is not None
|
||||||
|
and azimuth_span_deg <= max_azimuth_span_deg
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": is_ok,
|
||||||
|
"sample_local": sample_local,
|
||||||
|
"sun_alt_deg": sun_alt_deg,
|
||||||
|
"visible_planets": visible_planets,
|
||||||
|
"azimuth_span_deg": azimuth_span_deg,
|
||||||
|
}
|
||||||
|
|
||||||
|
morning_local = None
|
||||||
|
if sunrise is not None:
|
||||||
|
sunrise_local = time_to_datetime(sunrise).astimezone(tz)
|
||||||
|
morning_local = sunrise_local - timedelta(minutes=sample_offset_minutes)
|
||||||
|
|
||||||
|
evening_local = None
|
||||||
|
if sunset is not None:
|
||||||
|
sunset_local = time_to_datetime(sunset).astimezone(tz)
|
||||||
|
evening_local = sunset_local + timedelta(minutes=sample_offset_minutes)
|
||||||
|
|
||||||
|
daily_checks.append({
|
||||||
|
"date": current_local.strftime("%d.%m.%Y"),
|
||||||
|
"morning": build_period("morning", morning_local),
|
||||||
|
"evening": build_period("evening", evening_local),
|
||||||
|
})
|
||||||
|
|
||||||
|
current_local += timedelta(days=1)
|
||||||
|
|
||||||
|
def build_windows(period_key: str, label_text: str) -> list[dict]:
|
||||||
|
events = []
|
||||||
|
start_index = None
|
||||||
|
|
||||||
|
for index, day in enumerate(daily_checks):
|
||||||
|
period = day[period_key]
|
||||||
|
is_ok = bool(period["ok"])
|
||||||
|
if is_ok and start_index is None:
|
||||||
|
start_index = index
|
||||||
|
|
||||||
|
is_last = index == len(daily_checks) - 1
|
||||||
|
if start_index is not None and (not is_ok or is_last):
|
||||||
|
end_index = index if (is_ok and is_last) else index - 1
|
||||||
|
duration_days = end_index - start_index + 1
|
||||||
|
if duration_days >= min_consecutive_days:
|
||||||
|
window_days = daily_checks[start_index:end_index + 1]
|
||||||
|
start_day = window_days[0]
|
||||||
|
end_day = window_days[-1]
|
||||||
|
start_period = start_day[period_key]
|
||||||
|
start_local = start_period["sample_local"]
|
||||||
|
|
||||||
|
if start_local is not None:
|
||||||
|
range_text = (
|
||||||
|
start_day["date"]
|
||||||
|
if start_index == end_index
|
||||||
|
else f'{start_day["date"]} bis {end_day["date"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
common_labels: set[str] | None = None
|
||||||
|
max_visible_labels: list[str] = []
|
||||||
|
max_visible_count = 0
|
||||||
|
min_span_deg = None
|
||||||
|
|
||||||
|
for window_day in window_days:
|
||||||
|
day_period = window_day[period_key]
|
||||||
|
visible_labels = [planet["label"] for planet in day_period["visible_planets"]]
|
||||||
|
visible_set = set(visible_labels)
|
||||||
|
common_labels = visible_set if common_labels is None else (common_labels & visible_set)
|
||||||
|
if len(visible_labels) > max_visible_count:
|
||||||
|
max_visible_count = len(visible_labels)
|
||||||
|
max_visible_labels = visible_labels
|
||||||
|
span_value = day_period["azimuth_span_deg"]
|
||||||
|
if span_value is not None:
|
||||||
|
min_span_deg = span_value if min_span_deg is None else min(min_span_deg, span_value)
|
||||||
|
|
||||||
|
parade_labels = sorted(common_labels) if common_labels else max_visible_labels
|
||||||
|
planet_text = ", ".join(parade_labels)
|
||||||
|
count_text = f"{max_visible_count} Planeten"
|
||||||
|
detail_parts = [count_text]
|
||||||
|
if planet_text:
|
||||||
|
detail_parts.append(planet_text)
|
||||||
|
detail_parts.append(range_text)
|
||||||
|
|
||||||
|
events.append({
|
||||||
|
"period": period_key,
|
||||||
|
"label": f"Planetenparade am {label_text} ({'; '.join(detail_parts)})",
|
||||||
|
"planet_count": max_visible_count,
|
||||||
|
"planet_labels": parade_labels,
|
||||||
|
"best_azimuth_span_deg": min_span_deg,
|
||||||
|
"utc_iso": start_local.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||||
|
"local_iso": start_local.isoformat(),
|
||||||
|
"local_date": start_local.strftime("%d.%m.%Y"),
|
||||||
|
"local_time": start_local.strftime("%H:%M"),
|
||||||
|
"end_local_iso": end_day[period_key]["sample_local"].isoformat() if end_day[period_key]["sample_local"] is not None else start_local.isoformat(),
|
||||||
|
"end_local_time": end_day[period_key]["sample_local"].strftime("%H:%M") if end_day[period_key]["sample_local"] is not None else start_local.strftime("%H:%M"),
|
||||||
|
"end_local_date": end_day["date"],
|
||||||
|
"duration_minutes": max(
|
||||||
|
1,
|
||||||
|
int(round((((end_day[period_key]["sample_local"] or start_local) - start_local).total_seconds()) / 60.0)),
|
||||||
|
),
|
||||||
|
"duration_days": duration_days,
|
||||||
|
})
|
||||||
|
start_index = None
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
events = build_windows("morning", "Morgen")
|
||||||
|
events.extend(build_windows("evening", "Abend"))
|
||||||
|
events.sort(key=lambda item: item["local_iso"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"action": "planet_parades_for_month",
|
||||||
|
"selected": {
|
||||||
|
"year": year,
|
||||||
|
"month": month,
|
||||||
|
"timezone": timezone_name,
|
||||||
|
"sample_offset_minutes": sample_offset_minutes,
|
||||||
|
"min_planet_alt_deg": min_planet_alt_deg,
|
||||||
|
"max_sun_alt_deg": max_sun_alt_deg,
|
||||||
|
"max_azimuth_span_deg": max_azimuth_span_deg,
|
||||||
|
"min_planet_count": min_planet_count,
|
||||||
|
"min_consecutive_days": min_consecutive_days,
|
||||||
|
},
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def action_planet_constellation_changes_for_month(args: list[str]) -> dict:
|
||||||
|
if len(args) != 3:
|
||||||
|
fail(
|
||||||
|
"Aktion planet_constellation_changes_for_month erwartet 3 Argumente: year month timezone",
|
||||||
|
extra={"argv": args},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
year = int(args[0])
|
||||||
|
month = int(args[1])
|
||||||
|
except ValueError as exc:
|
||||||
|
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
|
||||||
|
|
||||||
|
if month < 1 or month > 12:
|
||||||
|
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
|
||||||
|
|
||||||
|
timezone_name = args[2]
|
||||||
|
try:
|
||||||
|
tz = ZoneInfo(timezone_name)
|
||||||
|
except Exception as exc:
|
||||||
|
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
|
||||||
|
|
||||||
|
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
|
||||||
|
if month == 12:
|
||||||
|
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
|
||||||
|
utc_next_month = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
|
||||||
|
utc_next_month = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
utc_start = datetime(year, month, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||||
|
utc_end = utc_next_month - timedelta(seconds=1)
|
||||||
|
|
||||||
|
constellation_names = {
|
||||||
|
"Aries": "Widder",
|
||||||
|
"Taurus": "Stier",
|
||||||
|
"Gemini": "Zwillinge",
|
||||||
|
"Cancer": "Krebs",
|
||||||
|
"Leo": "Loewe",
|
||||||
|
"Virgo": "Jungfrau",
|
||||||
|
"Libra": "Waage",
|
||||||
|
"Scorpius": "Skorpion",
|
||||||
|
"Sagittarius": "Schuetze",
|
||||||
|
"Capricornus": "Steinbock",
|
||||||
|
"Aquarius": "Wassermann",
|
||||||
|
"Pisces": "Fische",
|
||||||
|
"Ophiuchus": "Schlangentraeger",
|
||||||
|
"Cetus": "Walfisch",
|
||||||
|
}
|
||||||
|
|
||||||
|
planet_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 display_constellation_name(info: astronomy.ConstellationInfo) -> str:
|
||||||
|
return constellation_names.get(info.name, info.name)
|
||||||
|
|
||||||
|
def refine_change_time(body: astronomy.Body, left_utc: datetime, right_utc: datetime, left_symbol: str) -> datetime:
|
||||||
|
left = left_utc
|
||||||
|
right = right_utc
|
||||||
|
|
||||||
|
for _ in range(40):
|
||||||
|
mid = left + (right - left) / 2
|
||||||
|
mid_symbol = body_constellation_info(body, mid).symbol
|
||||||
|
if mid_symbol == left_symbol:
|
||||||
|
left = mid
|
||||||
|
else:
|
||||||
|
right = mid
|
||||||
|
|
||||||
|
return right
|
||||||
|
|
||||||
|
events: list[dict] = []
|
||||||
|
scan_step = timedelta(hours=6)
|
||||||
|
|
||||||
|
for label, body, key in planet_defs:
|
||||||
|
left_utc = utc_start
|
||||||
|
left_info = body_constellation_info(body, left_utc)
|
||||||
|
current_utc = min(utc_end, left_utc + scan_step)
|
||||||
|
|
||||||
|
while current_utc <= utc_end:
|
||||||
|
current_info = body_constellation_info(body, current_utc)
|
||||||
|
if current_info.symbol != left_info.symbol:
|
||||||
|
change_utc = refine_change_time(body, left_utc, current_utc, left_info.symbol)
|
||||||
|
before_info = body_constellation_info(body, change_utc - timedelta(seconds=1))
|
||||||
|
after_info = body_constellation_info(body, change_utc)
|
||||||
|
local_dt = change_utc.astimezone(tz)
|
||||||
|
|
||||||
|
if local_start <= local_dt < local_end:
|
||||||
|
from_name = display_constellation_name(before_info)
|
||||||
|
to_name = display_constellation_name(after_info)
|
||||||
|
events.append({
|
||||||
|
"planet_key": key,
|
||||||
|
"planet_label": label,
|
||||||
|
"from_constellation_symbol": before_info.symbol,
|
||||||
|
"from_constellation_name": from_name,
|
||||||
|
"to_constellation_symbol": after_info.symbol,
|
||||||
|
"to_constellation_name": to_name,
|
||||||
|
"label": f"{label} wechselt von {from_name} nach {to_name}",
|
||||||
|
"utc_iso": change_utc.isoformat().replace("+00:00", "Z"),
|
||||||
|
"local_iso": local_dt.isoformat(),
|
||||||
|
"local_date": local_dt.strftime("%d.%m.%Y"),
|
||||||
|
"local_time": local_dt.strftime("%H:%M"),
|
||||||
|
})
|
||||||
|
|
||||||
|
left_utc = change_utc + timedelta(seconds=1)
|
||||||
|
left_info = body_constellation_info(body, left_utc)
|
||||||
|
current_utc = min(utc_end, left_utc + scan_step)
|
||||||
|
continue
|
||||||
|
|
||||||
|
left_utc = current_utc
|
||||||
|
left_info = current_info
|
||||||
|
current_utc = min(utc_end, current_utc + scan_step)
|
||||||
|
if current_utc == left_utc:
|
||||||
|
break
|
||||||
|
|
||||||
|
events.sort(key=lambda item: item["local_iso"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"action": "planet_constellation_changes_for_month",
|
||||||
|
"selected": {
|
||||||
|
"year": year,
|
||||||
|
"month": month,
|
||||||
|
"timezone": timezone_name,
|
||||||
|
"utc_scan_start": utc_start.isoformat().replace("+00:00", "Z"),
|
||||||
|
"utc_scan_end": utc_end.isoformat().replace("+00:00", "Z"),
|
||||||
|
},
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def action_month_sky_context(args: list[str]) -> dict:
|
def action_month_sky_context(args: list[str]) -> dict:
|
||||||
if len(args) != 8:
|
if len(args) != 8:
|
||||||
fail(
|
fail(
|
||||||
@@ -3619,7 +4007,7 @@ def action_satellite_passes(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", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "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", "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", "moon_star_occultations_for_month"]})
|
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "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", "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", "moon_star_occultations_for_month"]})
|
||||||
|
|
||||||
action = sys.argv[1]
|
action = sys.argv[1]
|
||||||
args = sys.argv[2:]
|
args = sys.argv[2:]
|
||||||
@@ -3739,12 +4127,22 @@ def main() -> None:
|
|||||||
print(json.dumps(result, ensure_ascii=False))
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if action == "planet_parades_for_month":
|
||||||
|
result = action_planet_parades_for_month(args)
|
||||||
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
|
if action == "planet_constellation_changes_for_month":
|
||||||
|
result = action_planet_constellation_changes_for_month(args)
|
||||||
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
|
||||||
if action == "moon_star_occultations_for_month":
|
if action == "moon_star_occultations_for_month":
|
||||||
result = action_moon_star_occultations_for_month(args)
|
result = action_moon_star_occultations_for_month(args)
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
return
|
return
|
||||||
|
|
||||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "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", "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", "moon_star_occultations_for_month"]})
|
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "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", "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", "moon_star_occultations_for_month"]})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
# Monatsvorschau TODO
|
# Monatsvorschau TODO
|
||||||
|
|
||||||
- Besonders enge Mond-Stern-Begegnungen heller Sterne ergaenzen, auch ohne Bedeckung
|
|
||||||
- Helle Stern-/Planeten-Begegnungen aufnehmen, z. B. Venus nahe Regulus oder Mars nahe Spica
|
|
||||||
- Auffaellige Planeten-Sichtbarkeit redaktionell ausgeben, z. B. Morgenstern/Abendstern, ganze Nacht sichtbar
|
|
||||||
- Supermond / Minimond als populaere Monatsereignisse pruefen
|
|
||||||
- Sichtbare ISS-Ueberfluege fuer den Standort ergaenzen
|
- Sichtbare ISS-Ueberfluege fuer den Standort ergaenzen
|
||||||
- Sehr dunkle Naechte um Neumond / Milchstrassen-Beobachtungsfenster als Hinweis pruefen
|
|
||||||
- Hesiodusstrahl als eigenes Mondlichtphaenomen fuer die Monatsvorschau ergaenzen
|
- Hesiodusstrahl als eigenes Mondlichtphaenomen fuer die Monatsvorschau ergaenzen
|
||||||
|
|||||||
Reference in New Issue
Block a user