// CelesTrak GP data. CelesTrak updates every 2 hours and blocks IPs that fetch more // often or keep requesting after errors, so poll every 2 hours and stop on any non-200. /** ISS, Tiangong (CSS Tianhe) and Hubble; all three are in the visual group. */ export const FEATURED = [25544, 48274, 20580]; /** Median mean motion above which a Starlink launch is still low enough to be seen as a train (about 400 km). */ const TRAIN_MEAN_MOTION = 15.45; export interface OMM { OBJECT_NAME: string; NORAD_CAT_ID: number; EPOCH: string; MEAN_MOTION: number; ECCENTRICITY: number; INCLINATION: number; RA_OF_ASC_NODE: number; ARG_OF_PERICENTER: number; MEAN_ANOMALY: number; BSTAR: number; } const KEYS: (keyof OMM)[] = ["OBJECT_NAME", "NORAD_CAT_ID", "EPOCH", "MEAN_MOTION", "ECCENTRICITY", "INCLINATION", "RA_OF_ASC_NODE", "ARG_OF_PERICENTER", "MEAN_ANOMALY", "BSTAR"]; export type Row = Record; export function pick(rows: Row[]): OMM[] { return rows.map((r) => Object.fromEntries(KEYS.map((k) => [k, r[k]])) as unknown as OMM); } /** Objects grouped by launch, keyed by international designator ("2026-232"). */ export function byDesignator(rows: Row[]): Map { const groups = new Map(); for (const r of rows) { const launch = String(r.OBJECT_ID).slice(0, 8); groups.set(launch, [...(groups.get(launch) ?? []), r]); } return groups; } const isStarlink = (r: Row) => String(r.OBJECT_NAME).startsWith("STARLINK"); /** The Starlink satellite with the median mean motion, named "Starlink train (