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');
|
||||
$selectedWeekYear = isset($_GET['week_year']) ? (int) $_GET['week_year'] : $currentYear;
|
||||
$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'])) {
|
||||
$_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
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Grenzwerte fuer Sternkarte und Objektanzeige
|
||||
// Grenzwerte für Sternkarte und Objektanzeige
|
||||
// -------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar
|
||||
@@ -67,95 +68,95 @@ $messierDatabaseMagnitudeLimit = 2.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;
|
||||
|
||||
// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und ausgewaehlten
|
||||
// Maximaler Winkelabstand für Begegnungen zwischen Mond und ausgewählten
|
||||
// Sternhaufen wie Plejaden oder Praesepe.
|
||||
$moonDeepSkyMaxSeparationDeg = 1.0;
|
||||
|
||||
// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und
|
||||
// ausgewaehlten hellen Sternen wie Regulus oder Spica.
|
||||
// Maximaler Winkelabstand für Begegnungen zwischen hellen Planeten und
|
||||
// ausgewählten hellen Sternen wie Regulus oder Spica.
|
||||
$planetBrightStarMaxSeparationDeg = 1.0;
|
||||
|
||||
// Maximaler Winkelabstand fuer Planet-Planet-Konjunktionen.
|
||||
// Maximaler Winkelabstand für Planet-Planet-Konjunktionen.
|
||||
$planetConjunctionMaxSeparationDeg = 1.0;
|
||||
|
||||
// Nur Sterne bis zu dieser Helligkeit werden fuer Mondbedeckungen
|
||||
// beruecksichtigt.
|
||||
// Nur Sterne bis zu dieser Helligkeit werden für Mondbedeckungen
|
||||
// berücksichtigt.
|
||||
$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
|
||||
// Hoehepunkt mindestens diese Hoehe ueber dem Horizont erreicht.
|
||||
// Ein Überflug zählt als guter Beobachtungstermin nur dann, wenn er am
|
||||
// Höhepunkt mindestens diese Höhe über dem Horizont erreicht.
|
||||
$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
|
||||
// nautischen Daemmerung und sorgt fuer ausreichend dunklen Himmel.
|
||||
// nautischen Dämmerung und sorgt für ausreichend dunklen Himmel.
|
||||
$favoriteSatelliteBestPassMaxSunAltitudeDeg = -14.0;
|
||||
|
||||
// Optional koennen wir die Liste auf die erste Nachthaelfte begrenzen.
|
||||
// "Erste Nachthaelfte" ist hier bewusst pragmatisch als lokaler Abend vor
|
||||
// Mitternacht definiert. Ueberfluege nach 00:00 Ortszeit werden damit
|
||||
// Optional können wir die Liste auf die erste Nachthälfte begrenzen.
|
||||
// "Erste Nachthälfte" ist hier bewusst pragmatisch als lokaler Abend vor
|
||||
// Mitternacht definiert. Überflüge nach 00:00 Ortszeit werden damit
|
||||
// ausgefiltert.
|
||||
$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,
|
||||
// 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;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Schwellen fuer "Gute Merkur-Sichtbarkeit"
|
||||
// Schwellen für "Gute Merkur-Sichtbarkeit"
|
||||
// -------------------------------------------------------------------------
|
||||
// Gute Merkur-Sichtbarkeit wird nicht nur ueber Elongation definiert, sondern
|
||||
// ueber echte Beobachtbarkeit in der Daemmerung.
|
||||
// Geprueft wird pro Tag:
|
||||
// Gute Merkur-Sichtbarkeit wird nicht nur über Elongation definiert, sondern
|
||||
// über echte Beobachtbarkeit in der Dämmerung.
|
||||
// Geprüft wird pro Tag:
|
||||
// morgens eine feste Zahl Minuten VOR Sonnenaufgang,
|
||||
// abends eine feste Zahl Minuten NACH Sonnenuntergang.
|
||||
$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.
|
||||
$mercuryGoodVisibilityMinAltitudeDeg = 7.0;
|
||||
|
||||
// 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.
|
||||
// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar wäre, aber
|
||||
// noch zu stark in der hellen Dämmerung untergeht.
|
||||
$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0;
|
||||
|
||||
// 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;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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.
|
||||
$planetParadeSampleOffsetMinutes = 45;
|
||||
|
||||
// Ein Planet zaehlt fuer die Parade nur dann mit, wenn er zum Pruefzeitpunkt
|
||||
// mindestens diese Hoehe ueber dem Horizont erreicht.
|
||||
// Ein Planet zählt für die Parade nur dann mit, wenn er zum Prüfzeitpunkt
|
||||
// mindestens diese Höhe über 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.
|
||||
// Die beteiligten Planeten müssen innerhalb dieses Azimutbogens liegen.
|
||||
// So vermeiden wir Treffer, bei denen die Planeten zu weit über den Himmel
|
||||
// verteilt sind und nicht mehr als zusammenhängende Parade wirken.
|
||||
$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.
|
||||
$planetParadeMinPlanetCount = 4;
|
||||
|
||||
@@ -164,10 +165,10 @@ $planetParadeMinPlanetCount = 4;
|
||||
$planetParadeMinConsecutiveDays = 2;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Schwellen fuer ausgewaehlte Kometen
|
||||
// Schwellen für ausgewählte Kometen
|
||||
// -------------------------------------------------------------------------
|
||||
// Gute Kometensichtbarkeit wird morgens vor Sonnenaufgang und abends nach
|
||||
// Sonnenuntergang geprueft.
|
||||
// Sonnenuntergang geprüft.
|
||||
$favoriteCometVisibilitySampleOffsetMinutes = 60;
|
||||
|
||||
// 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.
|
||||
$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.
|
||||
$favoriteCometVisibilityMaxMagnitude = 10.0;
|
||||
|
||||
@@ -273,6 +274,7 @@ $planetDataJson = '[]';
|
||||
$moonDataJson = 'null';
|
||||
$monthChartCometsJson = '[]';
|
||||
$moonPhaseEventsJson = '[]';
|
||||
$monthEventListJson = '[]';
|
||||
$starDataErrorJson = 'null';
|
||||
$observationTipCalendarIcsJson = '""';
|
||||
$observationTipCalendarFilenameJson = json_encode(
|
||||
@@ -1023,7 +1025,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
|
||||
}
|
||||
|
||||
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.';
|
||||
}
|
||||
|
||||
$meteorEvents = $findEvents(
|
||||
$meteorCandidates = array_values(array_filter(
|
||||
$filteredEvents,
|
||||
static fn (array $event): bool => (string) ($event['type'] ?? '') === 'meteor_shower_peak'
|
||||
&& (
|
||||
(int) ($event['zhr_min_estimate'] ?? 0) >= 20
|
||||
|| (int) ($event['zhr_max_estimate'] ?? 0) >= 30
|
||||
),
|
||||
2
|
||||
);
|
||||
&& max(
|
||||
(int) ($event['zhr_min_estimate'] ?? 0),
|
||||
(int) ($event['zhr_max_estimate'] ?? 0)
|
||||
) >= 10
|
||||
));
|
||||
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 !== []) {
|
||||
$meteorIntro = $pickRandomText([
|
||||
'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.',
|
||||
'Auch die Meteorströme verdienen in diesem Monat Aufmerksamkeit.',
|
||||
'Wer nach Sternschnuppen Ausschau halten möchte, sollte auch die Meteorströme beachten.',
|
||||
'Ein Blick auf die Meteorstroeme kann sich in diesem Monat ebenfalls lohnen.',
|
||||
], 'Freunde von Meteoren sollten zudem die Aktivitaet der Meteorstroeme im Blick behalten.');
|
||||
'Ein Blick auf die Meteorströme kann sich in diesem Monat ebenfalls lohnen.',
|
||||
], 'Freunde von Meteoren sollten zudem die Aktivität der Meteorströme im Blick behalten.');
|
||||
$paragraphs[] = $meteorIntro . ' Erwähnenswert sind besonders ' . $describeEvents($meteorEvents) . '.';
|
||||
} 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 !== []) {
|
||||
@@ -1719,7 +1724,7 @@ if (!empty($favoriteSatellitePayload)) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($favoriteCometPayload)) {
|
||||
if ($includeComets && !empty($favoriteCometPayload)) {
|
||||
$tempFavoriteCometFile = tempnam(sys_get_temp_dir(), 'month_comets_');
|
||||
if ($tempFavoriteCometFile !== false) {
|
||||
try {
|
||||
@@ -2679,6 +2684,7 @@ foreach ($meteorShowerPeaks as $meteorShowerPeak) {
|
||||
'local_iso' => $peakDateTime->format(DateTimeInterface::ATOM),
|
||||
'zhr_min_estimate' => $zhrMinEstimate,
|
||||
'zhr_max_estimate' => $zhrMaxEstimate,
|
||||
'meteor_shower_code' => $showerCode,
|
||||
'type' => 'meteor_shower_peak',
|
||||
];
|
||||
|
||||
@@ -2743,6 +2749,7 @@ usort(
|
||||
);
|
||||
|
||||
$monthNarrativeText = monthForecastBuildNarrativeText($monthEventList, $selectedMonth, $months[$selectedMonth], $selectedYear);
|
||||
$monthEventListJson = json_encode($monthEventList, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
||||
|
||||
$observationTipInsertSql = buildObservationTipInsertSql($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,
|
||||
];
|
||||
},
|
||||
$favoriteCometPayload
|
||||
$includeComets ? $favoriteCometPayload : []
|
||||
);
|
||||
$monthChartCometsJson = json_encode($monthChartComets, 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>
|
||||
</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">
|
||||
<button type="submit">Monatsvorhersage berechnen</button>
|
||||
</div>
|
||||
@@ -2878,16 +2890,26 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
</label>
|
||||
|
||||
<div class="monatsvorhersage-actions">
|
||||
<button type="button" disabled>Wochenansicht folgt spaeter</button>
|
||||
<button type="button" disabled>Wochenansicht folgt später</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<?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>
|
||||
<h3>Monat Im Ueberblick</h3>
|
||||
<h3>Monat im Überblick</h3>
|
||||
<p><?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2961,7 +2983,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
|
||||
<div class="monatskarte-stage monatsphase-stage">
|
||||
<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>
|
||||
|
||||
@@ -3023,7 +3045,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p>Fuer diesen Monat wurden keine Ereignisse gefunden.</p>
|
||||
<p>Für diesen Monat wurden keine Ereignisse gefunden.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
@@ -3036,7 +3058,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
@@ -3057,6 +3079,19 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
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 {
|
||||
margin: 0 0 14px;
|
||||
padding: 10px 12px;
|
||||
@@ -3179,6 +3214,22 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
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 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -3294,8 +3345,7 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei
|
||||
|
||||
#monatsphasenImage {
|
||||
display: block;
|
||||
width: min(100%, 1200px);
|
||||
min-width: 920px;
|
||||
width: min(100%, 773px);
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
border-radius: 20px;
|
||||
@@ -3335,6 +3385,7 @@ const MONTH_CHART_PLANETS = <?= $planetDataJson ?: '[]' ?>;
|
||||
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
|
||||
const MONTH_CHART_COMETS = <?= $monthChartCometsJson ?: '[]' ?>;
|
||||
const MONTH_PHASE_EVENTS = <?= $moonPhaseEventsJson ?: '[]' ?>;
|
||||
const MONTH_EVENT_LIST = <?= $monthEventListJson ?: '[]' ?>;
|
||||
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
|
||||
const OBSERVATION_TIP_ICAL = <?= $observationTipCalendarIcsJson ?: '""' ?>;
|
||||
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 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 drawX = padding;
|
||||
const drawY = logoPosition === 'bottom-left'
|
||||
const drawX = logoPosition === 'bottom-right'
|
||||
? exportCanvas.width - padding - targetWidth
|
||||
: padding;
|
||||
const drawY = logoPosition === 'bottom-left' || logoPosition === 'bottom-right'
|
||||
? exportCanvas.height - padding - targetHeight
|
||||
: padding;
|
||||
exportCtx.drawImage(logo, drawX, drawY, targetWidth, targetHeight);
|
||||
@@ -3480,8 +3533,8 @@ function monthChartExportFilename(viewMode) {
|
||||
const year = String(MONTH_CHART_CONFIG.year || '');
|
||||
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 safeMode = viewMode === 'sued' ? 'sued' : 'polar';
|
||||
return 'Himmelsanblick_' + safeMode + '_' + year + '_' + month + '_' + hour + '-' + minute + '.png';
|
||||
const label = viewMode === 'sued' ? 'Himmelsanblick_Sued' : 'Himmelsanblick_Polar';
|
||||
return year + '_' + month + '_' + label + '_' + hour + '_' + minute + '.png';
|
||||
}
|
||||
|
||||
function monthChartExportCaption() {
|
||||
@@ -3708,7 +3761,7 @@ function monthChartCometRaDec(comet, date) {
|
||||
if (insertStatus) {
|
||||
insertStatus.hidden = false;
|
||||
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) {
|
||||
@@ -3807,7 +3860,7 @@ function monthChartCometRaDec(comet, date) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const phaseImage = new Image();
|
||||
let phaseViewportWidth = 1200;
|
||||
let phaseViewportHeight = 220;
|
||||
let phaseViewportHeight = 700;
|
||||
let phaseRenderDpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
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() {
|
||||
const cssWidth = Math.max(920, Math.floor(imageEl.clientWidth || 1200));
|
||||
const cssHeight = 220;
|
||||
const cssWidth = Math.max(320, Math.floor(imageEl.clientWidth || 773));
|
||||
const cssHeight = Math.round(cssWidth * (700 / 773));
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
renderPhaseCanvas(cssWidth, cssHeight, dpr, true);
|
||||
}
|
||||
@@ -3924,6 +3977,7 @@ function monthChartCometRaDec(comet, date) {
|
||||
|
||||
const width = phaseViewportWidth;
|
||||
const height = phaseViewportHeight;
|
||||
const layoutScale = width / 773;
|
||||
const year = Number(MONTH_CHART_CONFIG.year);
|
||||
const month = Number(MONTH_CHART_CONFIG.month);
|
||||
const monthName = String(MONTH_CHART_CONFIG.monthName || '');
|
||||
@@ -3938,30 +3992,37 @@ function monthChartCometRaDec(comet, date) {
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
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.textBaseline = 'top';
|
||||
ctx.fillText('Mondphasen im ' + monthName + ' ' + year, 28, 18);
|
||||
ctx.fillText('Mondphasen im ' + monthName + ' ' + year, 42 * layoutScale, 27 * layoutScale);
|
||||
|
||||
const leftPad = 22;
|
||||
const rightPad = 22;
|
||||
const leftPad = 33 * layoutScale;
|
||||
const rightPad = 33 * layoutScale;
|
||||
const usableWidth = width - leftPad - rightPad;
|
||||
const spacing = usableWidth / totalDays;
|
||||
const radius = Math.max(11, Math.min(18, spacing * 0.42));
|
||||
const centerY = 82;
|
||||
const daysPerRow = 9;
|
||||
const rowCenters = [108, 260, 411, 563].map(function (value) { return value * layoutScale; });
|
||||
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.font = '500 10px \"Share Tech Mono\", monospace';
|
||||
ctx.font = '500 ' + (17 * layoutScale) + 'px \"Share Tech Mono\", monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
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));
|
||||
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)';
|
||||
|
||||
for (const event of Array.isArray(MONTH_PHASE_EVENTS) ? MONTH_PHASE_EVENTS : []) {
|
||||
@@ -3975,10 +4036,13 @@ function monthChartCometRaDec(comet, date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const x = leftPad + spacing * (day - 0.5);
|
||||
ctx.fillText(String(event.label || ''), x, 158);
|
||||
const row = Math.floor((day - 1) / daysPerRow);
|
||||
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.fillText(day + '.' + month + '.', x, 175);
|
||||
ctx.fillText(day + '.' + month + '.', x, eventDateY[row]);
|
||||
ctx.fillStyle = 'rgba(225, 232, 246, 0.94)';
|
||||
}
|
||||
|
||||
@@ -3992,31 +4056,23 @@ function monthChartCometRaDec(comet, date) {
|
||||
|
||||
const year = String(MONTH_CHART_CONFIG.year || '');
|
||||
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) {
|
||||
downloadButton.addEventListener('click', function () {
|
||||
const previous = {
|
||||
width: phaseViewportWidth,
|
||||
height: phaseViewportHeight,
|
||||
dpr: phaseRenderDpr,
|
||||
};
|
||||
|
||||
try {
|
||||
statusEl.textContent = 'Mondphasen-PNG wird in 2000 px neu gerendert...';
|
||||
statusEl.textContent = 'Mondphasen-PNG wird erstellt...';
|
||||
downloadButton.disabled = true;
|
||||
|
||||
const exportWidth = MONTH_CHART_EXPORT_WIDTH;
|
||||
const exportHeight = Math.round(exportWidth * (220 / 1200));
|
||||
renderPhaseCanvas(exportWidth, exportHeight, 1, false);
|
||||
composeExportCanvas(canvas, 'bottom-left', { showCaption: false, logoScale: 0.6 }).then(function (exportCanvas) {
|
||||
composeExportCanvas(canvas, 'bottom-right', { showCaption: false, logoScale: 0.6 })
|
||||
.then(function (exportCanvas) {
|
||||
triggerCanvasDownload(exportCanvas || canvas, monthPhaseExportFilename());
|
||||
});
|
||||
} finally {
|
||||
renderPhaseCanvas(previous.width, previous.height, previous.dpr, true);
|
||||
})
|
||||
.finally(function () {
|
||||
draw();
|
||||
imageEl.src = canvas.toDataURL('image/png');
|
||||
downloadButton.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4496,7 +4552,7 @@ function monthChartCometRaDec(comet, date) {
|
||||
const y = cy - Math.cos(angleRad) * (radius + offset);
|
||||
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) {
|
||||
rotation -= Math.PI;
|
||||
}
|
||||
@@ -6100,6 +6156,306 @@ function monthChartCometRaDec(comet, date) {
|
||||
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>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user