/** * Overpass calculator — finds the next N closest approaches between * a vessel and a target (vessel, body, or ground station). * * Approach: sample the distance at fixed steps, then refine each * minimum with ternary search. */ import type { CelestialBody, Vessel, GroundStation } from '@kerbal-rt/shared-types'; import { bodyPositionAt, vesselPositionAt } from '../scene/layout.js'; export type TargetKind = 'vessel' | 'body' | 'station'; export interface Target { kind: TargetKind; id: string; name: string; } export interface OverpassWindow { /** UT at which the closest approach occurs. */ utPeak: number; /** Distance at closest approach (meters). */ minDistance: number; /** The target. */ target: Target; } export interface OverpassOptions { observer: Vessel; target: Target; bodies: CelestialBody[]; vessels: Vessel[]; groundStations: GroundStation[]; startUt: number; count?: number; maxSearchTime?: number; stepSec?: number; /** Distance threshold for "interesting" passes (default 1000 km). */ distanceThreshold?: number; } /** * Find the next `count` closest-approach events. * * Coarse scan: at each step, compute distance. Track the local minima * (a point lower than both neighbors). For each local min, refine * with ternary search to find the true peak (min distance). * * Returns at most `count` events with minDistance < distanceThreshold. */ export function findOverpasses(options: OverpassOptions): OverpassWindow[] { const { observer, target, bodies, vessels, groundStations, startUt, count = 5, maxSearchTime = 426 * 6 * 3600, // 1 KSP year stepSec = 120, // 2 minutes distanceThreshold = 1_000_000, // 1000 km } = options; const dist = (t: number) => distanceAt(bodies, vessels, groundStations, observer, target, t); const passes: OverpassWindow[] = []; const end = startUt + maxSearchTime; let prev = dist(startUt); let curr = prev; let t = startUt + stepSec; while (t < end && passes.length < count) { const next = dist(t); // Local minimum: curr < prev AND curr < next if (curr < prev && curr < next && curr < distanceThreshold) { // Refine const utPeak = refineMinimum((a: number) => dist(a), t - stepSec, t + stepSec); const minDistance = dist(utPeak); passes.push({ utPeak, minDistance, target }); // Skip ahead past the peak so we don't double-count t = utPeak + stepSec * 5; prev = dist(t); curr = prev; t += stepSec; continue; } prev = curr; curr = next; t += stepSec; } return passes; } function distanceAt( bodies: CelestialBody[], vessels: Vessel[], groundStations: GroundStation[], observer: Vessel, target: Target, ut: number, ): number { const obs = vesselPositionAt(bodies, observer, ut); let tgt: { x: number; y: number; z: number }; if (target.kind === 'vessel') { const v = vessels.find((x) => x.id === target.id); if (!v) return Number.POSITIVE_INFINITY; tgt = vesselPositionAt(bodies, v, ut); } else if (target.kind === 'body') { tgt = bodyPositionAt(bodies, target.id, ut); } else { // station const s = groundStations.find((x) => x.id === target.id); if (!s) return Number.POSITIVE_INFINITY; const body = bodies.find((b) => b.id === s.bodyId); if (!body) return Number.POSITIVE_INFINITY; const bodyPos = bodyPositionAt(bodies, body.id, ut); // Ground station sits on the body surface. Convert (lat, lon, alt) // to a heliocentric offset relative to the body's center. // For simplicity (no rotation) we just add a small offset along // the body's orbital direction. const R = body.radius + s.alt; const lat = (s.lat * Math.PI) / 180; const lon = (s.lon * Math.PI) / 180; tgt = { x: bodyPos.x + R * Math.cos(lat) * Math.cos(lon), y: bodyPos.y + R * Math.cos(lat) * Math.sin(lon), z: bodyPos.z + R * Math.sin(lat), }; } const dx = obs.x - tgt.x; const dy = obs.y - tgt.y; const dz = obs.z - tgt.z; return Math.hypot(dx, dy, dz); } /** Ternary search to find the minimum of dist in [tLo, tHi]. */ function refineMinimum(dist: (t: number) => number, tLo: number, tHi: number): number { let lo = tLo; let hi = tHi; for (let i = 0; i < 40; i++) { const m1 = lo + (hi - lo) / 3; const m2 = hi - (hi - lo) / 3; const f1 = dist(m1); const f2 = dist(m2); if (f1 > f2) lo = m1; else hi = m2; } return (lo + hi) / 2; }