Files
skyview.astronomiemuseum.de/public/js/occult-worker.js
T
2026-04-05 14:42:17 +02:00

201 lines
7.2 KiB
JavaScript

'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: 'Dämmerung' };
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 });
};