'use strict'; // Astronomy Engine wird per importScripts geladen importScripts('astronomy.browser.min.js'); const STAR_BODIES = ['Star1', 'Star2', 'Star3', 'Star4', 'Star5', 'Star6', 'Star7', 'Star8']; const COARSE_STEP_MS = 2 * 60 * 1000; const CANDIDATE_MARGIN_DEG = 0.32; const MOON_RADIUS_KM = 1737.4; let OBSERVER = null; function moonAngularRadiusDeg(distanceAu) { if (!Number.isFinite(distanceAu) || distanceAu <= 0) return 0; const ratio = MOON_RADIUS_KM / (distanceAu * Astronomy.KM_PER_AU); return Math.asin(Math.max(-1, Math.min(1, ratio))) * Astronomy.RAD2DEG; } function evaluateGeometry(bodyName, timeInput) { const time = timeInput instanceof Date ? timeInput : (timeInput && timeInput.date instanceof Date ? timeInput.date : new Date(timeInput)); const moonEq = Astronomy.Equator('Moon', time, OBSERVER, true, true); const starEq = Astronomy.Equator(bodyName, time, OBSERVER, true, true); const sunEq = Astronomy.Equator('Sun', time, OBSERVER, true, true); const moonHor = Astronomy.Horizon(time, OBSERVER, moonEq.ra, moonEq.dec, 'normal'); const starHor = Astronomy.Horizon(time, OBSERVER, starEq.ra, starEq.dec, 'normal'); const sunHor = Astronomy.Horizon(time, OBSERVER, sunEq.ra, sunEq.dec, 'normal'); const sepDeg = Astronomy.AngleBetween(moonEq.vec, starEq.vec); const radiusDeg = moonAngularRadiusDeg(Number(moonEq.dist || 0)); return { timeMs: time.getTime(), sepDeg, radiusDeg, marginDeg: sepDeg - radiusDeg, moonAltDeg: Number(moonHor.altitude || 0), starAltDeg: Number(starHor.altitude || 0), sunAltDeg: Number(sunHor.altitude || 0), }; } function makeMarginEvaluator(bodyName) { return function (astroTime) { return evaluateGeometry(bodyName, astroTime).marginDeg; }; } function findRoot(bodyName, leftMs, rightMs) { const evaluator = makeMarginEvaluator(bodyName); try { const root = Astronomy.Search( evaluator, new Astronomy.AstroTime(new Date(leftMs)), new Astronomy.AstroTime(new Date(rightMs)), { dt_tolerance_seconds: 0.05, iter_limit: 60 } ); if (root && root.date instanceof Date) return root.date.getTime(); } catch (e) { // fallback: bisection } let left = leftMs; let right = rightMs; let leftVal = evaluateGeometry(bodyName, left).marginDeg; for (let i = 0; i < 48; i += 1) { const mid = (left + right) / 2; const midVal = evaluateGeometry(bodyName, mid).marginDeg; if (Math.sign(leftVal) === Math.sign(midVal)) { left = mid; leftVal = midVal; } else { right = mid; } } return Math.round((left + right) / 2); } function findMinimum(bodyName, startMs, endMs) { let left = startMs; let right = endMs; for (let i = 0; i < 60; i += 1) { const span = (right - left) / 3; const m1 = left + span; const m2 = right - span; if (evaluateGeometry(bodyName, m1).marginDeg <= evaluateGeometry(bodyName, m2).marginDeg) { right = m2; } else { left = m1; } } return Math.round((left + right) / 2); } function classifyVisibility(sunAltDeg) { if (sunAltDeg > -5) return { key: 'day', label: 'Tag' }; if (sunAltDeg >= -10) return { key: 'twilight', label: 'Daemmerung' }; return { key: 'night', label: 'Nacht' }; } function chunkStars(stars, size) { const chunks = []; for (let i = 0; i < stars.length; i += size) chunks.push(stars.slice(i, i + size)); return chunks; } function defineChunk(chunk) { STAR_BODIES.forEach((body, index) => { const star = chunk[index]; if (star) { Astronomy.DefineStar(body, Number(star.ra), Number(star.dec), Number(star.distLy || 1000)); } else { Astronomy.DefineStar(body, 0, 0, 1000); } }); } function evaluateStar(bodyName, star, dayStartMs, dayEndMs) { let minMarginDeg = Infinity; let minTimeMs = dayStartMs; let everNegative = false; let nearHit = false; let previousMargin = null; let previousTimeMs = null; let startMarginDeg = null; let endMarginDeg = null; let ingressBracket = null; let egressBracket = null; for (let timeMs = dayStartMs; timeMs <= dayEndMs; timeMs += COARSE_STEP_MS) { const geometry = evaluateGeometry(bodyName, timeMs); if (geometry.marginDeg < minMarginDeg) { minMarginDeg = geometry.marginDeg; minTimeMs = timeMs; } if (geometry.marginDeg <= 0) everNegative = true; if (geometry.marginDeg <= CANDIDATE_MARGIN_DEG) nearHit = true; if (startMarginDeg === null) startMarginDeg = geometry.marginDeg; endMarginDeg = geometry.marginDeg; if (previousMargin !== null && previousTimeMs !== null && Math.sign(previousMargin) !== Math.sign(geometry.marginDeg)) { nearHit = true; if (previousMargin > 0 && geometry.marginDeg <= 0 && ingressBracket === null) ingressBracket = [previousTimeMs, timeMs]; if (previousMargin <= 0 && geometry.marginDeg > 0) egressBracket = [previousTimeMs, timeMs]; } previousMargin = geometry.marginDeg; previousTimeMs = timeMs; } if (!everNegative && !nearHit) return null; if (!everNegative || minMarginDeg > 0) return null; const ingressMs = ingressBracket ? findRoot(bodyName, ingressBracket[0], ingressBracket[1]) : null; const egressMs = egressBracket ? findRoot(bodyName, egressBracket[0], egressBracket[1]) : null; let minSearchStart = Math.max(dayStartMs, minTimeMs - COARSE_STEP_MS); let minSearchEnd = Math.min(dayEndMs, minTimeMs + COARSE_STEP_MS); if (ingressMs !== null && egressMs !== null) { minSearchStart = ingressMs; minSearchEnd = egressMs; } const maxMs = findMinimum(bodyName, minSearchStart, minSearchEnd); const maxGeom = evaluateGeometry(bodyName, maxMs); if (maxGeom.marginDeg > 0 || maxGeom.moonAltDeg <= 0 || maxGeom.starAltDeg <= 0) return null; return { star, ingressMs, maxMs, egressMs, partialStart: ingressMs === null && startMarginDeg !== null && startMarginDeg <= 0, partialEnd: egressMs === null && endMarginDeg !== null && endMarginDeg <= 0, durationMs: ingressMs !== null && egressMs !== null ? egressMs - ingressMs : null, limbDistanceArcmin: Math.abs(maxGeom.marginDeg) * 60, moonAltDeg: maxGeom.moonAltDeg, starAltDeg: maxGeom.starAltDeg, sunAltDeg: maxGeom.sunAltDeg, visibility: classifyVisibility(maxGeom.sunAltDeg), }; } // Nachricht vom Hauptthread: { location, stars, dayStartMs, dayEndMs, dayString, dayIndex, totalDays } self.onmessage = function (e) { const { location, stars, dayStartMs, dayEndMs, dayString, dayIndex, totalDays } = e.data; OBSERVER = new Astronomy.Observer( Number(location.latitude), Number(location.longitude), Number(location.elevation || 0) ); const chunks = chunkStars(stars, STAR_BODIES.length); const results = []; for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) { const chunk = chunks[chunkIndex]; defineChunk(chunk); chunk.forEach((star, index) => { const event = evaluateStar(STAR_BODIES[index], star, dayStartMs, dayEndMs); if (event) { event._dateString = dayString; results.push(event); } }); // Fortschritt melden: chunksDone / totalChunks = Anteil dieses Tages fertig self.postMessage({ type: 'progress', dayIndex, chunksDone: chunkIndex + 1, totalChunks: chunks.length }); } self.postMessage({ type: 'done', dayIndex, dayString, results }); };