Files
dc-observatio/services/pcp-tgu-ui-service/src/features/constellation/sim.js
T

184 lines
7.8 KiB
JavaScript

/* ============================================================
sim.js — geometry + schedule (ESM)
Чистая геометрия подспутниковой точки, тень, расписание событий.
См. INTEGRATION.md — subSat()/statusAt() можно заменить на реальную эфемериду/API.
============================================================ */
const D2R = Math.PI / 180, R2D = 180 / Math.PI;
const TWO_PI = Math.PI * 2;
const SIDEREAL_MIN = 1436.07; // earth rotation period (min)
function wrapLon(lon) {
lon = ((lon + 180) % 360 + 360) % 360 - 180;
return lon;
}
// sub-satellite point at sim time t(ms), given epoch ms.
function subSat(sat, tMs, epochMs) {
const m = (tMs - epochMs) / 60000; // minutes since epoch
const o = sat.orbit;
const M = o.phase * TWO_PI + TWO_PI * (m / o.period);
const lat = o.latAmp * Math.sin(M);
// one sine wave per orbit across full longitude, drifting west via earth rotation
const lon = wrapLon(o.lon0 + 360 * (m / o.period) - 360 * (m / SIDEREAL_MIN));
return { lon, lat };
}
// subsolar point (approx, declination ignored -> equatorial)
function subSolar(tMs) {
const secOfDay = (tMs / 1000) % 86400;
const lon = wrapLon(180 - (secOfDay / 86400) * 360);
// small seasonal declination wobble for life
const dayFrac = (tMs / 86400000) % 365.25;
const lat = 23.4 * Math.sin((dayFrac / 365.25) * TWO_PI - 1.4);
return { lon, lat };
}
// angular (great-circle) distance in degrees
function angDist(lon1, lat1, lon2, lat2) {
const φ1 = lat1 * D2R, φ2 = lat2 * D2R;
const = (lat2 - lat1) * D2R;
const = (lon2 - lon1) * D2R;
const a = Math.sin( / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin( / 2) ** 2;
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * R2D;
}
function inShadow(lon, lat, tMs) {
const ss = subSolar(tMs);
return angDist(lon, lat, ss.lon, ss.lat) > 90;
}
// point in polygon (ray casting), poly = [[lon,lat],...]
function pointInPoly(lon, lat, poly) {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const xi = poly[i][0], yi = poly[i][1];
const xj = poly[j][0], yj = poly[j][1];
const intersect = (yi > lat) !== (yj > lat) &&
lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
const STATION_RANGE = 22; // deg ground range for downlink visibility
// which ROI is the sat over (matching payload), null if none
function overROI(sat, p, rois) {
for (const r of rois) {
if (r.payload !== sat.payload) continue;
if (pointInPoly(p.lon, p.lat, r.poly)) return r;
}
return null;
}
// best (nearest in-range) station, null if none
function nearStation(p, stations) {
let best = null, bd = STATION_RANGE;
for (const st of stations) {
const d = angDist(p.lon, p.lat, st.lon, st.lat);
if (d < bd) { bd = d; best = st; }
}
return best;
}
// --- prohibition windows (интервалы запрета) per sat ----------
function buildProhibits(sats, epochMs) {
const H = 3600e3;
const out = {};
sats.forEach((s, i) => {
const list = [];
// deterministic-ish spread
const a0 = (i * 1.9) % 22 - 4; // hours from epoch
list.push({ start: epochMs + a0 * H, end: epochMs + (a0 + 0.7 + (i % 3) * 0.15) * H, reason: "ВНЗ" });
if (i % 2 === 0) {
const a1 = a0 + 8 + (i % 4);
list.push({ start: epochMs + a1 * H, end: epochMs + (a1 + 0.9) * H, reason: "ОГРАН. ПИТАНИЕ" });
}
out[s.id] = list;
});
return out;
}
function inProhibit(list, tMs) {
if (!list) return null;
for (const w of list) if (tMs >= w.start && tMs < w.end) return w;
return null;
}
// sub-satellite point: реальный сэмплер из env.subSat (эфемерида/flight-line),
// иначе встроенная синусоидальная симуляция. См. INTEGRATION.md §5.2.
function subSatOf(sat, tMs, env) {
return env.subSat ? env.subSat(sat, tMs) : subSat(sat, tMs, env.epochMs);
}
// interval containing t in a sorted/unsorted list, else null
function intervalAt(list, tMs) {
if (!list) return null;
for (const iv of list) if (tMs >= iv.start && tMs < iv.end) return iv;
return null;
}
// live status of a sat at time t (priority order)
// returns {state, roi, station, eclipse, prohibit}
// С реальным расписанием (env.sched) состояние берём из готовых интервалов плана
// (съёмка/сброс/запрет); иначе — встроенная геометрическая симуляция прототипа.
function statusAt(sat, tMs, env) {
const p = subSatOf(sat, tMs, env);
const eclipse = inShadow(p.lon, p.lat, tMs);
if (env.sched) {
const tr = env.sched[sat.id] || {};
const prohibit = intervalAt(tr.prohibit, tMs);
if (prohibit) return { state: "prohibit", p, prohibit: { reason: prohibit.label }, eclipse };
const cap = intervalAt(tr.capture, tMs);
if (cap) return { state: "capture", p, roi: { name: cap.label, poly: cap.poly }, eclipse };
const dl = intervalAt(tr.downlink, tMs);
if (dl) return { state: "downlink", p, station: { name: dl.label, lon: dl.lon, lat: dl.lat }, eclipse };
return { state: "operational", p, eclipse };
}
const prohibit = inProhibit(env.prohibits[sat.id], tMs);
if (prohibit) return { state: "prohibit", p, prohibit, eclipse };
const roi = overROI(sat, p, env.rois);
if (roi) return { state: "capture", p, roi, eclipse };
const st = nearStation(p, env.stations);
if (st) return { state: "downlink", p, station: st, eclipse };
return { state: "operational", p, eclipse };
}
// --- precompute event intervals over a domain for the timeline -
function buildSchedule(sats, env, domainStart, domainEnd, stepMin) {
const step = stepMin * 60000;
const sched = {};
for (const s of sats) {
const tracks = { capture: [], downlink: [], prohibit: [], shadow: [] };
// prohibits are explicit
(env.prohibits[s.id] || []).forEach((w) =>
tracks.prohibit.push({ start: w.start, end: w.end, label: w.reason }));
let cur = { capture: null, downlink: null, shadow: null };
for (let t = domainStart; t <= domainEnd; t += step) {
const p = subSatOf(s, t, env);
const proh = inProhibit(env.prohibits[s.id], t);
const shadow = inShadow(p.lon, p.lat, t);
const roi = !proh ? overROI(s, p, env.rois) : null;
const st = !proh && !roi ? nearStation(p, env.stations) : null;
// shadow band
if (shadow) { if (!cur.shadow) cur.shadow = { start: t, end: t }; else cur.shadow.end = t; }
else if (cur.shadow) { tracks.shadow.push(cur.shadow); cur.shadow = null; }
// capture band
if (roi) { if (!cur.capture) cur.capture = { start: t, end: t, label: roi.name }; else cur.capture.end = t; }
else if (cur.capture) { tracks.capture.push(cur.capture); cur.capture = null; }
// downlink band
if (st) { if (!cur.downlink) cur.downlink = { start: t, end: t, label: st.name }; else cur.downlink.end = t; }
else if (cur.downlink) { tracks.downlink.push(cur.downlink); cur.downlink = null; }
}
if (cur.shadow) tracks.shadow.push(cur.shadow);
if (cur.capture) tracks.capture.push(cur.capture);
if (cur.downlink) tracks.downlink.push(cur.downlink);
sched[s.id] = tracks;
}
return sched;
}
export {
D2R, R2D, wrapLon, subSat, subSolar, angDist, inShadow,
pointInPoly, overROI, nearStation, buildProhibits, inProhibit,
statusAt, intervalAt, buildSchedule, STATION_RANGE,
};