Files
skyview.astronomiemuseum.de/public/planetensichtbarkeit.php
T
2026-04-02 11:17:37 +02:00

604 lines
20 KiB
PHP

<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Planetensichtbarkeit - AstroTools';
$bodyClass = 'planetvis-page';
?>
<?php require __DIR__ . '/header.php'; ?>
<?php
$pageLocation = null;
if (!empty($currentLocation) && is_array($currentLocation)) {
$pageLocation = [
'name' => (string)($currentLocation['name'] ?? ''),
'latitude' => isset($currentLocation['latitude']) ? (float)$currentLocation['latitude'] : null,
'longitude' => isset($currentLocation['longitude']) ? (float)$currentLocation['longitude'] : null,
'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float)$currentLocation['elevation'] : 0.0,
'timezone' => (string)($currentLocation['timezone'] ?? 'Europe/Berlin'),
];
}
?>
<script src="js/astronomy.browser.min.js"></script>
<div class="planetvis-tool">
<?php if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null): ?>
<section class="card planetvis-card">
<h2>Kein Standard-Standort gesetzt</h2>
<p class="hint">
Diese Seite verwendet den im Header geladenen Standard-Standort aus der Datenbank.
Bitte waehle in den <a href="settings.php">Einstellungen</a> einen Standard-Standort aus.
</p>
</section>
<?php else: ?>
<section class="card planetvis-card">
<p class="planetvis-intro">
Sichtbarkeit der Planeten und des Mondes ueber 24 Stunden fuer deinen Standard-Standort aus der Datenbank.
</p>
<div class="date-section">
<span class="date-lbl">Datum:</span>
<input type="date" id="datePicker">
<button class="btn btn-secondary" id="btnToday">Heute</button>
</div>
</section>
<section class="card planetvis-card">
<div class="time-axis-wrap" id="timeAxis"></div>
<div class="legend">
<div class="legend-item">
<div class="leg-bar"></div>
<span>Tag / Nacht</span>
</div>
<div class="legend-item">
<div class="leg-line"></div>
<span>Ueber Horizont</span>
</div>
<div class="legend-item">
<div class="leg-line leg-line-best"></div>
<span>Beste Sichtbarkeit</span>
</div>
<div class="legend-item">
<div class="leg-now"></div>
<span>Aktuelle Uhrzeit</span>
</div>
</div>
<div id="loadOverlay" class="planetvis-overlay">
<div class="spin-ring"></div>
Berechne Positionen...
</div>
<div id="chartRows"></div>
</section>
<section>
<div id="infoCards" class="info-row planetvis-info-row"></div>
</section>
<?php endif; ?>
</div>
<?php if ($pageLocation !== null && $pageLocation['latitude'] !== null && $pageLocation['longitude'] !== null): ?>
<div class="tooltip" id="tooltip">
<div class="tt-title" id="tt-title"></div>
<div id="tt-body"></div>
</div>
<script>
'use strict';
const SITE_LOCATION = <?= json_encode($pageLocation, JSON_UNESCAPED_SLASHES) ?>;
const LAT = Number(SITE_LOCATION.latitude);
const LNG = Number(SITE_LOCATION.longitude);
const ELEV = Number(SITE_LOCATION.elevation);
const DAYS_DE = ['SO', 'MO', 'DI', 'MI', 'DO', 'FR', 'SA'];
const MONTHS_DE = ['JAN', 'FEB', 'MRZ', 'APR', 'MAI', 'JUN', 'JUL', 'AUG', 'SEP', 'OKT', 'NOV', 'DEZ'];
const PLANET_DEFS = [
{name:'MERKUR', body:'Mercury', symbol:'Me', color:'#b0b0c8', glow:'rgba(176,176,200,.45)'},
{name:'VENUS', body:'Venus', symbol:'Ve', color:'#f5e090', glow:'rgba(245,224,144,.55)'},
{name:'MARS', body:'Mars', symbol:'Ma', color:'#e8602a', glow:'rgba(232,96,42,.5)'},
{name:'JUPITER', body:'Jupiter', symbol:'Ju', color:'#d4b880', glow:'rgba(212,184,128,.5)'},
{name:'SATURN', body:'Saturn', symbol:'Sa', color:'#e8d898', glow:'rgba(232,216,152,.45)'},
{name:'URANUS', body:'Uranus', symbol:'Ur', color:'#70e0e8', glow:'rgba(112,224,232,.4)'},
{name:'NEPTUN', body:'Neptune', symbol:'Ne', color:'#4070e0', glow:'rgba(64,112,224,.4)'},
];
const hToP = h => Math.min(100, Math.max(0, h / 24 * 100));
const fmt2 = n => String(n).padStart(2, '0');
const fmtDate = d => `${fmt2(d.getDate())}.${fmt2(d.getMonth() + 1)}.${d.getFullYear()}`;
const getNowH = () => {
const d = new Date();
return d.getHours() + d.getMinutes() / 60 + d.getSeconds() / 3600;
};
function hStr(h) {
if (h === null || h === undefined || Number.isNaN(h)) {
return '--:--';
}
const t = ((h % 24) + 24) % 24;
return `${fmt2(Math.floor(t))}:${fmt2(Math.floor((t - Math.floor(t)) * 60))}`;
}
function getTzH(date) {
function lastSunUTC(y, m) {
const d = new Date(Date.UTC(y, m + 1, 1));
d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 7) % 7 || 7));
return d;
}
const lsm = lastSunUTC(date.getFullYear(), 2);
const lso = lastSunUTC(date.getFullYear(), 9);
return (date >= lsm && date < lso) ? 2 : 1;
}
function localMidUTC(localDate, tzH) {
return new Date(Date.UTC(localDate.getFullYear(), localDate.getMonth(), localDate.getDate()) - tzH * 3600000);
}
function atToH(at, midMs) {
return at ? (at.date.getTime() - midMs) / 3600000 : null;
}
function buildSegments(bodyName, obs, midUTC, midMs) {
const Ast = Astronomy;
let isUpAtMid = false;
try {
const tMid = Ast.MakeTime(midUTC);
const eq = Ast.Equator(bodyName, tMid, obs, true, true);
const hz = Ast.Horizon(tMid, obs, eq.ra, eq.dec, 'normal');
isUpAtMid = hz.altitude > 0;
} catch (error) {
console.warn('Startstatus konnte nicht berechnet werden.', error);
}
const events = [];
let tSearch = new Date(midMs - 15 * 60000);
for (let i = 0; i < 3; i += 1) {
const rise = Ast.SearchRiseSet(bodyName, obs, +1, tSearch, 1.05);
if (!rise) {
break;
}
const h = (rise.date.getTime() - midMs) / 3600000;
if (h > 24.1) {
break;
}
events.push({ h, type: 'rise' });
tSearch = new Date(rise.date.getTime() + 60000);
}
tSearch = new Date(midMs - 15 * 60000);
for (let i = 0; i < 3; i += 1) {
const set = Ast.SearchRiseSet(bodyName, obs, -1, tSearch, 1.05);
if (!set) {
break;
}
const h = (set.date.getTime() - midMs) / 3600000;
if (h > 24.1) {
break;
}
events.push({ h, type: 'set' });
tSearch = new Date(set.date.getTime() + 60000);
}
events.sort((a, b) => a.h - b.h);
const segs = [];
let isUp = isUpAtMid;
let segStart = isUp ? 0 : null;
for (const ev of events) {
const clampedH = Math.max(-0.1, Math.min(24.1, ev.h));
if (ev.type === 'rise' && !isUp) {
segStart = Math.max(0, clampedH);
isUp = true;
} else if (ev.type === 'set' && isUp) {
const segEnd = Math.min(24, clampedH);
if (segEnd > (segStart ?? 0)) {
segs.push({ from: segStart ?? 0, to: segEnd });
}
segStart = null;
isUp = false;
}
}
if (isUp && segStart !== null) {
segs.push({ from: segStart, to: 24 });
}
return segs;
}
function getTwilight(obs, midUTC, midMs) {
const Ast = Astronomy;
function searchAlt(dir, alt, startOffsetH) {
const t0 = new Date(midMs + startOffsetH * 3600000);
try {
return Ast.SearchAltitude('Sun', obs, dir, t0, 0.6, alt);
} catch (error) {
return null;
}
}
return {
sunRise: Ast.SearchRiseSet('Sun', obs, +1, midUTC, 1),
sunSet: Ast.SearchRiseSet('Sun', obs, -1, midUTC, 1),
civDawn: searchAlt(+1, -6, 0),
civDusk: searchAlt(-1, -6, 10),
nautDawn: searchAlt(+1, -12, 0),
nautDusk: searchAlt(-1, -12, 10),
astroDawn: searchAlt(+1, -18, 0),
astroDusk: searchAlt(-1, -18, 10),
};
}
function getMoonInfo(midUTC) {
const Ast = Astronomy;
const t = Ast.MakeTime(new Date(midUTC.getTime() + 43200000));
const phase = Ast.MoonPhase(t);
const illum = Ast.Illumination('Moon', t);
const waning = phase > 180;
let label = 'NM';
if (phase < 22.5 || phase >= 337.5) label = 'NM';
else if (!waning && phase < 67.5) label = 'ZS';
else if (!waning && phase < 112.5) label = 'ZH';
else if (!waning && phase < 157.5) label = 'ZG';
else if (phase >= 157.5 && phase < 202.5) label = 'VM';
else if (waning && phase < 247.5) label = 'AG';
else if (waning && phase < 292.5) label = 'AH';
else label = 'AS';
return {
phase,
illum: illum.phase_fraction ?? ((1 + Math.cos(illum.phase_angle * Math.PI / 180)) / 2),
waning,
label
};
}
function buildGradient(tw, midMs) {
const p = at => at ? Math.max(0, Math.min(100, (at.date.getTime() - midMs) / 3600000 / 24 * 100)).toFixed(2) : null;
const stops = [];
const add = (pos, col) => {
if (pos !== null) {
stops.push({ pos: parseFloat(pos), col });
}
};
const sr = p(tw.sunRise);
const ss = p(tw.sunSet);
const cd = p(tw.civDawn);
const cu = p(tw.civDusk);
const nd = p(tw.nautDawn);
const nu = p(tw.nautDusk);
const ad = p(tw.astroDawn);
const au = p(tw.astroDusk);
add('0', '#010108');
if (ad) add(Math.max(0, parseFloat(ad) - 2).toFixed(2), '#010108');
if (ad) add(ad, '#06060f');
if (nd) add(nd, '#0d1430');
if (cd) add(cd, '#1e1a48');
if (cd) add((parseFloat(cd) + 0.4).toFixed(2), '#2e1858');
if (sr) {
add(sr, '#cc7038');
add((parseFloat(sr) + 0.45).toFixed(2), '#f0a860');
add((parseFloat(sr) + 2.5).toFixed(2), '#b8d0ea');
}
if (sr && ss) {
const noon = ((parseFloat(sr) + parseFloat(ss)) / 2).toFixed(2);
add(noon, '#e2f4ff');
}
if (ss) {
add((parseFloat(ss) - 2.5).toFixed(2), '#b8d0ea');
add((parseFloat(ss) - 0.45).toFixed(2), '#f0a860');
add(ss, '#cc7038');
}
if (cu) {
add((parseFloat(cu) - 0.4).toFixed(2), '#2e1858');
add(cu, '#1e1a48');
}
if (nu) add(nu, '#0d1430');
if (au) {
add(au, '#06060f');
add(Math.min(100, parseFloat(au) + 2).toFixed(2), '#010108');
}
add('100', '#010108');
stops.sort((a, b) => a.pos - b.pos);
const uniq = stops.filter((s, i) => i === 0 || stops[i - 1].pos !== s.pos);
return 'linear-gradient(to right,' + uniq.map(s => `${s.col} ${s.pos}%`).join(',') + ')';
}
const axisEl = document.getElementById('timeAxis');
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24].forEach(t => {
const d = document.createElement('div');
d.className = 'time-tick';
d.textContent = t === 24 ? '24:00' : `${fmt2(t)}:00`;
axisEl.appendChild(d);
});
let needleEls = [];
function updateNeedles() {
const pct = hToP(getNowH());
needleEls.forEach(n => {
n.style.left = `calc(${pct}% - 1px)`;
});
}
window.setInterval(updateNeedles, 15000);
function computeBestSegs(segs, astroDawnH, astroDuskH) {
if (astroDawnH === null || astroDuskH === null) {
return [];
}
const result = [];
for (const seg of segs) {
if (seg.from < astroDawnH) {
const to = Math.min(seg.to, astroDawnH);
if (to - seg.from > 0.05) {
result.push({ from: seg.from, to });
}
}
if (seg.to > astroDuskH) {
const from = Math.max(seg.from, astroDuskH);
const to = Math.min(seg.to, 24);
if (to - from > 0.05) {
result.push({ from, to });
}
}
}
return result;
}
function addStripe(bar, from, to, def) {
if (to - from < 0.05) {
return;
}
const s = document.createElement('div');
s.className = 'vis-stripe';
s.style.left = hToP(from) + '%';
s.style.width = (hToP(to) - hToP(from)) + '%';
s.style.background = `linear-gradient(to bottom,
rgba(0,0,0,.25) 0%,
${def.color}ee 15%,
${def.color}ff 50%,
${def.color}ee 85%,
rgba(0,0,0,.25) 100%)`;
s.style.boxShadow = `0 0 0 1px rgba(0,0,0,.5), 0 0 10px ${def.glow}`;
s.style.opacity = '0.85';
bar.appendChild(s);
}
function addBest(bar, from, to, def) {
if (to - from < 0.05) {
return;
}
const s = document.createElement('div');
s.className = 'vis-stripe-best';
s.style.left = hToP(from) + '%';
s.style.width = (hToP(to) - hToP(from)) + '%';
s.style.background = def.color;
s.style.boxShadow = `0 0 0 1px rgba(0,0,0,.6), 0 0 14px 4px ${def.glow}, 0 0 4px ${def.color}`;
s.style.opacity = '1';
bar.appendChild(s);
}
const tooltip = document.getElementById('tooltip');
function renderChart(localDate) {
const container = document.getElementById('chartRows');
const infoDiv = document.getElementById('infoCards');
const loadDiv = document.getElementById('loadOverlay');
container.innerHTML = '';
infoDiv.innerHTML = '';
needleEls = [];
loadDiv.innerHTML = '<div class="spin-ring"></div>Berechne Positionen...';
loadDiv.classList.remove('is-hidden');
window.setTimeout(() => {
try {
const tzH = getTzH(localDate);
const midUTC = localMidUTC(localDate, tzH);
const midMs = midUTC.getTime();
const obs = new Astronomy.Observer(LAT, LNG, ELEV);
const tw = getTwilight(obs, midUTC, midMs);
const gradient = buildGradient(tw, midMs);
const astroDawnH = atToH(tw.astroDawn, midMs);
const astroDuskH = atToH(tw.astroDusk, midMs);
const moonInfo = getMoonInfo(midUTC);
const moonSegs = buildSegments('Moon', obs, midUTC, midMs);
const moonBestSegs = computeBestSegs(moonSegs, astroDawnH, astroDuskH);
const allDefs = [
...PLANET_DEFS.map(d => ({ ...d, type: 'planet' })),
{
name: 'MOND',
body: 'Moon',
symbol: moonInfo.label,
type: 'moon',
color: '#d0d8f0',
glow: 'rgba(208,216,240,.55)',
segs: moonSegs,
bestSegs: moonBestSegs,
illum: moonInfo.illum,
waning: moonInfo.waning,
label: moonInfo.label
}
];
allDefs.forEach((def, idx) => {
if (def.type === 'moon') {
container.appendChild(Object.assign(document.createElement('div'), { className: 'moon-sep' }));
}
let segs = def.segs || [];
let bestSegs = def.bestSegs || [];
let mag = '-';
if (def.type === 'planet') {
segs = buildSegments(def.body, obs, midUTC, midMs);
bestSegs = computeBestSegs(segs, astroDawnH, astroDuskH);
try {
const il = Astronomy.Illumination(def.body, Astronomy.MakeTime(new Date(midMs + 43200000)));
mag = (il.mag >= 0 ? '+' : '') + il.mag.toFixed(1);
} catch (error) {
console.warn('Magnitude konnte nicht berechnet werden.', error);
}
}
let actualRiseH = segs.length ? segs[0].from : null;
let actualSetH = segs.length ? segs[segs.length - 1].to : null;
if (segs.length && segs[0].from < 0.08) {
try {
const rise = Astronomy.SearchRiseSet(def.body, obs, +1, new Date(midMs - 36 * 3600000), 2.5);
if (rise) {
const rh = (rise.date.getTime() - midMs) / 3600000;
if (rh < 0.08) {
actualRiseH = rh;
}
}
} catch (error) {
console.warn('Aufgang ausserhalb des Fensters konnte nicht berechnet werden.', error);
}
}
if (segs.length && segs[segs.length - 1].to > 23.95) {
try {
const set = Astronomy.SearchRiseSet(def.body, obs, -1, new Date(midMs + 23 * 3600000), 2.0);
if (set) {
const sh = (set.date.getTime() - midMs) / 3600000;
if (sh > 23.95) {
actualSetH = sh;
}
}
} catch (error) {
console.warn('Untergang ausserhalb des Fensters konnte nicht berechnet werden.', error);
}
}
const hStrDay = h => h === null ? '--:--' : hStr(h);
const row = document.createElement('div');
row.className = 'planet-row';
const metaStr = def.type === 'moon'
? `${Math.round(def.illum * 100)}% ${def.waning ? 'abnehmend' : 'zunehmend'}`
: `Mag ${mag}`;
row.innerHTML = `
<div class="planet-label">
<div class="planet-orb">${def.symbol}</div>
<div><div class="planet-name">${def.name}</div><div class="planet-meta">${metaStr}</div></div>
</div>
`;
const orbEl = row.querySelector('.planet-orb');
orbEl.style.setProperty('--planet-orb-color', def.color);
orbEl.style.setProperty('--planet-orb-glow', def.glow);
const barOuter = document.createElement('div');
barOuter.className = 'bar-outer';
const barBg = document.createElement('div');
barBg.className = 'bar-bg';
barBg.style.background = gradient;
barOuter.appendChild(barBg);
segs.forEach(s => addStripe(barOuter, s.from, s.to, def));
bestSegs.forEach(s => addBest(barOuter, s.from, s.to, def));
const needle = document.createElement('div');
needle.className = 'needle';
needle.innerHTML = `${idx === 0 ? '<div class="needle-lbl">JETZT</div>' : ''}<div class="needle-glow"></div><div class="needle-cap"></div>`;
barOuter.appendChild(needle);
needleEls.push(needle);
barOuter.addEventListener('mousemove', e => {
const rect = barOuter.getBoundingClientRect();
const h = ((e.clientX - rect.left) / rect.width) * 24;
const above = segs.some(s => h >= s.from && h <= s.to);
const rStr = segs.length ? hStrDay(actualRiseH) : '--:--';
const sStr = segs.length ? hStrDay(actualSetH) : '--:--';
document.getElementById('tt-title').textContent = def.name;
document.getElementById('tt-body').innerHTML =
`Uhrzeit: ${hStr(h)}<br>Aufgang: ${rStr}<br>Untergang: ${sStr}<br>` +
(def.type === 'moon' ? `Beleuchtung: ${Math.round(def.illum * 100)}%<br>` : `Magnitude: ${mag}<br>`) +
`Status: ${above ? '<span class="tt-up">ueber Horizont</span>' : '<span class="tt-down">unter Horizont</span>'}`;
tooltip.style.left = (e.clientX + 16) + 'px';
tooltip.style.top = (e.clientY - 14) + 'px';
tooltip.classList.add('vis');
});
barOuter.addEventListener('mouseleave', () => tooltip.classList.remove('vis'));
row.appendChild(barOuter);
container.appendChild(row);
if (idx < allDefs.length - 1 && allDefs[idx + 1].type !== 'moon' && def.type !== 'moon') {
container.appendChild(Object.assign(document.createElement('div'), { className: 'planet-div' }));
}
});
const hF = at => at ? hStr(atToH(at, midMs)) + ' Uhr' : '-';
infoDiv.innerHTML = `
<div class="info-card"><strong>Standort</strong><br>${SITE_LOCATION.name}<br>${LAT.toFixed(3)}deg N · ${LNG.toFixed(3)}deg O · ${ELEV} m</div>
<div class="info-card"><strong>Sonne (${fmtDate(localDate)})</strong><br>Aufgang: ${hF(tw.sunRise)}<br>Untergang: ${hF(tw.sunSet)}</div>
<div class="info-card"><strong>Daemmerung</strong><br>Buergerlich: ${hF(tw.civDawn)} / ${hF(tw.civDusk)}<br>Nautisch: ${hF(tw.nautDawn)} / ${hF(tw.nautDusk)}<br>Astronomisch: ${hF(tw.astroDawn)} / ${hF(tw.astroDusk)}</div>
<div class="info-card"><strong>Mond (${fmtDate(localDate)})</strong><br>${Math.round(moonInfo.illum * 100)}% ${moonInfo.waning ? 'abnehmend' : 'zunehmend'}<br>Phase: ${moonInfo.phase.toFixed(1)}deg</div>
<div class="info-card"><strong>Zeitzone</strong><br>${tzH === 2 ? 'MESZ (UTC+2)' : 'MEZ (UTC+1)'}<br>${SITE_LOCATION.timezone}</div>
`;
loadDiv.classList.add('is-hidden');
updateNeedles();
} catch (error) {
loadDiv.innerHTML = `<span class="planetvis-error-text">Fehler: ${error.message}</span>`;
loadDiv.classList.remove('is-hidden');
console.error(error);
}
}, 30);
}
const datePicker = document.getElementById('datePicker');
const localDateStr = d => `${d.getFullYear()}-${fmt2(d.getMonth() + 1)}-${fmt2(d.getDate())}`;
const pickerDate = () => {
const [y, m, d] = datePicker.value.split('-').map(Number);
return new Date(y, m - 1, d, 12);
};
document.getElementById('btnToday').addEventListener('click', () => {
const t = new Date();
datePicker.value = localDateStr(t);
renderChart(new Date(t.getFullYear(), t.getMonth(), t.getDate(), 12));
});
datePicker.addEventListener('input', () => {
if (datePicker.value) {
renderChart(pickerDate());
}
});
function init() {
const today = new Date();
datePicker.value = localDateStr(today);
renderChart(new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12));
}
if (typeof Astronomy !== 'undefined') {
init();
} else {
document.getElementById('loadOverlay').innerHTML =
'<span class="planetvis-error-text">Bibliothek konnte nicht geladen werden.</span>';
}
</script>
<?php endif; ?>
<?php require __DIR__ . '/footer.php'; ?>