diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 2fe237109..153a7396a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4449,7 +4449,7 @@ its criteria cannot be silently ignored. | Mapped Installations ⌖ | OpenFreeMap military areas; optional configured Overpass names and Google Places search | `src/data/militaryInstallations.js` | OpenFreeMap tiles, `/api/military-installations`, `/api/google/text-search` | viewport-driven; transient errors back off, missing capability switches to tiles | | Earthquakes | USGS | `src/data/earthquakes.js` | — | 60s | | Air quality | ECCC MSC GeoMet OGC API (AQHI, Canada only) | `src/layers/airQuality/` | — | 20 min readings, 12 h station catalog | -| Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s | +| Satellites | CelesTrak TLE + SATCAT; bundled UCS pack (purpose) | `src/data/satellites.js` | `/api/celestrak` + `/api/celestrak-satcat` | 120s | | Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min | | Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | `src/data/traffic.js` | browser-direct OpenFreeMap tiles; `/api/tomtom` for flow | viewport-driven; capped tile cache | | CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Iceland (Vegagerðin / IRCA) + Hong Kong Transport Department + FL511 (FL, opt-in) + Freeway Bureau, MOTC (Taiwan) + Houston TranStar (opt-in) + Autostrada del Brennero (IT, A22 stills) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | @@ -5458,7 +5458,7 @@ silently demoting every later lookup for the session. - **Satellite classes** (`src/layers/satellites/classes.js`): every satellite is filed under one of six classes, each with its own point colour, legend swatch and toggle: STATIONS (warm white), IMAGING (cyan), TELECOM (violet; communications and navigation, so GPS, GLONASS, Galileo and BeiDou read as one family), STARLINK (dim slate), PROBES (muted blue-gray) and DEBRIS (receding brown-gray). That module is the single source of truth for class, label and colour, so the dot, the card and the legend swatch cannot disagree. Two deliberate palette rules: no class may sit in the 40–48° amber band, which is the app-wide known-military convention; and STARLINK stays far below PROBES in Rec.601 luminance so the dense shell stays separable when NVG/FLIR collapse the scene to one channel. The ISS keeps its long-standing red hero dot rather than the STATIONS colour — it carries a permanent name label and its card still reads `STATIONS · ISS`; it is a station whichever group ingested it, so a stations-feed outage cannot refile it. `src/data/satelliteClass.js` keeps the group-based labels beneath the classes: the constellation subtype and a group's own label for a satellite with no class. - Class is also a **text field**, not just a color: `TELECOM · GPS` / `STARLINK` / `STATIONS · ISS` leads the tracked card's detail block and replaces the raw CelesTrak tag on the detection-overlay label. Because that canvas composites above the post-FX chain, the class stays readable in NVG/FLIR after the in-scene dot colors are flattened. - The satellites row in DATA LAYERS carries per-layer sub-controls (`DataLayerManager._syncRowControls`, the first consumer of the optional `getRowControls()` layer hook): one chip per class (the STARLINK chip exposes the existing `catalog` param), a swatch legend with live per-class counts, and an info line while SATCAT is stale or unavailable (`Classes may lag: SATCAT is a cached copy` / `Classes may lag: SATCAT unavailable`). Default is the sparse core catalog. The chips are stateless — each declares the params to apply and the manager owns the write — so the Space Missions capture/restore path over the same param stays authoritative. Controls stay hidden while the layer is off. An explicit CORE or DENSE choice participates in versioned local and share-link state; temporary Space Missions overrides do not. -- **Class toggles** (`src/layers/satellites/classes.js`): the layer row carries one chip per class — STATIONS, IMAGING, TELECOM (communications and navigation), PROBES (science, technology development and demonstration, unknown purpose) and DEBRIS (rocket bodies, fragments, unknown objects and dead payloads) on by default, so the layer shows what it did before classes; STARLINK off, as DENSE was. Points of a hidden class stay built and are only hidden, so a toggle needs no reload; hidden classes are left out of the detection overlay, the voice/framing position lists and analyst answers (the tracked satellite still answers), and the legend keys only the classes on screen. The class comes, strongest evidence first, from CelesTrak SATCAT (object type and operational status, fetched per group through `/api/celestrak-satcat/`, cached a day), the bundled UCS Satellite Database pack (UCS-Satellite-Database 5-1-2023: each satellite's purpose; `src/data/local_data/ucs_satellites/`), name patterns for satellites launched after May 2023, and finally the CelesTrak group. Only crewed stations and the craft that visit them, by name, are STATIONS: the CubeSats CelesTrak's stations group also carries after their deployment from the ISS are classed like any other satellite. A payload whose SATCAT record gives no status, with no UCS record and no recognisable name, that launched 20 or more years ago (SATCAT's launch date, else the TLE designator's year) is presumed dead and filed as debris; without a SATCAT record (a failed read) nothing is presumed. To cover imaging launched after the UCS snapshot the core catalog also reads CelesTrak's `resource`, `weather`, `planet`, `sar` and `science` groups. Retired stations and the shuttles have re-entered and have no orbit to show. An explicit class choice is kept in local and share-link state as the `classes` option, one letter per class shown (`sitpd`: stations, imaging, telecom, probes, debris; `0`: none; `src/data/satelliteClassMask.js`), and the layer takes those letters back through `setParams({classes})`; STARLINK maps to the existing `catalog: 'dense'` param instead. +- **Class toggles** (`src/layers/satellites/classes.js`): the layer row carries one chip per class — STATIONS, IMAGING, TELECOM (communications and navigation), PROBES (science, technology development and demonstration, unknown purpose) and DEBRIS (rocket bodies, fragments, unknown objects two or more years after launch and dead payloads) on by default, so the layer shows what it did before classes; STARLINK off, as DENSE was. Points of a hidden class stay built and are only hidden, so a toggle needs no reload; hidden classes are left out of the detection overlay, the voice/framing position lists and analyst answers (the tracked satellite still answers), and the legend keys only the classes on screen. The class comes, strongest evidence first, from CelesTrak SATCAT (object type and operational status, fetched per group through `/api/celestrak-satcat/`, cached a day by the proxy; the layer keeps a current read six hours rather than asking on every five-minute update), the bundled UCS Satellite Database pack (UCS-Satellite-Database 5-1-2023: each satellite's purpose; `src/data/local_data/ucs_satellites/`), name patterns for satellites launched after May 2023, and finally the CelesTrak group. Only crewed stations and the craft that visit them, by name, are STATIONS: the CubeSats CelesTrak's stations group also carries after their deployment from the ISS are classed like any other satellite. A payload whose SATCAT record gives no status, with no UCS record and no recognisable name, that launched 20 or more years ago (SATCAT's launch date, else the TLE designator's year) is presumed dead and filed as debris; without a SATCAT record (a failed read) nothing is presumed. To cover imaging launched after the UCS snapshot the core catalog also reads CelesTrak's `resource`, `weather`, `planet`, `sar` and `science` groups. Retired stations and the shuttles have re-entered and have no orbit to show. An explicit class choice is kept in local and share-link state as the `classes` option, one letter per class shown (`sitpd`: stations, imaging, telecom, probes, debris; `0`: none; `src/data/satelliteClassMask.js`), and the layer takes those letters back through `setParams({classes})`; STARLINK maps to the existing `catalog: 'dense'` param instead. - **The STARLINK chip (formerly DENSE) reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `STARLINK ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `STARLINK ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale. - **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert. - The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe. diff --git a/src/layers/satellites/classes.js b/src/layers/satellites/classes.js index 3d1337328..0315d4ebb 100644 --- a/src/layers/satellites/classes.js +++ b/src/layers/satellites/classes.js @@ -93,7 +93,12 @@ export const SATELLITE_CLASS_DISPLAY = Object.freeze({ const ISS_NORAD = 25544; /** SATCAT object types that are never a working payload. */ -const DEBRIS_OBJECT_TYPES = new Set(['R/B', 'DEB', 'UNK']); +const DEBRIS_OBJECT_TYPES = new Set(['R/B', 'DEB']); +/** + * SATCAT files a new launch's objects as UNK until it identifies them, so an + * unknown object launched this recently (years) is not presumed debris. + */ +export const UNIDENTIFIED_GRACE_YEARS = 2; /** SATCAT operational status: nonoperational, decayed. */ const DEAD_STATUSES = new Set(['-', 'D']); /** SATCAT operational status left blank or unknown. */ @@ -116,7 +121,7 @@ const TELECOM_NAME = /^(ONEWEB|IRIDIUM|GLOBALSTAR|ORBCOMM|INTELSAT|IS-\d|SES[ -]?\d|ASTRA|EUTELSAT|INMARSAT|VIASAT|ECHOSTAR|DIRECTV|SIRIUS|XM-|TDRS|NAVSTAR|GPS\b|GLONASS|GSAT0|GALILEO|BEIDOU|QZS|IRNSS|NVS-|KUIPER|LYNK|BLUEBIRD|O3B|TELESAT|ANIK|NIMIQ|AMAZONAS|HISPASAT|ARABSAT|NILESAT|TURKSAT|YAMAL|EXPRESS-|CHINASAT|ZHONGXING|APSTAR|ASIASAT|THAICOM|MEASAT|JCSAT|SUPERBIRD|OPTUS|SKYNET|WGS|MUOS|AEHF|MILSTAR|QIANFAN|GUOWANG|SPACEMOBILE|GONETS|MERIDIAN|LUCH|RADUGA|SKYTERRA|TERRESTAR|SPACEWAY|HYLAS|ARSAT|AMOS|BADR|KA-SAT|YAHSAT|THURAYA)/i; const IMAGING_PURPOSE = - /earth observation|earth science|meteorolog|surveillance/i; + /earth observation|earth\/space observation|earth science|meteorolog|surveillance/i; const TELECOM_PURPOSE = /communications|navigation|positioning/i; /** @@ -173,7 +178,11 @@ export function classifySatellite({ const label = String(name || '').trim(); const objectType = satcat?.objectType || null; const status = satcat?.opsStatus ?? null; + const launched = launchYearOf(satcat?.launchDate) ?? launchYear; + const recent = + Number.isFinite(launched) && nowYear - launched < UNIDENTIFIED_GRACE_YEARS; if (objectType && DEBRIS_OBJECT_TYPES.has(objectType)) return 'debris'; + if (objectType === 'UNK' && !recent) return 'debris'; if (!objectType && DEBRIS_NAME.test(label)) return 'debris'; if (status !== null && DEAD_STATUSES.has(status)) return 'debris'; @@ -195,7 +204,6 @@ export function classifySatellite({ // record gives no status and whose launch is decades old, is presumed // dead. Without a SATCAT record (its read failed or has not landed) there // is no evidence either way, so nothing is presumed. - const launched = launchYearOf(satcat?.launchDate) ?? launchYear; if ( satcat && UNKNOWN_STATUSES.has(status ?? '') && diff --git a/src/layers/satellites/classes.test.mjs b/src/layers/satellites/classes.test.mjs index aa134fa2d..8e148a32d 100644 --- a/src/layers/satellites/classes.test.mjs +++ b/src/layers/satellites/classes.test.mjs @@ -60,6 +60,24 @@ test('SATCAT object type and status file debris first, overruling UCS', () => { }), 'imaging', ); + // SATCAT calls a new launch's objects UNK until it names them: only an + // old unknown object, or one with no launch date, is debris. + assert.equal( + classify({ + name: 'OBJECT C', + group: 'resource', + satcat: { objectType: 'UNK', opsStatus: '', launchDate: '2025-11-02' }, + }), + 'imaging', + ); + assert.equal( + classify({ + name: 'OBJECT C', + group: 'resource', + satcat: { objectType: 'UNK', opsStatus: '', launchDate: '2019-04-01' }, + }), + 'debris', + ); // Debris riding in the stations group (CelesTrak lists FREGAT DEB there). assert.equal(classify({ name: 'FREGAT DEB', group: 'stations' }), 'debris'); assert.equal(classify({ name: 'SL-16 R/B', group: 'visual' }), 'debris'); @@ -104,6 +122,7 @@ test('UCS purpose decides imaging, telecom and probes', () => { ['Meteorological', 'imaging'], ['Surveillance', 'imaging'], ['Earth Observation/Navigation', 'imaging'], + ['Earth/Space Observation', 'imaging'], ['Communications', 'telecom'], ['Navigation/Global Positioning', 'telecom'], ['Navigation/Regional Positioning', 'telecom'],