184 lines
7.8 KiB
JavaScript
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 dφ = (lat2 - lat1) * D2R;
|
|
const dλ = (lon2 - lon1) * D2R;
|
|
const a = Math.sin(dφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(dλ / 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,
|
|
};
|