mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 04:44:01 +00:00
fix(satellites): the ISS ring follows its class through re-enable and rebuilds and stays while tracked; a mission group outranks geo; an Unknown UCS purpose is no evidence; the UNK grace is stated in calendar years
This commit is contained in:
1 parent
83dd1af22e
commit
eaf2b8f5b4
8 files changed
+142
-19
No files matched your search
@@ -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 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/<group>`, 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.
|
||||
- **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 launched before last year 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/<group>`, 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 groups the TLE came in (a mission group such as `weather` before the orbit group `geo`; a UCS purpose of "Unknown" counts as none). 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.
|
||||
|
||||
@@ -96,7 +96,9 @@ const ISS_NORAD = 25544;
|
||||
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.
|
||||
* unknown object launched within this many calendar years (2: this year or
|
||||
* last) is not presumed debris. Years, not dates: the TLE designator, the
|
||||
* fallback when SATCAT has no launch date, carries only the year.
|
||||
*/
|
||||
export const UNIDENTIFIED_GRACE_YEARS = 2;
|
||||
/** SATCAT operational status: nonoperational, decayed. */
|
||||
@@ -143,6 +145,9 @@ const GROUP_CLASS = Object.freeze({
|
||||
science: 'probes',
|
||||
});
|
||||
|
||||
/** CelesTrak groups named for an orbit, not a mission: the last resort. */
|
||||
const ORBIT_GROUPS = new Set(['geo']);
|
||||
|
||||
/** The year of a SATCAT launch date (`2024-03-15`), or null. */
|
||||
function launchYearOf(launchDate) {
|
||||
const match = /^(\d{4})-/.exec(String(launchDate || ''));
|
||||
@@ -192,7 +197,10 @@ export function classifySatellite({
|
||||
if (STATION_NAME.test(label)) return 'stations';
|
||||
if (group === 'dense' || STARLINK_NAME.test(label)) return 'starlink';
|
||||
|
||||
const purpose = ucs?.purpose || '';
|
||||
// UCS writes "Unknown" for a purpose it does not know: no evidence.
|
||||
const purpose = /^unknown$/i.test(ucs?.purpose || '')
|
||||
? ''
|
||||
: ucs?.purpose || '';
|
||||
if (IMAGING_PURPOSE.test(purpose)) return 'imaging';
|
||||
if (TELECOM_PURPOSE.test(purpose)) return 'telecom';
|
||||
if (purpose) return 'probes';
|
||||
@@ -212,8 +220,13 @@ export function classifySatellite({
|
||||
)
|
||||
return 'debris';
|
||||
|
||||
// A mission group (weather, resource, science…) says more than an orbit
|
||||
// group: a weather satellite in geostationary orbit is in both.
|
||||
const memberships = groups?.length ? groups : [group];
|
||||
for (const membership of memberships)
|
||||
for (const membership of [
|
||||
...memberships.filter((m) => !ORBIT_GROUPS.has(m)),
|
||||
...memberships.filter((m) => ORBIT_GROUPS.has(m)),
|
||||
])
|
||||
if (GROUP_CLASS[membership]) return GROUP_CLASS[membership];
|
||||
return 'probes';
|
||||
}
|
||||
|
||||
@@ -385,3 +385,101 @@ test('every ingested group names the class its unnamed members fall back to', ()
|
||||
expected,
|
||||
);
|
||||
});
|
||||
|
||||
test('enabling the layer leaves a hidden Stations class without its ISS ring', async () => {
|
||||
const { createLifecycle } = await import('./lifecycle.js');
|
||||
const iss = { primitive: { show: false } };
|
||||
const other = { primitive: { show: false } };
|
||||
let issVisible = false;
|
||||
const state = {
|
||||
_params: { showPoints: true, showOrbits: true },
|
||||
_orbitPaths: new Map([
|
||||
[25544, iss],
|
||||
[90001, other],
|
||||
]),
|
||||
_points: new Map(),
|
||||
};
|
||||
const { methods } = createLifecycle({
|
||||
state,
|
||||
services: {
|
||||
focus: { clearFocusTarget() {} },
|
||||
render: { holdContinuousRender() {}, releaseContinuousRender() {} },
|
||||
picking: { registerPickOwner() {}, unregisterPickOwner() {} },
|
||||
},
|
||||
parts: {
|
||||
controls: {
|
||||
satelliteVisualsVisible: (enabled, on) => enabled && on !== false,
|
||||
_issOrbitVisible: () => issVisible,
|
||||
},
|
||||
labels: { _syncIssOverlay() {} },
|
||||
interaction: { _installClickHandler() {} },
|
||||
tracking: { _applyPendingTrackingRestore() {} },
|
||||
rendering: {},
|
||||
},
|
||||
});
|
||||
methods.enable(null);
|
||||
assert.equal(iss.primitive.show, false, 'the ISS ring follows its class');
|
||||
assert.equal(other.primitive.show, true, 'other rings follow showOrbits');
|
||||
issVisible = true;
|
||||
methods.enable(null);
|
||||
assert.equal(iss.primitive.show, true);
|
||||
});
|
||||
|
||||
test('a mission group outranks an orbit group, and an Unknown UCS purpose is no evidence', () => {
|
||||
assert.equal(
|
||||
classify({ name: 'MTG-X1', group: 'geo', groups: ['geo', 'weather'] }),
|
||||
'imaging',
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'geo', groups: ['geo'] }),
|
||||
'telecom',
|
||||
);
|
||||
assert.equal(
|
||||
classify({
|
||||
name: 'GPS BIII-9',
|
||||
group: 'visual',
|
||||
ucs: { purpose: 'Unknown' },
|
||||
}),
|
||||
'telecom',
|
||||
);
|
||||
assert.equal(
|
||||
classify({
|
||||
name: 'UNNAMED-1',
|
||||
group: 'resource',
|
||||
ucs: { purpose: 'Unknown' },
|
||||
}),
|
||||
'imaging',
|
||||
);
|
||||
});
|
||||
|
||||
test('a tracked ISS keeps its ring while Stations is hidden', async () => {
|
||||
const { createControls } = await import('./controls.js');
|
||||
const ring = { primitive: { show: true } };
|
||||
const state = {
|
||||
_enabled: true,
|
||||
_params: {
|
||||
catalog: 'core',
|
||||
showOrbits: true,
|
||||
classes: { ...DEFAULT_SATELLITE_CLASSES, stations: false },
|
||||
},
|
||||
_catalog: new Map([[25544, { satelliteClass: 'stations' }]]),
|
||||
_points: new Map([[25544, { show: true }]]),
|
||||
_orbitPaths: new Map([[25544, ring]]),
|
||||
_detectionObjects: new Map(),
|
||||
_trackedNorad: null,
|
||||
};
|
||||
const controls = createControls({
|
||||
state,
|
||||
services: { layerState: { isExplicitLayerStateOrigin: () => false } },
|
||||
parts: { labels: { _syncIssOverlay() {} } },
|
||||
source: {},
|
||||
});
|
||||
controls._syncIssOrbit();
|
||||
assert.equal(ring.primitive.show, false);
|
||||
state._trackedNorad = 25544;
|
||||
controls._syncIssOrbit();
|
||||
assert.equal(ring.primitive.show, true, 'tracked, it keeps its ring');
|
||||
state._params.showOrbits = false;
|
||||
controls._syncIssOrbit();
|
||||
assert.equal(ring.primitive.show, false, 'orbits off still hides it');
|
||||
});
|
||||
Vendored
+14
-4
@@ -56,22 +56,31 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
if (noradId === layerState._trackedNorad) continue;
|
||||
point.show = _classVisible(noradId);
|
||||
}
|
||||
const issPath = layerState._orbitPaths.get(ISS_NORAD);
|
||||
if (issPath) issPath.primitive.show = _issOrbitVisible();
|
||||
_syncIssOrbit();
|
||||
parts.labels?._syncIssOverlay?.();
|
||||
layerState._detectionObjects.clear();
|
||||
}
|
||||
|
||||
/** Whether the ISS's ring shows: orbits on, the layer drawing, its class on. */
|
||||
/**
|
||||
* Whether the ISS's ring shows: orbits on, the layer drawing, and its
|
||||
* class on or the ISS tracked (a tracked satellite keeps its ring).
|
||||
*/
|
||||
function _issOrbitVisible() {
|
||||
return (
|
||||
satelliteVisualsVisible(
|
||||
layerState._enabled,
|
||||
layerState._params.showOrbits,
|
||||
) && _classVisible(ISS_NORAD)
|
||||
) &&
|
||||
(_classVisible(ISS_NORAD) || layerState._trackedNorad === ISS_NORAD)
|
||||
);
|
||||
}
|
||||
|
||||
/** Show or hide the ISS's ring by _issOrbitVisible, if it is built. */
|
||||
function _syncIssOrbit() {
|
||||
const issPath = layerState._orbitPaths.get(ISS_NORAD);
|
||||
if (issPath) issPath.primitive.show = _issOrbitVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* The row's info line while the classes rest on old or missing SATCAT
|
||||
* evidence (`_satcatState`), and an empty one otherwise.
|
||||
@@ -708,6 +717,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
_classVisible,
|
||||
_applyClassVisibility,
|
||||
_issOrbitVisible,
|
||||
_syncIssOrbit,
|
||||
satelliteVisualsVisible,
|
||||
satelliteCatalogModeChanged,
|
||||
methods,
|
||||
|
||||
@@ -367,11 +367,7 @@ export function createIngestion({
|
||||
// Show ISS orbital path by default
|
||||
if (layerState._catalog.has(ISS_NORAD)) {
|
||||
parts.rendering._showOrbitPath(ISS_NORAD, POINT_STYLES.iss.color);
|
||||
const issPath = layerState._orbitPaths.get(ISS_NORAD);
|
||||
if (issPath)
|
||||
issPath.primitive.show =
|
||||
layerState._params.showOrbits &&
|
||||
parts.controls._classVisible(ISS_NORAD);
|
||||
parts.controls._syncIssOrbit();
|
||||
|
||||
parts.labels._syncIssOverlay();
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ function ingestionFor(catalogText, { source: sourceExtras = {} } = {}) {
|
||||
controls: {
|
||||
_pointStyleFor: () => ({}),
|
||||
_classVisible: () => true,
|
||||
_syncIssOrbit() {},
|
||||
_applyClassVisibility() {
|
||||
state.visibilityApplied = (state.visibilityApplied || 0) + 1;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { ISS_OVERLAY_SOURCE_ID } from './policy.js';
|
||||
import { ISS_NORAD, ISS_OVERLAY_SOURCE_ID } from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
state: layerState,
|
||||
@@ -72,11 +72,14 @@ export function createLifecycle({
|
||||
layerState._params.showPoints,
|
||||
);
|
||||
// Orbit ring primitives + persistent ISS host label — show them
|
||||
for (const path of layerState._orbitPaths.values())
|
||||
path.primitive.show = parts.controls.satelliteVisualsVisible(
|
||||
layerState._enabled,
|
||||
layerState._params.showOrbits,
|
||||
);
|
||||
for (const [noradId, path] of layerState._orbitPaths)
|
||||
path.primitive.show =
|
||||
noradId === ISS_NORAD
|
||||
? parts.controls._issOrbitVisible()
|
||||
: parts.controls.satelliteVisualsVisible(
|
||||
layerState._enabled,
|
||||
layerState._params.showOrbits,
|
||||
);
|
||||
parts.labels._syncIssOverlay();
|
||||
// Re-attach input handlers and preRender propagation
|
||||
parts.interaction._installClickHandler(viewer);
|
||||
|
||||
@@ -119,6 +119,7 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
layerState._trackedEntity = null;
|
||||
}
|
||||
layerState._trackedNorad = null;
|
||||
parts.controls._syncIssOrbit?.();
|
||||
parts.labels._syncIssOverlay();
|
||||
clearTrackedSubjectContext('satellites');
|
||||
layerState._contextRefreshedAtMs = 0;
|
||||
@@ -421,6 +422,7 @@ export function createTracking({ state: layerState, services, parts, source }) {
|
||||
|
||||
// Show orbital path
|
||||
parts.rendering._showOrbitPath(noradId, Cesium.Color.YELLOW);
|
||||
if (noradId === ISS_NORAD) parts.controls._syncIssOrbit?.();
|
||||
|
||||
// Tracked entity position propagates per evaluation through the per-frame
|
||||
// cache (WS-D2) — dot, host readout, and camera share one SGP4 epoch per frame.
|
||||
|
||||
Reference in new issue
Block a user