431 lines
15 KiB
PHP
431 lines
15 KiB
PHP
<?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 · EV = Erstes Viertel · VM = Vollmond · LV = Letztes Viertel
|
||
· 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'; ?>
|