Files
skyview.astronomiemuseum.de/public/moonyear.php
T
2026-04-22 11:37:49 +02:00

431 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
session_start();
$currentYear = (int) date('Y');
$selectedYear = isset($_GET['year']) ? max(1900, min(2100, (int) $_GET['year'])) : $currentYear;
$pageTitle = 'Mondphasen-Jahresübersicht ' . $selectedYear . ' - AstroTools';
$bodyClass = 'moonyear-page';
$publicBasePath = './';
require __DIR__ . '/header.php';
// -------------------------------------------------------------------------
// Zeitzone aus dem Benutzer-Standort; Fallback auf Europe/Berlin
// -------------------------------------------------------------------------
$chartTimezone = 'Europe/Berlin';
if (!empty($currentLocation['timezone'])) {
$chartTimezone = (string) $currentLocation['timezone'];
}
// -------------------------------------------------------------------------
// Python: Mondphasen für alle 12 Monate abrufen
// -------------------------------------------------------------------------
$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python';
$pythonScriptPath = __DIR__ . '/py/api.py';
$allPhaseEvents = [];
for ($m = 1; $m <= 12; $m++) {
$output = [];
$exitCode = 0;
$args = [
$pythonScriptPath,
'moon_phases_for_month',
(string) $selectedYear,
(string) $m,
$chartTimezone,
];
$cmd = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $args)) . ' 2>&1';
exec($cmd, $output, $exitCode);
$raw = trim(implode("\n", $output));
$decoded = json_decode($raw, true);
if (is_array($decoded) && ($decoded['ok'] ?? false) && is_array($decoded['phases'] ?? null)) {
foreach ($decoded['phases'] as $phase) {
// local_date format: "DD.MM.YYYY"
$parts = explode('.', (string) ($phase['local_date'] ?? ''));
if (count($parts) === 3) {
$day = (int) $parts[0];
$month = (int) $parts[1];
if ($month === $m && $day >= 1 && $day <= 31) {
$allPhaseEvents[] = [
'month' => $month,
'day' => $day,
'label' => (string) ($phase['label'] ?? ''),
'time' => (string) ($phase['local_time'] ?? ''),
];
}
}
}
}
}
?>
<style>
.moonyear-controls {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.moonyear-controls label {
color: var(--text-dim);
font-size: 0.9rem;
}
.moonyear-controls input[type="number"] {
width: 100px;
}
.moonyear-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
#moonyearImage {
display: block;
max-width: 100%;
height: auto;
border-radius: var(--radius);
border: 1px solid var(--border);
}
#moonyearStatus {
color: var(--text-dim);
font-size: 0.85rem;
margin-top: 0.75rem;
}
.moonyear-legend {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
margin-top: 1rem;
font-size: 0.82rem;
color: var(--text-dim);
}
</style>
<div class="card">
<h2>Mondphasen <?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?></h2>
<form method="get" class="moonyear-controls">
<label for="yearInput">Jahr:</label>
<input type="number" id="yearInput" name="year"
value="<?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?>"
min="1900" max="2100">
<button type="submit" class="btn btn-primary btn-sm">Anzeigen</button>
<?php if ($selectedYear !== $currentYear): ?>
<a href="moonyear.php?year=<?= $currentYear ?>" class="btn btn-secondary btn-sm">Aktuelles Jahr</a>
<?php endif; ?>
<a id="moonyearDownload" href="#" download="Mondphasen-<?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?>.png" class="btn btn-secondary btn-sm" style="display:none">Bild herunterladen (A3)</a>
</form>
<div class="moonyear-wrap">
<img id="moonyearImage" alt="Mondphasen <?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?>">
</div>
<p id="moonyearStatus">Zeichne Mondphasen-Jahresübersicht…</p>
<div class="moonyear-legend">
NM = Neumond &nbsp;·&nbsp; EV = Erstes Viertel &nbsp;·&nbsp; VM = Vollmond &nbsp;·&nbsp; LV = Letztes Viertel
&nbsp;·&nbsp; Zeitzone: <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>
</div>
</div>
<script src="<?= htmlspecialchars($publicBasePath . 'js/astronomy.browser.min.js', ENT_QUOTES, 'UTF-8') ?>"></script>
<script>
(function () {
const YEAR = <?= json_encode($selectedYear) ?>;
const imageEl = document.getElementById('moonyearImage');
const statusEl = document.getElementById('moonyearStatus');
// Phase events from Python: [{month, day, label, time}, ...]
// label = "Neumond" | "Erstes Viertel" | "Vollmond" | "Letztes Viertel"
const PHASE_EVENTS = <?= json_encode($allPhaseEvents, JSON_UNESCAPED_UNICODE) ?>;
if (!imageEl || !statusEl) return;
if (!window.Astronomy) {
statusEl.textContent = 'Astronomy Engine konnte nicht geladen werden.';
return;
}
const MONTH_NAMES = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
];
// Short label + color per phase
const PHASE_LABEL_MAP = {
'Neumond': { short: 'NM', color: 'rgba(180,180,180,0.80)' },
'Erstes Viertel': { short: 'EV', color: 'rgba(201,168,76,0.90)' },
'Vollmond': { short: 'VM', color: 'rgba(240,217,132,1.00)' },
'Letztes Viertel': { short: 'LV', color: 'rgba(201,168,76,0.90)' },
};
function daysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
function clamp01(v) { return Math.max(0, Math.min(1, v)); }
function phaseInfoForDay(year, month, day) {
const d = new Date(year, month - 1, day, 12, 0, 0);
const angle = ((Astronomy.MoonPhase(d) % 360) + 360) % 360;
return {
phaseAngleDeg: angle,
illumination: clamp01((1 - Math.cos(angle * Math.PI / 180)) / 2),
waxing: angle <= 180,
};
}
// Build fast lookup: "month-day" → {short, color, time}
const eventMap = {};
for (const e of PHASE_EVENTS) {
const key = e.month + '-' + e.day;
const meta = PHASE_LABEL_MAP[e.label] || { short: '?', color: 'rgba(255,255,255,0.7)' };
eventMap[key] = { short: meta.short, color: meta.color, time: e.time };
}
const downloadEl = document.getElementById('moonyearDownload');
const phaseImage = new Image();
phaseImage.src = <?= json_encode($publicBasePath . 'images/moons/moon4geocron.png', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
function drawMoonDisc(ctx, x, y, radius, phaseInfo) {
const phaseRad = phaseInfo.phaseAngleDeg * Math.PI / 180;
const cosPhase = Math.cos(phaseRad);
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = 'rgba(8, 10, 16, 0.96)';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.96;
ctx.drawImage(phaseImage, x - radius, y - radius, radius * 2, radius * 2);
ctx.globalAlpha = 1;
ctx.fillStyle = 'rgba(8, 10, 16, 0.96)';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.clip();
if (phaseInfo.illumination >= 0.999) {
ctx.drawImage(phaseImage, x - radius, y - radius, radius * 2, radius * 2);
} else if (phaseInfo.illumination > 0.001) {
ctx.beginPath();
for (let i = 0; i <= 72; i++) {
const t = -radius + (2 * radius * i / 72);
const xr = Math.sqrt(Math.max(0, radius * radius - t * t));
const xt = (phaseInfo.waxing ? cosPhase : -cosPhase) * xr;
if (i === 0) ctx.moveTo(x + xt, y + t);
else ctx.lineTo(x + xt, y + t);
}
for (let i = 72; i >= 0; i--) {
const t = -radius + (2 * radius * i / 72);
const xr = Math.sqrt(Math.max(0, radius * radius - t * t));
ctx.lineTo(x + (phaseInfo.waxing ? xr : -xr), y + t);
}
ctx.closePath();
ctx.clip();
ctx.drawImage(phaseImage, x - radius, y - radius, radius * 2, radius * 2);
}
ctx.restore();
ctx.restore();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.16)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.stroke();
}
function draw() {
if (!phaseImage.complete || !phaseImage.naturalWidth) return;
// DIN A3 Querformat bei 150 DPI: 420mm × 297mm → 2480 × 1754 px
const W = 2480;
const H = 1754;
const canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
// --- Hintergrund ---
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#02050a');
bg.addColorStop(1, '#04080f');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
// --- Layout ---
const padH = 48; // links/rechts
const padTop = 48;
const titleH = 72; // Titelzeile oben
const headerH = 44; // Zeile mit Tageszahlen
const footerH = 52; // untere Beschriftungszeile
const labelW = 180; // Spalte für Monatsnamen
const gridLeft = padH + labelW;
const gridRight = W - padH;
const gridW = gridRight - gridLeft;
const dayW = gridW / 31;
const gridTop = padTop + titleH + headerH;
const gridBot = H - footerH - 16;
const gridH = gridBot - gridTop;
const rowH = gridH / 12;
const discRadius = Math.min(dayW * 0.38, rowH * 0.33);
// --- Titel ---
ctx.fillStyle = 'rgba(240, 215, 132, 0.95)';
ctx.font = '700 52px Rajdhani, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('Mondphasen ' + YEAR, padH + labelW, padTop + titleH / 2);
// Zeitzone rechts im Titel
ctx.fillStyle = 'rgba(201, 168, 76, 0.55)';
ctx.font = '400 22px "Share Tech Mono", monospace';
ctx.textAlign = 'right';
ctx.fillText('Zeitzone: <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>', W - padH, padTop + titleH / 2);
// --- Tageszahlen-Header ---
const headerY = padTop + titleH + headerH / 2;
ctx.fillStyle = 'rgba(201, 168, 76, 0.70)';
ctx.font = '500 20px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let d = 1; d <= 31; d++) {
ctx.fillText(String(d), gridLeft + (d - 0.5) * dayW, headerY);
}
// Trennlinie unter Header
ctx.strokeStyle = 'rgba(201,168,76,0.25)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padH, padTop + titleH + headerH);
ctx.lineTo(W - padH, padTop + titleH + headerH);
ctx.stroke();
// --- Monatszeilen ---
for (let m = 1; m <= 12; m++) {
const rowTop = gridTop + (m - 1) * rowH;
const centerY = rowTop + rowH * 0.44;
const days = daysInMonth(YEAR, m);
// Alternierende Zeile
if (m % 2 === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.018)';
ctx.fillRect(padH, rowTop, W - 2 * padH, rowH);
}
// Monatsname
ctx.fillStyle = 'rgba(240, 217, 132, 0.90)';
ctx.font = '600 28px Rajdhani, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(MONTH_NAMES[m - 1], padH + labelW - 14, centerY);
// Mondscheiben & Phasenbeschriftungen
for (let d = 1; d <= 31; d++) {
const x = gridLeft + (d - 0.5) * dayW;
if (d > days) {
ctx.fillStyle = 'rgba(255,255,255,0.04)';
ctx.fillRect(x - 12, centerY - 0.5, 24, 1);
continue;
}
const info = phaseInfoForDay(YEAR, m, d);
drawMoonDisc(ctx, x, centerY, discRadius, info);
const key = m + '-' + d;
if (eventMap[key]) {
const ev = eventMap[key];
const labelY = centerY + discRadius + 6;
ctx.fillStyle = ev.color;
ctx.font = '700 15px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(ev.short, x, labelY);
if (ev.time) {
ctx.fillStyle = 'rgba(180,180,180,0.60)';
ctx.font = '400 13px "Share Tech Mono", monospace';
ctx.fillText(ev.time, x, labelY + 17);
}
}
}
// Zeilentrenner
ctx.strokeStyle = 'rgba(201,168,76,0.07)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padH, rowTop + rowH);
ctx.lineTo(W - padH, rowTop + rowH);
ctx.stroke();
}
// Vertikaler Trenner nach Monatsnamen-Spalte
ctx.strokeStyle = 'rgba(201,168,76,0.18)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padH + labelW, padTop + titleH);
ctx.lineTo(padH + labelW, gridBot);
ctx.stroke();
// --- Fußzeile ---
const fy = H - footerH / 2 - 8;
// Links: Legende
ctx.fillStyle = 'rgba(201, 168, 76, 0.55)';
ctx.font = '400 18px "Share Tech Mono", monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('NM = Neumond · EV = Erstes Viertel · VM = Vollmond · LV = Letztes Viertel', padH, fy);
// Rechts: Museumssignatur
ctx.fillStyle = 'rgba(240, 215, 132, 0.70)';
ctx.font = '500 20px Rajdhani, sans-serif';
ctx.textAlign = 'right';
ctx.fillText('Astronomiemuseum der Sternwarte Sonneberg', W - padH, fy);
// Trennlinie über Fußzeile
ctx.strokeStyle = 'rgba(201,168,76,0.20)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padH, H - footerH - 16);
ctx.lineTo(W - padH, H - footerH - 16);
ctx.stroke();
// --- Bild ausgeben ---
const dataUrl = canvas.toDataURL('image/png');
imageEl.src = dataUrl;
if (downloadEl) {
downloadEl.href = dataUrl;
downloadEl.style.display = '';
}
statusEl.textContent = 'Mondphasen ' + YEAR + ' (DIN A3, 150 DPI) — Zeitzone: <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>';
}
phaseImage.onload = draw;
phaseImage.onerror = function () {
statusEl.textContent = 'Mondgrafik konnte nicht geladen werden.';
};
if (phaseImage.complete && phaseImage.naturalWidth) draw();
})();
</script>
<?php require __DIR__ . '/footer.php'; ?>