- 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)) {
@@ -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 = `
-
- `;
- 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 = `
+
+ `;
+
+ 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);
+ }
+}