Monatsvorhersage und Exporte erweitern
This commit is contained in:
+475
-119
@@ -14,6 +14,7 @@ $selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) :
|
|||||||
$selectedWeek = isset($_GET['week']) ? max(1, min(53, (int) $_GET['week'])) : (int) date('W');
|
$selectedWeek = isset($_GET['week']) ? max(1, min(53, (int) $_GET['week'])) : (int) date('W');
|
||||||
$selectedWeekYear = isset($_GET['week_year']) ? (int) $_GET['week_year'] : $currentYear;
|
$selectedWeekYear = isset($_GET['week_year']) ? (int) $_GET['week_year'] : $currentYear;
|
||||||
$shouldRunForecast = isset($_GET['run']) && $_GET['run'] === '1';
|
$shouldRunForecast = isset($_GET['run']) && $_GET['run'] === '1';
|
||||||
|
$includeComets = isset($_GET['include_comets']) && $_GET['include_comets'] === '1';
|
||||||
if (!isset($_SESSION['observation_tip_insert_token']) || !is_string($_SESSION['observation_tip_insert_token'])) {
|
if (!isset($_SESSION['observation_tip_insert_token']) || !is_string($_SESSION['observation_tip_insert_token'])) {
|
||||||
$_SESSION['observation_tip_insert_token'] = bin2hex(random_bytes(16));
|
$_SESSION['observation_tip_insert_token'] = bin2hex(random_bytes(16));
|
||||||
}
|
}
|
||||||
@@ -45,10 +46,10 @@ $observationTipInsertToken = $_SESSION['observation_tip_insert_token'];
|
|||||||
// - Zusammengeführte Monatsereignisliste für Übersicht, SQL-Export und iCal
|
// - Zusammengeführte Monatsereignisliste für Übersicht, SQL-Export und iCal
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Grenzwerte fuer Sternkarte und Objektanzeige
|
// Grenzwerte für Sternkarte und Objektanzeige
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Maximale Helligkeit (Magnitude) der Sterne, die aus der Datenbank geladen
|
// Maximale Helligkeit (Magnitude) der Sterne, die aus der Datenbank geladen
|
||||||
// werden. Hoehere Werte laden mehr, aber auch schwaechere Sterne.
|
// werden. Höhere Werte laden mehr, aber auch schwächere Sterne.
|
||||||
$starDatabaseMagnitudeLimit = 6.0;
|
$starDatabaseMagnitudeLimit = 6.0;
|
||||||
|
|
||||||
// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar
|
// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar
|
||||||
@@ -67,95 +68,95 @@ $messierDatabaseMagnitudeLimit = 2.0;
|
|||||||
$messierDisplayMagnitudeLimit = 1.0;
|
$messierDisplayMagnitudeLimit = 1.0;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Grenzwerte fuer Monatsereignisse
|
// Grenzwerte für Monatsereignisse
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und Planeten.
|
// Maximaler Winkelabstand für Begegnungen zwischen Mond und Planeten.
|
||||||
$moonPlanetMaxSeparationDeg = 1.5;
|
$moonPlanetMaxSeparationDeg = 1.5;
|
||||||
|
|
||||||
// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und ausgewaehlten
|
// Maximaler Winkelabstand für Begegnungen zwischen Mond und ausgewählten
|
||||||
// Sternhaufen wie Plejaden oder Praesepe.
|
// Sternhaufen wie Plejaden oder Praesepe.
|
||||||
$moonDeepSkyMaxSeparationDeg = 1.0;
|
$moonDeepSkyMaxSeparationDeg = 1.0;
|
||||||
|
|
||||||
// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und
|
// Maximaler Winkelabstand für Begegnungen zwischen hellen Planeten und
|
||||||
// ausgewaehlten hellen Sternen wie Regulus oder Spica.
|
// ausgewählten hellen Sternen wie Regulus oder Spica.
|
||||||
$planetBrightStarMaxSeparationDeg = 1.0;
|
$planetBrightStarMaxSeparationDeg = 1.0;
|
||||||
|
|
||||||
// Maximaler Winkelabstand fuer Planet-Planet-Konjunktionen.
|
// Maximaler Winkelabstand für Planet-Planet-Konjunktionen.
|
||||||
$planetConjunctionMaxSeparationDeg = 1.0;
|
$planetConjunctionMaxSeparationDeg = 1.0;
|
||||||
|
|
||||||
// Nur Sterne bis zu dieser Helligkeit werden fuer Mondbedeckungen
|
// Nur Sterne bis zu dieser Helligkeit werden für Mondbedeckungen
|
||||||
// beruecksichtigt.
|
// berücksichtigt.
|
||||||
$moonOccultationMaxMag = 2.0;
|
$moonOccultationMaxMag = 2.0;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Schwellen fuer "Gute Satelliten-Ueberfluege"
|
// Schwellen für "Gute Satelliten-Überflüge"
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Es wird nur der Favoriten-Satellit des angemeldeten Users geprueft.
|
// Es wird nur der Favoriten-Satellit des angemeldeten Users geprüft.
|
||||||
//
|
//
|
||||||
// Ein Ueberflug zaehlt als guter Beobachtungstermin nur dann, wenn er am
|
// Ein Überflug zählt als guter Beobachtungstermin nur dann, wenn er am
|
||||||
// Hoehepunkt mindestens diese Hoehe ueber dem Horizont erreicht.
|
// Höhepunkt mindestens diese Höhe über dem Horizont erreicht.
|
||||||
$favoriteSatelliteBestPassMinAltitudeDeg = 80.0;
|
$favoriteSatelliteBestPassMinAltitudeDeg = 80.0;
|
||||||
|
|
||||||
// Gleichzeitig muss die Sonne zum Maximum des Ueberflugs mindestens so weit
|
// Gleichzeitig muss die Sonne zum Maximum des Überflugs mindestens so weit
|
||||||
// unter dem Horizont stehen. -12 Grad entspricht dem Beginn/Ende der
|
// unter dem Horizont stehen. -12 Grad entspricht dem Beginn/Ende der
|
||||||
// nautischen Daemmerung und sorgt fuer ausreichend dunklen Himmel.
|
// nautischen Dämmerung und sorgt für ausreichend dunklen Himmel.
|
||||||
$favoriteSatelliteBestPassMaxSunAltitudeDeg = -14.0;
|
$favoriteSatelliteBestPassMaxSunAltitudeDeg = -14.0;
|
||||||
|
|
||||||
// Optional koennen wir die Liste auf die erste Nachthaelfte begrenzen.
|
// Optional können wir die Liste auf die erste Nachthälfte begrenzen.
|
||||||
// "Erste Nachthaelfte" ist hier bewusst pragmatisch als lokaler Abend vor
|
// "Erste Nachthälfte" ist hier bewusst pragmatisch als lokaler Abend vor
|
||||||
// Mitternacht definiert. Ueberfluege nach 00:00 Ortszeit werden damit
|
// Mitternacht definiert. Überflüge nach 00:00 Ortszeit werden damit
|
||||||
// ausgefiltert.
|
// ausgefiltert.
|
||||||
$favoriteSatelliteBestPassOnlyFirstNightHalf = true;
|
$favoriteSatelliteBestPassOnlyFirstNightHalf = true;
|
||||||
|
|
||||||
// Optional koennen wir nur besonders helle Ueberfluege anzeigen.
|
// Optional können wir nur besonders helle Überflüge anzeigen.
|
||||||
// Bei Magnituden gilt: kleinere Werte sind heller. 0.0 mag bedeutet also,
|
// Bei Magnituden gilt: kleinere Werte sind heller. 0.0 mag bedeutet also,
|
||||||
// dass nur Ueberfluege mit Peak-Helligkeit von 0 mag oder heller bleiben.
|
// dass nur Überflüge mit Peak-Helligkeit von 0 mag oder heller bleiben.
|
||||||
$favoriteSatelliteBestPassMaxPeakMagnitude = 0.0;
|
$favoriteSatelliteBestPassMaxPeakMagnitude = 0.0;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Schwellen fuer "Gute Merkur-Sichtbarkeit"
|
// Schwellen für "Gute Merkur-Sichtbarkeit"
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Gute Merkur-Sichtbarkeit wird nicht nur ueber Elongation definiert, sondern
|
// Gute Merkur-Sichtbarkeit wird nicht nur über Elongation definiert, sondern
|
||||||
// ueber echte Beobachtbarkeit in der Daemmerung.
|
// über echte Beobachtbarkeit in der Dämmerung.
|
||||||
// Geprueft wird pro Tag:
|
// Geprüft wird pro Tag:
|
||||||
// morgens eine feste Zahl Minuten VOR Sonnenaufgang,
|
// morgens eine feste Zahl Minuten VOR Sonnenaufgang,
|
||||||
// abends eine feste Zahl Minuten NACH Sonnenuntergang.
|
// abends eine feste Zahl Minuten NACH Sonnenuntergang.
|
||||||
$mercuryGoodVisibilitySampleOffsetMinutes = 45;
|
$mercuryGoodVisibilitySampleOffsetMinutes = 45;
|
||||||
|
|
||||||
// Merkur muss zum Pruefzeitpunkt mindestens diese Hoehe ueber dem Horizont
|
// Merkur muss zum Prüfzeitpunkt mindestens diese Höhe über dem Horizont
|
||||||
// erreichen, damit der Tag als gut sichtbar gilt.
|
// erreichen, damit der Tag als gut sichtbar gilt.
|
||||||
$mercuryGoodVisibilityMinAltitudeDeg = 7.0;
|
$mercuryGoodVisibilityMinAltitudeDeg = 7.0;
|
||||||
|
|
||||||
// Gleichzeitig muss die Sonne ausreichend tief unter dem Horizont stehen.
|
// Gleichzeitig muss die Sonne ausreichend tief unter dem Horizont stehen.
|
||||||
// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar waere, aber
|
// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar wäre, aber
|
||||||
// noch zu stark in der hellen Daemmerung untergeht.
|
// noch zu stark in der hellen Dämmerung untergeht.
|
||||||
$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0;
|
$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0;
|
||||||
|
|
||||||
// Erst ab dieser Anzahl aufeinanderfolgender guter Tage erzeugen wir ein
|
// Erst ab dieser Anzahl aufeinanderfolgender guter Tage erzeugen wir ein
|
||||||
// Monatsereignis. Einzelne Ausreisser-Tage werden dadurch herausgefiltert.
|
// Monatsereignis. Einzelne Ausreißer-Tage werden dadurch herausgefiltert.
|
||||||
$mercuryGoodVisibilityMinConsecutiveDays = 3;
|
$mercuryGoodVisibilityMinConsecutiveDays = 3;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Schwellen fuer "Planetenparade"
|
// Schwellen für "Planetenparade"
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Geprueft wird morgens eine feste Zahl Minuten VOR Sonnenaufgang und abends
|
// Geprüft wird morgens eine feste Zahl Minuten VOR Sonnenaufgang und abends
|
||||||
// eine feste Zahl Minuten NACH Sonnenuntergang.
|
// eine feste Zahl Minuten NACH Sonnenuntergang.
|
||||||
$planetParadeSampleOffsetMinutes = 45;
|
$planetParadeSampleOffsetMinutes = 45;
|
||||||
|
|
||||||
// Ein Planet zaehlt fuer die Parade nur dann mit, wenn er zum Pruefzeitpunkt
|
// Ein Planet zählt für die Parade nur dann mit, wenn er zum Prüfzeitpunkt
|
||||||
// mindestens diese Hoehe ueber dem Horizont erreicht.
|
// mindestens diese Höhe über dem Horizont erreicht.
|
||||||
$planetParadeMinAltitudeDeg = 7.0;
|
$planetParadeMinAltitudeDeg = 7.0;
|
||||||
|
|
||||||
// Gleichzeitig muss die Sonne bereits tief genug unter dem Horizont stehen,
|
// Gleichzeitig muss die Sonne bereits tief genug unter dem Horizont stehen,
|
||||||
// damit die Parade nicht nur rechnerisch, sondern auch praktisch sichtbar ist.
|
// damit die Parade nicht nur rechnerisch, sondern auch praktisch sichtbar ist.
|
||||||
$planetParadeMaxSunAltitudeDeg = -4.0;
|
$planetParadeMaxSunAltitudeDeg = -4.0;
|
||||||
|
|
||||||
// Die beteiligten Planeten muessen innerhalb dieses Azimutbogens liegen.
|
// Die beteiligten Planeten müssen innerhalb dieses Azimutbogens liegen.
|
||||||
// So vermeiden wir Treffer, bei denen die Planeten zu weit ueber den Himmel
|
// So vermeiden wir Treffer, bei denen die Planeten zu weit über den Himmel
|
||||||
// verteilt sind und nicht mehr als zusammenhaengende Parade wirken.
|
// verteilt sind und nicht mehr als zusammenhängende Parade wirken.
|
||||||
$planetParadeMaxAzimuthSpanDeg = 120.0;
|
$planetParadeMaxAzimuthSpanDeg = 120.0;
|
||||||
|
|
||||||
// Mindestens so viele helle Planeten muessen gleichzeitig sichtbar sein,
|
// Mindestens so viele helle Planeten müssen gleichzeitig sichtbar sein,
|
||||||
// damit wir von einer Planetenparade sprechen.
|
// damit wir von einer Planetenparade sprechen.
|
||||||
$planetParadeMinPlanetCount = 4;
|
$planetParadeMinPlanetCount = 4;
|
||||||
|
|
||||||
@@ -164,10 +165,10 @@ $planetParadeMinPlanetCount = 4;
|
|||||||
$planetParadeMinConsecutiveDays = 2;
|
$planetParadeMinConsecutiveDays = 2;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Schwellen fuer ausgewaehlte Kometen
|
// Schwellen für ausgewählte Kometen
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Gute Kometensichtbarkeit wird morgens vor Sonnenaufgang und abends nach
|
// Gute Kometensichtbarkeit wird morgens vor Sonnenaufgang und abends nach
|
||||||
// Sonnenuntergang geprueft.
|
// Sonnenuntergang geprüft.
|
||||||
$favoriteCometVisibilitySampleOffsetMinutes = 60;
|
$favoriteCometVisibilitySampleOffsetMinutes = 60;
|
||||||
|
|
||||||
// Der Komet sollte ausreichend hoch stehen, damit die Beobachtung praktisch
|
// Der Komet sollte ausreichend hoch stehen, damit die Beobachtung praktisch
|
||||||
@@ -177,7 +178,7 @@ $favoriteCometVisibilityMinAltitudeDeg = 10.0;
|
|||||||
// Die Sonne muss tief genug unter dem Horizont stehen.
|
// Die Sonne muss tief genug unter dem Horizont stehen.
|
||||||
$favoriteCometVisibilityMaxSunAltitudeDeg = -8.0;
|
$favoriteCometVisibilityMaxSunAltitudeDeg = -8.0;
|
||||||
|
|
||||||
// Optional koennen wir eine grobe Helligkeitsschwelle setzen, damit nur
|
// Optional können wir eine grobe Helligkeitsschwelle setzen, damit nur
|
||||||
// realistisch interessante Fenster erscheinen.
|
// realistisch interessante Fenster erscheinen.
|
||||||
$favoriteCometVisibilityMaxMagnitude = 10.0;
|
$favoriteCometVisibilityMaxMagnitude = 10.0;
|
||||||
|
|
||||||
@@ -273,6 +274,7 @@ $planetDataJson = '[]';
|
|||||||
$moonDataJson = 'null';
|
$moonDataJson = 'null';
|
||||||
$monthChartCometsJson = '[]';
|
$monthChartCometsJson = '[]';
|
||||||
$moonPhaseEventsJson = '[]';
|
$moonPhaseEventsJson = '[]';
|
||||||
|
$monthEventListJson = '[]';
|
||||||
$starDataErrorJson = 'null';
|
$starDataErrorJson = 'null';
|
||||||
$observationTipCalendarIcsJson = '""';
|
$observationTipCalendarIcsJson = '""';
|
||||||
$observationTipCalendarFilenameJson = json_encode(
|
$observationTipCalendarFilenameJson = json_encode(
|
||||||
@@ -1023,7 +1025,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($moonSentences !== []) {
|
if ($moonSentences !== []) {
|
||||||
$paragraphs[] = implode(' ', $moonSentences);
|
$paragraphs[] = implode("\n", $moonSentences);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1311,42 +1313,45 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
|||||||
$paragraphs[] = $eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.';
|
$paragraphs[] = $eclipseIntro . ' In diesem Monat stechen ' . $describeEvents($eclipseEvents) . ' hervor.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$meteorEvents = $findEvents(
|
$meteorCandidates = array_values(array_filter(
|
||||||
$filteredEvents,
|
$filteredEvents,
|
||||||
static fn (array $event): bool => (string) ($event['type'] ?? '') === 'meteor_shower_peak'
|
static fn (array $event): bool => (string) ($event['type'] ?? '') === 'meteor_shower_peak'
|
||||||
&& (
|
&& max(
|
||||||
(int) ($event['zhr_min_estimate'] ?? 0) >= 20
|
(int) ($event['zhr_min_estimate'] ?? 0),
|
||||||
|| (int) ($event['zhr_max_estimate'] ?? 0) >= 30
|
(int) ($event['zhr_max_estimate'] ?? 0)
|
||||||
),
|
) >= 10
|
||||||
2
|
));
|
||||||
);
|
usort($meteorCandidates, static function (array $left, array $right): int {
|
||||||
|
$leftRate = max((int) ($left['zhr_min_estimate'] ?? 0), (int) ($left['zhr_max_estimate'] ?? 0));
|
||||||
|
$rightRate = max((int) ($right['zhr_min_estimate'] ?? 0), (int) ($right['zhr_max_estimate'] ?? 0));
|
||||||
|
return $rightRate <=> $leftRate;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Der stärkste Strom wird immer genannt. Ist er kein bekannter Jahresstrom,
|
||||||
|
// ergänzen wir einen solchen, damit etwa die Leoniden nicht untergehen.
|
||||||
|
$meteorEvents = $meteorCandidates === [] ? [] : [$meteorCandidates[0]];
|
||||||
|
$establishedMeteorCodes = ['QUA', 'LYR', 'ETA', 'SDA', 'PER', 'DRA', 'ORI', 'NTA', 'STA', 'LEO', 'GEM', 'URS'];
|
||||||
|
$strongestCode = $meteorEvents === []
|
||||||
|
? ''
|
||||||
|
: strtoupper(trim((string) ($meteorEvents[0]['meteor_shower_code'] ?? '')));
|
||||||
|
if ($meteorEvents !== [] && !in_array($strongestCode, $establishedMeteorCodes, true)) {
|
||||||
|
foreach (array_slice($meteorCandidates, 1) as $meteorCandidate) {
|
||||||
|
$candidateCode = strtoupper(trim((string) ($meteorCandidate['meteor_shower_code'] ?? '')));
|
||||||
|
if (in_array($candidateCode, $establishedMeteorCodes, true)) {
|
||||||
|
$meteorEvents[] = $meteorCandidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($meteorEvents !== []) {
|
if ($meteorEvents !== []) {
|
||||||
$meteorIntro = $pickRandomText([
|
$meteorIntro = $pickRandomText([
|
||||||
'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.',
|
'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.',
|
||||||
'Auch die Meteorströme verdienen in diesem Monat Aufmerksamkeit.',
|
'Auch die Meteorströme verdienen in diesem Monat Aufmerksamkeit.',
|
||||||
'Wer nach Sternschnuppen Ausschau halten möchte, sollte auch die Meteorströme beachten.',
|
'Wer nach Sternschnuppen Ausschau halten möchte, sollte auch die Meteorströme beachten.',
|
||||||
'Ein Blick auf die Meteorstroeme kann sich in diesem Monat ebenfalls lohnen.',
|
'Ein Blick auf die Meteorströme kann sich in diesem Monat ebenfalls lohnen.',
|
||||||
], 'Freunde von Meteoren sollten zudem die Aktivitaet der Meteorstroeme im Blick behalten.');
|
], 'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.');
|
||||||
$paragraphs[] = $meteorIntro . ' Erwähnenswert sind besonders ' . $describeEvents($meteorEvents) . '.';
|
$paragraphs[] = $meteorIntro . ' Erwähnenswert sind besonders ' . $describeEvents($meteorEvents) . '.';
|
||||||
} else {
|
|
||||||
$meteorEvents = $findEvents(
|
|
||||||
$filteredEvents,
|
|
||||||
static fn (array $event): bool => (string) ($event['type'] ?? '') === 'meteor_shower_peak'
|
|
||||||
&& (
|
|
||||||
(int) ($event['zhr_min_estimate'] ?? 0) >= 10
|
|
||||||
|| (int) ($event['zhr_max_estimate'] ?? 0) >= 20
|
|
||||||
),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
if ($meteorEvents !== []) {
|
|
||||||
$meteorModerateIntro = $pickRandomText([
|
|
||||||
'Bei den Meteorströmen lohnt in diesem Monat vor allem ein Blick auf',
|
|
||||||
'Unter den Meteorströmen verdienen in diesem Monat besonders',
|
|
||||||
'Auch bei den Meteorströmen fallen in diesem Monat vor allem',
|
|
||||||
'Wer Meteorströme verfolgen möchte, sollte in diesem Monat vor allem auf',
|
|
||||||
], 'Bei den Meteorstroemen lohnt in diesem Monat vor allem ein Blick auf');
|
|
||||||
$paragraphs[] = $meteorModerateIntro . ' ' . $describeEvents($meteorEvents) . '.';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($paragraphs) === 1 && $filteredEvents !== []) {
|
if (count($paragraphs) === 1 && $filteredEvents !== []) {
|
||||||
@@ -1719,7 +1724,7 @@ if (!empty($favoriteSatellitePayload)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($favoriteCometPayload)) {
|
if ($includeComets && !empty($favoriteCometPayload)) {
|
||||||
$tempFavoriteCometFile = tempnam(sys_get_temp_dir(), 'month_comets_');
|
$tempFavoriteCometFile = tempnam(sys_get_temp_dir(), 'month_comets_');
|
||||||
if ($tempFavoriteCometFile !== false) {
|
if ($tempFavoriteCometFile !== false) {
|
||||||
try {
|
try {
|
||||||
@@ -2679,6 +2684,7 @@ foreach ($meteorShowerPeaks as $meteorShowerPeak) {
|
|||||||
'local_iso' => $peakDateTime->format(DateTimeInterface::ATOM),
|
'local_iso' => $peakDateTime->format(DateTimeInterface::ATOM),
|
||||||
'zhr_min_estimate' => $zhrMinEstimate,
|
'zhr_min_estimate' => $zhrMinEstimate,
|
||||||
'zhr_max_estimate' => $zhrMaxEstimate,
|
'zhr_max_estimate' => $zhrMaxEstimate,
|
||||||
|
'meteor_shower_code' => $showerCode,
|
||||||
'type' => 'meteor_shower_peak',
|
'type' => 'meteor_shower_peak',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2743,6 +2749,7 @@ usort(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$monthNarrativeText = monthForecastBuildNarrativeText($monthEventList, $selectedMonth, $months[$selectedMonth], $selectedYear);
|
$monthNarrativeText = monthForecastBuildNarrativeText($monthEventList, $selectedMonth, $months[$selectedMonth], $selectedYear);
|
||||||
|
$monthEventListJson = json_encode($monthEventList, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
||||||
|
|
||||||
$observationTipInsertSql = buildObservationTipInsertSql($monthEventList, new DateTimeZone($chartTimezone));
|
$observationTipInsertSql = buildObservationTipInsertSql($monthEventList, new DateTimeZone($chartTimezone));
|
||||||
$observationTipCalendarIcs = buildObservationTipCalendarIcs($monthEventList, new DateTimeZone($chartTimezone));
|
$observationTipCalendarIcs = buildObservationTipCalendarIcs($monthEventList, new DateTimeZone($chartTimezone));
|
||||||
@@ -2795,7 +2802,7 @@ $monthChartComets = array_map(
|
|||||||
'G' => array_key_exists('slope_parameter_g', $comet) && $comet['slope_parameter_g'] !== null ? (float) $comet['slope_parameter_g'] : null,
|
'G' => array_key_exists('slope_parameter_g', $comet) && $comet['slope_parameter_g'] !== null ? (float) $comet['slope_parameter_g'] : null,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
$favoriteCometPayload
|
$includeComets ? $favoriteCometPayload : []
|
||||||
);
|
);
|
||||||
$monthChartCometsJson = json_encode($monthChartComets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$monthChartCometsJson = json_encode($monthChartComets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
$moonPhaseEventsJson = json_encode($moonPhaseEvents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$moonPhaseEventsJson = json_encode($moonPhaseEvents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
@@ -2843,6 +2850,11 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="monatsvorhersage-checkbox">
|
||||||
|
<input type="checkbox" name="include_comets" value="1" <?= $includeComets ? 'checked' : '' ?>>
|
||||||
|
<span>Kometen in der Monatsvorhersage anzeigen</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="monatsvorhersage-actions">
|
<div class="monatsvorhersage-actions">
|
||||||
<button type="submit">Monatsvorhersage berechnen</button>
|
<button type="submit">Monatsvorhersage berechnen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2878,16 +2890,26 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="monatsvorhersage-actions">
|
<div class="monatsvorhersage-actions">
|
||||||
<button type="button" disabled>Wochenansicht folgt spaeter</button>
|
<button type="button" disabled>Wochenansicht folgt später</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<?php if ($shouldRunForecast): ?>
|
<?php if ($shouldRunForecast): ?>
|
||||||
<section class="monatsnarrativ-card">
|
<div class="monatszip-export">
|
||||||
|
<button type="button" class="monatskarte-export-button" id="downloadMonatsueberblickZip">
|
||||||
|
Monatspaket als ZIP herunterladen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="monatskarte-export-button" id="copyMonatsueberblickHtml">
|
||||||
|
HTML in Zwischenablage kopieren
|
||||||
|
</button>
|
||||||
|
<p id="monatsueberblickZipStatus" role="status" aria-live="polite"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="monatsnarrativ-card" id="monatsueberblick">
|
||||||
<div class="monatskarte-header">
|
<div class="monatskarte-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>Monat Im Ueberblick</h3>
|
<h3>Monat im Überblick</h3>
|
||||||
<p><?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></p>
|
<p><?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2961,7 +2983,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
|
|
||||||
<div class="monatskarte-stage monatsphase-stage">
|
<div class="monatskarte-stage monatsphase-stage">
|
||||||
<div class="monatsphase-strip-wrap">
|
<div class="monatsphase-strip-wrap">
|
||||||
<img id="monatsphasenImage" alt="Mondphasen des Monats in einer Zeile">
|
<img id="monatsphasenImage" alt="Mondphasen des Monats in bis zu vier Zeilen">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3023,7 +3045,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p>Fuer diesen Monat wurden keine Ereignisse gefunden.</p>
|
<p>Für diesen Monat wurden keine Ereignisse gefunden.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -3036,7 +3058,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="monatskarte-footer">
|
<div class="monatskarte-footer">
|
||||||
<p class="monatsnarrativ-text">Bitte zuerst Jahr und Monat auswaehlen und dann auf „Monatsvorhersage berechnen“ klicken.</p>
|
<p class="monatsnarrativ-text">Bitte zuerst Jahr und Monat auswählen und dann auf „Monatsvorhersage berechnen“ klicken.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -3057,6 +3079,19 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monatszip-export {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatszip-export p {
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(222, 232, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
.monatskarte-insert-status {
|
.monatskarte-insert-status {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -3179,6 +3214,22 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monatsvorhersage-checkbox {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: rgba(235, 242, 255, 0.95);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monatsvorhersage-checkbox input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
.monatsvorhersage-actions {
|
.monatsvorhersage-actions {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
@@ -3294,8 +3345,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
|||||||
|
|
||||||
#monatsphasenImage {
|
#monatsphasenImage {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(100%, 1200px);
|
width: min(100%, 773px);
|
||||||
min-width: 920px;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -3335,6 +3385,7 @@ const MONTH_CHART_PLANETS = <?= $planetDataJson ?: '[]' ?>;
|
|||||||
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
|
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
|
||||||
const MONTH_CHART_COMETS = <?= $monthChartCometsJson ?: '[]' ?>;
|
const MONTH_CHART_COMETS = <?= $monthChartCometsJson ?: '[]' ?>;
|
||||||
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
||||||
|
const MONTH_EVENT_LIST = <?= $monthEventListJson ?: '[]' ?>;
|
||||||
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
||||||
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
|
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
|
||||||
const OBSERVATION_TIP_ICAL_FILENAME = <?= $observationTipCalendarFilenameJson ?: '"astrotools-monatsereignisse.ics"' ?>;
|
const OBSERVATION_TIP_ICAL_FILENAME = <?= $observationTipCalendarFilenameJson ?: '"astrotools-monatsereignisse.ics"' ?>;
|
||||||
@@ -3439,8 +3490,10 @@ async function composeExportCanvas(sourceCanvas, logoPosition, options) {
|
|||||||
const padding = Math.round(exportCanvas.width * 0.024);
|
const padding = Math.round(exportCanvas.width * 0.024);
|
||||||
const targetWidth = Math.max(90, Math.min(320, Math.round(exportCanvas.width * 0.14 * logoScale)));
|
const targetWidth = Math.max(90, Math.min(320, Math.round(exportCanvas.width * 0.14 * logoScale)));
|
||||||
const targetHeight = Math.round(targetWidth * (logo.naturalHeight / Math.max(1, logo.naturalWidth)));
|
const targetHeight = Math.round(targetWidth * (logo.naturalHeight / Math.max(1, logo.naturalWidth)));
|
||||||
const drawX = padding;
|
const drawX = logoPosition === 'bottom-right'
|
||||||
const drawY = logoPosition === 'bottom-left'
|
? exportCanvas.width - padding - targetWidth
|
||||||
|
: padding;
|
||||||
|
const drawY = logoPosition === 'bottom-left' || logoPosition === 'bottom-right'
|
||||||
? exportCanvas.height - padding - targetHeight
|
? exportCanvas.height - padding - targetHeight
|
||||||
: padding;
|
: padding;
|
||||||
exportCtx.drawImage(logo, drawX, drawY, targetWidth, targetHeight);
|
exportCtx.drawImage(logo, drawX, drawY, targetWidth, targetHeight);
|
||||||
@@ -3480,8 +3533,8 @@ function monthChartExportFilename(viewMode) {
|
|||||||
const year = String(MONTH_CHART_CONFIG.year || '');
|
const year = String(MONTH_CHART_CONFIG.year || '');
|
||||||
const hour = String(Number(MONTH_CHART_CONFIG.monthSkyHour ?? 0)).padStart(2, '0');
|
const hour = String(Number(MONTH_CHART_CONFIG.monthSkyHour ?? 0)).padStart(2, '0');
|
||||||
const minute = String(Number(MONTH_CHART_CONFIG.monthSkyMinute ?? 0)).padStart(2, '0');
|
const minute = String(Number(MONTH_CHART_CONFIG.monthSkyMinute ?? 0)).padStart(2, '0');
|
||||||
const safeMode = viewMode === 'sued' ? 'sued' : 'polar';
|
const label = viewMode === 'sued' ? 'Himmelsanblick_Sued' : 'Himmelsanblick_Polar';
|
||||||
return 'Himmelsanblick_' + safeMode + '_' + year + '_' + month + '_' + hour + '-' + minute + '.png';
|
return year + '_' + month + '_' + label + '_' + hour + '_' + minute + '.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
function monthChartExportCaption() {
|
function monthChartExportCaption() {
|
||||||
@@ -3708,7 +3761,7 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
if (insertStatus) {
|
if (insertStatus) {
|
||||||
insertStatus.hidden = false;
|
insertStatus.hidden = false;
|
||||||
insertStatus.className = 'monatskarte-insert-status ' + (payload && payload.success ? 'is-success' : 'is-error');
|
insertStatus.className = 'monatskarte-insert-status ' + (payload && payload.success ? 'is-success' : 'is-error');
|
||||||
insertStatus.textContent = payload && payload.message ? payload.message : 'Unbekannte Rueckmeldung vom Server.';
|
insertStatus.textContent = payload && payload.message ? payload.message : 'Unbekannte Rückmeldung vom Server.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload && payload.success) {
|
if (payload && payload.success) {
|
||||||
@@ -3807,7 +3860,7 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const phaseImage = new Image();
|
const phaseImage = new Image();
|
||||||
let phaseViewportWidth = 1200;
|
let phaseViewportWidth = 1200;
|
||||||
let phaseViewportHeight = 220;
|
let phaseViewportHeight = 700;
|
||||||
let phaseRenderDpr = Math.max(1, window.devicePixelRatio || 1);
|
let phaseRenderDpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
phaseImage.src = <?= json_encode($publicBasePath . 'images/moons/moon4geocron.png', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
phaseImage.src = <?= json_encode($publicBasePath . 'images/moons/moon4geocron.png', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|
||||||
@@ -3848,8 +3901,8 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
const cssWidth = Math.max(920, Math.floor(imageEl.clientWidth || 1200));
|
const cssWidth = Math.max(320, Math.floor(imageEl.clientWidth || 773));
|
||||||
const cssHeight = 220;
|
const cssHeight = Math.round(cssWidth * (700 / 773));
|
||||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
renderPhaseCanvas(cssWidth, cssHeight, dpr, true);
|
renderPhaseCanvas(cssWidth, cssHeight, dpr, true);
|
||||||
}
|
}
|
||||||
@@ -3924,6 +3977,7 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
|
|
||||||
const width = phaseViewportWidth;
|
const width = phaseViewportWidth;
|
||||||
const height = phaseViewportHeight;
|
const height = phaseViewportHeight;
|
||||||
|
const layoutScale = width / 773;
|
||||||
const year = Number(MONTH_CHART_CONFIG.year);
|
const year = Number(MONTH_CHART_CONFIG.year);
|
||||||
const month = Number(MONTH_CHART_CONFIG.month);
|
const month = Number(MONTH_CHART_CONFIG.month);
|
||||||
const monthName = String(MONTH_CHART_CONFIG.monthName || '');
|
const monthName = String(MONTH_CHART_CONFIG.monthName || '');
|
||||||
@@ -3938,30 +3992,37 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(240, 215, 132, 0.96)';
|
ctx.fillStyle = 'rgba(240, 215, 132, 0.96)';
|
||||||
ctx.font = '600 16px Rajdhani, sans-serif';
|
ctx.font = '600 ' + (24 * layoutScale) + 'px Rajdhani, sans-serif';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
ctx.fillText('Mondphasen im ' + monthName + ' ' + year, 28, 18);
|
ctx.fillText('Mondphasen im ' + monthName + ' ' + year, 42 * layoutScale, 27 * layoutScale);
|
||||||
|
|
||||||
const leftPad = 22;
|
const leftPad = 33 * layoutScale;
|
||||||
const rightPad = 22;
|
const rightPad = 33 * layoutScale;
|
||||||
const usableWidth = width - leftPad - rightPad;
|
const usableWidth = width - leftPad - rightPad;
|
||||||
const spacing = usableWidth / totalDays;
|
const daysPerRow = 9;
|
||||||
const radius = Math.max(11, Math.min(18, spacing * 0.42));
|
const rowCenters = [108, 260, 411, 563].map(function (value) { return value * layoutScale; });
|
||||||
const centerY = 82;
|
const eventTextY = [174, 326, 477, 629].map(function (value) { return value * layoutScale; });
|
||||||
|
const eventDateY = [200, 351, 503, 655].map(function (value) { return value * layoutScale; });
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.72)';
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.72)';
|
||||||
ctx.font = '500 10px \"Share Tech Mono\", monospace';
|
ctx.font = '500 ' + (17 * layoutScale) + 'px \"Share Tech Mono\", monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
for (let day = 1; day <= totalDays; day++) {
|
for (let day = 1; day <= totalDays; day++) {
|
||||||
const x = leftPad + spacing * (day - 0.5);
|
const row = Math.floor((day - 1) / daysPerRow);
|
||||||
|
const rowStartDay = row * daysPerRow + 1;
|
||||||
|
const dayInRow = day - rowStartDay + 1;
|
||||||
|
const spacing = usableWidth / daysPerRow;
|
||||||
|
const radius = Math.max(23 * layoutScale, Math.min(33 * layoutScale, spacing * 0.52));
|
||||||
|
const x = leftPad + spacing * (dayInRow - 0.5);
|
||||||
|
const centerY = rowCenters[row];
|
||||||
drawMoonDisc(x, centerY, radius, phaseInfoForDay(year, month, day));
|
drawMoonDisc(x, centerY, radius, phaseInfoForDay(year, month, day));
|
||||||
ctx.fillText(String(day), x, centerY + radius + 8);
|
ctx.fillText(String(day), x, centerY + radius + (12 * layoutScale));
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.font = '600 12px Rajdhani, sans-serif';
|
ctx.font = '600 ' + (21 * layoutScale) + 'px Rajdhani, sans-serif';
|
||||||
ctx.fillStyle = 'rgba(225, 232, 246, 0.94)';
|
ctx.fillStyle = 'rgba(225, 232, 246, 0.94)';
|
||||||
|
|
||||||
for (const event of Array.isArray(MONTH_PHASE_EVENTS) ? MONTH_PHASE_EVENTS : []) {
|
for (const event of Array.isArray(MONTH_PHASE_EVENTS) ? MONTH_PHASE_EVENTS : []) {
|
||||||
@@ -3975,10 +4036,13 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = leftPad + spacing * (day - 0.5);
|
const row = Math.floor((day - 1) / daysPerRow);
|
||||||
ctx.fillText(String(event.label || ''), x, 158);
|
const rowStartDay = row * daysPerRow + 1;
|
||||||
|
const dayInRow = day - rowStartDay + 1;
|
||||||
|
const x = leftPad + (usableWidth / daysPerRow) * (dayInRow - 0.5);
|
||||||
|
ctx.fillText(String(event.label || ''), x, eventTextY[row]);
|
||||||
ctx.fillStyle = 'rgba(182, 196, 224, 0.94)';
|
ctx.fillStyle = 'rgba(182, 196, 224, 0.94)';
|
||||||
ctx.fillText(day + '.' + month + '.', x, 175);
|
ctx.fillText(day + '.' + month + '.', x, eventDateY[row]);
|
||||||
ctx.fillStyle = 'rgba(225, 232, 246, 0.94)';
|
ctx.fillStyle = 'rgba(225, 232, 246, 0.94)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3992,31 +4056,23 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
|
|
||||||
const year = String(MONTH_CHART_CONFIG.year || '');
|
const year = String(MONTH_CHART_CONFIG.year || '');
|
||||||
const month = String(MONTH_CHART_CONFIG.monthNumber || MONTH_CHART_CONFIG.month || '').padStart(2, '0');
|
const month = String(MONTH_CHART_CONFIG.monthNumber || MONTH_CHART_CONFIG.month || '').padStart(2, '0');
|
||||||
return 'Himmelsanblick_mondphasen_' + year + '_' + month + '.png';
|
return year + '_' + month + '_Mondphasen.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadButton) {
|
if (downloadButton) {
|
||||||
downloadButton.addEventListener('click', function () {
|
downloadButton.addEventListener('click', function () {
|
||||||
const previous = {
|
statusEl.textContent = 'Mondphasen-PNG wird erstellt...';
|
||||||
width: phaseViewportWidth,
|
|
||||||
height: phaseViewportHeight,
|
|
||||||
dpr: phaseRenderDpr,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
statusEl.textContent = 'Mondphasen-PNG wird in 2000 px neu gerendert...';
|
|
||||||
downloadButton.disabled = true;
|
downloadButton.disabled = true;
|
||||||
|
|
||||||
const exportWidth = MONTH_CHART_EXPORT_WIDTH;
|
composeExportCanvas(canvas, 'bottom-right', { showCaption: false, logoScale: 0.6 })
|
||||||
const exportHeight = Math.round(exportWidth * (220 / 1200));
|
.then(function (exportCanvas) {
|
||||||
renderPhaseCanvas(exportWidth, exportHeight, 1, false);
|
|
||||||
composeExportCanvas(canvas, 'bottom-left', { showCaption: false, logoScale: 0.6 }).then(function (exportCanvas) {
|
|
||||||
triggerCanvasDownload(exportCanvas || canvas, monthPhaseExportFilename());
|
triggerCanvasDownload(exportCanvas || canvas, monthPhaseExportFilename());
|
||||||
});
|
})
|
||||||
} finally {
|
.finally(function () {
|
||||||
renderPhaseCanvas(previous.width, previous.height, previous.dpr, true);
|
draw();
|
||||||
|
imageEl.src = canvas.toDataURL('image/png');
|
||||||
downloadButton.disabled = false;
|
downloadButton.disabled = false;
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4496,7 +4552,7 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
const y = cy - Math.cos(angleRad) * (radius + offset);
|
const y = cy - Math.cos(angleRad) * (radius + offset);
|
||||||
let rotation = angleRad;
|
let rotation = angleRad;
|
||||||
|
|
||||||
// Text tangential zum Horizontkreis ausrichten, dabei moeglichst gut lesbar halten.
|
// Text tangential zum Horizontkreis ausrichten, dabei möglichst gut lesbar halten.
|
||||||
if (rotation > Math.PI / 2 && rotation < Math.PI * 1.5) {
|
if (rotation > Math.PI / 2 && rotation < Math.PI * 1.5) {
|
||||||
rotation -= Math.PI;
|
rotation -= Math.PI;
|
||||||
}
|
}
|
||||||
@@ -6100,6 +6156,306 @@ function monthChartCometRaDec(comet, date) {
|
|||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
window.addEventListener('resize', resizeCanvas);
|
window.addEventListener('resize', resizeCanvas);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const downloadButton = document.getElementById('downloadMonatsueberblickZip');
|
||||||
|
const copyButton = document.getElementById('copyMonatsueberblickHtml');
|
||||||
|
const statusEl = document.getElementById('monatsueberblickZipStatus');
|
||||||
|
const overviewEl = document.getElementById('monatsueberblick');
|
||||||
|
const polarCanvas = document.getElementById('monatskarteCanvas');
|
||||||
|
const southCanvas = document.getElementById('monatskarteSouthCanvas');
|
||||||
|
const phaseImage = document.getElementById('monatsphasenImage');
|
||||||
|
const htmlImageMaxWidth = 900;
|
||||||
|
|
||||||
|
if (!downloadButton || !copyButton || !statusEl || !overviewEl || !polarCanvas || !southCanvas || !phaseImage || !MONTH_CHART_CONFIG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToPngBlob(sourceCanvas) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
sourceCanvas.toBlob(function (blob) {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
} else {
|
||||||
|
reject(new Error('PNG konnte nicht erzeugt werden.'));
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCanvasAsPng(sourceCanvas, logoPosition, options) {
|
||||||
|
const exportCanvas = await composeExportCanvas(sourceCanvas, logoPosition || 'bottom-left', options);
|
||||||
|
return canvasToPngBlob(exportCanvas || sourceCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPhaseImageAsPng() {
|
||||||
|
if (!phaseImage.complete || !phaseImage.naturalWidth || !phaseImage.naturalHeight) {
|
||||||
|
throw new Error('Die Mondphasengrafik ist noch nicht bereit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCanvas = document.createElement('canvas');
|
||||||
|
sourceCanvas.width = phaseImage.naturalWidth;
|
||||||
|
sourceCanvas.height = phaseImage.naturalHeight;
|
||||||
|
const sourceCtx = sourceCanvas.getContext('2d');
|
||||||
|
if (!sourceCtx) {
|
||||||
|
throw new Error('Mondphasengrafik konnte nicht vorbereitet werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCtx.drawImage(phaseImage, 0, 0);
|
||||||
|
return exportCanvasAsPng(sourceCanvas, 'bottom-right', { showCaption: false, logoScale: 0.6 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function blobToDataUrl(blob) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function () { resolve(String(reader.result)); };
|
||||||
|
reader.onerror = function () { reject(new Error('Bild konnte nicht eingebettet werden.')); };
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scaleImageBlob(blob, maxWidth) {
|
||||||
|
const sourceUrl = window.URL.createObjectURL(blob);
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise(function (resolve, reject) {
|
||||||
|
image.onload = resolve;
|
||||||
|
image.onerror = function () { reject(new Error('Bild konnte nicht skaliert werden.')); };
|
||||||
|
image.src = sourceUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (image.naturalWidth <= maxWidth) {
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaledCanvas = document.createElement('canvas');
|
||||||
|
scaledCanvas.width = maxWidth;
|
||||||
|
scaledCanvas.height = Math.round(image.naturalHeight * (maxWidth / image.naturalWidth));
|
||||||
|
const scaledContext = scaledCanvas.getContext('2d');
|
||||||
|
if (!scaledContext) {
|
||||||
|
throw new Error('Bild konnte nicht skaliert werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
scaledContext.drawImage(image, 0, 0, scaledCanvas.width, scaledCanvas.height);
|
||||||
|
return canvasToPngBlob(scaledCanvas);
|
||||||
|
} finally {
|
||||||
|
window.URL.revokeObjectURL(sourceUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return '<h2>' + escapeHtml(image.title) + '</h2>\n'
|
||||||
|
+ '<p><img src="' + imageDataUrl + '" alt="' + escapeHtml(image.title) + '" style="max-width: 100%; height: auto;"></p>\n';
|
||||||
|
}));
|
||||||
|
|
||||||
|
return '<h3>' + escapeHtml(monthLabel) + '</h3>\n'
|
||||||
|
+ '<p>' + escapedNarrative + '</p>\n'
|
||||||
|
+ imageMarkup.join('')
|
||||||
|
+ eventTableHtml()
|
||||||
|
+ '<p>Alle Zeiten gelten für MEZ/MESZ.</p>\n'
|
||||||
|
+ '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventTableHtml() {
|
||||||
|
const rows = (Array.isArray(MONTH_EVENT_LIST) ? MONTH_EVENT_LIST : [])
|
||||||
|
.map(function (event) {
|
||||||
|
return '<tr><td style="padding: 6px;">' + escapeHtml(event.event || '') + '</td><td style="padding: 6px; text-align: center;">'
|
||||||
|
+ escapeHtml(event.date || '') + '</td><td style="padding: 6px; text-align: center;">'
|
||||||
|
+ escapeHtml(event.time || '') + '</td></tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<h2>Ereignisse</h2>\n'
|
||||||
|
+ '<table style="border-collapse: collapse;">\n'
|
||||||
|
+ '<thead><tr><th style="padding: 6px; text-align: center;">Ereignis</th><th style="padding: 6px; text-align: center;">Datum</th><th style="padding: 6px; text-align: center;">Uhrzeit</th></tr></thead>\n'
|
||||||
|
+ '<tbody>\n' + rows.join('\n') + '\n</tbody>\n'
|
||||||
|
+ '</table>\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function crc32(bytes) {
|
||||||
|
let crc = 0xffffffff;
|
||||||
|
for (let index = 0; index < bytes.length; index++) {
|
||||||
|
crc ^= bytes[index];
|
||||||
|
for (let bit = 0; bit < 8; bit++) {
|
||||||
|
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (crc ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zipHeader(size) {
|
||||||
|
const bytes = new Uint8Array(size);
|
||||||
|
return { bytes: bytes, view: new DataView(bytes.buffer) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createZip(entries) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const localChunks = [];
|
||||||
|
const centralChunks = [];
|
||||||
|
const now = new Date();
|
||||||
|
const dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | Math.floor(now.getSeconds() / 2);
|
||||||
|
const dosDate = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const nameBytes = encoder.encode(entry.name);
|
||||||
|
const content = entry.content instanceof Blob
|
||||||
|
? new Uint8Array(await entry.content.arrayBuffer())
|
||||||
|
: encoder.encode(entry.content);
|
||||||
|
const checksum = crc32(content);
|
||||||
|
const local = zipHeader(30);
|
||||||
|
local.view.setUint32(0, 0x04034b50, true);
|
||||||
|
local.view.setUint16(4, 20, true);
|
||||||
|
local.view.setUint16(6, 0x0800, true);
|
||||||
|
local.view.setUint16(8, 0, true);
|
||||||
|
local.view.setUint16(10, dosTime, true);
|
||||||
|
local.view.setUint16(12, dosDate, true);
|
||||||
|
local.view.setUint32(14, checksum, true);
|
||||||
|
local.view.setUint32(18, content.length, true);
|
||||||
|
local.view.setUint32(22, content.length, true);
|
||||||
|
local.view.setUint16(26, nameBytes.length, true);
|
||||||
|
local.view.setUint16(28, 0, true);
|
||||||
|
localChunks.push(local.bytes, nameBytes, content);
|
||||||
|
|
||||||
|
const central = zipHeader(46);
|
||||||
|
central.view.setUint32(0, 0x02014b50, true);
|
||||||
|
central.view.setUint16(4, 20, true);
|
||||||
|
central.view.setUint16(6, 20, true);
|
||||||
|
central.view.setUint16(8, 0x0800, true);
|
||||||
|
central.view.setUint16(10, 0, true);
|
||||||
|
central.view.setUint16(12, dosTime, true);
|
||||||
|
central.view.setUint16(14, dosDate, true);
|
||||||
|
central.view.setUint32(16, checksum, true);
|
||||||
|
central.view.setUint32(20, content.length, true);
|
||||||
|
central.view.setUint32(24, content.length, true);
|
||||||
|
central.view.setUint16(28, nameBytes.length, true);
|
||||||
|
central.view.setUint16(30, 0, true);
|
||||||
|
central.view.setUint16(32, 0, true);
|
||||||
|
central.view.setUint16(34, 0, true);
|
||||||
|
central.view.setUint16(36, 0, true);
|
||||||
|
central.view.setUint32(38, 0, true);
|
||||||
|
central.view.setUint32(42, offset, true);
|
||||||
|
centralChunks.push(central.bytes, nameBytes);
|
||||||
|
|
||||||
|
offset += local.bytes.length + nameBytes.length + content.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centralSize = centralChunks.reduce(function (size, chunk) { return size + chunk.length; }, 0);
|
||||||
|
const end = zipHeader(22);
|
||||||
|
end.view.setUint32(0, 0x06054b50, true);
|
||||||
|
end.view.setUint16(8, entries.length, true);
|
||||||
|
end.view.setUint16(10, entries.length, true);
|
||||||
|
end.view.setUint32(12, centralSize, true);
|
||||||
|
end.view.setUint32(16, offset, true);
|
||||||
|
end.view.setUint16(20, 0, true);
|
||||||
|
|
||||||
|
return new Blob([...localChunks, ...centralChunks, end.bytes], { type: 'application/zip' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBlob(blob, filename) {
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = objectUrl;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.setTimeout(function () {
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOverviewImages() {
|
||||||
|
const exports = await Promise.all([
|
||||||
|
exportCanvasAsPng(polarCanvas),
|
||||||
|
exportCanvasAsPng(southCanvas),
|
||||||
|
exportPhaseImageAsPng(),
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
{ title: 'Zenitprojektion', content: exports[0] },
|
||||||
|
{ title: 'Südhimmel', content: exports[1] },
|
||||||
|
{ title: 'Mondphasen', content: exports[2] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTextToClipboard(text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute('readonly', '');
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
const copied = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
if (!copied) {
|
||||||
|
throw new Error('Zwischenablage nicht verfügbar.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadButton.addEventListener('click', async function () {
|
||||||
|
const year = String(MONTH_CHART_CONFIG.year || '');
|
||||||
|
const month = String(MONTH_CHART_CONFIG.monthNumber || MONTH_CHART_CONFIG.month || '').padStart(2, '0');
|
||||||
|
|
||||||
|
try {
|
||||||
|
statusEl.textContent = 'Monatspaket wird erstellt...';
|
||||||
|
downloadButton.disabled = true;
|
||||||
|
|
||||||
|
const images = await createOverviewImages();
|
||||||
|
const zip = await createZip([
|
||||||
|
{ name: monthChartExportFilename('polar'), content: images[0].content },
|
||||||
|
{ name: monthChartExportFilename('sued'), content: images[1].content },
|
||||||
|
{ name: year + '_' + month + '_Mondphasen.png', content: images[2].content },
|
||||||
|
{ name: year + '_' + month + '_Monat_im_Ueberblick.html', content: await overviewHtml(images) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
downloadBlob(zip, year + '_' + month + '_Monatspaket.zip');
|
||||||
|
statusEl.textContent = 'Monatspaket wurde heruntergeladen.';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Monatspaket konnte nicht erstellt werden.';
|
||||||
|
} finally {
|
||||||
|
downloadButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyButton.addEventListener('click', async function () {
|
||||||
|
try {
|
||||||
|
statusEl.textContent = 'HTML für die Zwischenablage wird erstellt...';
|
||||||
|
copyButton.disabled = true;
|
||||||
|
const html = await overviewHtml(await createOverviewImages());
|
||||||
|
await copyTextToClipboard(html);
|
||||||
|
statusEl.textContent = 'HTML wurde in die Zwischenablage kopiert.';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'HTML konnte nicht in die Zwischenablage kopiert werden.';
|
||||||
|
} finally {
|
||||||
|
copyButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user