/* ============================================================ 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, };