From b59eb8b18c06922e3fda870af3e69dad8bf7656d Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 8 Nov 2026 14:49:24 -0800 Subject: [PATCH] fix(satellites): a hidden class takes the ISS ring, label and docked names Class toggles only hid points: with Stations off the ISS's orbit ring and its label still drew, and each catalog refresh showed the ring again. The ring and label now follow the ISS's class, and the tracked card no longer names a docked companion whose class is hidden. A failed Starlink load reverted to the core catalog without applying class visibility again, so the core catalog's own Starlink points stayed on while the chip, legend and lists said Starlink was off, and classes.starlink kept reading true. Both are put right on failure. --- src/layers/satellites/catalog.js | 8 ++++ src/layers/satellites/classes.test.mjs | 66 ++++++++++++++++++++++++++ src/layers/satellites/controls.js | 30 +++++++++--- src/layers/satellites/ingestion.js | 5 +- src/layers/satellites/labels.js | 3 ++ 5 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/layers/satellites/catalog.js b/src/layers/satellites/catalog.js index 1e7c939bb..bb20e646a 100644 --- a/src/layers/satellites/catalog.js +++ b/src/layers/satellites/catalog.js @@ -209,6 +209,14 @@ export function createCatalog({ state: layerState, services, parts, source }) { if (token !== layerState._denseLoadToken) return; layerState._params.catalog = 'core'; _removeDenseCatalog(); + // Back on core, Starlink is off: hide the core catalog's own Starlink + // points again, and say so in the class choice. + if (layerState._params.classes) + layerState._params.classes = { + ...layerState._params.classes, + starlink: false, + }; + parts.controls._applyClassVisibility?.(); layerState._denseStatus = 'failed'; layerState._denseError = reason; parts.controls._notifyRowControls(); diff --git a/src/layers/satellites/classes.test.mjs b/src/layers/satellites/classes.test.mjs index cf73fde14..b0cd9307d 100644 --- a/src/layers/satellites/classes.test.mjs +++ b/src/layers/satellites/classes.test.mjs @@ -258,3 +258,69 @@ test('SATCAT CSV parses, quoted commas included, keyed by NORAD id', () => { assert.equal(parseSatcatCsv('busy').size, 0); assert.equal(parseSatcatCsv('').size, 0); }); + +test('turning Stations off takes the ISS ring and label with its point', async () => { + const { createControls } = await import('./controls.js'); + let overlaySyncs = 0; + 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: () => (overlaySyncs += 1) } }, + source: {}, + }); + controls._applyClassVisibility(); + assert.equal(state._points.get(25544).show, false); + assert.equal(ring.primitive.show, false, 'the ring goes with the class'); + assert.ok(overlaySyncs >= 1, 'the label is re-synced'); + state._params.classes = { ...DEFAULT_SATELLITE_CLASSES }; + controls._applyClassVisibility(); + assert.equal(ring.primitive.show, true, 'and comes back with it'); +}); + +test('a failed Starlink load hides the core catalog Starlink points again', async () => { + const { createCatalog } = await import('./catalog.js'); + let applied = 0; + const state = { + _params: { + catalog: 'dense', + classes: { ...DEFAULT_SATELLITE_CLASSES, starlink: true }, + }, + _denseLoadToken: 3, + _denseIds: [], + _catalog: new Map(), + _points: new Map(), + _detectionObjects: new Map(), + _trackedNorad: null, + _activeUpdateControllers: new Set(), + }; + const catalog = createCatalog({ + state, + services: {}, + parts: { + controls: { + _applyClassVisibility: () => (applied += 1), + _notifyRowControls() {}, + }, + labels: { _syncIssOverlay() {} }, + }, + source: {}, + }); + catalog._denseLoadFailed(state._denseLoadToken, 'feed unreachable'); + assert.equal(state._params.catalog, 'core'); + assert.equal(state._params.classes.starlink, false, 'the choice says so'); + assert.ok(applied >= 1, 'class visibility is applied again'); +}); diff --git a/src/layers/satellites/controls.js b/src/layers/satellites/controls.js index 0baedfcd8..c4c26f2f8 100644 --- a/src/layers/satellites/controls.js +++ b/src/layers/satellites/controls.js @@ -48,16 +48,30 @@ export function createControls({ state: layerState, services, parts, source }) { /** * Show or hide every point by its class. The tracked satellite's point - * stays hidden (its entity draws the dot). + * stays hidden (its entity draws the dot). The ISS's ring and label go + * with its class. */ function _applyClassVisibility() { for (const [noradId, point] of layerState._points) { if (noradId === layerState._trackedNorad) continue; point.show = _classVisible(noradId); } + const issPath = layerState._orbitPaths.get(ISS_NORAD); + if (issPath) issPath.primitive.show = _issOrbitVisible(); + parts.labels?._syncIssOverlay?.(); layerState._detectionObjects.clear(); } + /** Whether the ISS's ring shows: orbits on, the layer drawing, its class on. */ + function _issOrbitVisible() { + return ( + satelliteVisualsVisible( + layerState._enabled, + layerState._params.showOrbits, + ) && _classVisible(ISS_NORAD) + ); + } + /** * The row's info line while the classes rest on old or missing SATCAT * evidence (`_satcatState`), and an empty one otherwise. @@ -530,11 +544,14 @@ export function createControls({ state: layerState, services, parts, source }) { } if (params.showOrbits !== undefined) { layerState._params.showOrbits = params.showOrbits !== false; - for (const path of layerState._orbitPaths.values()) - path.primitive.show = satelliteVisualsVisible( - layerState._enabled, - layerState._params.showOrbits, - ); + for (const [noradId, path] of layerState._orbitPaths) + path.primitive.show = + noradId === ISS_NORAD + ? _issOrbitVisible() + : satelliteVisualsVisible( + layerState._enabled, + layerState._params.showOrbits, + ); parts.labels._syncIssOverlay(); } if (catalogChanged && catalog === 'dense') { @@ -690,6 +707,7 @@ export function createControls({ state: layerState, services, parts, source }) { _classTally, _classVisible, _applyClassVisibility, + _issOrbitVisible, satelliteVisualsVisible, satelliteCatalogModeChanged, methods, diff --git a/src/layers/satellites/ingestion.js b/src/layers/satellites/ingestion.js index f65b476ee..e5fdb5caa 100644 --- a/src/layers/satellites/ingestion.js +++ b/src/layers/satellites/ingestion.js @@ -356,7 +356,10 @@ export function createIngestion({ 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; + if (issPath) + issPath.primitive.show = + layerState._params.showOrbits && + parts.controls._classVisible(ISS_NORAD); parts.labels._syncIssOverlay(); } diff --git a/src/layers/satellites/labels.js b/src/layers/satellites/labels.js index c62bbb308..612996968 100644 --- a/src/layers/satellites/labels.js +++ b/src/layers/satellites/labels.js @@ -83,6 +83,8 @@ export function createLabels({ state: layerState, services, parts, source }) { function _dockedCompanionNames() { const names = []; for (const noradId of layerState._dockedCompanions) { + // A hidden class is not named on the tracked card either. + if (parts.controls?._classVisible?.(noradId) === false) continue; names.push( layerState._catalog.get(noradId)?.name?.trim() || `SAT-${noradId}`, ); @@ -102,6 +104,7 @@ export function createLabels({ state: layerState, services, parts, source }) { layerState._trackedNorad !== ISS_NORAD && !layerState._dockedCompanions.has(ISS_NORAD) && layerState._catalog.has(ISS_NORAD) && + parts.controls?._classVisible?.(ISS_NORAD) !== false && _issDisplayCached(); if (!visible) { layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID);