696 lines
34 KiB
PHP
696 lines
34 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
session_start();
|
|
|
|
$currentYear = (int) date('Y');
|
|
$currentMonth = (int) date('n');
|
|
$selectedYear = isset($_GET['year']) ? max(1900, min(2100, (int) $_GET['year'])) : $currentYear;
|
|
$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : $currentMonth;
|
|
$activeTab = (isset($_GET['tab']) && $_GET['tab'] === 'month') ? 'month' : 'year';
|
|
$doCalc = isset($_GET['calc']);
|
|
|
|
$pageTitle = 'Mondphasen - 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'];
|
|
}
|
|
|
|
$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python';
|
|
$pythonScriptPath = __DIR__ . '/py/api.py';
|
|
|
|
$monthNamesDE = [
|
|
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
|
|
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
|
|
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember',
|
|
];
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Python: Jahresübersicht — nur wenn Tab=Jahr und Berechnen geklickt
|
|
// -------------------------------------------------------------------------
|
|
$allPhaseEvents = [];
|
|
$lunarEclipses = []; // [{kind, month, day, local_time}, ...]
|
|
if ($doCalc && $activeTab === 'year') {
|
|
for ($m = 1; $m <= 12; $m++) {
|
|
$output = [];
|
|
$exitCode = 0;
|
|
$cmd = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', [
|
|
$pythonScriptPath, 'moon_phases_for_month',
|
|
(string) $selectedYear, (string) $m, $chartTimezone,
|
|
])) . ' 2>&1';
|
|
exec($cmd, $output, $exitCode);
|
|
$decoded = json_decode(trim(implode("\n", $output)), true);
|
|
if (is_array($decoded) && ($decoded['ok'] ?? false) && is_array($decoded['phases'] ?? null)) {
|
|
foreach ($decoded['phases'] as $phase) {
|
|
$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'] ?? ''),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mondfinsternisse für das Jahr
|
|
$ecOutput = [];
|
|
$ecExitCode = 0;
|
|
$ecCmd = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', [
|
|
$pythonScriptPath, 'lunar_eclipses_for_year',
|
|
(string) $selectedYear, $chartTimezone,
|
|
])) . ' 2>&1';
|
|
exec($ecCmd, $ecOutput, $ecExitCode);
|
|
$ecDecoded = json_decode(trim(implode("\n", $ecOutput)), true);
|
|
if (is_array($ecDecoded) && ($ecDecoded['ok'] ?? false) && is_array($ecDecoded['eclipses'] ?? null)) {
|
|
$lunarEclipses = $ecDecoded['eclipses'];
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Python: Monatskalender — nur wenn Tab=Monat und Berechnen geklickt
|
|
// -------------------------------------------------------------------------
|
|
$calPhaseEvents = [];
|
|
if ($doCalc && $activeTab === 'month') {
|
|
$output = [];
|
|
$exitCode = 0;
|
|
$cmd = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', [
|
|
$pythonScriptPath, 'moon_phases_for_month',
|
|
(string) $selectedYear, (string) $selectedMonth, $chartTimezone,
|
|
])) . ' 2>&1';
|
|
exec($cmd, $output, $exitCode);
|
|
$decoded = json_decode(trim(implode("\n", $output)), true);
|
|
if (is_array($decoded) && ($decoded['ok'] ?? false) && is_array($decoded['phases'] ?? null)) {
|
|
foreach ($decoded['phases'] as $phase) {
|
|
$parts = explode('.', (string) ($phase['local_date'] ?? ''));
|
|
if (count($parts) === 3) {
|
|
$day = (int) $parts[0];
|
|
if ($day >= 1 && $day <= 31) {
|
|
$calPhaseEvents[$day] = [
|
|
'label' => (string) ($phase['label'] ?? ''),
|
|
'time' => (string) ($phase['local_time'] ?? ''),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mondfinsternisse für das Jahr (für den Monats-Tab benötigt)
|
|
$ecOutput = [];
|
|
$ecExitCode = 0;
|
|
$ecCmd = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', [
|
|
$pythonScriptPath, 'lunar_eclipses_for_year',
|
|
(string) $selectedYear, $chartTimezone,
|
|
])) . ' 2>&1';
|
|
exec($ecCmd, $ecOutput, $ecExitCode);
|
|
$ecDecoded = json_decode(trim(implode("\n", $ecOutput)), true);
|
|
if (is_array($ecDecoded) && ($ecDecoded['ok'] ?? false) && is_array($ecDecoded['eclipses'] ?? null)) {
|
|
$lunarEclipses = $ecDecoded['eclipses'];
|
|
}
|
|
}
|
|
?>
|
|
|
|
<style>
|
|
/* --- Tabs --- */
|
|
.moontabs {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.moontab-btn {
|
|
background: none;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
font: 600 1rem Rajdhani, sans-serif;
|
|
letter-spacing: .04em;
|
|
padding: .6rem 1.4rem;
|
|
text-transform: uppercase;
|
|
transition: color .15s, border-color .15s;
|
|
}
|
|
.moontab-btn:hover { color: var(--gold); }
|
|
.moontab-btn.active {
|
|
border-bottom-color: var(--gold);
|
|
color: var(--gold-pale);
|
|
}
|
|
.moontab-pane { display: none; }
|
|
.moontab-pane.active { display: block; }
|
|
|
|
/* --- Gemeinsame Controls --- */
|
|
.moon-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.moon-controls label { color: var(--text-dim); font-size: .85rem; white-space: nowrap; }
|
|
.moon-controls input[type="number"] { width: 88px; }
|
|
.moon-controls select { width: auto; }
|
|
.moon-field {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .4rem;
|
|
}
|
|
.moon-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .5rem;
|
|
margin-left: .5rem;
|
|
}
|
|
|
|
/* --- Bild-Ausgabe --- */
|
|
.moon-imgwrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
.moon-imgwrap img {
|
|
display: block;
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.moon-status {
|
|
color: var(--text-dim);
|
|
font-size: .85rem;
|
|
margin-top: .75rem;
|
|
}
|
|
.moon-placeholder {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 180px;
|
|
border: 1px dashed rgba(201,168,76,.25);
|
|
border-radius: var(--radius);
|
|
color: var(--text-dim);
|
|
font-size: .95rem;
|
|
}
|
|
</style>
|
|
|
|
<div class="card">
|
|
<h2>Mondphasen</h2>
|
|
|
|
<div class="moontabs">
|
|
<button type="button" class="moontab-btn <?= $activeTab === 'year' ? 'active' : '' ?>" data-tab="year">Jahresübersicht</button>
|
|
<button type="button" class="moontab-btn <?= $activeTab === 'month' ? 'active' : '' ?>" data-tab="month">Monatskalender</button>
|
|
</div>
|
|
|
|
<!-- ===================== TAB: JAHR ===================== -->
|
|
<div class="moontab-pane <?= $activeTab === 'year' ? 'active' : '' ?>" id="pane-year">
|
|
<form method="get" class="moon-controls">
|
|
<input type="hidden" name="tab" value="year">
|
|
<input type="hidden" name="calc" value="1">
|
|
<div class="moon-field">
|
|
<label for="yearInputY">Jahr:</label>
|
|
<input type="number" id="yearInputY" name="year"
|
|
value="<?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?>"
|
|
min="1900" max="2100">
|
|
</div>
|
|
<div class="moon-actions">
|
|
<button type="submit" class="btn btn-primary btn-sm">Berechnen</button>
|
|
<?php if ($doCalc && $activeTab === 'year'): ?>
|
|
<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>
|
|
<?php endif; ?>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if ($doCalc && $activeTab === 'year'): ?>
|
|
<div class="moon-imgwrap">
|
|
<img id="moonyearImage" alt="Mondphasen <?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?>">
|
|
</div>
|
|
<p class="moon-status" id="moonyearStatus">Zeichne Jahresübersicht…</p>
|
|
<?php else: ?>
|
|
<div class="moon-placeholder">Jahr wählen und auf <strong> Berechnen </strong> klicken.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- ===================== TAB: MONAT ===================== -->
|
|
<div class="moontab-pane <?= $activeTab === 'month' ? 'active' : '' ?>" id="pane-month">
|
|
<form method="get" class="moon-controls">
|
|
<input type="hidden" name="tab" value="month">
|
|
<input type="hidden" name="calc" value="1">
|
|
<div class="moon-field">
|
|
<label for="monthSelect">Monat:</label>
|
|
<select id="monthSelect" name="month">
|
|
<?php foreach ($monthNamesDE as $mn => $ml): ?>
|
|
<option value="<?= $mn ?>" <?= $mn === $selectedMonth ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($ml, ENT_QUOTES, 'UTF-8') ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="moon-field">
|
|
<label for="yearInputM">Jahr:</label>
|
|
<input type="number" id="yearInputM" name="year"
|
|
value="<?= htmlspecialchars((string) $selectedYear, ENT_QUOTES, 'UTF-8') ?>"
|
|
min="1900" max="2100">
|
|
</div>
|
|
<div class="moon-actions">
|
|
<button type="submit" class="btn btn-primary btn-sm">Berechnen</button>
|
|
<?php if ($doCalc && $activeTab === 'month'): ?>
|
|
<a id="moonmonthDownload" href="#"
|
|
download="Mondkalender-<?= htmlspecialchars($monthNamesDE[$selectedMonth] . '-' . $selectedYear, ENT_QUOTES, 'UTF-8') ?>.png"
|
|
class="btn btn-secondary btn-sm" style="display:none">Bild herunterladen (A4)</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if ($doCalc && $activeTab === 'month'): ?>
|
|
<div class="moon-imgwrap">
|
|
<img id="moonmonthImage" alt="Mondkalender <?= htmlspecialchars($monthNamesDE[$selectedMonth] . ' ' . $selectedYear, ENT_QUOTES, 'UTF-8') ?>">
|
|
</div>
|
|
<p class="moon-status" id="moonmonthStatus">Zeichne Monatskalender…</p>
|
|
<?php else: ?>
|
|
<div class="moon-placeholder">Monat wählen und auf <strong> Berechnen </strong> klicken.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab-Umschaltung per JS (kein Reload) -->
|
|
<script>
|
|
(function () {
|
|
document.querySelectorAll('.moontab-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
document.querySelectorAll('.moontab-btn').forEach(function (b) { b.classList.remove('active'); });
|
|
document.querySelectorAll('.moontab-pane').forEach(function (p) { p.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
var pane = document.getElementById('pane-' + btn.dataset.tab);
|
|
if (pane) pane.classList.add('active');
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<?php if ($doCalc): ?>
|
|
<script src="<?= htmlspecialchars($publicBasePath . 'js/astronomy.browser.min.js', ENT_QUOTES, 'UTF-8') ?>"></script>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($doCalc && $activeTab === 'year'): ?>
|
|
<script>
|
|
(function () {
|
|
const YEAR = <?= json_encode($selectedYear) ?>;
|
|
const PHASE_EVENTS = <?= json_encode($allPhaseEvents, JSON_UNESCAPED_UNICODE) ?>;
|
|
const LUNAR_ECLIPSES = <?= json_encode($lunarEclipses, JSON_UNESCAPED_UNICODE) ?>;
|
|
const imageEl = document.getElementById('moonyearImage');
|
|
const statusEl = document.getElementById('moonyearStatus');
|
|
const downloadEl = document.getElementById('moonyearDownload');
|
|
if (!imageEl || !window.Astronomy) return;
|
|
|
|
const MONTH_NAMES = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
|
|
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(y, m) { return new Date(y, m, 0).getDate(); }
|
|
function clamp01(v) { return Math.max(0, Math.min(1, v)); }
|
|
function phaseInfoForDay(y, m, d) {
|
|
const angle = ((Astronomy.MoonPhase(new Date(y, m-1, d, 12, 0, 0)) % 360) + 360) % 360;
|
|
return { phaseAngleDeg: angle, illumination: clamp01((1 - Math.cos(angle * Math.PI / 180)) / 2), waxing: angle <= 180 };
|
|
}
|
|
|
|
const eventMap = {};
|
|
for (const e of PHASE_EVENTS) {
|
|
const meta = PHASE_LABEL_MAP[e.label] || { short: '?', color: 'rgba(255,255,255,0.7)' };
|
|
eventMap[e.month + '-' + e.day] = { short: meta.short, color: meta.color, time: e.time };
|
|
}
|
|
|
|
// Eclipse lookup: "month-day" → {kind: 'total'|'partial'|'penumbral', time}
|
|
const eclipseMap = {};
|
|
for (const ec of LUNAR_ECLIPSES) {
|
|
eclipseMap[ec.month + '-' + ec.day] = { kind: ec.kind, time: ec.local_time };
|
|
}
|
|
|
|
const phaseImage = new Image();
|
|
phaseImage.src = <?= json_encode($publicBasePath . 'images/moons/moon4calendar.png', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
// eclipseKind: 'total' | 'partial' | 'penumbral' | null
|
|
function drawMoonDisc(ctx, x, y, radius, pi, eclipseKind) {
|
|
const cosP = Math.cos(pi.phaseAngleDeg * Math.PI / 180);
|
|
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 (pi.illumination >= 0.999) {
|
|
ctx.drawImage(phaseImage, x-radius, y-radius, radius*2, radius*2);
|
|
} else if (pi.illumination > 0.001) {
|
|
ctx.beginPath();
|
|
for (let i = 0; i <= 72; i++) {
|
|
const t = -radius + 2*radius*i/72, xr = Math.sqrt(Math.max(0, radius*radius - t*t));
|
|
const xt = (pi.waxing ? cosP : -cosP) * xr;
|
|
i === 0 ? ctx.moveTo(x+xt, y+t) : ctx.lineTo(x+xt, y+t);
|
|
}
|
|
for (let i = 72; i >= 0; i--) {
|
|
const t = -radius + 2*radius*i/72, xr = Math.sqrt(Math.max(0, radius*radius - t*t));
|
|
ctx.lineTo(x + (pi.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();
|
|
|
|
// Eclipse-Einfärbung als Overlay
|
|
if (eclipseKind === 'total') {
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.clip();
|
|
ctx.fillStyle = 'rgba(180, 50, 10, 0.55)';
|
|
ctx.fillRect(x - radius, y - radius, radius*2, radius*2);
|
|
ctx.restore();
|
|
ctx.strokeStyle = 'rgba(220, 80, 20, 0.90)'; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.stroke();
|
|
} else if (eclipseKind === 'partial') {
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.clip();
|
|
ctx.fillStyle = 'rgba(160, 60, 10, 0.35)';
|
|
ctx.fillRect(x - radius, y - radius, radius*2, radius*2);
|
|
ctx.restore();
|
|
ctx.strokeStyle = 'rgba(200, 100, 30, 0.80)'; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.stroke();
|
|
} else if (eclipseKind === 'penumbral') {
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.clip();
|
|
ctx.fillStyle = 'rgba(80, 80, 100, 0.25)';
|
|
ctx.fillRect(x - radius, y - radius, radius*2, radius*2);
|
|
ctx.restore();
|
|
ctx.strokeStyle = 'rgba(150, 150, 180, 0.60)'; ctx.lineWidth = 1.5;
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function draw() {
|
|
if (!phaseImage.complete || !phaseImage.naturalWidth) return;
|
|
// DIN A3 Querformat 150 DPI
|
|
const W = 2480, H = 1754;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = W; canvas.height = H;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
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);
|
|
|
|
const padH = 48, padTop = 48, titleH = 72, headerH = 44, footerH = 52, labelW = 180;
|
|
const gridLeft = padH + labelW, gridRight = W - padH, gridW = gridRight - gridLeft;
|
|
const dayW = gridW / 31;
|
|
const gridTop = padTop + titleH + headerH, gridBot = H - footerH - 16;
|
|
const rowH = (gridBot - gridTop) / 12;
|
|
const discRadius = Math.min(dayW * 0.38, rowH * 0.33);
|
|
|
|
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);
|
|
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);
|
|
|
|
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, padTop + titleH + headerH / 2);
|
|
|
|
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();
|
|
|
|
for (let m = 1; m <= 12; m++) {
|
|
const rowTop = gridTop + (m - 1) * rowH, centerY = rowTop + rowH * 0.44;
|
|
const days = daysInMonth(YEAR, m);
|
|
if (m % 2 === 0) { ctx.fillStyle = 'rgba(255,255,255,0.018)'; ctx.fillRect(padH, rowTop, W - 2*padH, rowH); }
|
|
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);
|
|
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 ecKey = m + '-' + d;
|
|
const ecInfo = eclipseMap[ecKey] || null;
|
|
drawMoonDisc(ctx, x, centerY, discRadius, phaseInfoForDay(YEAR, m, d), ecInfo ? ecInfo.kind : null);
|
|
const ev = eventMap[ecKey];
|
|
const labelY = centerY + discRadius + 6;
|
|
if (ecInfo) {
|
|
const ecLabel = ecInfo.kind === 'total' ? 'MF tot.' : ecInfo.kind === 'partial' ? 'MF part.' : 'MF Hs.';
|
|
const ecColor = ecInfo.kind === 'total' ? 'rgba(230,100,40,1.00)' : ecInfo.kind === 'partial' ? 'rgba(210,120,50,0.95)' : 'rgba(160,160,190,0.80)';
|
|
ctx.fillStyle = ecColor; ctx.font = '700 13px "Share Tech Mono", monospace';
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
|
ctx.fillText(ecLabel, x, labelY);
|
|
} else if (ev) {
|
|
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); }
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
|
|
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();
|
|
|
|
const fy = H - footerH / 2 - 8;
|
|
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();
|
|
ctx.fillStyle = 'rgba(201,168,76,0.55)'; ctx.font = '400 18px "Share Tech Mono", monospace';
|
|
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
|
let legendText = 'NM = Neumond · EV = Erstes Viertel · VM = Vollmond · LV = Letztes Viertel';
|
|
if (LUNAR_ECLIPSES.length > 0) {
|
|
const hasTotal = LUNAR_ECLIPSES.some(function(e) { return e.kind === 'total'; });
|
|
const hasPartial = LUNAR_ECLIPSES.some(function(e) { return e.kind === 'partial'; });
|
|
const hasPenumbral = LUNAR_ECLIPSES.some(function(e) { return e.kind === 'penumbral'; });
|
|
const ecParts = [];
|
|
if (hasTotal) ecParts.push('MF tot. = Totale Mondfinsternis');
|
|
if (hasPartial) ecParts.push('MF part. = Partielle Mondfinsternis');
|
|
if (hasPenumbral) ecParts.push('MF Hs. = Halbschatten-Mondfinsternis');
|
|
legendText += ' · ' + ecParts.join(' · ');
|
|
}
|
|
ctx.fillText(legendText, padH, fy);
|
|
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);
|
|
|
|
const dataUrl = canvas.toDataURL('image/png');
|
|
imageEl.src = dataUrl;
|
|
if (downloadEl) { downloadEl.href = dataUrl; downloadEl.style.display = ''; }
|
|
if (statusEl) statusEl.textContent = 'Mondphasen ' + YEAR + ' (DIN A3, 150 DPI) — Zeitzone: <?= htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>';
|
|
}
|
|
|
|
phaseImage.onload = draw;
|
|
phaseImage.onerror = function () { if (statusEl) statusEl.textContent = 'Mondgrafik konnte nicht geladen werden.'; };
|
|
if (phaseImage.complete && phaseImage.naturalWidth) draw();
|
|
})();
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($doCalc && $activeTab === 'month'): ?>
|
|
<script>
|
|
(function () {
|
|
const YEAR = <?= json_encode($selectedYear) ?>;
|
|
const MONTH = <?= json_encode($selectedMonth) ?>;
|
|
const MONTH_NAME = <?= json_encode($monthNamesDE[$selectedMonth], JSON_UNESCAPED_UNICODE) ?>;
|
|
const TIMEZONE = <?= json_encode($chartTimezone) ?>;
|
|
const CAL_EVENTS = <?= json_encode($calPhaseEvents, JSON_UNESCAPED_UNICODE) ?>;
|
|
// Mondfinsternisse für diesen Monat: { day: {kind, time} }
|
|
const CAL_ECLIPSES = <?= json_encode(
|
|
array_column(
|
|
array_filter($lunarEclipses, fn($e) => ($e['month'] ?? 0) === $selectedMonth),
|
|
null,
|
|
'day'
|
|
),
|
|
JSON_UNESCAPED_UNICODE
|
|
) ?>;
|
|
const imageEl = document.getElementById('moonmonthImage');
|
|
const statusEl = document.getElementById('moonmonthStatus');
|
|
const downloadEl = document.getElementById('moonmonthDownload');
|
|
if (!imageEl || !window.Astronomy) return;
|
|
|
|
const PHASE_LABEL_MAP = {
|
|
'Neumond': { short: 'Neumond', color: 'rgba(180,180,180,0.90)' },
|
|
'Erstes Viertel': { short: 'Erstes Viertel', color: 'rgba(201,168,76,0.95)' },
|
|
'Vollmond': { short: 'Vollmond', color: 'rgba(240,217,132,1.00)' },
|
|
'Letztes Viertel': { short: 'Letztes Viertel', color: 'rgba(201,168,76,0.95)' },
|
|
};
|
|
const DAY_NAMES = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
|
|
|
function clamp01(v) { return Math.max(0, Math.min(1, v)); }
|
|
function daysInMonth(y, m) { return new Date(y, m, 0).getDate(); }
|
|
function weekdayMon(y, m, d) { return (new Date(y, m-1, d).getDay() + 6) % 7; }
|
|
function phaseInfoForDay(y, m, d) {
|
|
const angle = ((Astronomy.MoonPhase(new Date(y, m-1, d, 12, 0, 0)) % 360) + 360) % 360;
|
|
return { phaseAngleDeg: angle, illumination: clamp01((1 - Math.cos(angle * Math.PI / 180)) / 2), waxing: angle <= 180 };
|
|
}
|
|
|
|
const phaseImage = new Image();
|
|
phaseImage.src = <?= json_encode($publicBasePath . 'images/moons/moon4calendar.png', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
|
|
|
function drawMoonDisc(ctx, x, y, radius, pi, eclipseKind) {
|
|
const cosP = Math.cos(pi.phaseAngleDeg * Math.PI / 180);
|
|
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 (pi.illumination >= 0.999) {
|
|
ctx.drawImage(phaseImage, x-radius, y-radius, radius*2, radius*2);
|
|
} else if (pi.illumination > 0.001) {
|
|
ctx.beginPath();
|
|
for (let i = 0; i <= 72; i++) {
|
|
const t = -radius + 2*radius*i/72, xr = Math.sqrt(Math.max(0, radius*radius - t*t));
|
|
const xt = (pi.waxing ? cosP : -cosP) * xr;
|
|
i === 0 ? ctx.moveTo(x+xt, y+t) : ctx.lineTo(x+xt, y+t);
|
|
}
|
|
for (let i = 72; i >= 0; i--) {
|
|
const t = -radius + 2*radius*i/72, xr = Math.sqrt(Math.max(0, radius*radius - t*t));
|
|
ctx.lineTo(x + (pi.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();
|
|
|
|
if (eclipseKind === 'total') {
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.clip();
|
|
ctx.fillStyle = 'rgba(180,50,10,0.55)'; ctx.fillRect(x-radius, y-radius, radius*2, radius*2);
|
|
ctx.restore();
|
|
ctx.strokeStyle = 'rgba(220,80,20,0.90)'; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.stroke();
|
|
} else if (eclipseKind === 'partial') {
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.clip();
|
|
ctx.fillStyle = 'rgba(160,60,10,0.35)'; ctx.fillRect(x-radius, y-radius, radius*2, radius*2);
|
|
ctx.restore();
|
|
ctx.strokeStyle = 'rgba(200,100,30,0.80)'; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.stroke();
|
|
} else if (eclipseKind === 'penumbral') {
|
|
ctx.save();
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.clip();
|
|
ctx.fillStyle = 'rgba(80,80,100,0.25)'; ctx.fillRect(x-radius, y-radius, radius*2, radius*2);
|
|
ctx.restore();
|
|
ctx.strokeStyle = 'rgba(150,150,180,0.60)'; ctx.lineWidth = 1.5;
|
|
ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI*2); ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function draw() {
|
|
if (!phaseImage.complete || !phaseImage.naturalWidth) return;
|
|
// DIN A4 Hochformat 150 DPI
|
|
const W = 1240, H = 1754;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = W; canvas.height = H;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
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);
|
|
|
|
const pad = 48, titleH = 80, dowH = 48, footerH = 52, cols = 7;
|
|
const gridLeft = pad, gridRight = W - pad, colW = (gridRight - gridLeft) / cols;
|
|
const gridTop = pad + titleH + dowH, gridBot = H - footerH - 20;
|
|
const totalDays = daysInMonth(YEAR, MONTH), firstDow = weekdayMon(YEAR, MONTH, 1);
|
|
const rows = Math.ceil((firstDow + totalDays) / 7), rowH = (gridBot - gridTop) / rows;
|
|
const discRadius = Math.min(colW * 0.28, rowH * 0.28);
|
|
|
|
ctx.fillStyle = 'rgba(240,215,132,0.95)'; ctx.font = '700 52px Rajdhani, sans-serif';
|
|
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
|
ctx.fillText(MONTH_NAME + ' ' + YEAR, pad, pad + titleH / 2);
|
|
ctx.fillStyle = 'rgba(201,168,76,0.55)'; ctx.font = '400 20px "Share Tech Mono", monospace';
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText('Zeitzone: ' + TIMEZONE, W - pad, pad + titleH / 2);
|
|
|
|
const dowY = pad + titleH + dowH / 2;
|
|
for (let c = 0; c < 7; c++) {
|
|
ctx.fillStyle = c === 6 ? 'rgba(220,100,80,0.85)' : c === 5 ? 'rgba(201,168,76,0.75)' : 'rgba(201,168,76,0.60)';
|
|
ctx.font = '600 22px Rajdhani, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
ctx.fillText(DAY_NAMES[c], gridLeft + (c + 0.5) * colW, dowY);
|
|
}
|
|
ctx.strokeStyle = 'rgba(201,168,76,0.25)'; ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(pad, pad + titleH + dowH); ctx.lineTo(W - pad, pad + titleH + dowH); ctx.stroke();
|
|
|
|
for (let day = 1; day <= totalDays; day++) {
|
|
const ci = firstDow + day - 1, col = ci % 7, row = Math.floor(ci / 7);
|
|
const cellX = gridLeft + col * colW, cellY = gridTop + row * rowH;
|
|
const cx = cellX + colW / 2, discY = cellY + rowH * 0.42;
|
|
const isSun = col === 6, isSat = col === 5;
|
|
if (isSun) { ctx.fillStyle = 'rgba(200,80,60,0.04)'; ctx.fillRect(cellX, cellY, colW, rowH); }
|
|
else if (isSat) { ctx.fillStyle = 'rgba(201,168,76,0.03)'; ctx.fillRect(cellX, cellY, colW, rowH); }
|
|
const calEc = CAL_ECLIPSES[String(day)] || null;
|
|
drawMoonDisc(ctx, cx, discY, discRadius, phaseInfoForDay(YEAR, MONTH, day), calEc ? calEc.kind : null);
|
|
ctx.fillStyle = isSun ? 'rgba(220,100,80,0.90)' : isSat ? 'rgba(240,210,120,0.85)' : 'rgba(240,217,132,0.80)';
|
|
ctx.font = '700 26px Rajdhani, sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
|
ctx.fillText(String(day), cellX + 10, cellY + 8);
|
|
const labelY = discY + discRadius + 8;
|
|
if (calEc) {
|
|
const ecLabel = calEc.kind === 'total' ? 'Totale Mondfinsternis' : calEc.kind === 'partial' ? 'Partielle Mondfinsternis' : 'Halbschatten-MF';
|
|
const ecColor = calEc.kind === 'total' ? 'rgba(230,100,40,1.00)' : calEc.kind === 'partial' ? 'rgba(210,120,50,0.95)' : 'rgba(160,160,190,0.80)';
|
|
ctx.fillStyle = ecColor; ctx.font = '600 14px Rajdhani, sans-serif';
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
|
ctx.fillText(ecLabel, cx, labelY);
|
|
if (calEc.local_time) { ctx.fillStyle = 'rgba(200,150,100,0.75)'; ctx.font = '400 13px "Share Tech Mono", monospace'; ctx.fillText(calEc.local_time + ' Uhr', cx, labelY + 18); }
|
|
} else {
|
|
const ev = CAL_EVENTS[String(day)];
|
|
if (ev) {
|
|
const meta = PHASE_LABEL_MAP[ev.label] || { short: ev.label, color: 'rgba(255,255,255,0.8)' };
|
|
ctx.fillStyle = meta.color; ctx.font = '600 16px Rajdhani, sans-serif';
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
|
ctx.fillText(meta.short, cx, labelY);
|
|
if (ev.time) { ctx.fillStyle = 'rgba(180,180,180,0.65)'; ctx.font = '400 14px "Share Tech Mono", monospace'; ctx.fillText(ev.time + ' Uhr', cx, labelY + 20); }
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.strokeStyle = 'rgba(201,168,76,0.10)'; ctx.lineWidth = 1;
|
|
for (let r = 0; r <= rows; r++) { const y = gridTop + r * rowH; ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke(); }
|
|
for (let c = 0; c <= cols; c++) { const x = gridLeft + c * colW; ctx.beginPath(); ctx.moveTo(x, pad + titleH + dowH); ctx.lineTo(x, gridBot); ctx.stroke(); }
|
|
|
|
const fy = H - footerH / 2 - 8;
|
|
ctx.strokeStyle = 'rgba(201,168,76,0.20)'; ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(pad, H - footerH - 20); ctx.lineTo(W - pad, H - footerH - 20); ctx.stroke();
|
|
ctx.fillStyle = 'rgba(240,215,132,0.70)'; ctx.font = '500 20px Rajdhani, sans-serif';
|
|
ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
|
|
ctx.fillText('Astronomiemuseum der Sternwarte Sonneberg', W - pad, fy);
|
|
|
|
const dataUrl = canvas.toDataURL('image/png');
|
|
imageEl.src = dataUrl;
|
|
if (downloadEl) { downloadEl.href = dataUrl; downloadEl.style.display = ''; }
|
|
if (statusEl) statusEl.textContent = 'Monatskalender ' + MONTH_NAME + ' ' + YEAR + ' (DIN A4, 150 DPI)';
|
|
}
|
|
|
|
phaseImage.onload = draw;
|
|
phaseImage.onerror = function () { if (statusEl) statusEl.textContent = 'Mondgrafik konnte nicht geladen werden.'; };
|
|
if (phaseImage.complete && phaseImage.naturalWidth) draw();
|
|
})();
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<?php require __DIR__ . '/footer.php'; ?>
|