Monatgsvorhersage verbessert

This commit is contained in:
Eskimue
2026-04-30 15:31:48 +02:00
parent 0a9ce8be1e
commit 91694867e4
+363 -73
View File
@@ -835,45 +835,282 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['moon_phase', 'golden_handle', 'moon_planet', 'moon_planet_approach', 'moon_deep_sky', 'moon_deep_sky_approach', 'moon_occultation', 'moon_apsis'], true), static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['moon_phase', 'golden_handle', 'moon_planet', 'moon_planet_approach', 'moon_deep_sky', 'moon_deep_sky_approach', 'moon_occultation', 'moon_apsis'], true),
10 10
); );
// Jahreszahl aus Datum entfernen (z. B. "19.04.2026" → "19.04.")
$shortDate = static function (string $d): string {
return (string) preg_replace('/\.\d{4}$/', '.', $d);
};
if ($moonEvents !== []) { if ($moonEvents !== []) {
// Chronologisch sortieren nach local_iso
$sortedMoonEvents = $moonEvents; $sortedMoonEvents = $moonEvents;
usort($sortedMoonEvents, static fn (array $a, array $b): int => strcmp( usort($sortedMoonEvents, static fn (array $a, array $b): int => strcmp(
(string) ($a['local_iso'] ?? $a['date'] ?? ''), (string) ($a['local_iso'] ?? $a['date'] ?? ''),
(string) ($b['local_iso'] ?? $b['date'] ?? '') (string) ($b['local_iso'] ?? $b['date'] ?? '')
)); ));
// Natürlichsprachliche Liste: "Vollmond ist am 01.05., Apogäum am 05.05. und Neumond am 16.05." $phaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'moon_phase'));
$moonParts = []; $nonPhaseEvents = array_values(array_filter($sortedMoonEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') !== 'moon_phase'));
foreach ($sortedMoonEvents as $me) {
$label = trim((string) ($me['event'] ?? '')); $moonSentences = [];
$date = trim((string) ($me['date'] ?? ''));
if ($label === '' || $date === '') { // 1. Mondphasen in einem natürlichen Satz zusammenfassen
if ($phaseEvents !== []) {
$phaseParts = [];
foreach ($phaseEvents as $pe) {
$rawLabel = trim((string) ($pe['event'] ?? ''));
$date = trim((string) ($pe['date'] ?? ''));
if ($rawLabel === '' || $date === '') {
continue; continue;
} }
// "Mond bedeckt X" → "der Mond bedeckt X" für natürlicheren Satzbau // "Mond Erstes Viertel" → "Erstes Viertel"
if (str_starts_with($label, 'Mond bedeckt ')) { $cleanLabel = (string) preg_replace('/^Mond\s+/', '', $rawLabel);
$label = 'der ' . $label; $phaseParts[] = $cleanLabel . ' am ' . $shortDate($date);
}
if ($phaseParts !== []) {
$lastPhase = array_pop($phaseParts);
$phaseList = $phaseParts !== [] ? implode(', ', $phaseParts) . ' und ' . $lastPhase : $lastPhase;
$moonSentences[] = 'Die Mondphasen im Überblick: ' . $phaseList . '.';
} }
$moonParts[] = ['label' => $label, 'date' => $date];
} }
if ($moonParts !== []) { // 2. Apsis, Begegnungen und Goldener Henkel – nach Datum gruppiert
$moonSentenceParts = []; $byDate = [];
foreach ($moonParts as $idx => $part) { foreach ($nonPhaseEvents as $ne) {
if ($idx === 0) { $date = trim((string) ($ne['date'] ?? ''));
$moonSentenceParts[] = $part['label'] . ' ist am ' . $part['date']; if ($date === '') {
continue;
}
$byDate[$date][] = $ne;
}
foreach ($byDate as $date => $dateEvents) {
$apsis = null;
$approaches = [];
$goldenHandle = null;
foreach ($dateEvents as $de) {
$type = (string) ($de['type'] ?? '');
if ($type === 'moon_apsis') {
$apsis = $de;
} elseif ($type === 'golden_handle') {
$goldenHandle = $de;
} else { } else {
$moonSentenceParts[] = $part['label'] . ' am ' . $part['date']; $approaches[] = $de;
} }
} }
$lastPart = array_pop($moonSentenceParts);
$moonSentence = $moonSentenceParts !== [] $sd = $shortDate($date);
? implode(', ', $moonSentenceParts) . ' und ' . $lastPart
: $lastPart; if ($goldenHandle !== null) {
$paragraphs[] = $moonSentence . '.'; $moonSentences[] = 'Am ' . $sd . ' zeigt der Mond den Goldenen Henkel: Ein Gebirgskamm am Mondrand leuchtet bereits im Sonnenlicht, während das Tal dahinter noch im Schatten liegt – ein lohnender Anblick schon im kleinen Teleskop.';
}
if ($apsis !== null) {
$apsisLabel = trim((string) ($apsis['event'] ?? ''));
// Distanz aus dem Label extrahieren: "(404.960 km)"
$distanceMatch = [];
$distanceStr = '';
if (preg_match('/\(([0-9.,]+\s*km)\)\s*$/', $apsisLabel, $distanceMatch)) {
$distanceStr = ' mit nur ' . $distanceMatch[1];
}
$isPerigee = str_contains($apsisLabel, 'Erdnähe') || str_contains($apsisLabel, 'Perigäum');
if ($isPerigee) {
$apsisText = 'Am ' . $sd . ' erreicht der Mond sein Perigäum' . $distanceStr . ' – er steht der Erde in diesem Monat am nächsten';
} else {
$apsisText = 'Am ' . $sd . ' erreicht der Mond sein Apogäum' . $distanceStr . ' – er steht der Erde in diesem Monat am fernsten';
}
if ($approaches !== []) {
// Begegnungen natürlich formulieren: "und zieht dabei in nur X Grad Abstand an Y vorbei"
$approachFragments = [];
foreach ($approaches as $ap) {
$apLabel = trim((string) ($ap['event'] ?? ''));
$sepMatch = [];
$sepStr = '';
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $apLabel, $sepMatch)) {
$apLabel = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $apLabel));
$sepStr = ' in nur ' . $sepMatch[1] . ' Abstand';
}
if (str_starts_with($apLabel, 'Mond nahe ')) {
$target = substr($apLabel, strlen('Mond nahe '));
$approachFragments[] = $sepStr . ' an ' . $target . ' vorbei';
} elseif (str_starts_with($apLabel, 'Mond bedeckt ')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$approachFragments[] = ' und bedeckt dabei ' . $target;
} else {
$approachFragments[] = $sepStr !== '' ? $sepStr . ' bei ' . $apLabel : ' bei ' . $apLabel;
} }
} }
$approachClause = count($approachFragments) === 1
? ' und zieht dabei' . $approachFragments[0]
: ' und zieht dabei' . implode(' sowie', $approachFragments);
$moonSentences[] = $apsisText . $approachClause . '.';
} else {
$moonSentences[] = $apsisText . '.';
}
} elseif ($approaches !== []) {
foreach ($approaches as $ap) {
$apLabel = trim((string) ($ap['event'] ?? ''));
$apDate = trim((string) ($ap['date'] ?? ''));
$sepMatch = [];
$sepStr = '';
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $apLabel, $sepMatch)) {
$apLabel = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $apLabel));
$sepStr = ' in nur ' . $sepMatch[1] . ' Abstand';
}
$apSd = $shortDate($apDate !== '' ? $apDate : $date);
if (str_starts_with($apLabel, 'Mond nahe ')) {
$target = substr($apLabel, strlen('Mond nahe '));
$moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.';
} elseif (str_starts_with($apLabel, 'Mond bedeckt ')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$moonSentences[] = 'Am ' . $apSd . ' bedeckt der Mond ' . $target . '.';
} else {
$moonSentences[] = $apLabel . ' am ' . $apSd . ($sepStr !== '' ? ' – ' . ltrim($sepStr) : '') . '.';
}
}
}
}
if ($moonSentences !== []) {
$paragraphs[] = implode(' ', $moonSentences);
}
}
// Hilfsfunktion: natürlichen Satz aus einem Planeten-Event bauen
$buildPlanetSentence = static function (array $event) use ($shortDate): ?string {
$type = (string) ($event['type'] ?? '');
$label = trim((string) ($event['event'] ?? ''));
$date = trim((string) ($event['date'] ?? ''));
if ($label === '' || $date === '') {
return null;
}
$sd = $shortDate($date);
if ($type === 'planet_conjunction') {
$sepMatch = [];
$sep = '';
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) {
$sep = $sepMatch[1];
$label = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $label));
}
if (preg_match('/^(.+?)\s+nahe\s+(.+)$/i', $label, $m)) {
$p1 = trim($m[1]);
$p2 = trim($m[2]);
$sepNote = $sep !== '' ? ' – der Abstand beträgt nur ' . $sep : '';
return 'Am ' . $sd . ' rücken ' . $p1 . ' und ' . $p2 . ' eng zusammen' . $sepNote . '.';
}
}
if ($type === 'golden_gate_of_ecliptic') {
$planet = '';
if (preg_match('/^(\S+)\s+im\s+Goldenen\s+Tor/i', $label, $m)) {
$planet = $m[1];
}
if ($planet !== '') {
return 'Am ' . $sd . ' durchquert ' . $planet . ' das Goldene Tor der Ekliptik und zieht dabei zwischen Hyaden und Plejaden hindurch – ein lohnender Anblick im Fernglas.';
}
}
if ($type === 'inner_planet_elongation') {
$sepMatch = [];
$sepStr = '';
if (preg_match('/\(([0-9.,]+\s*Grad)\)/', $label, $sepMatch)) {
$sepStr = ' (' . $sepMatch[1] . ')';
$label = trim((string) preg_replace('/\s*\([0-9.,]+\s*Grad\)/', '', $label));
}
$isWest = str_contains($label, 'westlich') || str_contains($label, 'West');
$planet = '';
if (preg_match('/^(\S+)\s+/', $label, $m)) {
$planet = $m[1];
}
$visibility = $isWest ? 'als Morgenstern am besten zu beobachten' : 'als Abendstern am besten zu beobachten';
return 'Am ' . $sd . ' erreicht ' . ($planet ?: 'der Planet') . ' seine größte Elongation' . $sepStr . ' und ist dann ' . $visibility . '.';
}
if ($type === 'venus_peak_magnitude') {
$magMatch = [];
$magStr = '';
if (preg_match('/\(([^)]+)\)/', $label, $magMatch)) {
$magStr = ' mit ' . $magMatch[1];
}
return 'Am ' . $sd . ' erreicht Venus ihre größte Helligkeit' . $magStr . ' – sie ist dann selbst in der Dämmerung kaum zu übersehen.';
}
if ($type === 'outer_planet_event') {
$planet = '';
if (preg_match('/^(\S+)\s+/', $label, $m)) {
$planet = $m[1];
}
if (str_contains($label, 'Opposition')) {
return 'Am ' . $sd . ' steht ' . ($planet ?: 'der Planet') . ' in Opposition – er geht bei Sonnenuntergang auf, ist die ganze Nacht sichtbar und leuchtet so hell wie selten.';
}
if (str_contains($label, 'Konjunktion')) {
return 'Am ' . $sd . ' steht ' . ($planet ?: 'der Planet') . ' in Konjunktion mit der Sonne und ist für einige Wochen nicht zu beobachten.';
}
if (str_contains($label, 'Quadratur')) {
return 'Am ' . $sd . ' befindet sich ' . ($planet ?: 'der Planet') . ' in Quadratur – er steht 90 Grad von der Sonne entfernt und ist in der ersten oder zweiten Nachthälfte gut sichtbar.';
}
}
if ($type === 'mercury_good_visibility') {
return 'Um den ' . $sd . ' zeigt sich Merkur günstig am Himmel und lässt sich in der Dämmerung tief über dem Horizont aufspüren.';
}
if ($type === 'planet_parade') {
return 'Am ' . $sd . ' sind mehrere Planeten gleichzeitig am Himmel zu sehen – eine seltene Planetenparade, die einen Blick Richtung Süden lohnt.';
}
// Fallback
return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.';
};
$buildPlanetMotionSentence = static function (array $event) use ($shortDate): ?string {
$type = (string) ($event['type'] ?? '');
$label = trim((string) ($event['event'] ?? ''));
$date = trim((string) ($event['date'] ?? ''));
if ($label === '' || $date === '') {
return null;
}
$sd = $shortDate($date);
if ($type === 'outer_planet_station') {
$planet = '';
$isRetrograde = str_contains($label, 'rückläufig') || str_contains($label, 'Rückläufig');
$isPrograde = str_contains($label, 'rechtläufig') || str_contains($label, 'Rechtläufig') || str_contains($label, 'direkt');
if (preg_match('/^(\S+)\s+/', $label, $m)) {
$planet = $m[1];
}
if ($isRetrograde) {
return 'Ab dem ' . $sd . ' läuft ' . ($planet ?: 'der Planet') . ' rückläufig – er bewegt sich scheinbar rückwärts vor dem Sternhintergrund, was seine Beobachtung besonders interessant macht.';
}
if ($isPrograde) {
return 'Ab dem ' . $sd . ' setzt ' . ($planet ?: 'der Planet') . ' seine normale Vorwärtsbewegung am Himmel wieder fort.';
}
return 'Am ' . $sd . ' wird ' . ($planet ?: 'der Planet') . ' stationär und ändert seine Bewegungsrichtung am Himmel.';
}
if ($type === 'planet_constellation_change') {
// Label-Format: "Mars wechselt vom Sternbild Wassermann in das Sternbild Fische"
if (preg_match('/^(.+?)\s+wechselt\s+vom\s+Sternbild\s+(.+?)\s+in\s+das\s+Sternbild\s+(.+)$/i', $label, $m)) {
$planet = trim($m[1]);
$from = trim($m[2]);
$to = trim($m[3]);
// Formulierung deterministisch variieren (nach erstem Buchstaben des Planetennamens)
$variant = ord($planet[0]) % 3;
if ($variant === 1) {
return 'Am ' . $sd . ' verlässt ' . $planet . ' das Sternbild ' . $from . ' und zieht weiter in das Sternbild ' . $to . '.';
}
if ($variant === 2) {
return 'Am ' . $sd . ' tritt ' . $planet . ' aus dem Sternbild ' . $from . ' in das Sternbild ' . $to . ' über.';
}
return 'Am ' . $sd . ' wechselt ' . $planet . ' vom Sternbild ' . $from . ' in das Sternbild ' . $to . '.';
}
// Fallback ohne "vom"-Angabe
if (preg_match('/^(.+?)\s+wechselt\s+(?:in\s+das\s+Sternbild\s+|ins?\s+(?:Sternbild\s+)?)(.+)$/i', $label, $m)) {
return 'Am ' . $sd . ' wechselt ' . trim($m[1]) . ' in das Sternbild ' . trim($m[2]) . '.';
}
}
return 'Am ' . $sd . ' ist zu beobachten: ' . $label . '.';
};
$planetObservationEvents = $findEvents( $planetObservationEvents = $findEvents(
$filteredEvents, $filteredEvents,
@@ -881,34 +1118,108 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
5 5
); );
if ($planetObservationEvents !== []) { if ($planetObservationEvents !== []) {
$planetIntro = $pickRandomText([ $planetSentences = [];
'Bei den Planeten lohnt sich vor allem der Blick auf', foreach ($planetObservationEvents as $pe) {
'Planetarisch besonders interessant sind in diesem Monat', $sentence = $buildPlanetSentence($pe);
'Bei den Planeten stechen in diesem Monat vor allem', if ($sentence !== null) {
'Einen planetaren Schwerpunkt bilden in diesem Monat', $planetSentences[] = $sentence;
], 'Bei den Planeten lohnt sich vor allem der Blick auf'); }
$planetOutro = $pickRandomText([ }
'Diese Termine geben dem Monat seinen charakteristischen planetaren Akzent.', if ($planetSentences !== []) {
'Damit bekommt der Monat einen klaren planetaren Schwerpunkt.', $paragraphs[] = implode(' ', $planetSentences);
'Solche Termine prägen den planetaren Charakter dieses Monats besonders deutlich.', }
'Sie setzen im Monatsverlauf markante planetare Akzente.',
], 'Diese Termine geben dem Monat seinen charakteristischen planetaren Akzent.');
$paragraphs[] = $planetIntro . ' ' . $describeEvents($planetObservationEvents) . '. ' . $planetOutro;
} }
$planetMotionEvents = $findEvents( $planetMotionEvents = $findEvents(
$filteredEvents, $filteredEvents,
static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['outer_planet_station', 'planet_constellation_change'], true), static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['outer_planet_station', 'planet_constellation_change'], true),
4 8
); );
if ($planetMotionEvents !== []) { if ($planetMotionEvents !== []) {
$planetMotionIntro = $pickRandomText([ // Sternbild-Akkusativformen für natürliche Sätze ohne "Sternbild"-Wiederholung
'Im Monatsverlauf zeigen sich bei den Planeten auch auffällige Positionswechsel.', $constellationAkk = [
'Auch in ihrer Stellung am Himmel verändern sich die Planeten in diesem Monat sichtbar.', 'Widder' => 'den Widder', 'Stier' => 'den Stier', 'Zwillinge' => 'die Zwillinge',
'Neben den Beobachtungshighlights fallen bei den Planeten auch einige Positionswechsel auf.', 'Krebs' => 'den Krebs', 'Löwe' => 'den Löwen', 'Jungfrau' => 'die Jungfrau',
'Auch die Bewegung der Planeten vor dem Sternhintergrund macht sich in diesem Monat bemerkbar.', 'Waage' => 'die Waage', 'Skorpion' => 'den Skorpion', 'Schütze' => 'den Schützen',
], 'Im Monatsverlauf zeigen sich bei den Planeten auch auffaellige Positionswechsel.'); 'Steinbock' => 'den Steinbock', 'Wassermann' => 'den Wassermann', 'Fische' => 'die Fische',
$paragraphs[] = $planetMotionIntro . ' Dazu gehören ' . $describeEvents($planetMotionEvents) . '.'; 'Walfisch' => 'den Walfisch', 'Orion' => 'den Orion', 'Fuhrmann' => 'den Fuhrmann',
'Stier' => 'den Stier', 'Kassiopeia' => 'die Kassiopeia', 'Andromeda' => 'die Andromeda',
'Perseus' => 'den Perseus', 'Pegasus' => 'den Pegasus', 'Leier' => 'die Leier',
'Schwan' => 'den Schwan', 'Adler' => 'den Adler', 'Schlange' => 'die Schlange',
'Schlangenträger' => 'den Schlangenträger', 'Herkules' => 'den Herkules',
'Bärenhüter' => 'den Bärenhüter', 'Großer Bär' => 'den Großen Bären',
'Drache' => 'den Drachen', 'Nördliche Krone' => 'die Nördliche Krone',
'Kleiner Löwe' => 'den Kleinen Löwen', 'Rabe' => 'den Raben', 'Becher' => 'den Becher',
'Wasserschlange' => 'die Wasserschlange', 'Eridanus' => 'den Eridanus',
'Hase' => 'den Hasen', 'Großer Hund' => 'den Großen Hund',
'Kleiner Hund' => 'den Kleinen Hund', 'Einhorn' => 'das Einhorn',
'Delfin' => 'den Delfin', 'Pfeil' => 'den Pfeil', 'Fuchs' => 'den Fuchs',
'Kepheus' => 'den Kepheus', 'Südlicher Fisch' => 'den Südlichen Fisch',
'Jagdhunde' => 'die Jagdhunde', 'Löwchen' => 'das Löwchen',
];
$akkusativ = static function (string $name) use ($constellationAkk): string {
return $constellationAkk[$name] ?? 'das Sternbild ' . $name;
};
$stationEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'outer_planet_station'));
$changeEvents = array_values(array_filter($planetMotionEvents, static fn (array $e): bool => (string) ($e['type'] ?? '') === 'planet_constellation_change'));
$motionSentences = [];
// Rückläufigkeits-Ereignisse einzeln als Satz
foreach ($stationEvents as $se) {
$sentence = $buildPlanetMotionSentence($se);
if ($sentence !== null) {
$motionSentences[] = $sentence;
}
}
// Sternbild-Wechsel: nach Planet gruppieren, kompakt als eine Aussage
if ($changeEvents !== []) {
$byPlanet = [];
$planetOrder = [];
foreach ($changeEvents as $ce) {
$label = trim((string) ($ce['event'] ?? ''));
$date = trim((string) ($ce['date'] ?? ''));
if (!preg_match('/^(.+?)\s+wechselt\s+vom\s+Sternbild\s+(.+?)\s+in\s+das\s+Sternbild\s+(.+)$/i', $label, $m)) {
continue;
}
$planet = trim($m[1]);
$to = trim($m[3]);
if (!isset($byPlanet[$planet])) {
$byPlanet[$planet] = [];
$planetOrder[] = $planet;
}
$byPlanet[$planet][] = ['date' => $date, 'to' => $to];
}
if ($byPlanet !== []) {
$parts = [];
foreach ($planetOrder as $planet) {
$moves = $byPlanet[$planet];
if (count($moves) === 1) {
$parts[] = $planet . ' am ' . $shortDate($moves[0]['date']) . ' in ' . $akkusativ($moves[0]['to']);
} else {
// Mehrfacher Wechsel desselben Planeten
$moveParts = [];
foreach ($moves as $move) {
$moveParts[] = 'am ' . $shortDate($move['date']) . ' in ' . $akkusativ($move['to']);
}
$lastMove = array_pop($moveParts);
$parts[] = $planet . ' ' . ($moveParts !== [] ? implode(', ', $moveParts) . ' und dann ' . $lastMove : $lastMove);
}
}
$lastPart = array_pop($parts);
$changeText = count($byPlanet) > 1
? implode(', ', $parts) . ' und ' . $lastPart
: $lastPart;
$motionSentences[] = 'Sternbildwechsel im Monatsverlauf: ' . $changeText . '.';
}
}
if ($motionSentences !== []) {
$paragraphs[] = implode(' ', $motionSentences);
}
} }
$cometEvents = $findEvents( $cometEvents = $findEvents(
@@ -3040,40 +3351,13 @@ async function composeExportCanvas(sourceCanvas, logoPosition, options) {
const caption = monthChartExportCaption(); const caption = monthChartExportCaption();
if (showCaption && caption !== '') { if (showCaption && caption !== '') {
const padding = Math.round(exportCanvas.width * 0.024); const padding = Math.round(exportCanvas.width * 0.024);
const fontSize = Math.max(26, Math.round(exportCanvas.width * 0.022)); const fontSize = Math.max(18, Math.round(exportCanvas.width * 0.015));
exportCtx.save(); exportCtx.save();
exportCtx.font = '600 ' + fontSize + 'px Rajdhani, sans-serif'; exportCtx.font = '600 ' + fontSize + 'px Rajdhani, sans-serif';
exportCtx.textAlign = 'right'; exportCtx.textAlign = 'right';
exportCtx.textBaseline = 'bottom'; exportCtx.textBaseline = 'bottom';
const metrics = exportCtx.measureText(caption);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2;
const boxPadX = Math.round(fontSize * 0.45);
const boxPadY = Math.round(fontSize * 0.3);
const boxWidth = textWidth + boxPadX * 2;
const boxHeight = ascent + descent + boxPadY * 2;
const boxX = exportCanvas.width - padding - boxWidth;
const boxY = exportCanvas.height - padding - boxHeight;
const radius = Math.round(fontSize * 0.28);
exportCtx.fillStyle = 'rgba(3, 7, 14, 0.7)';
exportCtx.beginPath();
exportCtx.moveTo(boxX + radius, boxY);
exportCtx.lineTo(boxX + boxWidth - radius, boxY);
exportCtx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + radius);
exportCtx.lineTo(boxX + boxWidth, boxY + boxHeight - radius);
exportCtx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - radius, boxY + boxHeight);
exportCtx.lineTo(boxX + radius, boxY + boxHeight);
exportCtx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - radius);
exportCtx.lineTo(boxX, boxY + radius);
exportCtx.quadraticCurveTo(boxX, boxY, boxX + radius, boxY);
exportCtx.closePath();
exportCtx.fill();
exportCtx.fillStyle = 'rgba(235, 242, 255, 0.96)'; exportCtx.fillStyle = 'rgba(235, 242, 255, 0.96)';
exportCtx.fillText(caption, exportCanvas.width - padding - boxPadX, exportCanvas.height - padding - boxPadY); exportCtx.fillText(caption, exportCanvas.width - padding, exportCanvas.height - padding);
exportCtx.restore(); exportCtx.restore();
} }
@@ -3113,7 +3397,13 @@ function monthChartExportCaption() {
return ''; return '';
} }
return 'Sternhimmel für ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ' Uhr'; const parts = String(MONTH_CHART_CONFIG.localTimeLabel || '').split(' ');
const date = parts[0] || '';
const hh = String(hour).padStart(2, '0');
const mm = String(minute).padStart(2, '0');
return date !== ''
? 'Sternhimmel am ' + date + ' um ' + hh + ':' + mm + ' Uhr'
: 'Sternhimmel um ' + hh + ':' + mm + ' Uhr';
} }
function formatGermanDate(date) { function formatGermanDate(date) {