mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +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
@@ -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