201 lines
7.2 KiB
JavaScript
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 });
|
|
};
|