mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
86 lines
2.4 KiB
JavaScript
86 lines
2.4 KiB
JavaScript
// src/data/issPass.js
|
|
/**
|
|
* Next-ISS-pass prediction from a satrec: coarse 30 s scan to find a window
|
|
* where elevation ≥ minElevDeg, refined to ~5 s at the edges, tracking peak
|
|
* elevation. ~2880 SGP4 propagations for a 24 h horizon — tens of ms, fine for
|
|
* an on-demand voice call. Pattern from skylight (MIT) shared/src/celestial.ts
|
|
* nextISSPass, extended with set-time + peak tracking.
|
|
*/
|
|
import { propagate, gstime, eciToEcf, ecfToLookAngles } from 'satellite.js';
|
|
|
|
const R2D = 180 / Math.PI;
|
|
const D2R = Math.PI / 180;
|
|
|
|
/** Observer look angles at an instant, or null when propagation fails. */
|
|
export function lookAnglesAt(satrec, dateMs, latDeg, lonDeg) {
|
|
const date = new Date(dateMs);
|
|
const pv = propagate(satrec, date);
|
|
const pos = pv && pv.position;
|
|
if (!pos || typeof pos === 'boolean') return null;
|
|
const ecf = eciToEcf(pos, gstime(date));
|
|
const look = ecfToLookAngles(
|
|
{ latitude: latDeg * D2R, longitude: lonDeg * D2R, height: 0 },
|
|
ecf,
|
|
);
|
|
return {
|
|
elevDeg: look.elevation * R2D,
|
|
azDeg: (((look.azimuth * R2D) % 360) + 360) % 360,
|
|
};
|
|
}
|
|
|
|
export function findNextIssPass({
|
|
satrec,
|
|
latDeg,
|
|
lonDeg,
|
|
fromMs,
|
|
minElevDeg = 10,
|
|
horizonHours = 24,
|
|
coarseStepSec = 30,
|
|
fineStepSec = 5,
|
|
}) {
|
|
const elev = (t) => lookAnglesAt(satrec, t, latDeg, lonDeg)?.elevDeg ?? -90;
|
|
const horizonMs = fromMs + horizonHours * 3600_000;
|
|
const coarse = coarseStepSec * 1000;
|
|
const fine = fineStepSec * 1000;
|
|
|
|
// Coarse scan for the first sample above threshold. If we START inside a
|
|
// pass, that's still "the next pass" for a voice answer — accept it.
|
|
let hit = null;
|
|
for (let t = fromMs; t <= horizonMs; t += coarse) {
|
|
if (elev(t) >= minElevDeg) {
|
|
hit = t;
|
|
break;
|
|
}
|
|
}
|
|
if (hit == null) return null;
|
|
|
|
// Refine rise: walk back in fine steps to the first sample ≥ threshold.
|
|
let riseMs = hit;
|
|
while (riseMs - fine > fromMs && elev(riseMs - fine) >= minElevDeg)
|
|
riseMs -= fine;
|
|
|
|
// Walk forward through the pass tracking the peak until we drop below.
|
|
let maxElevDeg = -90;
|
|
let maxElevMs = riseMs;
|
|
let t = riseMs;
|
|
while (t <= horizonMs) {
|
|
const e = elev(t);
|
|
if (e < minElevDeg && t > riseMs) break;
|
|
if (e > maxElevDeg) {
|
|
maxElevDeg = e;
|
|
maxElevMs = t;
|
|
}
|
|
t += fine;
|
|
}
|
|
const setMs = t;
|
|
|
|
const rise = lookAnglesAt(satrec, riseMs, latDeg, lonDeg);
|
|
return {
|
|
riseMs,
|
|
setMs,
|
|
maxElevDeg,
|
|
maxElevMs,
|
|
riseAzDeg: rise ? rise.azDeg : 0,
|
|
};
|
|
}
|