Files
skyview.astronomiemuseum.de/public/monatsvorhersage_old.php
T

2712 lines
90 KiB
PHP

<?php
declare(strict_types=1);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Monatsvorhersage - AstroTools';
$bodyClass = 'admin-page monatsvorhersage-page';
$publicBasePath = './';
$currentYear = (int) date('Y');
$selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear;
$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n');
$starDatabaseMagnitudeLimit = 6.0;
$starDisplayMagnitudeLimit = 4.4;
$starLabelMagnitudeLimit = 2.0;
$messierDatabaseMagnitudeLimit = 4.0;
$messierDisplayMagnitudeLimit = 1.0;
$moonPlanetMaxSeparationDeg = 1.5;
$planetConjunctionMaxSeparationDeg = 1.0;
$moonOccultationMaxMag = 2.0;
$years = range($currentYear - 2, $currentYear + 5);
$months = [
1 => 'Januar',
2 => 'Februar',
3 => 'Maerz',
4 => 'April',
5 => 'Mai',
6 => 'Juni',
7 => 'Juli',
8 => 'August',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Dezember',
];
$monthSkyTimes = [
1 => ['hour' => 22, 'minute' => 0],
2 => ['hour' => 22, 'minute' => 0],
3 => ['hour' => 22, 'minute' => 0],
4 => ['hour' => 23, 'minute' => 0],
5 => ['hour' => 23, 'minute' => 0],
6 => ['hour' => 23, 'minute' => 30],
7 => ['hour' => 23, 'minute' => 30],
8 => ['hour' => 23, 'minute' => 0],
9 => ['hour' => 23, 'minute' => 0],
10 => ['hour' => 23, 'minute' => 0],
11 => ['hour' => 22, 'minute' => 0],
12 => ['hour' => 22, 'minute' => 0],
];
$starData = [];
$lineStarData = [];
$constellationLines = [];
$messierData = [];
$planetData = [];
$moonData = null;
$moonPhaseEvents = [];
$moonPlanetApproaches = [];
$planetConjunctions = [];
$eclipseEvents = [];
$moonApsisEvents = [];
$sunApsisEvents = [];
$moonOccultations = [];
$seasonChanges = [];
$monthEventList = [];
$starDataError = null;
if (!function_exists('buildMonthOccultationStarLabel')) {
function buildMonthOccultationStarLabel(array $row): string
{
$proper = trim((string) ($row['proper'] ?? ''));
if ($proper !== '') {
return $proper;
}
$bayer = trim((string) ($row['bayer'] ?? ''));
$con = trim((string) ($row['con'] ?? ''));
if ($bayer !== '') {
return $con !== '' ? $bayer . ' ' . $con : $bayer;
}
$hip = isset($row['hip']) ? (int) $row['hip'] : 0;
return $hip > 0 ? 'HIP ' . $hip : 'Unbenannter Stern';
}
}
try {
$dbConfig = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$dbConfig['host'],
$dbConfig['dbname'],
$dbConfig['charset'] ?? 'utf8mb4'
);
$pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$starStmt = $pdo->query(
"SELECT hip, proper, bayer, con, spect, mag, ci, rarad, decrad
FROM star_hipparcos
WHERE mag IS NOT NULL
AND rarad IS NOT NULL
AND decrad IS NOT NULL
AND mag <= " . (float) $starDatabaseMagnitudeLimit . "
ORDER BY mag ASC"
);
while ($row = $starStmt->fetch()) {
$starData[] = [
'hip' => $row['hip'] !== null ? (int) $row['hip'] : null,
'proper' => trim((string) ($row['proper'] ?? '')),
'bayer' => trim((string) ($row['bayer'] ?? '')),
'con' => trim((string) ($row['con'] ?? '')),
'spect' => trim((string) ($row['spect'] ?? '')),
'ra' => (float) $row['rarad'],
'dec' => (float) $row['decrad'],
'mag' => (float) $row['mag'],
'ci' => $row['ci'] !== null ? (float) $row['ci'] : null,
];
}
$constellationIndex = [];
$ensureConstellation = static function (string $id) use (&$constellationIndex): void {
if (!isset($constellationIndex[$id])) {
$constellationIndex[$id] = [
'id' => $id,
'lines' => [],
'label_positions' => [],
'common_name' => [
'german' => null,
'native' => null,
'byname' => null,
],
];
}
};
$nameStmt = $pdo->query(
"SELECT constellation_id, german_name, native_name, byname
FROM constellation_names
ORDER BY constellation_id"
);
foreach ($nameStmt as $row) {
$constellationId = (string) $row['constellation_id'];
$ensureConstellation($constellationId);
$constellationIndex[$constellationId]['common_name'] = [
'german' => $row['german_name'] !== null ? (string) $row['german_name'] : null,
'native' => $row['native_name'] !== null ? (string) $row['native_name'] : null,
'byname' => $row['byname'] !== null ? (string) $row['byname'] : null,
];
}
$labelStmt = $pdo->query(
"SELECT constellation_id, position_index, ra_hours, dec_degrees
FROM constellation_label_positions
ORDER BY constellation_id, position_index"
);
foreach ($labelStmt as $row) {
$constellationId = (string) $row['constellation_id'];
$ensureConstellation($constellationId);
$constellationIndex[$constellationId]['label_positions'][] = [
(float) $row['ra_hours'],
(float) $row['dec_degrees'],
];
}
$lineStmt = $pdo->query(
"SELECT cl.constellation_id, cl.line_index, clp.point_index, clp.hip_number
FROM constellation_lines cl
INNER JOIN constellation_line_points clp ON clp.line_id = cl.id
ORDER BY cl.constellation_id, cl.line_index, clp.point_index"
);
foreach ($lineStmt as $row) {
$constellationId = (string) $row['constellation_id'];
$lineIndex = (int) $row['line_index'];
$ensureConstellation($constellationId);
$constellationIndex[$constellationId]['lines'][$lineIndex] ??= [];
$constellationIndex[$constellationId]['lines'][$lineIndex][] = (int) $row['hip_number'];
}
foreach ($constellationIndex as &$constellation) {
if ($constellation['lines']) {
ksort($constellation['lines']);
$constellation['lines'] = array_values($constellation['lines']);
}
}
unset($constellation);
$constellationLines = array_values($constellationIndex);
$lineStarStmt = $pdo->query(
"SELECT DISTINCT sh.hip, sh.proper, sh.bayer, sh.con, sh.spect, sh.mag, sh.ci, sh.rarad, sh.decrad
FROM constellation_line_points clp
INNER JOIN star_hipparcos sh ON sh.hip = clp.hip_number
WHERE sh.rarad IS NOT NULL
AND sh.decrad IS NOT NULL"
);
while ($row = $lineStarStmt->fetch()) {
$lineStarData[] = [
'hip' => $row['hip'] !== null ? (int) $row['hip'] : null,
'proper' => trim((string) ($row['proper'] ?? '')),
'bayer' => trim((string) ($row['bayer'] ?? '')),
'con' => trim((string) ($row['con'] ?? '')),
'spect' => trim((string) ($row['spect'] ?? '')),
'ra' => (float) $row['rarad'],
'dec' => (float) $row['decrad'],
'mag' => $row['mag'] !== null ? (float) $row['mag'] : 99.0,
'ci' => $row['ci'] !== null ? (float) $row['ci'] : null,
];
}
$messierStmt = $pdo->query(
"SELECT messier, name, object_type_abrev, ra_deg, dec_deg, v_mag, major_axis
FROM dso_objects
WHERE messier IS NOT NULL
AND messier <> ''
AND ra_deg IS NOT NULL
AND dec_deg IS NOT NULL
AND v_mag IS NOT NULL
AND v_mag <= " . (float) $messierDatabaseMagnitudeLimit . "
ORDER BY v_mag ASC, CAST(REGEXP_REPLACE(messier, '[^0-9]', '') AS UNSIGNED)"
);
while ($row = $messierStmt->fetch()) {
if (!preg_match('/(\d+)/', (string) $row['messier'], $match)) {
continue;
}
$messierData[] = [
'm' => (int) $match[1],
'name' => trim((string) ($row['name'] ?? '')),
'type' => trim((string) ($row['object_type_abrev'] ?? 'As')),
'ra' => ((float) $row['ra_deg']) / 15.0,
'dec' => (float) $row['dec_deg'],
'mag' => (float) $row['v_mag'],
'size' => $row['major_axis'] !== null ? (float) $row['major_axis'] : 8.0,
];
}
} catch (Throwable $e) {
$starDataError = $e->getMessage();
}
require __DIR__ . '/header.php';
$chartLocationName = (string) (($currentLocation['name'] ?? null) ?: 'Sternwarte Sonneberg');
$chartLatitude = isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : 50.3547;
$chartLongitude = isset($currentLocation['longitude']) ? (float) $currentLocation['longitude'] : 11.1900;
$chartElevation = isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float) $currentLocation['elevation'] : 640.0;
$chartTimezone = (string) (($currentLocation['timezone'] ?? null) ?: 'Europe/Berlin');
$monthTime = $monthSkyTimes[$selectedMonth];
$chartLocalTime = new DateTimeImmutable(
sprintf('%04d-%02d-15 %02d:%02d:00', $selectedYear, $selectedMonth, $monthTime['hour'], $monthTime['minute']),
new DateTimeZone($chartTimezone)
);
$chartUtcTime = $chartLocalTime->setTimezone(new DateTimeZone('UTC'));
$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python';
$pythonScriptPath = __DIR__ . '/py/api.py';
$pythonOutput = [];
$pythonExitCode = 0;
$planetErrorText = null;
$pythonArgs = [
$pythonScriptPath,
'month_sky_context',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
(string) $monthTime['hour'],
(string) $monthTime['minute'],
$chartTimezone,
];
$pythonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1';
exec($pythonCommand, $pythonOutput, $pythonExitCode);
$pythonRaw = trim(implode("\n", $pythonOutput));
$pythonDecoded = json_decode($pythonRaw, true);
if (is_array($pythonDecoded) && ($pythonDecoded['ok'] ?? false)) {
if (is_array($pythonDecoded['bodies']['moon'] ?? null)) {
$moon = $pythonDecoded['bodies']['moon'];
$moonData = [
'label' => (string) ($moon['label'] ?? 'Mond'),
'az' => isset($moon['azimuth_deg']) ? (float) $moon['azimuth_deg'] : null,
'alt' => isset($moon['altitude_deg']) ? (float) $moon['altitude_deg'] : null,
'visible' => !empty($moon['visible']),
'phaseFraction' => isset($moon['phase_fraction']) ? (float) $moon['phase_fraction'] : null,
'waxing' => isset($moon['waxing']) ? (bool) $moon['waxing'] : null,
];
}
foreach (($pythonDecoded['bodies']['planets'] ?? []) as $planet) {
if (!is_array($planet)) {
continue;
}
$planetData[] = [
'key' => (string) ($planet['key'] ?? ''),
'label' => (string) ($planet['label'] ?? ''),
'az' => isset($planet['azimuth_deg']) ? (float) $planet['azimuth_deg'] : null,
'alt' => isset($planet['altitude_deg']) ? (float) $planet['altitude_deg'] : null,
'mag' => isset($planet['magnitude']) ? (float) $planet['magnitude'] : null,
'visible' => !empty($planet['visible']),
];
}
} elseif ($pythonRaw !== '') {
$planetErrorText = $pythonRaw;
}
$moonPhaseOutput = [];
$moonPhaseExitCode = 0;
$moonPhaseArgs = [
$pythonScriptPath,
'moon_phases_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$moonPhaseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPhaseArgs)) . ' 2>&1';
exec($moonPhaseCommand, $moonPhaseOutput, $moonPhaseExitCode);
$moonPhaseRaw = trim(implode("\n", $moonPhaseOutput));
$moonPhaseDecoded = json_decode($moonPhaseRaw, true);
if (is_array($moonPhaseDecoded) && ($moonPhaseDecoded['ok'] ?? false)) {
$moonPhaseEvents = is_array($moonPhaseDecoded['phases'] ?? null) ? $moonPhaseDecoded['phases'] : [];
}
$seasonOutput = [];
$seasonExitCode = 0;
$seasonArgs = [
$pythonScriptPath,
'season_changes_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$seasonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $seasonArgs)) . ' 2>&1';
exec($seasonCommand, $seasonOutput, $seasonExitCode);
$seasonRaw = trim(implode("\n", $seasonOutput));
$seasonDecoded = json_decode($seasonRaw, true);
if (is_array($seasonDecoded) && ($seasonDecoded['ok'] ?? false)) {
$seasonChanges = is_array($seasonDecoded['changes'] ?? null) ? $seasonDecoded['changes'] : [];
}
$eclipseOutput = [];
$eclipseExitCode = 0;
$eclipseArgs = [
$pythonScriptPath,
'eclipses_for_month',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$eclipseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $eclipseArgs)) . ' 2>&1';
exec($eclipseCommand, $eclipseOutput, $eclipseExitCode);
$eclipseRaw = trim(implode("\n", $eclipseOutput));
$eclipseDecoded = json_decode($eclipseRaw, true);
if (is_array($eclipseDecoded) && ($eclipseDecoded['ok'] ?? false)) {
$eclipseEvents = is_array($eclipseDecoded['events'] ?? null) ? $eclipseDecoded['events'] : [];
}
$moonApsisOutput = [];
$moonApsisExitCode = 0;
$moonApsisArgs = [
$pythonScriptPath,
'moon_apsides_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$moonApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonApsisArgs)) . ' 2>&1';
exec($moonApsisCommand, $moonApsisOutput, $moonApsisExitCode);
$moonApsisRaw = trim(implode("\n", $moonApsisOutput));
$moonApsisDecoded = json_decode($moonApsisRaw, true);
if (is_array($moonApsisDecoded) && ($moonApsisDecoded['ok'] ?? false)) {
$moonApsisEvents = is_array($moonApsisDecoded['events'] ?? null) ? $moonApsisDecoded['events'] : [];
}
$sunApsisOutput = [];
$sunApsisExitCode = 0;
$sunApsisArgs = [
$pythonScriptPath,
'sun_apsides_for_month',
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
];
$sunApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $sunApsisArgs)) . ' 2>&1';
exec($sunApsisCommand, $sunApsisOutput, $sunApsisExitCode);
$sunApsisRaw = trim(implode("\n", $sunApsisOutput));
$sunApsisDecoded = json_decode($sunApsisRaw, true);
if (is_array($sunApsisDecoded) && ($sunApsisDecoded['ok'] ?? false)) {
$sunApsisEvents = is_array($sunApsisDecoded['events'] ?? null) ? $sunApsisDecoded['events'] : [];
}
$moonPlanetOutput = [];
$moonPlanetExitCode = 0;
$moonPlanetArgs = [
$pythonScriptPath,
'moon_planet_approaches',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
(string) $moonPlanetMaxSeparationDeg,
];
$moonPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPlanetArgs)) . ' 2>&1';
exec($moonPlanetCommand, $moonPlanetOutput, $moonPlanetExitCode);
$moonPlanetRaw = trim(implode("\n", $moonPlanetOutput));
$moonPlanetDecoded = json_decode($moonPlanetRaw, true);
if (is_array($moonPlanetDecoded) && ($moonPlanetDecoded['ok'] ?? false)) {
$moonPlanetApproaches = is_array($moonPlanetDecoded['approaches'] ?? null) ? $moonPlanetDecoded['approaches'] : [];
}
$planetConjunctionOutput = [];
$planetConjunctionExitCode = 0;
$planetConjunctionArgs = [
$pythonScriptPath,
'planet_conjunctions_for_month',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
(string) $planetConjunctionMaxSeparationDeg,
];
$planetConjunctionCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConjunctionArgs)) . ' 2>&1';
exec($planetConjunctionCommand, $planetConjunctionOutput, $planetConjunctionExitCode);
$planetConjunctionRaw = trim(implode("\n", $planetConjunctionOutput));
$planetConjunctionDecoded = json_decode($planetConjunctionRaw, true);
if (is_array($planetConjunctionDecoded) && ($planetConjunctionDecoded['ok'] ?? false)) {
$planetConjunctions = is_array($planetConjunctionDecoded['conjunctions'] ?? null) ? $planetConjunctionDecoded['conjunctions'] : [];
}
$moonOccultationStars = [];
try {
if (isset($pdo)) {
$stmtOccultStars = $pdo->prepare(
"SELECT hip, proper, bayer, con, mag, rarad, decrad, dist
FROM star_hipparcos
WHERE rarad IS NOT NULL
AND decrad IS NOT NULL
AND mag IS NOT NULL
AND mag <= :max_mag
ORDER BY mag ASC, proper ASC, bayer ASC, hip ASC"
);
$stmtOccultStars->execute([':max_mag' => $moonOccultationMaxMag]);
foreach ($stmtOccultStars->fetchAll() as $row) {
$moonOccultationStars[] = [
'hip' => isset($row['hip']) ? (int) $row['hip'] : 0,
'label' => buildMonthOccultationStarLabel($row),
'constellation' => trim((string) ($row['con'] ?? '')),
'mag' => isset($row['mag']) ? (float) $row['mag'] : 99.0,
'ra' => isset($row['rarad']) ? (float) $row['rarad'] * (12 / M_PI) : 0.0,
'dec' => isset($row['decrad']) ? (float) $row['decrad'] * (180 / M_PI) : 0.0,
'distLy' => isset($row['dist']) && $row['dist'] !== null && (float) $row['dist'] > 0.0
? (float) $row['dist'] * 3.26156
: 1000.0,
];
}
}
} catch (Throwable $e) {
}
if ($moonOccultationStars) {
$tempOccultationFile = tempnam(sys_get_temp_dir(), 'month_occult_');
if ($tempOccultationFile !== false) {
try {
file_put_contents($tempOccultationFile, json_encode($moonOccultationStars, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
$moonOccultOutput = [];
$moonOccultExitCode = 0;
$moonOccultArgs = [
$pythonScriptPath,
'moon_star_occultations_for_month',
(string) $chartLatitude,
(string) $chartLongitude,
(string) $chartElevation,
(string) $selectedYear,
(string) $selectedMonth,
$chartTimezone,
$tempOccultationFile,
];
$moonOccultCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonOccultArgs)) . ' 2>&1';
exec($moonOccultCommand, $moonOccultOutput, $moonOccultExitCode);
$moonOccultRaw = trim(implode("\n", $moonOccultOutput));
$moonOccultDecoded = json_decode($moonOccultRaw, true);
if (is_array($moonOccultDecoded) && ($moonOccultDecoded['ok'] ?? false)) {
$moonOccultations = is_array($moonOccultDecoded['results'] ?? null) ? $moonOccultDecoded['results'] : [];
}
} catch (Throwable $e) {
} finally {
if (is_file($tempOccultationFile)) {
@unlink($tempOccultationFile);
}
}
}
}
foreach ($moonPhaseEvents as $phaseEvent) {
if (!is_array($phaseEvent)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($phaseEvent['label'] ?? ''),
'date' => (string) ($phaseEvent['local_date'] ?? ''),
'time' => (string) ($phaseEvent['local_time'] ?? ''),
'local_iso' => (string) ($phaseEvent['local_iso'] ?? ''),
'type' => 'moon_phase',
];
}
foreach ($seasonChanges as $seasonChange) {
if (!is_array($seasonChange)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($seasonChange['label'] ?? 'Jahreszeitenwechsel'),
'date' => (string) ($seasonChange['local_date'] ?? ''),
'time' => (string) ($seasonChange['local_time'] ?? ''),
'local_iso' => (string) ($seasonChange['local_iso'] ?? ''),
'type' => 'season_change',
];
}
foreach ($eclipseEvents as $eclipseEvent) {
if (!is_array($eclipseEvent)) {
continue;
}
$monthEventList[] = [
'event' => (string) ($eclipseEvent['label'] ?? 'Finsternis'),
'date' => (string) ($eclipseEvent['local_date'] ?? ''),
'time' => (string) ($eclipseEvent['local_time'] ?? ''),
'local_iso' => (string) ($eclipseEvent['local_iso'] ?? ''),
'type' => (string) ($eclipseEvent['category'] ?? 'eclipse'),
];
}
foreach ($moonApsisEvents as $moonApsisEvent) {
if (!is_array($moonApsisEvent)) {
continue;
}
$moonApsisKind = (string) ($moonApsisEvent['kind'] ?? '');
$distanceText = isset($moonApsisEvent['distance_km'])
? number_format((float) $moonApsisEvent['distance_km'], 0, ',', '.') . ' km'
: '';
$moonApsisLabel = $moonApsisKind === 'perigee'
? 'Mond in Erdnähe (Perigäum)'
: ($moonApsisKind === 'apogee' ? 'Mond in Erdferne (Apogäum)' : (string) ($moonApsisEvent['label'] ?? 'Mondabstand'));
$monthEventList[] = [
'event' => trim($moonApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($moonApsisEvent['local_date'] ?? ''),
'time' => (string) ($moonApsisEvent['local_time'] ?? ''),
'local_iso' => (string) ($moonApsisEvent['local_iso'] ?? ''),
'type' => 'moon_apsis',
];
}
foreach ($sunApsisEvents as $sunApsisEvent) {
if (!is_array($sunApsisEvent)) {
continue;
}
$sunApsisKind = (string) ($sunApsisEvent['kind'] ?? '');
$distanceText = isset($sunApsisEvent['distance_km'])
? number_format((float) $sunApsisEvent['distance_km'], 0, ',', '.') . ' km'
: '';
$sunApsisLabel = $sunApsisKind === 'perihelion'
? 'Erde erreicht sonnennächsten Punkt (Perihel)'
: ($sunApsisKind === 'aphelion' ? 'Erde erreicht sonnenfernsten Punkt (Aphel)' : (string) ($sunApsisEvent['label'] ?? 'Sonnenabstand'));
$monthEventList[] = [
'event' => trim($sunApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($sunApsisEvent['local_date'] ?? ''),
'time' => (string) ($sunApsisEvent['local_time'] ?? ''),
'local_iso' => (string) ($sunApsisEvent['local_iso'] ?? ''),
'type' => 'sun_apsis',
];
}
foreach ($moonPlanetApproaches as $approachEvent) {
if (!is_array($approachEvent)) {
continue;
}
$distanceText = isset($approachEvent['separation_deg'])
? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . ' Grad'
: '';
$monthEventList[] = [
'event' => trim((string) ($approachEvent['label'] ?? 'Mond nahe Planet') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($approachEvent['local_date'] ?? ''),
'time' => (string) ($approachEvent['local_time'] ?? ''),
'local_iso' => (string) ($approachEvent['local_iso'] ?? ''),
'type' => 'moon_planet',
];
}
foreach ($planetConjunctions as $conjunctionEvent) {
if (!is_array($conjunctionEvent)) {
continue;
}
$distanceText = isset($conjunctionEvent['separation_deg'])
? number_format((float) $conjunctionEvent['separation_deg'], 2, ',', '') . ' Grad'
: '';
$monthEventList[] = [
'event' => trim((string) ($conjunctionEvent['label'] ?? 'Planetenkonjunktion') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')),
'date' => (string) ($conjunctionEvent['local_date'] ?? ''),
'time' => (string) ($conjunctionEvent['local_time'] ?? ''),
'local_iso' => (string) ($conjunctionEvent['local_iso'] ?? ''),
'type' => 'planet_conjunction',
];
}
foreach ($moonOccultations as $occultationEvent) {
if (!is_array($occultationEvent)) {
continue;
}
$maximum = is_array($occultationEvent['maximum'] ?? null) ? $occultationEvent['maximum'] : null;
$star = is_array($occultationEvent['star'] ?? null) ? $occultationEvent['star'] : null;
if ($maximum === null || $star === null) {
continue;
}
$starLabel = (string) ($star['label'] ?? 'Stern');
$starMag = isset($star['mag']) ? number_format((float) $star['mag'], 1, ',', '') : '';
$monthEventList[] = [
'event' => 'Mond bedeckt ' . $starLabel . ($starMag !== '' ? ' (' . $starMag . ' mag)' : ''),
'date' => (string) ($maximum['local_date'] ?? ''),
'time' => (string) ($maximum['local_time'] ?? ''),
'local_iso' => (string) ($maximum['local_iso'] ?? ''),
'type' => 'moon_occultation',
];
}
usort(
$monthEventList,
static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? ''))
);
$chartConfig = [
'monthName' => $months[$selectedMonth],
'year' => $selectedYear,
'month' => $selectedMonth,
'location' => [
'name' => $chartLocationName,
'latitude' => $chartLatitude,
'longitude' => $chartLongitude,
'elevation' => $chartElevation,
'timezone' => $chartTimezone,
],
'localTimeLabel' => $chartLocalTime->format('d.m.Y H:i') . ' ' . $chartTimezone,
'utcTimeIso' => $chartUtcTime->format(DateTimeInterface::ATOM),
'starLimit' => $starDisplayMagnitudeLimit,
'starLabelLimit' => $starLabelMagnitudeLimit,
'messierLimit' => $messierDisplayMagnitudeLimit,
];
$chartConfigJson = json_encode($chartConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$starDataJson = json_encode($starData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$lineStarDataJson = json_encode($lineStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$constellationLinesJson = json_encode($constellationLines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$messierDataJson = json_encode($messierData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$planetDataJson = json_encode($planetData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$moonDataJson = json_encode($moonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<section class="card monatsvorhersage-card">
<div class="monatsvorhersage-shell">
<form class="monatsvorhersage-form" method="get">
<div class="monatsvorhersage-form-header">
<h3>Zeitraum waehlen</h3>
<p>Die Sternkarte wird fuer den 15. des gewaehlten Monats mit der festen Monatsuhrzeit erzeugt.</p>
</div>
<label class="monatsvorhersage-field">
<span>Jahr</span>
<select name="year">
<?php foreach ($years as $year): ?>
<option value="<?= htmlspecialchars((string) $year, ENT_QUOTES, 'UTF-8') ?>" <?= $year === $selectedYear ? 'selected' : '' ?>>
<?= htmlspecialchars((string) $year, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="monatsvorhersage-field">
<span>Monat</span>
<select name="month">
<?php foreach ($months as $monthNumber => $monthLabel): ?>
<option value="<?= htmlspecialchars((string) $monthNumber, ENT_QUOTES, 'UTF-8') ?>" <?= $monthNumber === $selectedMonth ? 'selected' : '' ?>>
<?= htmlspecialchars($monthLabel, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</label>
<div class="monatsvorhersage-actions">
<button type="submit">Sternkarte laden</button>
</div>
</form>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Zenitprojektion <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
<p id="monatskarteMeta">
<?= htmlspecialchars($chartConfig['localTimeLabel'], ENT_QUOTES, 'UTF-8') ?>,
<?= htmlspecialchars($chartLocationName, ENT_QUOTES, 'UTF-8') ?>
</p>
</div>
</div>
<div class="monatskarte-stage">
<canvas id="monatskarteCanvas" width="900" height="900" aria-label="Sternkarte in Zenitprojektion"></canvas>
</div>
<div class="monatskarte-footer">
<p id="monatskarteStatus">Bereite Sternkarte vor...</p>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Südhimmel <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
<p id="monatskarteSouthMeta">
<?= htmlspecialchars($chartConfig['localTimeLabel'], ENT_QUOTES, 'UTF-8') ?>,
<?= htmlspecialchars($chartLocationName, ENT_QUOTES, 'UTF-8') ?>
</p>
</div>
</div>
<div class="monatskarte-stage monatskarte-stage-south">
<canvas id="monatskarteSouthCanvas" width="1200" height="720" aria-label="Südhimmel-Projektion"></canvas>
</div>
<div class="monatskarte-footer">
<p id="monatskarteSouthStatus">Bereite Südhimmelkarte vor...</p>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Ereignisse <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
<p>Chronologische Monatsliste fuer <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?></p>
</div>
</div>
<div class="monatskarte-footer">
<?php if ($monthEventList): ?>
<div class="riseset-grid" style="grid-template-columns: 1.5fr 1fr 1fr; row-gap: 10px;">
<span class="riseset-label riseset-dim">Ereignis</span>
<span class="riseset-label riseset-dim">Datum</span>
<span class="riseset-label riseset-dim">Uhrzeit</span>
<?php foreach ($monthEventList as $eventItem): ?>
<span class="riseset-val"><?= htmlspecialchars((string) ($eventItem['event'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($eventItem['date'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($eventItem['time'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Fuer diesen Monat wurden keine Ereignisse gefunden.</p>
<?php endif; ?>
</div>
</section>
</div>
</section>
<style>
.monatsvorhersage-page main > .container {
max-width: min(1280px, calc(100% - 28px));
}
.monatsvorhersage-card {
padding: 18px;
}
.monatsvorhersage-shell {
display: grid;
gap: 18px;
}
.monatsvorhersage-form,
.monatskarte-card {
border: 1px solid rgba(201, 168, 76, 0.14);
border-radius: calc(var(--radius) + 4px);
background:
radial-gradient(circle at top left, rgba(74, 158, 255, 0.1), transparent 34%),
linear-gradient(180deg, rgba(10, 15, 30, 0.96) 0%, rgba(7, 11, 22, 0.98) 100%);
}
.monatsvorhersage-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 22px 20px;
}
.monatsvorhersage-form-header,
.monatskarte-header,
.monatskarte-footer {
padding: 22px 20px;
}
.monatsvorhersage-form-header {
grid-column: 1 / -1;
padding: 0;
}
.monatsvorhersage-form-header h3,
.monatskarte-header h3 {
margin: 0 0 8px;
}
.monatsvorhersage-form-header p,
.monatskarte-header p,
.monatskarte-footer p {
margin: 0;
color: rgba(222, 232, 255, 0.86);
line-height: 1.7;
}
.monatsvorhersage-field {
display: grid;
gap: 8px;
}
.monatsvorhersage-field span {
color: rgba(235, 242, 255, 0.95);
font-weight: 600;
}
.monatsvorhersage-field select {
width: 100%;
min-height: 46px;
padding: 10px 12px;
border: 1px solid rgba(108, 145, 220, 0.28);
border-radius: 12px;
background: rgba(8, 14, 28, 0.92);
color: rgba(235, 242, 255, 0.95);
font: inherit;
}
.monatsvorhersage-actions {
grid-column: 1 / -1;
}
.monatsvorhersage-actions button {
min-height: 44px;
padding: 10px 16px;
border: 1px solid rgba(150, 190, 255, 0.78);
border-radius: 999px;
background: rgba(38, 66, 122, 0.92);
color: rgba(245, 248, 255, 0.96);
font: inherit;
cursor: pointer;
}
.monatskarte-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
}
.monatskarte-stage {
padding: 0 20px 20px;
}
.monatskarte-stage canvas {
display: block;
width: min(100%, 900px);
aspect-ratio: 1;
margin: 0 auto;
border-radius: 20px;
background: #010203;
box-shadow: inset 0 0 0 1px rgba(140, 165, 220, 0.12);
}
.monatskarte-stage-south canvas {
width: min(100%, 1100px);
aspect-ratio: 5 / 3;
}
.monatskarte-footer {
padding-top: 0;
}
@media (max-width: 820px) {
.monatsvorhersage-form {
grid-template-columns: 1fr;
}
.monatskarte-header {
flex-direction: column;
}
}
</style>
<script src="<?= htmlspecialchars($publicBasePath . 'js/astronomy.browser.min.js', ENT_QUOTES, 'UTF-8') ?>"></script>
<script>
const MONTH_CHART_CONFIG = <?= $chartConfigJson ?: 'null' ?>;
const MONTH_CHART_STARS = <?= $starDataJson ?: '[]' ?>;
const MONTH_CHART_LINE_STARS = <?= $lineStarDataJson ?: '[]' ?>;
const MONTH_CHART_CONSTELLATIONS = <?= $constellationLinesJson ?: '[]' ?>;
const MONTH_CHART_MESSIER = <?= $messierDataJson ?: '[]' ?>;
const MONTH_CHART_PLANETS = <?= $planetDataJson ?: '[]' ?>;
const MONTH_CHART_MOON = <?= $moonDataJson ?: 'null' ?>;
const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
(function () {
const canvas = document.getElementById('monatskarteCanvas');
const statusEl = document.getElementById('monatskarteStatus');
if (!canvas || !statusEl) {
return;
}
if (!window.Astronomy) {
statusEl.textContent = 'Astronomy Engine konnte nicht geladen werden.';
return;
}
if (!MONTH_CHART_CONFIG) {
statusEl.textContent = 'Die Monatskonfiguration konnte nicht geladen werden.';
return;
}
if (MONTH_CHART_ERROR) {
statusEl.textContent = 'Stern-Datenbank konnte nicht geladen werden: ' + MONTH_CHART_ERROR;
return;
}
const ctx = canvas.getContext('2d');
const date = new Date(MONTH_CHART_CONFIG.utcTimeIso);
const observer = new Astronomy.Observer(
Number(MONTH_CHART_CONFIG.location.latitude),
Number(MONTH_CHART_CONFIG.location.longitude),
Number(MONTH_CHART_CONFIG.location.elevation || 0)
);
const starByHip = new Map();
const constellationHipSet = new Set();
const displayStarMap = new Map();
let labelPlacer = null;
let projectionLayout = null;
const projCenterAzDeg = 180;
const projCenterAltDeg = 45;
for (const star of MONTH_CHART_LINE_STARS) {
if (Number.isFinite(Number(star.hip))) {
starByHip.set(Number(star.hip), star);
}
}
for (const star of MONTH_CHART_STARS) {
if (Number.isFinite(Number(star.hip))) {
displayStarMap.set(Number(star.hip), star);
} else {
displayStarMap.set(`star-${displayStarMap.size}`, star);
}
}
for (const constellation of MONTH_CHART_CONSTELLATIONS) {
if (!Array.isArray(constellation.lines)) {
continue;
}
for (const segment of constellation.lines) {
if (!Array.isArray(segment)) {
continue;
}
for (const hip of segment) {
if (Number.isFinite(Number(hip))) {
constellationHipSet.add(Number(hip));
}
}
}
}
for (const hip of constellationHipSet) {
if (!displayStarMap.has(hip) && starByHip.has(hip)) {
displayStarMap.set(hip, starByHip.get(hip));
}
}
function clamp01(value) {
return Math.max(0, Math.min(1, value));
}
function createLabelPlacer(context) {
const occupiedBoxes = [];
function overlaps(box) {
return occupiedBoxes.some((other) =>
box.x < other.x + other.width &&
box.x + box.width > other.x &&
box.y < other.y + other.height &&
box.y + box.height > other.y
);
}
function insideStage(box) {
return box.x >= 0 &&
box.y >= 0 &&
box.x + box.width <= canvas.clientWidth &&
box.y + box.height <= canvas.clientHeight;
}
return {
reset() {
occupiedBoxes.length = 0;
},
drawText(options) {
const {
text,
x,
y,
font = '12px Georgia, serif',
color = 'rgba(232, 239, 255, 0.82)',
align = 'left',
baseline = 'middle',
padding = 2,
} = options;
if (!text) {
return false;
}
context.save();
context.font = font;
context.textAlign = align;
context.textBaseline = baseline;
const metrics = context.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
let boxX = x;
if (align === 'center') {
boxX = x - textWidth / 2;
} else if (align === 'right') {
boxX = x - textWidth;
}
let boxY = y - ascent;
if (baseline === 'middle') {
boxY = y - (ascent + descent) / 2;
} else if (baseline === 'bottom' || baseline === 'alphabetic') {
boxY = y - ascent - descent;
}
const box = {
x: boxX - padding,
y: boxY - padding,
width: textWidth + padding * 2,
height: ascent + descent + padding * 2,
};
if (!insideStage(box) || overlaps(box)) {
context.restore();
return false;
}
occupiedBoxes.push(box);
context.fillStyle = color;
context.fillText(text, x, y);
context.restore();
return true;
},
};
}
function placePlanetLabel(text, x, y, radius) {
const attempts = [
{ x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' },
{ x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' },
{ x, y: y - radius - 7, align: 'center', baseline: 'bottom' },
{ x, y: y + radius + 7, align: 'center', baseline: 'top' },
{ x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' },
{ x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' },
{ x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' },
{ x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' },
];
function drawLabelBackdrop(drawX, drawY, align, baseline) {
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
const padX = 5;
const padY = 3;
let boxX = drawX;
if (align === 'center') boxX = drawX - textWidth / 2;
else if (align === 'right') boxX = drawX - textWidth;
let boxY = drawY - ascent;
if (baseline === 'middle') boxY = drawY - (ascent + descent) / 2;
else if (baseline === 'bottom' || baseline === 'alphabetic') boxY = drawY - ascent - descent;
const rectX = boxX - padX;
const rectY = boxY - padY;
const rectW = textWidth + padX * 2;
const rectH = ascent + descent + padY * 2;
const radiusPx = 6;
ctx.fillStyle = 'rgba(3, 6, 12, 0.52)';
ctx.beginPath();
ctx.moveTo(rectX + radiusPx, rectY);
ctx.lineTo(rectX + rectW - radiusPx, rectY);
ctx.quadraticCurveTo(rectX + rectW, rectY, rectX + rectW, rectY + radiusPx);
ctx.lineTo(rectX + rectW, rectY + rectH - radiusPx);
ctx.quadraticCurveTo(rectX + rectW, rectY + rectH, rectX + rectW - radiusPx, rectY + rectH);
ctx.lineTo(rectX + radiusPx, rectY + rectH);
ctx.quadraticCurveTo(rectX, rectY + rectH, rectX, rectY + rectH - radiusPx);
ctx.lineTo(rectX, rectY + radiusPx);
ctx.quadraticCurveTo(rectX, rectY, rectX + radiusPx, rectY);
ctx.closePath();
ctx.fill();
ctx.restore();
}
for (const attempt of attempts) {
if (labelPlacer.drawText({
text,
x: attempt.x,
y: attempt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(246, 235, 214, 0.84)',
align: attempt.align,
baseline: attempt.baseline,
padding: 2,
})) {
drawLabelBackdrop(attempt.x, attempt.y, attempt.align, attempt.baseline);
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.textAlign = attempt.align;
ctx.textBaseline = attempt.baseline;
ctx.fillStyle = 'rgba(246, 235, 214, 0.84)';
ctx.fillText(text, attempt.x, attempt.y);
ctx.restore();
return true;
}
}
return false;
}
function placePlanetLabel(text, x, y, radius) {
const attempts = [
{ x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' },
{ x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' },
{ x, y: y - radius - 7, align: 'center', baseline: 'bottom' },
{ x, y: y + radius + 7, align: 'center', baseline: 'top' },
{ x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' },
{ x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' },
{ x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' },
{ x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' },
];
function drawLabelBackdrop(drawX, drawY, align, baseline) {
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
const padX = 5;
const padY = 3;
let boxX = drawX;
if (align === 'center') boxX = drawX - textWidth / 2;
else if (align === 'right') boxX = drawX - textWidth;
let boxY = drawY - ascent;
if (baseline === 'middle') boxY = drawY - (ascent + descent) / 2;
else if (baseline === 'bottom' || baseline === 'alphabetic') boxY = drawY - ascent - descent;
const rectX = boxX - padX;
const rectY = boxY - padY;
const rectW = textWidth + padX * 2;
const rectH = ascent + descent + padY * 2;
const radiusPx = 6;
ctx.fillStyle = 'rgba(3, 6, 12, 0.52)';
ctx.beginPath();
ctx.moveTo(rectX + radiusPx, rectY);
ctx.lineTo(rectX + rectW - radiusPx, rectY);
ctx.quadraticCurveTo(rectX + rectW, rectY, rectX + rectW, rectY + radiusPx);
ctx.lineTo(rectX + rectW, rectY + rectH - radiusPx);
ctx.quadraticCurveTo(rectX + rectW, rectY + rectH, rectX + rectW - radiusPx, rectY + rectH);
ctx.lineTo(rectX + radiusPx, rectY + rectH);
ctx.quadraticCurveTo(rectX, rectY + rectH, rectX, rectY + rectH - radiusPx);
ctx.lineTo(rectX, rectY + radiusPx);
ctx.quadraticCurveTo(rectX, rectY, rectX + radiusPx, rectY);
ctx.closePath();
ctx.fill();
ctx.restore();
}
for (const attempt of attempts) {
if (labelPlacer.drawText({
text,
x: attempt.x,
y: attempt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(246, 235, 214, 0.84)',
align: attempt.align,
baseline: attempt.baseline,
padding: 2,
})) {
drawLabelBackdrop(attempt.x, attempt.y, attempt.align, attempt.baseline);
ctx.save();
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.textAlign = attempt.align;
ctx.textBaseline = attempt.baseline;
ctx.fillStyle = 'rgba(246, 235, 214, 0.84)';
ctx.fillText(text, attempt.x, attempt.y);
ctx.restore();
return true;
}
}
return false;
}
function resizeCanvas() {
const cssSize = Math.max(320, Math.min(canvas.parentElement.clientWidth, 900));
const ratio = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(cssSize * ratio);
canvas.height = Math.round(cssSize * ratio);
canvas.style.width = cssSize + 'px';
canvas.style.height = cssSize + 'px';
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(ratio, ratio);
labelPlacer = createLabelPlacer(ctx);
draw();
}
function project(altDeg, azDeg, radius, cx, cy) {
if (!Number.isFinite(altDeg) || !Number.isFinite(azDeg) || altDeg < 0) {
return null;
}
const rho = (90 - altDeg) / 90;
const azRad = azDeg * Math.PI / 180;
return {
x: cx - rho * radius * Math.sin(azRad),
y: cy - rho * radius * Math.cos(azRad),
};
}
function starHorizon(star) {
const raHours = star.ra * (12 / Math.PI);
const decDeg = star.dec * (180 / Math.PI);
return Astronomy.Horizon(date, observer, raHours, decDeg, 'normal');
}
function starColor(star) {
if (typeof star.ci === 'number') {
if (star.ci < -0.1) return '#b9d4ff';
if (star.ci < 0.3) return '#dbe7ff';
if (star.ci < 0.7) return '#fff6e0';
if (star.ci < 1.1) return '#ffe0b5';
return '#ffc98a';
}
const spect = String(star.spect || '').trim().toUpperCase();
if (spect.startsWith('O') || spect.startsWith('B')) return '#b9d4ff';
if (spect.startsWith('A')) return '#dbe7ff';
if (spect.startsWith('F') || spect.startsWith('G')) return '#fff6e0';
if (spect.startsWith('K')) return '#ffd4a0';
if (spect.startsWith('M')) return '#ffbd85';
return '#f4f7ff';
}
function getStarLabel(star) {
const proper = String(star.proper || '').trim();
if (proper !== '') {
return proper;
}
const bayer = String(star.bayer || '').trim();
const con = String(star.con || '').trim();
if (bayer !== '') {
return con !== '' ? `${bayer} ${con}` : bayer;
}
return '';
}
function drawSkyBackground(cx, cy, radius) {
const baseGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
baseGrad.addColorStop(0, '#010203');
baseGrad.addColorStop(0.45, '#02050b');
baseGrad.addColorStop(0.78, '#06101d');
baseGrad.addColorStop(1, '#0d1f38');
ctx.fillStyle = baseGrad;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
const zenithDarkening = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius * 0.92);
zenithDarkening.addColorStop(0, 'rgba(0, 0, 0, 0.44)');
zenithDarkening.addColorStop(0.42, 'rgba(0, 0, 0, 0.2)');
zenithDarkening.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = zenithDarkening;
ctx.beginPath();
ctx.arc(cx, cy, radius * 0.92, 0, Math.PI * 2);
ctx.fill();
const horizonGlow = ctx.createRadialGradient(cx, cy, radius * 0.74, cx, cy, radius * 1.04);
horizonGlow.addColorStop(0, 'rgba(0, 0, 0, 0)');
horizonGlow.addColorStop(0.72, 'rgba(78, 126, 214, 0.025)');
horizonGlow.addColorStop(0.9, 'rgba(118, 170, 255, 0.09)');
horizonGlow.addColorStop(1, 'rgba(168, 208, 255, 0.14)');
ctx.strokeStyle = horizonGlow;
ctx.lineWidth = Math.max(8, radius * 0.035);
ctx.beginPath();
ctx.arc(cx, cy, radius * 0.97, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = 'rgba(183, 205, 255, 0.28)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
}
function drawGuides(cx, cy, radius) {
ctx.strokeStyle = 'rgba(140, 165, 220, 0.18)';
ctx.lineWidth = 1;
for (const frac of [0.33, 0.66]) {
ctx.beginPath();
ctx.arc(cx, cy, radius * frac, 0, Math.PI * 2);
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(cx, cy - radius);
ctx.lineTo(cx, cy + radius);
ctx.moveTo(cx - radius, cy);
ctx.lineTo(cx + radius, cy);
ctx.stroke();
function drawDirectionLabel(angleDeg, label, offset, font, alpha) {
const angleRad = angleDeg * Math.PI / 180;
const x = cx + Math.sin(angleRad) * (radius + offset);
const y = cy - Math.cos(angleRad) * (radius + offset);
let rotation = angleRad;
// Text tangential zum Horizontkreis ausrichten, dabei moeglichst gut lesbar halten.
if (rotation > Math.PI / 2 && rotation < Math.PI * 1.5) {
rotation -= Math.PI;
}
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.font = font;
ctx.fillStyle = `rgba(214, 226, 255, ${alpha})`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, 0, 0);
ctx.restore();
}
drawDirectionLabel(0, 'Norden', 18, '600 13px Rajdhani, sans-serif', 0.78);
drawDirectionLabel(90, 'Westen', 22, '600 13px Rajdhani, sans-serif', 0.78);
drawDirectionLabel(180, 'Süden', 18, '600 13px Rajdhani, sans-serif', 0.78);
drawDirectionLabel(270, 'Osten', 22, '600 13px Rajdhani, sans-serif', 0.78);
drawDirectionLabel(45, 'Nordosten', 18, '600 11px Rajdhani, sans-serif', 0.64);
drawDirectionLabel(315, 'Nordwesten', 18, '600 11px Rajdhani, sans-serif', 0.64);
drawDirectionLabel(135, 'Südosten', 18, '600 11px Rajdhani, sans-serif', 0.64);
drawDirectionLabel(225, 'Südwesten', 18, '600 11px Rajdhani, sans-serif', 0.64);
ctx.fillStyle = 'rgba(214, 226, 255, 0.62)';
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.fillText('60°', cx, cy - radius * 0.66 - 6);
ctx.fillText('30°', cx, cy - radius * 0.33 - 6);
ctx.fillText('Zenit', cx, cy);
}
function drawConstellations(cx, cy, radius) {
for (const constellation of MONTH_CHART_CONSTELLATIONS) {
if (!Array.isArray(constellation.lines)) {
continue;
}
for (const segment of constellation.lines) {
if (!Array.isArray(segment) || segment.length < 2) {
continue;
}
const points = [];
for (const hip of segment) {
const star = starByHip.get(Number(hip));
if (!star) {
continue;
}
const hor = starHorizon(star);
if (!hor || hor.altitude < 0) {
continue;
}
const pt = project(hor.altitude, hor.azimuth, radius, cx, cy);
if (!pt) {
continue;
}
points.push(pt);
}
if (points.length < 2) {
continue;
}
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(210, 214, 224, 0.16)';
ctx.lineWidth = 2.1;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(36, 40, 48, 0.34)';
ctx.lineWidth = 0.95;
ctx.beginPath();
ctx.moveTo(points[0].x + 0.18, points[0].y - 0.12);
for (let i = 1; i < points.length; i++) {
const jitterX = (i % 2 === 0 ? 0.22 : -0.12);
const jitterY = (i % 2 === 0 ? -0.14 : 0.2);
ctx.lineTo(points[i].x + jitterX, points[i].y + jitterY);
}
ctx.stroke();
ctx.restore();
}
}
}
function drawConstellationLabels(cx, cy, radius) {
for (const constellation of MONTH_CHART_CONSTELLATIONS) {
const label = constellation.common_name?.german || constellation.common_name?.native || constellation.id;
if (!label) {
continue;
}
const labelPositions = Array.isArray(constellation.label_positions)
? constellation.label_positions
.filter((pos) => Array.isArray(pos) && pos.length >= 2)
.map((pos) => [Number(pos[0]), Number(pos[1])])
.filter(([raH, decD]) => Number.isFinite(raH) && Number.isFinite(decD))
: [];
for (const [raHours, decDeg] of labelPositions) {
const hor = equatorialHorizon(raHours, decDeg);
if (!hor || hor.altitude < 8) {
continue;
}
const pt = project(hor.altitude, hor.azimuth, radius, cx, cy);
if (!pt) {
continue;
}
const placed = labelPlacer.drawText({
text: label,
x: pt.x,
y: pt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(198, 214, 255, 0.54)',
align: 'center',
baseline: 'middle',
padding: 3,
});
if (placed) {
break;
}
}
}
}
function equatorialHorizon(raHours, decDeg) {
return Astronomy.Horizon(date, observer, raHours, decDeg, 'normal');
}
function getMessierStyle(type) {
switch (type) {
case 'Gc':
return { stroke: 'rgba(255, 180, 96, 0.92)', fill: 'rgba(255, 180, 96, 0.12)' };
case 'Oc':
return { stroke: 'rgba(255, 224, 150, 0.84)', fill: 'rgba(255, 224, 150, 0.08)' };
case 'Gx':
return { stroke: 'rgba(255, 140, 188, 0.88)', fill: 'rgba(255, 140, 188, 0.08)' };
case 'Pn':
return { stroke: 'rgba(156, 255, 186, 0.88)', fill: 'rgba(156, 255, 186, 0.08)' };
case 'Nb':
case 'Sc':
return { stroke: 'rgba(118, 204, 255, 0.88)', fill: 'rgba(118, 204, 255, 0.08)' };
default:
return { stroke: 'rgba(210, 218, 232, 0.74)', fill: 'rgba(210, 218, 232, 0.06)' };
}
}
function getMessierLabel(obj) {
const messierId = Number(obj.m);
if (!Number.isFinite(messierId)) {
return '';
}
const name = String(obj.name || '').trim();
return name !== '' ? `M${messierId} ${name}` : `M${messierId}`;
}
function getPlanetStyle(planet) {
switch (String(planet.key || '')) {
case 'mercury':
return { fill: '#c9b9a7', glow: 'rgba(201, 185, 167, 0.34)' };
case 'venus':
return { fill: '#f4ddb2', glow: 'rgba(244, 221, 178, 0.4)' };
case 'mars':
return { fill: '#d7724c', glow: 'rgba(215, 114, 76, 0.34)' };
case 'jupiter':
return { fill: '#d9b07e', glow: 'rgba(217, 176, 126, 0.36)' };
case 'saturn':
return { fill: '#d8ca8b', glow: 'rgba(216, 202, 139, 0.34)' };
case 'uranus':
return { fill: '#8fd3df', glow: 'rgba(143, 211, 223, 0.3)' };
case 'neptune':
return { fill: '#5a78d6', glow: 'rgba(90, 120, 214, 0.3)' };
default:
return { fill: '#d8dde8', glow: 'rgba(216, 221, 232, 0.28)' };
}
}
function drawMoon(cx, cy, radius) {
if (!MONTH_CHART_MOON) {
return;
}
const alt = Number(MONTH_CHART_MOON.alt);
const az = Number(MONTH_CHART_MOON.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) {
return;
}
const pt = project(alt, az, radius, cx, cy);
if (!pt) {
return;
}
const phaseFraction = clamp01(Number(MONTH_CHART_MOON.phaseFraction ?? 0.5));
const waxing = Boolean(MONTH_CHART_MOON.waxing);
const moonRadius = 10.5;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, moonRadius * 2.1);
glow.addColorStop(0, 'rgba(220, 226, 238, 0.28)');
glow.addColorStop(0.5, 'rgba(220, 226, 238, 0.08)');
glow.addColorStop(1, 'rgba(220, 226, 238, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius * 2.1, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#d6d9df';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
const phaseScale = 1 - 2 * phaseFraction;
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.ellipse((waxing ? -1 : 1) * moonRadius * phaseScale, 0, moonRadius, moonRadius, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius, 0, Math.PI * 2);
ctx.stroke();
}
function drawPlanets(cx, cy, radius) {
for (const planet of MONTH_CHART_PLANETS) {
const alt = Number(planet.alt);
const az = Number(planet.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) {
continue;
}
const pt = project(alt, az, radius, cx, cy);
if (!pt) {
continue;
}
const style = getPlanetStyle(planet);
const mag = Number(planet.mag);
const baseRadius = Number.isFinite(mag)
? Math.max(3.8, Math.min(8.6, 6.7 - mag * 0.72))
: 5.4;
const glowRadius = baseRadius * 2.2;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, style.glow);
glow.addColorStop(0.45, style.glow.replace(/0\.\d+\)$/, '0.12)'));
glow.addColorStop(1, style.glow.replace(/0\.\d+\)$/, '0)'));
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = style.fill;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.stroke();
if (planet.key === 'jupiter') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, baseRadius - 0.3, 0, Math.PI * 2);
ctx.clip();
ctx.strokeStyle = 'rgba(116, 84, 54, 0.78)';
ctx.lineWidth = Math.max(1, baseRadius * 0.34);
ctx.beginPath();
ctx.moveTo(-baseRadius * 0.95, -baseRadius * 0.34);
ctx.lineTo(baseRadius * 0.95, -baseRadius * 0.34);
ctx.moveTo(-baseRadius * 0.95, baseRadius * 0.2);
ctx.lineTo(baseRadius * 0.95, baseRadius * 0.2);
ctx.stroke();
ctx.fillStyle = 'rgba(194, 92, 68, 0.95)';
ctx.beginPath();
ctx.ellipse(baseRadius * 0.42, baseRadius * 0.06, baseRadius * 0.3, baseRadius * 0.17, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
if (planet.key === 'saturn') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.rotate(-0.35);
ctx.strokeStyle = 'rgba(232, 221, 170, 0.72)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(0, 0, baseRadius * 1.7, baseRadius * 0.72, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
const label = String(planet.label || '').trim();
if (label !== '') {
placePlanetLabel(label, pt.x, pt.y, baseRadius);
}
}
}
function drawMessier(cx, cy, radius) {
for (const obj of MONTH_CHART_MESSIER) {
if (Number(obj.mag) > Number(MONTH_CHART_CONFIG.messierLimit)) {
continue;
}
const hor = equatorialHorizon(Number(obj.ra), Number(obj.dec));
if (!hor || hor.altitude < 0) {
continue;
}
const pt = project(hor.altitude, hor.azimuth, radius, cx, cy);
if (!pt) {
continue;
}
const style = getMessierStyle(String(obj.type || 'As'));
const symbolRadius = Math.max(3.4, Math.min(6.8, 3.2 + Math.max(0, 6.5 - Number(obj.mag)) * 0.5));
ctx.strokeStyle = style.stroke;
ctx.fillStyle = style.fill;
ctx.lineWidth = 1;
if (obj.type === 'Gc') {
ctx.beginPath();
ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(pt.x - symbolRadius * 0.8, pt.y);
ctx.lineTo(pt.x + symbolRadius * 0.8, pt.y);
ctx.moveTo(pt.x, pt.y - symbolRadius * 0.8);
ctx.lineTo(pt.x, pt.y + symbolRadius * 0.8);
ctx.stroke();
} else if (obj.type === 'Oc') {
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
} else if (obj.type === 'Gx') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.rotate(Math.PI / 5);
ctx.beginPath();
ctx.ellipse(0, 0, symbolRadius, symbolRadius * 0.58, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
} else if (obj.type === 'Nb' || obj.type === 'Sc') {
ctx.beginPath();
ctx.rect(pt.x - symbolRadius, pt.y - symbolRadius * 0.62, symbolRadius * 2, symbolRadius * 1.24);
ctx.fill();
ctx.stroke();
} else if (obj.type === 'Pn') {
ctx.beginPath();
ctx.arc(pt.x, pt.y, symbolRadius * 0.62, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
} else {
ctx.beginPath();
ctx.arc(pt.x, pt.y, symbolRadius * 0.68, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
const label = getMessierLabel(obj);
if (label !== '') {
labelPlacer.drawText({
text: label,
x: pt.x + symbolRadius + 5,
y: pt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(210, 224, 255, 0.78)',
align: 'left',
baseline: 'middle',
padding: 2,
});
}
}
}
function drawStars(cx, cy, radius) {
for (const star of displayStarMap.values()) {
const hip = Number(star.hip);
const isConstellationStar = Number.isFinite(hip) && constellationHipSet.has(hip);
if (!isConstellationStar && Number(star.mag) > Number(MONTH_CHART_CONFIG.starLimit)) {
continue;
}
const hor = starHorizon(star);
if (!hor || hor.altitude < 0) {
continue;
}
const pt = project(hor.altitude, hor.azimuth, radius, cx, cy);
if (!pt) {
continue;
}
const magRange = Math.max(0.5, Number(MONTH_CHART_CONFIG.starLimit) + 1.6);
const effectiveMagnitude = isConstellationStar
? Math.min(Number(star.mag), Number(MONTH_CHART_CONFIG.starLimit) + 0.35)
: Number(star.mag);
const brightness = clamp01((Number(MONTH_CHART_CONFIG.starLimit) - effectiveMagnitude + 1.6) / magRange);
const starRadius = (isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2;
const glowRadius = starRadius * (effectiveMagnitude <= 1 ? 2.8 : 1.9);
const color = starColor(star);
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, color + '66');
glow.addColorStop(0.35, color + '22');
glow.addColorStop(1, color + '00');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(pt.x, pt.y, starRadius, 0, Math.PI * 2);
ctx.fill();
if (star.mag <= 0.8) {
ctx.strokeStyle = color + '44';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pt.x - starRadius * 1.35, pt.y);
ctx.lineTo(pt.x + starRadius * 1.35, pt.y);
ctx.moveTo(pt.x, pt.y - starRadius * 1.35);
ctx.lineTo(pt.x, pt.y + starRadius * 1.35);
ctx.stroke();
}
if (Number(star.mag) <= Number(MONTH_CHART_CONFIG.starLabelLimit)) {
const label = getStarLabel(star);
if (label !== '') {
labelPlacer.drawText({
text: label,
x: pt.x + starRadius + 5,
y: pt.y - 1,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(232, 239, 255, 0.82)',
align: 'left',
baseline: 'middle',
padding: 2,
});
}
}
}
}
function draw() {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const cx = width / 2;
const cy = height / 2;
const radius = Math.min(width, height) * 0.44;
ctx.clearRect(0, 0, width, height);
labelPlacer.reset();
drawSkyBackground(cx, cy, radius);
drawGuides(cx, cy, radius);
drawConstellations(cx, cy, radius);
drawConstellationLabels(cx, cy, radius);
drawMessier(cx, cy, radius);
drawMoon(cx, cy, radius);
drawPlanets(cx, cy, radius);
drawStars(cx, cy, radius);
statusEl.textContent = 'Sternkarte fuer ' + MONTH_CHART_CONFIG.localTimeLabel;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
})();
(function () {
const canvas = document.getElementById('monatskarteSouthCanvas');
const statusEl = document.getElementById('monatskarteSouthStatus');
if (!canvas || !statusEl || !window.Astronomy || !MONTH_CHART_CONFIG || MONTH_CHART_ERROR) {
return;
}
const ctx = canvas.getContext('2d');
const date = new Date(MONTH_CHART_CONFIG.utcTimeIso);
const observer = new Astronomy.Observer(
Number(MONTH_CHART_CONFIG.location.latitude),
Number(MONTH_CHART_CONFIG.location.longitude),
Number(MONTH_CHART_CONFIG.location.elevation || 0)
);
const starByHip = new Map();
const constellationHipSet = new Set();
const displayStarMap = new Map();
let labelPlacer = null;
let projectionLayout = null;
const projCenterAzDeg = 180;
const projCenterAltDeg = 45;
for (const star of MONTH_CHART_LINE_STARS) {
if (Number.isFinite(Number(star.hip))) {
starByHip.set(Number(star.hip), star);
}
}
for (const star of MONTH_CHART_STARS) {
if (Number.isFinite(Number(star.hip))) {
displayStarMap.set(Number(star.hip), star);
} else {
displayStarMap.set(`south-star-${displayStarMap.size}`, star);
}
}
for (const constellation of MONTH_CHART_CONSTELLATIONS) {
if (!Array.isArray(constellation.lines)) {
continue;
}
for (const segment of constellation.lines) {
if (!Array.isArray(segment)) {
continue;
}
for (const hip of segment) {
if (Number.isFinite(Number(hip))) {
constellationHipSet.add(Number(hip));
}
}
}
}
for (const hip of constellationHipSet) {
if (!displayStarMap.has(hip) && starByHip.has(hip)) {
displayStarMap.set(hip, starByHip.get(hip));
}
}
function clamp01(value) {
return Math.max(0, Math.min(1, value));
}
function createLabelPlacer(context) {
const occupiedBoxes = [];
function overlaps(box) {
return occupiedBoxes.some((other) =>
box.x < other.x + other.width &&
box.x + box.width > other.x &&
box.y < other.y + other.height &&
box.y + box.height > other.y
);
}
function insideStage(box) {
return box.x >= 0 &&
box.y >= 0 &&
box.x + box.width <= canvas.clientWidth &&
box.y + box.height <= canvas.clientHeight;
}
return {
reset() {
occupiedBoxes.length = 0;
},
drawText(options) {
const {
text,
x,
y,
font = '600 11px Rajdhani, sans-serif',
color = 'rgba(232, 239, 255, 0.82)',
align = 'left',
baseline = 'middle',
padding = 2,
} = options;
if (!text) {
return false;
}
context.save();
context.font = font;
context.textAlign = align;
context.textBaseline = baseline;
const metrics = context.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
let boxX = x;
if (align === 'center') boxX = x - textWidth / 2;
else if (align === 'right') boxX = x - textWidth;
let boxY = y - ascent;
if (baseline === 'middle') boxY = y - (ascent + descent) / 2;
else if (baseline === 'bottom' || baseline === 'alphabetic') boxY = y - ascent - descent;
const box = {
x: boxX - padding,
y: boxY - padding,
width: textWidth + padding * 2,
height: ascent + descent + padding * 2,
};
if (!insideStage(box) || overlaps(box)) {
context.restore();
return false;
}
occupiedBoxes.push(box);
context.fillStyle = color;
context.fillText(text, x, y);
context.restore();
return true;
},
};
}
function placePlanetLabel(text, x, y, radius) {
const attempts = [
{ x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' },
{ x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' },
{ x, y: y - radius - 7, align: 'center', baseline: 'bottom' },
{ x, y: y + radius + 7, align: 'center', baseline: 'top' },
{ x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' },
{ x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' },
{ x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' },
{ x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' },
];
for (const attempt of attempts) {
if (labelPlacer.drawText({
text,
x: attempt.x,
y: attempt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(246, 235, 214, 0.84)',
align: attempt.align,
baseline: attempt.baseline,
padding: 2,
})) {
return true;
}
}
return false;
}
function resizeCanvas() {
const cssWidth = Math.max(420, Math.min(canvas.parentElement.clientWidth, 1100));
const cssHeight = Math.round(cssWidth * 0.6);
const ratio = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(cssWidth * ratio);
canvas.height = Math.round(cssHeight * ratio);
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(ratio, ratio);
labelPlacer = createLabelPlacer(ctx);
draw();
}
function vectorFromAltAz(altDeg, azDeg) {
const altRad = altDeg * Math.PI / 180;
const azRad = azDeg * Math.PI / 180;
const cosAlt = Math.cos(altRad);
return {
x: cosAlt * Math.sin(azRad),
y: cosAlt * Math.cos(azRad),
z: Math.sin(altRad),
};
}
function dot(a, b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
function cross(a, b) {
return {
x: a.y * b.z - a.z * b.y,
y: a.z * b.x - a.x * b.z,
z: a.x * b.y - a.y * b.x,
};
}
function normalize(v) {
const len = Math.hypot(v.x, v.y, v.z) || 1;
return { x: v.x / len, y: v.y / len, z: v.z / len };
}
const projectionCenter = vectorFromAltAz(projCenterAltDeg, projCenterAzDeg);
const basisX = normalize(cross({ x: 0, y: 0, z: 1 }, projectionCenter));
const basisY = normalize(cross(projectionCenter, basisX));
function rawSouthProjection(altDeg, azDeg) {
if (!Number.isFinite(altDeg) || !Number.isFinite(azDeg) || altDeg < 0) {
return null;
}
const p = vectorFromAltAz(altDeg, azDeg);
const denom = 1 + dot(projectionCenter, p);
if (denom <= 1e-6) {
return null;
}
const k = 2 / denom;
return {
u: k * dot(p, basisX),
v: k * dot(p, basisY),
};
}
function computeProjectionLayout(width, height) {
let minU = Infinity;
let maxU = -Infinity;
let minV = Infinity;
let maxV = -Infinity;
for (let az = 90; az <= 270; az += 2) {
for (let alt = 0; alt <= 90; alt += 2) {
const raw = rawSouthProjection(alt, az);
if (!raw) {
continue;
}
minU = Math.min(minU, raw.u);
maxU = Math.max(maxU, raw.u);
minV = Math.min(minV, raw.v);
maxV = Math.max(maxV, raw.v);
}
}
const marginX = width * 0.08;
const marginY = height * 0.08;
const usableWidth = width - marginX * 2;
const usableHeight = height - marginY * 2;
const scale = Math.min(
usableWidth / Math.max(0.001, maxU - minU),
usableHeight / Math.max(0.001, maxV - minV)
);
return {
scale,
offsetX: width / 2 - ((minU + maxU) * 0.5) * scale,
offsetY: height / 2 + ((minV + maxV) * 0.5) * scale,
};
}
function southProject(altDeg, azDeg) {
const raw = rawSouthProjection(altDeg, azDeg);
if (!raw || !projectionLayout) {
return null;
}
return {
x: projectionLayout.offsetX - raw.u * projectionLayout.scale,
y: projectionLayout.offsetY - raw.v * projectionLayout.scale,
};
}
function starHorizon(star) {
const raHours = star.ra * (12 / Math.PI);
const decDeg = star.dec * (180 / Math.PI);
return Astronomy.Horizon(date, observer, raHours, decDeg, 'normal');
}
function equatorialHorizon(raHours, decDeg) {
return Astronomy.Horizon(date, observer, raHours, decDeg, 'normal');
}
function starColor(star) {
if (typeof star.ci === 'number') {
if (star.ci < -0.1) return '#b9d4ff';
if (star.ci < 0.3) return '#dbe7ff';
if (star.ci < 0.7) return '#fff6e0';
if (star.ci < 1.1) return '#ffe0b5';
return '#ffc98a';
}
const spect = String(star.spect || '').trim().toUpperCase();
if (spect.startsWith('O') || spect.startsWith('B')) return '#b9d4ff';
if (spect.startsWith('A')) return '#dbe7ff';
if (spect.startsWith('F') || spect.startsWith('G')) return '#fff6e0';
if (spect.startsWith('K')) return '#ffd4a0';
if (spect.startsWith('M')) return '#ffbd85';
return '#f4f7ff';
}
function getStarLabel(star) {
const proper = String(star.proper || '').trim();
if (proper !== '') return proper;
const bayer = String(star.bayer || '').trim();
const con = String(star.con || '').trim();
if (bayer !== '') return con !== '' ? `${bayer} ${con}` : bayer;
return '';
}
function getMessierStyle(type) {
switch (type) {
case 'Gc': return { stroke: 'rgba(255, 180, 96, 0.92)', fill: 'rgba(255, 180, 96, 0.12)' };
case 'Oc': return { stroke: 'rgba(255, 224, 150, 0.84)', fill: 'rgba(255, 224, 150, 0.08)' };
case 'Gx': return { stroke: 'rgba(255, 140, 188, 0.88)', fill: 'rgba(255, 140, 188, 0.08)' };
case 'Pn': return { stroke: 'rgba(156, 255, 186, 0.88)', fill: 'rgba(156, 255, 186, 0.08)' };
case 'Nb':
case 'Sc': return { stroke: 'rgba(118, 204, 255, 0.88)', fill: 'rgba(118, 204, 255, 0.08)' };
default: return { stroke: 'rgba(210, 218, 232, 0.74)', fill: 'rgba(210, 218, 232, 0.06)' };
}
}
function getMessierLabel(obj) {
const messierId = Number(obj.m);
if (!Number.isFinite(messierId)) return '';
const name = String(obj.name || '').trim();
return name !== '' ? `M${messierId} ${name}` : `M${messierId}`;
}
function getPlanetStyle(planet) {
switch (String(planet.key || '')) {
case 'mercury':
return { fill: '#c9b9a7', glow: 'rgba(201, 185, 167, 0.34)' };
case 'venus':
return { fill: '#f4ddb2', glow: 'rgba(244, 221, 178, 0.4)' };
case 'mars':
return { fill: '#d7724c', glow: 'rgba(215, 114, 76, 0.34)' };
case 'jupiter':
return { fill: '#d9b07e', glow: 'rgba(217, 176, 126, 0.36)' };
case 'saturn':
return { fill: '#d8ca8b', glow: 'rgba(216, 202, 139, 0.34)' };
case 'uranus':
return { fill: '#8fd3df', glow: 'rgba(143, 211, 223, 0.3)' };
case 'neptune':
return { fill: '#5a78d6', glow: 'rgba(90, 120, 214, 0.3)' };
default:
return { fill: '#d8dde8', glow: 'rgba(216, 221, 232, 0.28)' };
}
}
function drawMoon(width, height) {
if (!MONTH_CHART_MOON) return;
const alt = Number(MONTH_CHART_MOON.alt);
const az = Number(MONTH_CHART_MOON.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) return;
const pt = southProject(alt, az);
if (!pt) return;
const phaseFraction = clamp01(Number(MONTH_CHART_MOON.phaseFraction ?? 0.5));
const waxing = Boolean(MONTH_CHART_MOON.waxing);
const moonRadius = 10.5;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, moonRadius * 2.1);
glow.addColorStop(0, 'rgba(220, 226, 238, 0.28)');
glow.addColorStop(0.5, 'rgba(220, 226, 238, 0.08)');
glow.addColorStop(1, 'rgba(220, 226, 238, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius * 2.1, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#d6d9df';
ctx.beginPath();
ctx.arc(0, 0, moonRadius, 0, Math.PI * 2);
ctx.fill();
const phaseScale = 1 - 2 * phaseFraction;
ctx.fillStyle = '#515760';
ctx.beginPath();
ctx.ellipse((waxing ? -1 : 1) * moonRadius * phaseScale, 0, moonRadius, moonRadius, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, moonRadius, 0, Math.PI * 2);
ctx.stroke();
}
function drawPlanets(width, height) {
for (const planet of MONTH_CHART_PLANETS) {
const alt = Number(planet.alt);
const az = Number(planet.az);
if (!Number.isFinite(alt) || !Number.isFinite(az) || alt < 0) continue;
const pt = southProject(alt, az);
if (!pt) continue;
const style = getPlanetStyle(planet);
const mag = Number(planet.mag);
const baseRadius = Number.isFinite(mag)
? Math.max(3.8, Math.min(8.6, 6.7 - mag * 0.72))
: 5.4;
const glowRadius = baseRadius * 2.2;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, style.glow);
glow.addColorStop(0.45, style.glow.replace(/0\.\d+\)$/, '0.12)'));
glow.addColorStop(1, style.glow.replace(/0\.\d+\)$/, '0)'));
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = style.fill;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2);
ctx.stroke();
if (planet.key === 'jupiter') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.beginPath();
ctx.arc(0, 0, baseRadius - 0.3, 0, Math.PI * 2);
ctx.clip();
ctx.strokeStyle = 'rgba(116, 84, 54, 0.78)';
ctx.lineWidth = Math.max(1, baseRadius * 0.34);
ctx.beginPath();
ctx.moveTo(-baseRadius * 0.95, -baseRadius * 0.34);
ctx.lineTo(baseRadius * 0.95, -baseRadius * 0.34);
ctx.moveTo(-baseRadius * 0.95, baseRadius * 0.2);
ctx.lineTo(baseRadius * 0.95, baseRadius * 0.2);
ctx.stroke();
ctx.fillStyle = 'rgba(194, 92, 68, 0.95)';
ctx.beginPath();
ctx.ellipse(baseRadius * 0.42, baseRadius * 0.06, baseRadius * 0.3, baseRadius * 0.17, -0.18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
if (planet.key === 'saturn') {
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.rotate(-0.35);
ctx.strokeStyle = 'rgba(232, 221, 170, 0.72)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(0, 0, baseRadius * 1.7, baseRadius * 0.72, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
const label = String(planet.label || '').trim();
if (label !== '') {
placePlanetLabel(label, pt.x, pt.y, baseRadius);
}
}
}
function drawBackground(width, height) {
const zenitPt = southProject(88, 180) || { x: width / 2, y: height * 0.18 };
const horizonPt = southProject(0, 180) || { x: width / 2, y: height * 0.88 };
const baseRadius = Math.max(width, height) * 0.9;
const baseGrad = ctx.createRadialGradient(zenitPt.x, zenitPt.y, 0, zenitPt.x, zenitPt.y, baseRadius);
baseGrad.addColorStop(0, '#010203');
baseGrad.addColorStop(0.4, '#02050b');
baseGrad.addColorStop(0.74, '#06101d');
baseGrad.addColorStop(1, '#0d1f38');
ctx.fillStyle = baseGrad;
ctx.fillRect(0, 0, width, height);
const zenithDarkening = ctx.createRadialGradient(zenitPt.x, zenitPt.y, 0, zenitPt.x, zenitPt.y, baseRadius * 0.62);
zenithDarkening.addColorStop(0, 'rgba(0, 0, 0, 0.44)');
zenithDarkening.addColorStop(0.42, 'rgba(0, 0, 0, 0.2)');
zenithDarkening.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = zenithDarkening;
ctx.fillRect(0, 0, width, height);
const horizonGlow = ctx.createRadialGradient(
horizonPt.x,
horizonPt.y,
width * 0.14,
horizonPt.x,
horizonPt.y,
width * 0.62
);
horizonGlow.addColorStop(0, 'rgba(118, 170, 255, 0.11)');
horizonGlow.addColorStop(0.36, 'rgba(78, 126, 214, 0.055)');
horizonGlow.addColorStop(0.7, 'rgba(24, 46, 86, 0.02)');
horizonGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = horizonGlow;
ctx.fillRect(0, 0, width, height);
}
function strokeProjectedCurve(samples, strokeStyle, lineWidth) {
let penDown = false;
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.beginPath();
for (const sample of samples) {
const pt = southProject(sample.alt, sample.az);
if (!pt) {
penDown = false;
continue;
}
if (!penDown) {
ctx.moveTo(pt.x, pt.y);
penDown = true;
} else {
ctx.lineTo(pt.x, pt.y);
}
}
ctx.stroke();
}
function drawGuides(width, height) {
ctx.strokeStyle = 'rgba(140, 165, 220, 0.14)';
ctx.lineWidth = 1;
for (const alt of [0, 30, 60]) {
const samples = [];
for (let az = 90; az <= 270; az += 2) {
samples.push({ alt, az });
}
strokeProjectedCurve(samples, alt === 0 ? 'rgba(183, 205, 255, 0.18)' : 'rgba(140, 165, 220, 0.14)', 1);
}
for (const az of [90, 135, 180, 225, 270]) {
const samples = [];
for (let alt = 0; alt <= 90; alt += 2) {
samples.push({ alt, az });
}
strokeProjectedCurve(samples, 'rgba(140, 165, 220, 0.12)', 1);
}
function drawDirectionLabelOutside(azDeg, text, offset = 18) {
const horizonPt = southProject(0, azDeg);
const innerPt = southProject(4, azDeg);
const prevPt = southProject(0, azDeg - 2);
const nextPt = southProject(0, azDeg + 2);
if (!horizonPt || !innerPt || !prevPt || !nextPt) {
return;
}
let nx = horizonPt.x - innerPt.x;
let ny = horizonPt.y - innerPt.y;
const normalLen = Math.hypot(nx, ny) || 1;
nx /= normalLen;
ny /= normalLen;
let angle = Math.atan2(nextPt.y - prevPt.y, nextPt.x - prevPt.x);
if (angle > Math.PI / 2 || angle < -Math.PI / 2) {
angle += Math.PI;
}
ctx.save();
ctx.translate(horizonPt.x + nx * offset, horizonPt.y + ny * offset);
ctx.rotate(angle);
ctx.fillStyle = 'rgba(214, 226, 255, 0.72)';
ctx.font = '600 12px Rajdhani, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
ctx.restore();
}
const directionLabels = [
{ az: 90, text: 'Osten' },
{ az: 135, text: 'Südosten' },
{ az: 180, text: 'Süden' },
{ az: 225, text: 'Südwesten' },
{ az: 270, text: 'Westen' },
];
for (const label of directionLabels) {
drawDirectionLabelOutside(label.az, label.text, label.az === 180 ? 22 : 18);
}
const zenitPt = southProject(88, 180);
if (zenitPt) {
ctx.fillStyle = 'rgba(214, 226, 255, 0.72)';
ctx.font = '600 12px Rajdhani, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('Zenit', zenitPt.x, zenitPt.y - 8);
}
for (const alt of [30, 60]) {
const pt = southProject(alt, 182);
if (!pt) {
continue;
}
ctx.fillStyle = 'rgba(214, 226, 255, 0.58)';
ctx.font = '600 11px Rajdhani, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(`${alt}°`, pt.x + 8, pt.y);
}
}
function drawConstellations(width, height) {
for (const constellation of MONTH_CHART_CONSTELLATIONS) {
if (!Array.isArray(constellation.lines)) continue;
for (const segment of constellation.lines) {
if (!Array.isArray(segment) || segment.length < 2) continue;
const points = [];
for (const hip of segment) {
const star = starByHip.get(Number(hip));
if (!star) continue;
const hor = starHorizon(star);
const pt = southProject(hor.altitude, hor.azimuth);
if (pt) points.push(pt);
}
if (points.length < 2) continue;
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(210, 214, 224, 0.16)';
ctx.lineWidth = 2.1;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
ctx.strokeStyle = 'rgba(36, 40, 48, 0.34)';
ctx.lineWidth = 0.95;
ctx.beginPath();
ctx.moveTo(points[0].x + 0.18, points[0].y - 0.12);
for (let i = 1; i < points.length; i++) {
const jitterX = (i % 2 === 0 ? 0.22 : -0.12);
const jitterY = (i % 2 === 0 ? -0.14 : 0.2);
ctx.lineTo(points[i].x + jitterX, points[i].y + jitterY);
}
ctx.stroke();
ctx.restore();
}
}
}
function drawConstellationLabels(width, height) {
for (const constellation of MONTH_CHART_CONSTELLATIONS) {
const label = constellation.common_name?.german || constellation.common_name?.native || constellation.id;
if (!label) continue;
const labelPositions = Array.isArray(constellation.label_positions)
? constellation.label_positions.filter((pos) => Array.isArray(pos) && pos.length >= 2)
: [];
for (const [raH, decD] of labelPositions) {
const hor = equatorialHorizon(Number(raH), Number(decD));
const pt = southProject(hor.altitude, hor.azimuth);
if (!pt || hor.altitude < 8) continue;
if (labelPlacer.drawText({
text: label,
x: pt.x,
y: pt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(198, 214, 255, 0.54)',
align: 'center',
baseline: 'middle',
padding: 3,
})) break;
}
}
}
function drawMessier(width, height) {
for (const obj of MONTH_CHART_MESSIER) {
if (Number(obj.mag) > Number(MONTH_CHART_CONFIG.messierLimit)) continue;
const hor = equatorialHorizon(Number(obj.ra), Number(obj.dec));
const pt = southProject(hor.altitude, hor.azimuth);
if (!pt) continue;
const style = getMessierStyle(String(obj.type || 'As'));
const symbolRadius = Math.max(3.4, Math.min(6.8, 3.2 + Math.max(0, 6.5 - Number(obj.mag)) * 0.5));
ctx.strokeStyle = style.stroke;
ctx.fillStyle = style.fill;
ctx.lineWidth = 1;
if (obj.type === 'Gc') {
ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(pt.x - symbolRadius * 0.8, pt.y); ctx.lineTo(pt.x + symbolRadius * 0.8, pt.y);
ctx.moveTo(pt.x, pt.y - symbolRadius * 0.8); ctx.lineTo(pt.x, pt.y + symbolRadius * 0.8);
ctx.stroke();
} else if (obj.type === 'Oc') {
ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
} else if (obj.type === 'Gx') {
ctx.save(); ctx.translate(pt.x, pt.y); ctx.rotate(Math.PI / 5); ctx.beginPath(); ctx.ellipse(0, 0, symbolRadius, symbolRadius * 0.58, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.restore();
} else if (obj.type === 'Nb' || obj.type === 'Sc') {
ctx.beginPath(); ctx.rect(pt.x - symbolRadius, pt.y - symbolRadius * 0.62, symbolRadius * 2, symbolRadius * 1.24); ctx.fill(); ctx.stroke();
} else if (obj.type === 'Pn') {
ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius * 0.62, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
} else {
ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius * 0.68, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
const label = getMessierLabel(obj);
if (label) {
labelPlacer.drawText({
text: label,
x: pt.x + symbolRadius + 5,
y: pt.y,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(210, 224, 255, 0.78)',
align: 'left',
baseline: 'middle',
padding: 2,
});
}
}
}
function drawStars(width, height) {
for (const star of displayStarMap.values()) {
const hip = Number(star.hip);
const isConstellationStar = Number.isFinite(hip) && constellationHipSet.has(hip);
if (!isConstellationStar && Number(star.mag) > Number(MONTH_CHART_CONFIG.starLimit)) continue;
const hor = starHorizon(star);
const pt = southProject(hor.altitude, hor.azimuth);
if (!pt) continue;
const magRange = Math.max(0.5, Number(MONTH_CHART_CONFIG.starLimit) + 1.6);
const effectiveMagnitude = isConstellationStar
? Math.min(Number(star.mag), Number(MONTH_CHART_CONFIG.starLimit) + 0.35)
: Number(star.mag);
const brightness = clamp01((Number(MONTH_CHART_CONFIG.starLimit) - effectiveMagnitude + 1.6) / magRange);
const starRadius = (isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2;
const glowRadius = starRadius * (effectiveMagnitude <= 1 ? 2.8 : 1.9);
const color = starColor(star);
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, color + '66');
glow.addColorStop(0.35, color + '22');
glow.addColorStop(1, color + '00');
ctx.fillStyle = glow;
ctx.beginPath(); ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = color;
ctx.beginPath(); ctx.arc(pt.x, pt.y, starRadius, 0, Math.PI * 2); ctx.fill();
if (star.mag <= 0.8) {
ctx.strokeStyle = color + '44';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pt.x - starRadius * 1.35, pt.y); ctx.lineTo(pt.x + starRadius * 1.35, pt.y);
ctx.moveTo(pt.x, pt.y - starRadius * 1.35); ctx.lineTo(pt.x, pt.y + starRadius * 1.35);
ctx.stroke();
}
if (Number(star.mag) <= Number(MONTH_CHART_CONFIG.starLabelLimit)) {
const label = getStarLabel(star);
if (label) {
labelPlacer.drawText({
text: label,
x: pt.x + starRadius + 5,
y: pt.y - 1,
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(232, 239, 255, 0.82)',
align: 'left',
baseline: 'middle',
padding: 2,
});
}
}
}
}
function draw() {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
projectionLayout = computeProjectionLayout(width, height);
ctx.clearRect(0, 0, width, height);
labelPlacer.reset();
drawBackground(width, height);
drawGuides(width, height);
drawConstellations(width, height);
drawConstellationLabels(width, height);
drawMessier(width, height);
drawMoon(width, height);
drawPlanets(width, height);
drawStars(width, height);
statusEl.textContent = 'Südhimmel für ' + MONTH_CHART_CONFIG.localTimeLabel;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
})();
</script>
<?php require __DIR__ . '/footer.php'; ?>