diff --git a/public/astronomical_conversions.php b/public/astronomical_conversions.php index 0e47230..0e48659 100644 --- a/public/astronomical_conversions.php +++ b/public/astronomical_conversions.php @@ -4,6 +4,7 @@ declare(strict_types=1); ini_set('display_errors', '1'); error_reporting(E_ALL); session_start(); +require_once __DIR__ . '/python_api.php'; $loggedIn = isset($_SESSION['user_id']); $pageTitle = 'Astronomische Umrechnungen'; @@ -16,47 +17,6 @@ function h(?string $value): string return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } -function runPythonApi(string $action, array $args): array -{ - $scriptPath = __DIR__ . '/py/api.py'; - $pythonCandidates = PHP_OS_FAMILY === 'Windows' - ? ['python', 'py'] - : ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python']; - - $escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args)); - $lastError = 'Python konnte nicht gestartet werden.'; - - foreach ($pythonCandidates as $pythonBinary) { - $command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1'; - $output = []; - $resultCode = 0; - exec($command, $output, $resultCode); - - $joined = trim(implode("\n", $output)); - $decoded = json_decode($joined, true); - - if (is_array($decoded)) { - return [ - 'ok' => (bool) ($decoded['ok'] ?? false), - 'data' => $decoded, - 'raw' => $joined, - 'command' => $command, - ]; - } - - if ($joined !== '') { - $lastError = $joined; - } elseif ($resultCode !== 0) { - $lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary; - } - } - - return [ - 'ok' => false, - 'error' => $lastError, - ]; -} - if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') === '1') { header('Content-Type: application/json; charset=utf-8'); diff --git a/public/footer.php b/public/footer.php index b9df2b9..1b2990d 100644 --- a/public/footer.php +++ b/public/footer.php @@ -17,7 +17,7 @@ $publicBasePath = isset($publicBasePath) && is_string($publicBasePath) ? rtrim($  m - ✎ + ✎

diff --git a/public/meteorshowers.php b/public/meteorshowers.php index 2fe8c8b..f93845b 100644 --- a/public/meteorshowers.php +++ b/public/meteorshowers.php @@ -2,6 +2,7 @@ declare(strict_types=1); session_start(); +require_once __DIR__ . '/python_api.php'; $loggedIn = isset($_SESSION['user_id']); $pageTitle = 'Meteorströme - AstroTools'; @@ -81,47 +82,6 @@ function fetchCurrentSolarLongitude(DateTimeZone $displayTimezone): ?float return normalizeSolarLongitude((float) $solarLongitude); } -function runPythonApi(string $action, array $args): array -{ - $scriptPath = __DIR__ . '/py/api.py'; - $pythonCandidates = PHP_OS_FAMILY === 'Windows' - ? ['python', 'py'] - : ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python']; - - $escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args)); - $lastError = 'Python konnte nicht gestartet werden.'; - - foreach ($pythonCandidates as $pythonBinary) { - $command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1'; - $output = []; - $resultCode = 0; - exec($command, $output, $resultCode); - - $joined = trim(implode("\n", $output)); - $decoded = json_decode($joined, true); - - if (is_array($decoded)) { - return [ - 'ok' => (bool) ($decoded['ok'] ?? false), - 'data' => $decoded, - 'raw' => $joined, - 'command' => $command, - ]; - } - - if ($joined !== '') { - $lastError = $joined; - } elseif ($resultCode !== 0) { - $lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary; - } - } - - return [ - 'ok' => false, - 'error' => $lastError, - ]; -} - function isSolarLongitudeInRange(float $current, ?float $start, ?float $finish): bool { if ($start === null || $finish === null) { diff --git a/public/planetensichtbarkeit.php b/public/planetensichtbarkeit.php index 2c19d32..c353189 100644 --- a/public/planetensichtbarkeit.php +++ b/public/planetensichtbarkeit.php @@ -5,26 +5,97 @@ ini_set('display_errors', '1'); error_reporting(E_ALL); session_start(); +require_once __DIR__ . '/auth_helpers.php'; +require_once __DIR__ . '/python_api.php'; +applyLocalDevLoginBypass(); + $loggedIn = isset($_SESSION['user_id']); $pageTitle = 'Planetensichtbarkeit - AstroTools'; $bodyClass = 'planetvis-page'; -?> - - PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] + ); + + $stmtLoc = $pdo->prepare( + 'SELECT id, name, latitude, longitude, elevation, timezone + FROM app_user_locations + WHERE user_id = ? AND is_default = 1 + LIMIT 1' + ); + $stmtLoc->execute([(int) $_SESSION['user_id']]); + $currentLocation = $stmtLoc->fetch() ?: null; + } catch (Throwable $e) { + $currentLocation = null; + } +} + $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'), + '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'), ]; } -?> - +if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') === '1') { + header('Content-Type: application/json; charset=utf-8'); + + if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) { + echo json_encode([ + 'ok' => false, + 'error' => 'Kein gueltiger Standard-Standort verfuegbar.', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + $dateText = trim((string) ($_POST['date'] ?? '')); + $date = DateTimeImmutable::createFromFormat('Y-m-d', $dateText); + $dateErrors = DateTimeImmutable::getLastErrors(); + if ($dateErrors === false) { + $dateErrors = ['warning_count' => 0, 'error_count' => 0]; + } + if ($date === false || ($dateErrors['warning_count'] ?? 0) > 0 || ($dateErrors['error_count'] ?? 0) > 0) { + echo json_encode([ + 'ok' => false, + 'error' => 'Datum muss im Format YYYY-MM-DD uebergeben werden.', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + $pythonResult = runPythonApi('planet_visibility_chart', [ + (string) $pageLocation['latitude'], + (string) $pageLocation['longitude'], + (string) $pageLocation['elevation'], + $dateText, + (string) $pageLocation['timezone'], + ]); + + if (!$pythonResult['ok']) { + echo json_encode([ + 'ok' => false, + 'error' => (string) ($pythonResult['error'] ?? 'Python-Fehler'), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + echo json_encode($pythonResult['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} +?> +
@@ -32,13 +103,13 @@ if (!empty($currentLocation) && is_array($currentLocation)) {

Kein Standard-Standort gesetzt

Diese Seite verwendet den im Header geladenen Standard-Standort aus der Datenbank. - Bitte wähle in den Einstellungen einen Standard-Standort aus. + Bitte waehle in den Einstellungen einen Standard-Standort aus.

- Sichtbarkeit der Planeten und des Mondes über 24 Stunden für deinen Standard-Standort aus der Datenbank. + Sichtbarkeit der Planeten und des Mondes über 24 Stunden fuer deinen Standard-Standort aus der Datenbank.

@@ -58,7 +129,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
- Ueber Horizont + Über Horizont
@@ -97,19 +168,6 @@ 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()}`; @@ -126,148 +184,10 @@ function hStr(h) { 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; +function buildGradient(tw) { + const p = hourValue => hourValue === null || hourValue === undefined + ? null + : Math.max(0, Math.min(100, hourValue / 24 * 100)).toFixed(2); const stops = []; const add = (pos, col) => { if (pos !== null) { @@ -275,14 +195,14 @@ function buildGradient(tw, midMs) { } }; - 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); + const sr = p(tw.sunRiseH); + const ss = p(tw.sunSetH); + const cd = p(tw.civDawnH); + const cu = p(tw.civDuskH); + const nd = p(tw.nautDawnH); + const nu = p(tw.nautDuskH); + const ad = p(tw.astroDawnH); + const au = p(tw.astroDuskH); add('0', '#010108'); if (ad) add(Math.max(0, parseFloat(ad) - 2).toFixed(2), '#010108'); @@ -337,30 +257,6 @@ function updateNeedles() { } 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; @@ -395,177 +291,6 @@ function addBest(bar, from, to, def) { } 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 = '
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 = ` -
-
${def.symbol}
-
${def.name}
${metaStr}
-
- `; - 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 ? '
JETZT
' : ''}
`; - 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)}
Aufgang: ${rStr}
Untergang: ${sStr}
` + - (def.type === 'moon' ? `Beleuchtung: ${Math.round(def.illum * 100)}%
` : `Magnitude: ${mag}
`) + - `Status: ${above ? 'über Horizont' : 'unter Horizont'}`; - 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 = ` -
Standort
${SITE_LOCATION.name}
${LAT.toFixed(3)}deg N · ${LNG.toFixed(3)}deg O · ${ELEV} m
-
Sonne (${fmtDate(localDate)})
Aufgang: ${hF(tw.sunRise)}
Untergang: ${hF(tw.sunSet)}
-
Dämmerung
Bürgerlich: ${hF(tw.civDawn)} / ${hF(tw.civDusk)}
Nautisch: ${hF(tw.nautDawn)} / ${hF(tw.nautDusk)}
Astronomisch: ${hF(tw.astroDawn)} / ${hF(tw.astroDusk)}
-
Mond (${fmtDate(localDate)})
${Math.round(moonInfo.illum * 100)}% ${moonInfo.waning ? 'abnehmend' : 'zunehmend'}
Phase: ${moonInfo.phase.toFixed(1)}deg
-
Zeitzone
${tzH === 2 ? 'MESZ (UTC+2)' : 'MEZ (UTC+1)'}
${SITE_LOCATION.timezone}
- `; - - loadDiv.classList.add('is-hidden'); - updateNeedles(); - } catch (error) { - loadDiv.innerHTML = `Fehler: ${error.message}`; - 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 = () => { @@ -573,6 +298,139 @@ const pickerDate = () => { return new Date(y, m - 1, d, 12); }; +async function fetchVisibilityData(localDate) { + const formData = new FormData(); + formData.set('ajax', '1'); + formData.set('date', localDateStr(localDate)); + + const response = await fetch('planetensichtbarkeit.php', { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + const payload = await response.json(); + if (!response.ok || !payload.ok) { + throw new Error(payload.error || 'Die Sichtbarkeitsdaten konnten nicht geladen werden.'); + } + + return payload; +} + +function renderChartRows(rows, gradient) { + const container = document.getElementById('chartRows'); + container.innerHTML = ''; + needleEls = []; + + rows.forEach((def, idx) => { + if (def.type === 'moon') { + container.appendChild(Object.assign(document.createElement('div'), { className: 'moon-sep' })); + } + + const segs = Array.isArray(def.segs) ? def.segs : []; + const bestSegs = Array.isArray(def.bestSegs) ? def.bestSegs : []; + const magnitudeText = def.type === 'planet' && typeof def.magnitude === 'number' + ? `${def.magnitude >= 0 ? '+' : ''}${def.magnitude.toFixed(1)}` + : '-'; + const metaStr = def.type === 'moon' + ? `${Math.round((def.illum || 0) * 100)}% ${def.waning ? 'abnehmend' : 'zunehmend'}` + : `Mag ${magnitudeText}`; + + const row = document.createElement('div'); + row.className = 'planet-row'; + row.innerHTML = ` +
+
${def.symbol}
+
${def.name}
${metaStr}
+
+ `; + + 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 ? '
JETZT
' : ''}
`; + 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); + document.getElementById('tt-title').textContent = def.name; + document.getElementById('tt-body').innerHTML = + `Uhrzeit: ${hStr(h)}
Aufgang: ${hStr(def.actualRiseH)}
Untergang: ${hStr(def.actualSetH)}
` + + (def.type === 'moon' + ? `Beleuchtung: ${Math.round((def.illum || 0) * 100)}%
` + : `Magnitude: ${magnitudeText}
`) + + `Status: ${above ? 'über Horizont' : 'unter Horizont'}`; + 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 < rows.length - 1 && rows[idx + 1].type !== 'moon' && def.type !== 'moon') { + container.appendChild(Object.assign(document.createElement('div'), { className: 'planet-div' })); + } + }); +} + +function renderInfoCards(localDate, data) { + const infoDiv = document.getElementById('infoCards'); + const tw = data.twilight || {}; + const moon = data.moon || {}; + const offset = Number(data.utc_offset_hours); + const tzLabel = Number.isFinite(offset) + ? `${data.timezone_abbr} (UTC${offset >= 0 ? '+' : ''}${offset})` + : (data.timezone_abbr || SITE_LOCATION.timezone); + const hF = hourValue => hourValue === null || hourValue === undefined ? '-' : `${hStr(hourValue)} Uhr`; + + infoDiv.innerHTML = ` +
Standort
${SITE_LOCATION.name}
${LAT.toFixed(3)}deg N | ${LNG.toFixed(3)}deg O | ${ELEV} m
+
Sonne (${fmtDate(localDate)})
Aufgang: ${hF(tw.sunRiseH)}
Untergang: ${hF(tw.sunSetH)}
+
Dämmerung
Buergerlich: ${hF(tw.civDawnH)} / ${hF(tw.civDuskH)}
Nautisch: ${hF(tw.nautDawnH)} / ${hF(tw.nautDuskH)}
Astronomisch: ${hF(tw.astroDawnH)} / ${hF(tw.astroDuskH)}
+
Mond (${fmtDate(localDate)})
${Math.round((moon.illum || 0) * 100)}% ${moon.waning ? 'abnehmend' : 'zunehmend'}
Phase: ${typeof moon.phase === 'number' ? moon.phase.toFixed(1) : '--'}deg
+
Zeitzone
${tzLabel}
${SITE_LOCATION.timezone}
+ `; +} + +async function renderChart(localDate) { + const loadDiv = document.getElementById('loadOverlay'); + document.getElementById('chartRows').innerHTML = ''; + document.getElementById('infoCards').innerHTML = ''; + needleEls = []; + loadDiv.innerHTML = '
Berechne Positionen...'; + loadDiv.classList.remove('is-hidden'); + + try { + const data = await fetchVisibilityData(localDate); + const gradient = buildGradient(data.twilight || {}); + renderChartRows(Array.isArray(data.rows) ? data.rows : [], gradient); + renderInfoCards(localDate, data); + loadDiv.classList.add('is-hidden'); + updateNeedles(); + } catch (error) { + loadDiv.innerHTML = `Fehler: ${error.message}`; + loadDiv.classList.remove('is-hidden'); + console.error(error); + } +} + document.getElementById('btnToday').addEventListener('click', () => { const t = new Date(); datePicker.value = localDateStr(t); @@ -591,12 +449,7 @@ function init() { renderChart(new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12)); } -if (typeof Astronomy !== 'undefined') { - init(); -} else { - document.getElementById('loadOverlay').innerHTML = - 'Bibliothek konnte nicht geladen werden.'; -} +init(); diff --git a/public/py/api.py b/public/py/api.py index d770afd..9b8ef40 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -396,6 +396,361 @@ PLANETS = [ ("Neptun", astronomy.Body.Neptune), ] +PLANET_VISIBILITY_ROWS = [ + { + "key": "mercury", + "name": "MERKUR", + "body": astronomy.Body.Mercury, + "symbol": "Me", + "color": "#b0b0c8", + "glow": "rgba(176,176,200,.45)", + "type": "planet", + }, + { + "key": "venus", + "name": "VENUS", + "body": astronomy.Body.Venus, + "symbol": "Ve", + "color": "#f5e090", + "glow": "rgba(245,224,144,.55)", + "type": "planet", + }, + { + "key": "mars", + "name": "MARS", + "body": astronomy.Body.Mars, + "symbol": "Ma", + "color": "#e8602a", + "glow": "rgba(232,96,42,.5)", + "type": "planet", + }, + { + "key": "jupiter", + "name": "JUPITER", + "body": astronomy.Body.Jupiter, + "symbol": "Ju", + "color": "#d4b880", + "glow": "rgba(212,184,128,.5)", + "type": "planet", + }, + { + "key": "saturn", + "name": "SATURN", + "body": astronomy.Body.Saturn, + "symbol": "Sa", + "color": "#e8d898", + "glow": "rgba(232,216,152,.45)", + "type": "planet", + }, + { + "key": "uranus", + "name": "URANUS", + "body": astronomy.Body.Uranus, + "symbol": "Ur", + "color": "#70e0e8", + "glow": "rgba(112,224,232,.4)", + "type": "planet", + }, + { + "key": "neptune", + "name": "NEPTUN", + "body": astronomy.Body.Neptune, + "symbol": "Ne", + "color": "#4070e0", + "glow": "rgba(64,112,224,.4)", + "type": "planet", + }, +] + + +def hour_from_utc_start(event_time: astronomy.Time | None, utc_start: datetime) -> float | None: + if event_time is None: + return None + return (time_to_datetime(event_time) - utc_start).total_seconds() / 3600.0 + + +def build_visibility_segments( + body: astronomy.Body, + observer: astronomy.Observer, + utc_start: datetime, + utc_end: datetime, +) -> tuple[list[dict], float | None, float | None]: + start_time = dt_to_time(utc_start) + start_eq = astronomy.Equator(body, start_time, observer, True, True) + start_hor = astronomy.Horizon(start_time, observer, start_eq.ra, start_eq.dec, astronomy.Refraction.Normal) + is_up = float(start_hor.altitude) > 0.0 + + events: list[dict] = [] + + def gather(direction: astronomy.Direction) -> None: + search_dt = utc_start - timedelta(minutes=15) + while True: + limit_days = max(0.01, min(3.0, (utc_end + timedelta(hours=2) - search_dt).total_seconds() / 86400.0)) + event_time = astronomy.SearchRiseSet(body, observer, direction, dt_to_time(search_dt), limit_days, 1.05) + if event_time is None: + break + + hour_value = hour_from_utc_start(event_time, utc_start) + if hour_value is None or hour_value > 24.1: + break + + events.append({ + "hour": hour_value, + "type": "rise" if direction == astronomy.Direction.Rise else "set", + }) + search_dt = time_to_datetime(event_time) + timedelta(minutes=1) + + gather(astronomy.Direction.Rise) + gather(astronomy.Direction.Set) + events.sort(key=lambda item: item["hour"]) + + segments: list[dict] = [] + segment_start = 0.0 if is_up else None + + for event in events: + clamped_hour = max(-0.1, min(24.1, float(event["hour"]))) + if event["type"] == "rise" and not is_up: + segment_start = max(0.0, clamped_hour) + is_up = True + elif event["type"] == "set" and is_up: + segment_end = min(24.0, clamped_hour) + if segment_start is not None and segment_end > segment_start: + segments.append({"from": segment_start, "to": segment_end}) + segment_start = None + is_up = False + + if is_up and segment_start is not None: + segments.append({"from": segment_start, "to": 24.0}) + + actual_rise_hour = segments[0]["from"] if segments else None + actual_set_hour = segments[-1]["to"] if segments else None + + if segments and segments[0]["from"] < 0.08: + search_dt = utc_start - timedelta(hours=36) + previous_rise: astronomy.Time | None = None + while True: + limit_days = max(0.01, min(4.0, (utc_start - search_dt).total_seconds() / 86400.0 + 1.0)) + rise_time = astronomy.SearchRiseSet(body, observer, astronomy.Direction.Rise, dt_to_time(search_dt), limit_days, 2.5) + if rise_time is None: + break + rise_dt = time_to_datetime(rise_time) + if rise_dt >= utc_start: + break + previous_rise = rise_time + search_dt = rise_dt + timedelta(minutes=1) + previous_rise_hour = hour_from_utc_start(previous_rise, utc_start) + if previous_rise_hour is not None: + actual_rise_hour = previous_rise_hour + + if segments and segments[-1]["to"] > 23.95: + limit_days = 2.0 + next_set = astronomy.SearchRiseSet( + body, + observer, + astronomy.Direction.Set, + dt_to_time(utc_start + timedelta(hours=23)), + limit_days, + 2.0, + ) + next_set_hour = hour_from_utc_start(next_set, utc_start) + if next_set_hour is not None and next_set_hour > 23.95: + actual_set_hour = next_set_hour + + return segments, actual_rise_hour, actual_set_hour + + +def compute_best_segments( + segments: list[dict], + astro_dawn_hour: float | None, + astro_dusk_hour: float | None, +) -> list[dict]: + if astro_dawn_hour is None or astro_dusk_hour is None: + return [] + + result: list[dict] = [] + for segment in segments: + start_hour = float(segment["from"]) + end_hour = float(segment["to"]) + + if start_hour < astro_dawn_hour: + best_end = min(end_hour, astro_dawn_hour) + if best_end - start_hour > 0.05: + result.append({"from": start_hour, "to": best_end}) + + if end_hour > astro_dusk_hour: + best_start = max(start_hour, astro_dusk_hour) + best_end = min(end_hour, 24.0) + if best_end - best_start > 0.05: + result.append({"from": best_start, "to": best_end}) + + return result + + +def moon_phase_code(phase_angle: float) -> tuple[str, bool]: + waning = phase_angle > 180.0 + if phase_angle < 22.5 or phase_angle >= 337.5: + return "NM", waning + if not waning and phase_angle < 67.5: + return "ZS", waning + if not waning and phase_angle < 112.5: + return "ZH", waning + if not waning and phase_angle < 157.5: + return "ZG", waning + if 157.5 <= phase_angle < 202.5: + return "VM", waning + if waning and phase_angle < 247.5: + return "AG", waning + if waning and phase_angle < 292.5: + return "AH", waning + return "AS", waning + + +def action_planet_visibility_chart(args: list[str]) -> dict: + if len(args) != 5: + fail( + "Aktion planet_visibility_chart erwartet 5 Argumente: latitude longitude elevation date timezone", + extra={"argv": args}, + ) + + latitude = parse_float(args[0], "Latitude") + longitude = parse_float(args[1], "Longitude") + elevation = parse_float(args[2], "Elevation") + date_text = args[3] + timezone_name = args[4] + + try: + local_start = datetime.strptime(date_text, "%Y-%m-%d") + except ValueError: + fail("Datum muss im Format YYYY-MM-DD uebergeben werden.", extra={"date": date_text}) + + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: + fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)}) + + local_start = local_start.replace(tzinfo=tz) + local_noon = local_start + timedelta(hours=12) + local_end = local_start + timedelta(days=1) + utc_start = local_start.astimezone(timezone.utc) + utc_end = local_end.astimezone(timezone.utc) + observer = astronomy.Observer(latitude, longitude, elevation) + start_time = dt_to_time(utc_start) + + def search_altitude(direction: astronomy.Direction, altitude_deg: float) -> astronomy.Time | None: + limit_days = (utc_end - utc_start).total_seconds() / 86400.0 + result = astronomy.SearchAltitude(astronomy.Body.Sun, observer, direction, start_time, limit_days, altitude_deg) + if result is None: + return None + if time_to_datetime(result) >= utc_end: + return None + return result + + sun_rise = search_event(astronomy.Body.Sun, astronomy.Direction.Rise, observer, start_time, utc_end) + sun_set = search_event(astronomy.Body.Sun, astronomy.Direction.Set, observer, start_time, utc_end) + civil_dawn = search_altitude(astronomy.Direction.Rise, -6.0) + civil_dusk = search_altitude(astronomy.Direction.Set, -6.0) + nautical_dawn = search_altitude(astronomy.Direction.Rise, -12.0) + nautical_dusk = search_altitude(astronomy.Direction.Set, -12.0) + astro_dawn = search_altitude(astronomy.Direction.Rise, -18.0) + astro_dusk = search_altitude(astronomy.Direction.Set, -18.0) + + twilight = { + "sunRiseH": hour_from_utc_start(sun_rise, utc_start), + "sunSetH": hour_from_utc_start(sun_set, utc_start), + "civDawnH": hour_from_utc_start(civil_dawn, utc_start), + "civDuskH": hour_from_utc_start(civil_dusk, utc_start), + "nautDawnH": hour_from_utc_start(nautical_dawn, utc_start), + "nautDuskH": hour_from_utc_start(nautical_dusk, utc_start), + "astroDawnH": hour_from_utc_start(astro_dawn, utc_start), + "astroDuskH": hour_from_utc_start(astro_dusk, utc_start), + } + + moon_time = dt_to_time(local_noon.astimezone(timezone.utc)) + moon_phase = float(astronomy.MoonPhase(moon_time)) + moon_illumination = astronomy.Illumination(astronomy.Body.Moon, moon_time) + moon_label, moon_waning = moon_phase_code(moon_phase) + moon_segments, moon_rise_hour, moon_set_hour = build_visibility_segments( + astronomy.Body.Moon, + observer, + utc_start, + utc_end, + ) + moon_best_segments = compute_best_segments( + moon_segments, + twilight["astroDawnH"], + twilight["astroDuskH"], + ) + + rows: list[dict] = [] + for row_def in PLANET_VISIBILITY_ROWS: + midday_time = dt_to_time(local_noon.astimezone(timezone.utc)) + illumination = astronomy.Illumination(row_def["body"], midday_time) + segments, rise_hour, set_hour = build_visibility_segments( + row_def["body"], + observer, + utc_start, + utc_end, + ) + rows.append({ + "key": row_def["key"], + "type": row_def["type"], + "name": row_def["name"], + "symbol": row_def["symbol"], + "color": row_def["color"], + "glow": row_def["glow"], + "segs": segments, + "bestSegs": compute_best_segments( + segments, + twilight["astroDawnH"], + twilight["astroDuskH"], + ), + "actualRiseH": rise_hour, + "actualSetH": set_hour, + "magnitude": float(illumination.mag), + }) + + rows.append({ + "key": "moon", + "type": "moon", + "name": "MOND", + "symbol": moon_label, + "color": "#d0d8f0", + "glow": "rgba(208,216,240,.55)", + "segs": moon_segments, + "bestSegs": moon_best_segments, + "actualRiseH": moon_rise_hour, + "actualSetH": moon_set_hour, + "illum": float(moon_illumination.phase_fraction), + "waning": moon_waning, + "phase": moon_phase, + "label": moon_label, + }) + + utc_offset_hours = local_noon.utcoffset().total_seconds() / 3600.0 + timezone_abbr = local_noon.tzname() or timezone_name + + return { + "ok": True, + "action": "planet_visibility_chart", + "date": date_text, + "timezone": timezone_name, + "timezone_abbr": timezone_abbr, + "utc_offset_hours": utc_offset_hours, + "location": { + "latitude": latitude, + "longitude": longitude, + "elevation": elevation, + }, + "twilight": twilight, + "rows": rows, + "moon": { + "illum": float(moon_illumination.phase_fraction), + "waning": moon_waning, + "phase": moon_phase, + "label": moon_label, + }, + } + def action_planet_rise_set(args: list[str]) -> dict: if len(args) != 5: @@ -4259,6 +4614,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return + if action == "planet_visibility_chart": + result = action_planet_visibility_chart(args) + print(json.dumps(result, ensure_ascii=True)) + return + if action == "month_sky_context": result = action_month_sky_context(args) print(json.dumps(result, ensure_ascii=True)) @@ -4354,7 +4714,7 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_visibility_chart", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]}) if __name__ == "__main__": diff --git a/public/python_api.php b/public/python_api.php new file mode 100644 index 0000000..a031c16 --- /dev/null +++ b/public/python_api.php @@ -0,0 +1,77 @@ +&1'; + $output = []; + $resultCode = 0; + exec($command, $output, $resultCode); + + $joined = trim(implode("\n", $output)); + $decoded = json_decode($joined, true); + + if (is_array($decoded)) { + return [ + 'ok' => (bool) ($decoded['ok'] ?? false), + 'data' => $decoded, + 'raw' => $joined, + 'command' => $command, + ]; + } + + if ($joined !== '') { + $lastError = $joined; + } elseif ($resultCode !== 0) { + $lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary; + } + } + + return [ + 'ok' => false, + 'error' => $lastError, + ]; +} + +function runPythonApiWithJsonPayloadFile(string $action, array $payload): array +{ + $tempFile = tempnam(sys_get_temp_dir(), 'skyview_py_'); + if ($tempFile === false) { + return [ + 'ok' => false, + 'error' => 'Temporäre Datei für Python-Payload konnte nicht erstellt werden.', + ]; + } + + $jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($jsonPayload === false) { + @unlink($tempFile); + return [ + 'ok' => false, + 'error' => 'Python-Payload konnte nicht als JSON kodiert werden.', + ]; + } + + if (file_put_contents($tempFile, $jsonPayload) === false) { + @unlink($tempFile); + return [ + 'ok' => false, + 'error' => 'Temporäre Datei für Python-Payload konnte nicht geschrieben werden.', + ]; + } + + try { + return runPythonApi($action, ['@' . $tempFile]); + } finally { + @unlink($tempFile); + } +}