Monatsvorschau weiter gebaut

This commit is contained in:
2026-04-03 12:18:13 +02:00
parent dfd65b16e3
commit 3d98fef789
2 changed files with 1136 additions and 4 deletions
+836 -2
View File
@@ -12,10 +12,12 @@ $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.6;
$starDisplayMagnitudeLimit = 4.4;
$starLabelMagnitudeLimit = 2.0;
$messierDatabaseMagnitudeLimit = 4.0;
$messierDisplayMagnitudeLimit = 4.0;
$messierDisplayMagnitudeLimit = 1.0;
$moonPlanetMaxSeparationDeg = 1.5;
$moonOccultationMaxMag = 2.0;
$years = range($currentYear - 2, $currentYear + 5);
$months = [
@@ -51,8 +53,33 @@ $starData = [];
$lineStarData = [];
$constellationLines = [];
$messierData = [];
$planetData = [];
$moonData = null;
$moonPhaseEvents = [];
$moonPlanetApproaches = [];
$moonOccultations = [];
$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(
@@ -230,6 +257,221 @@ $chartLocalTime = new DateTimeImmutable(
);
$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'] : [];
}
$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'] : [];
}
$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 ($moonPlanetApproaches as $approachEvent) {
if (!is_array($approachEvent)) {
continue;
}
$distanceText = isset($approachEvent['separation_deg'])
? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . '°'
: '';
$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 ($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 !== '' ? ' (mag ' . $starMag . ')' : ''),
'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,
@@ -253,6 +495,8 @@ $starDataJson = json_encode($starData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_S
$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);
?>
@@ -330,6 +574,90 @@ $starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_U
<p id="monatskarteSouthStatus">Bereite Südhimmelkarte vor...</p>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Mondphasen <?= htmlspecialchars($months[$selectedMonth] . ' ' . (string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h3>
<p>Chronologisch für <?= 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.3fr 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>Keine Mondphasen für diesen Monat gefunden.</p>
<?php endif; ?>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Mond nahe Planeten</h3>
<p>Maximalabstand <?= htmlspecialchars(number_format($moonPlanetMaxSeparationDeg, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?>°</p>
</div>
</div>
<div class="monatskarte-footer">
<?php if ($moonPlanetApproaches): ?>
<div class="riseset-grid" style="grid-template-columns: 1.5fr 1fr 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>
<span class="riseset-label riseset-dim">Abstand</span>
<?php foreach ($moonPlanetApproaches as $approachEvent): ?>
<span class="riseset-val"><?= htmlspecialchars((string) ($approachEvent['label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($approachEvent['local_date'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($approachEvent['local_time'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars(number_format((float) ($approachEvent['separation_deg'] ?? 0), 2, ',', ''), ENT_QUOTES, 'UTF-8') ?>°</span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Keine Mond-Planet-Nähen innerhalb von <?= htmlspecialchars(number_format($moonPlanetMaxSeparationDeg, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?>° gefunden.</p>
<?php endif; ?>
</div>
</section>
<section class="monatskarte-card">
<div class="monatskarte-header">
<div>
<h3>Sternbedeckungen</h3>
<p>Bis Grenzgr&ouml;&szlig;e <?= htmlspecialchars(number_format($moonOccultationMaxMag, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?></p>
</div>
</div>
<div class="monatskarte-footer">
<?php if ($moonOccultations): ?>
<div class="riseset-grid" style="grid-template-columns: 1.6fr 1fr 1fr 0.9fr; row-gap: 10px;">
<span class="riseset-label riseset-dim">Stern</span>
<span class="riseset-label riseset-dim">Datum</span>
<span class="riseset-label riseset-dim">Uhrzeit</span>
<span class="riseset-label riseset-dim">Mag</span>
<?php foreach ($moonOccultations as $occultationEvent): ?>
<?php $star = is_array($occultationEvent['star'] ?? null) ? $occultationEvent['star'] : []; ?>
<?php $maximum = is_array($occultationEvent['maximum'] ?? null) ? $occultationEvent['maximum'] : []; ?>
<span class="riseset-val"><?= htmlspecialchars((string) ($star['label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($maximum['local_date'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars((string) ($maximum['local_time'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span class="riseset-val"><?= htmlspecialchars(number_format((float) ($star['mag'] ?? 0), 1, ',', ''), ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Keine Sternbedeckungen bis Magnitude <?= htmlspecialchars(number_format($moonOccultationMaxMag, 1, ',', ''), ENT_QUOTES, 'UTF-8') ?> gefunden.</p>
<?php endif; ?>
</div>
</section>
</div>
</section>
@@ -471,6 +799,8 @@ 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 () {
@@ -636,6 +966,162 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
};
}
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);
@@ -933,6 +1419,168 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
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)) {
@@ -1099,6 +1747,8 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
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;
@@ -1248,6 +1898,36 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
};
}
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);
@@ -1414,6 +2094,158 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
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 };
@@ -1734,6 +2566,8 @@ const MONTH_CHART_ERROR = <?= $starDataErrorJson ?: 'null' ?>;
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;
}