Monatsvorschau weiter gebaut
This commit is contained in:
@@ -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öß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;
|
||||
}
|
||||
|
||||
+300
-2
@@ -384,6 +384,289 @@ def action_month_sky_context(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def action_moon_phases_for_month(args: list[str]) -> dict:
|
||||
if len(args) != 3:
|
||||
fail(
|
||||
"Aktion moon_phases_for_month erwartet 3 Argumente: year month timezone",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
try:
|
||||
year = int(args[0])
|
||||
month = int(args[1])
|
||||
except ValueError as exc:
|
||||
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
|
||||
|
||||
if month < 1 or month > 12:
|
||||
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
|
||||
|
||||
timezone_name = args[2]
|
||||
try:
|
||||
tz = ZoneInfo(timezone_name)
|
||||
except Exception as exc:
|
||||
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
|
||||
|
||||
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
|
||||
if month == 12:
|
||||
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
|
||||
else:
|
||||
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
|
||||
|
||||
search_start = dt_to_time(local_start.astimezone(timezone.utc) - timedelta(days=3))
|
||||
search_limit_days = ((local_end - local_start).total_seconds() / 86400.0) + 10.0
|
||||
|
||||
phase_defs = [
|
||||
(0.0, "Neumond"),
|
||||
(90.0, "Erstes Viertel"),
|
||||
(180.0, "Vollmond"),
|
||||
(270.0, "Letztes Viertel"),
|
||||
]
|
||||
|
||||
phases = []
|
||||
for target_lon, label in phase_defs:
|
||||
probe = search_start
|
||||
found = None
|
||||
|
||||
for _ in range(3):
|
||||
result = astronomy.SearchMoonPhase(target_lon, probe, search_limit_days)
|
||||
if result is None:
|
||||
break
|
||||
|
||||
dt_utc = time_to_datetime(result)
|
||||
local_dt = dt_utc.astimezone(tz)
|
||||
if local_dt.year == year and local_dt.month == month:
|
||||
found = {
|
||||
"label": label,
|
||||
"utc_iso": dt_utc.isoformat().replace("+00:00", "Z"),
|
||||
"local_iso": local_dt.isoformat(),
|
||||
"local_date": local_dt.strftime("%d.%m.%Y"),
|
||||
"local_time": local_dt.strftime("%H:%M"),
|
||||
"sort_iso": local_dt.isoformat(),
|
||||
}
|
||||
break
|
||||
|
||||
probe = dt_to_time(dt_utc + timedelta(days=1))
|
||||
|
||||
if found is not None:
|
||||
phases.append(found)
|
||||
|
||||
phases.sort(key=lambda item: item["sort_iso"])
|
||||
for item in phases:
|
||||
item.pop("sort_iso", None)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "moon_phases_for_month",
|
||||
"selected": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"timezone": timezone_name,
|
||||
},
|
||||
"phases": phases,
|
||||
}
|
||||
|
||||
|
||||
def moon_planet_separation_deg(
|
||||
body: astronomy.Body,
|
||||
observer: astronomy.Observer,
|
||||
dt_utc: datetime,
|
||||
) -> float:
|
||||
time_value = dt_to_time(dt_utc)
|
||||
moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True)
|
||||
body_eq = astronomy.Equator(body, time_value, observer, True, True)
|
||||
return spherical_separation_deg(float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec))
|
||||
|
||||
|
||||
def refine_minimum_separation(
|
||||
body: astronomy.Body,
|
||||
observer: astronomy.Observer,
|
||||
left_utc: datetime,
|
||||
right_utc: datetime,
|
||||
) -> tuple[datetime, float]:
|
||||
left = left_utc
|
||||
right = right_utc
|
||||
|
||||
for _ in range(32):
|
||||
span = (right - left) / 3
|
||||
m1 = left + span
|
||||
m2 = right - span
|
||||
f1 = moon_planet_separation_deg(body, observer, m1)
|
||||
f2 = moon_planet_separation_deg(body, observer, m2)
|
||||
if f1 <= f2:
|
||||
right = m2
|
||||
else:
|
||||
left = m1
|
||||
|
||||
best = left + (right - left) / 2
|
||||
return best, moon_planet_separation_deg(body, observer, best)
|
||||
|
||||
|
||||
def action_moon_planet_approaches(args: list[str]) -> dict:
|
||||
if len(args) != 7:
|
||||
fail(
|
||||
"Aktion moon_planet_approaches erwartet 7 Argumente: latitude longitude elevation year month timezone max_sep_deg",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
latitude = parse_float(args[0], "Latitude")
|
||||
longitude = parse_float(args[1], "Longitude")
|
||||
elevation = parse_float(args[2], "Elevation")
|
||||
|
||||
try:
|
||||
year = int(args[3])
|
||||
month = int(args[4])
|
||||
except ValueError as exc:
|
||||
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
|
||||
|
||||
if month < 1 or month > 12:
|
||||
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
|
||||
|
||||
timezone_name = args[5]
|
||||
max_sep_deg = parse_float(args[6], "Maximalabstand")
|
||||
|
||||
try:
|
||||
tz = ZoneInfo(timezone_name)
|
||||
except Exception as exc:
|
||||
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
|
||||
|
||||
observer = astronomy.Observer(latitude, longitude, elevation)
|
||||
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
|
||||
if month == 12:
|
||||
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
|
||||
else:
|
||||
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
|
||||
|
||||
utc_start = local_start.astimezone(timezone.utc)
|
||||
utc_end = local_end.astimezone(timezone.utc)
|
||||
scan_step = timedelta(hours=1)
|
||||
coarse_threshold = max_sep_deg + 1.0
|
||||
|
||||
planet_defs = [
|
||||
("Merkur", astronomy.Body.Mercury, "mercury"),
|
||||
("Venus", astronomy.Body.Venus, "venus"),
|
||||
("Mars", astronomy.Body.Mars, "mars"),
|
||||
("Jupiter", astronomy.Body.Jupiter, "jupiter"),
|
||||
("Saturn", astronomy.Body.Saturn, "saturn"),
|
||||
("Uranus", astronomy.Body.Uranus, "uranus"),
|
||||
("Neptun", astronomy.Body.Neptune, "neptune"),
|
||||
]
|
||||
|
||||
approaches = []
|
||||
for label, body, key in planet_defs:
|
||||
samples: list[tuple[datetime, float]] = []
|
||||
current = utc_start
|
||||
while current <= utc_end:
|
||||
samples.append((current, moon_planet_separation_deg(body, observer, current)))
|
||||
current += scan_step
|
||||
if samples[-1][0] < utc_end:
|
||||
samples.append((utc_end, moon_planet_separation_deg(body, observer, utc_end)))
|
||||
|
||||
seen_ranges: list[tuple[datetime, datetime]] = []
|
||||
for index in range(1, len(samples) - 1):
|
||||
prev_t, prev_sep = samples[index - 1]
|
||||
curr_t, curr_sep = samples[index]
|
||||
next_t, next_sep = samples[index + 1]
|
||||
|
||||
if curr_sep > coarse_threshold:
|
||||
continue
|
||||
if curr_sep > prev_sep or curr_sep > next_sep:
|
||||
continue
|
||||
|
||||
left = max(utc_start, curr_t - scan_step)
|
||||
right = min(utc_end, curr_t + scan_step)
|
||||
|
||||
if any(not (right <= seen_left or left >= seen_right) for seen_left, seen_right in seen_ranges):
|
||||
continue
|
||||
|
||||
min_time_utc, min_sep = refine_minimum_separation(body, observer, left, right)
|
||||
local_dt = min_time_utc.astimezone(tz)
|
||||
if local_dt.year != year or local_dt.month != month:
|
||||
continue
|
||||
if min_sep > max_sep_deg:
|
||||
continue
|
||||
|
||||
seen_ranges.append((left, right))
|
||||
approaches.append({
|
||||
"planet_key": key,
|
||||
"planet_label": label,
|
||||
"label": f"Mond nahe {label}",
|
||||
"separation_deg": float(min_sep),
|
||||
"utc_iso": min_time_utc.isoformat().replace("+00:00", "Z"),
|
||||
"local_iso": local_dt.isoformat(),
|
||||
"local_date": local_dt.strftime("%d.%m.%Y"),
|
||||
"local_time": local_dt.strftime("%H:%M"),
|
||||
})
|
||||
|
||||
approaches.sort(key=lambda item: item["local_iso"])
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "moon_planet_approaches",
|
||||
"selected": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"timezone": timezone_name,
|
||||
"max_separation_deg": max_sep_deg,
|
||||
},
|
||||
"approaches": approaches,
|
||||
}
|
||||
|
||||
|
||||
def action_moon_star_occultations_for_month(args: list[str]) -> dict:
|
||||
if len(args) != 7:
|
||||
fail(
|
||||
"Aktion moon_star_occultations_for_month erwartet 7 Argumente: latitude longitude elevation year month timezone star_file",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
latitude = parse_float(args[0], "Latitude")
|
||||
longitude = parse_float(args[1], "Longitude")
|
||||
elevation = parse_float(args[2], "Elevation")
|
||||
|
||||
try:
|
||||
year = int(args[3])
|
||||
month = int(args[4])
|
||||
except ValueError as exc:
|
||||
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
|
||||
|
||||
if month < 1 or month > 12:
|
||||
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
|
||||
|
||||
timezone_name = args[5]
|
||||
star_file_path = args[6]
|
||||
if not star_file_path:
|
||||
fail("Sterndatei fehlt.")
|
||||
|
||||
try:
|
||||
tz = ZoneInfo(timezone_name)
|
||||
except Exception as exc:
|
||||
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
|
||||
|
||||
observer = astronomy.Observer(latitude, longitude, elevation)
|
||||
star_rows = load_star_file(star_file_path)
|
||||
|
||||
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
|
||||
if month == 12:
|
||||
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
|
||||
else:
|
||||
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
|
||||
|
||||
results, debug_profile = compute_star_occultation(star_rows, observer, local_start, local_end, tz)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "moon_star_occultations_for_month",
|
||||
"selected": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"timezone": timezone_name,
|
||||
},
|
||||
"star_count": len(star_rows),
|
||||
"debug": debug_profile,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def classify_visibility(sun_alt_deg: float) -> dict:
|
||||
if sun_alt_deg > -5:
|
||||
return {"key": "day", "label": "Tag"}
|
||||
@@ -1385,7 +1668,7 @@ def action_satellite_passes(args: list[str]) -> dict:
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context"]})
|
||||
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "moon_planet_approaches", "moon_star_occultations_for_month"]})
|
||||
|
||||
action = sys.argv[1]
|
||||
args = sys.argv[2:]
|
||||
@@ -1420,7 +1703,22 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return
|
||||
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context"]})
|
||||
if action == "moon_phases_for_month":
|
||||
result = action_moon_phases_for_month(args)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return
|
||||
|
||||
if action == "moon_planet_approaches":
|
||||
result = action_moon_planet_approaches(args)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return
|
||||
|
||||
if action == "moon_star_occultations_for_month":
|
||||
result = action_moon_star_occultations_for_month(args)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return
|
||||
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "moon_planet_approaches", "moon_star_occultations_for_month"]})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user