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

377 lines
13 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; ?>
</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">
Neumond &nbsp;·&nbsp; Erstes Viertel &nbsp;·&nbsp; Vollmond &nbsp;·&nbsp; 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 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;
const dpr = Math.max(1, window.devicePixelRatio || 1);
// Layout (CSS pixels)
const labelW = 92;
const dayW = 30;
const rowH = 46;
const headerH = 26;
const padTop = 10;
const padBot = 20;
const padRight = 10;
const radius = 11;
const cssW = labelW + 31 * dayW + padRight;
const cssH = padTop + headerH + 12 * rowH + padBot;
const canvas = document.createElement('canvas');
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Background
const bg = ctx.createLinearGradient(0, 0, 0, cssH);
bg.addColorStop(0, '#02050a');
bg.addColorStop(1, '#04080f');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, cssW, cssH);
// Day-number header
ctx.fillStyle = 'rgba(201, 168, 76, 0.65)';
ctx.font = '500 9px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const headerY = padTop + headerH / 2;
for (let d = 1; d <= 31; d++) {
ctx.fillText(String(d), labelW + (d - 1) * dayW + dayW / 2, headerY);
}
// Month rows
for (let m = 1; m <= 12; m++) {
const rowTop = padTop + headerH + (m - 1) * rowH;
const centerY = rowTop + rowH / 2;
const days = daysInMonth(YEAR, m);
// Alternating row tint
if (m % 2 === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.018)';
ctx.fillRect(0, rowTop, cssW, rowH);
}
// Month name
ctx.fillStyle = 'rgba(240, 217, 132, 0.85)';
ctx.font = '500 11px Rajdhani, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(MONTH_NAMES[m - 1], labelW - 8, centerY - 5);
// Moon discs & phase labels
for (let d = 1; d <= 31; d++) {
const x = labelW + (d - 1) * dayW + dayW / 2;
if (d > days) {
ctx.fillStyle = 'rgba(255,255,255,0.05)';
ctx.fillRect(x - 7, centerY - 0.5, 14, 1);
continue;
}
const info = phaseInfoForDay(YEAR, m, d);
drawMoonDisc(ctx, x, centerY - 5, radius, info);
// Phase event label from Python data
const key = m + '-' + d;
if (eventMap[key]) {
const ev = eventMap[key];
ctx.fillStyle = ev.color;
ctx.font = '600 7.5px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(ev.short, x, centerY - 5 + radius + 2);
// Time below label
if (ev.time) {
ctx.fillStyle = 'rgba(180,180,180,0.55)';
ctx.font = '400 6.5px "Share Tech Mono", monospace';
ctx.fillText(ev.time, x, centerY - 5 + radius + 11);
}
}
}
// Row separator
ctx.strokeStyle = 'rgba(201,168,76,0.07)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, rowTop + rowH);
ctx.lineTo(cssW, rowTop + rowH);
ctx.stroke();
}
// Vertical separator after label column
ctx.strokeStyle = 'rgba(201,168,76,0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(labelW, padTop);
ctx.lineTo(labelW, cssH - padBot);
ctx.stroke();
// Footer
ctx.fillStyle = 'rgba(240, 215, 132, 0.40)';
ctx.font = '400 8px "Share Tech Mono", monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText('NM=Neumond EV=Erstes Viertel VM=Vollmond LV=Letztes Viertel', cssW - padRight, cssH - 4);
imageEl.src = canvas.toDataURL('image/png');
statusEl.textContent = 'Mondphasen ' + YEAR + ' — 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'; ?>