mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The first formal release. - One-click install via Pinokio; keyless boot lands on a live Esri World Imagery satellite globe with keyless terrain, with automatic OSM fallback and graceful degradation when terrain is unavailable. - Provider Settings (the POWER UP panel): add, replace, or remove API keys inside the app; credential files made owner-only before any secret is written; external keys shown read-only; the panel refuses shared or proxied servers. - Keyless capability responses for the optional HUD summary and place search. - Aircraft-identity voice answers cover operator, type, and route, and say so plainly when enrichment is unavailable. - README rewritten keyless-first; concise 0.1.0 changelog section; CI workflow included. Gates: 2,672 unit tests pass / 0 fail, build clean, tracking regression 108/108, map-source tray QA pass, setup doctor ready.
341 lines
13 KiB
JavaScript
341 lines
13 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import * as Cesium from 'cesium';
|
|
import { twoline2satrec } from 'satellite.js';
|
|
import {
|
|
ISS_OVERLAY_SOURCE_OPTIONS,
|
|
_applyPendingSatelliteTrackingRestoreForTest,
|
|
_clearSatelliteLabelLifecycleForTest,
|
|
_pendingSatelliteTrackingRestoreForTest,
|
|
_removeSatelliteTrackingCandidateForTest,
|
|
_runSatellitePreRenderForTest,
|
|
_setSatelliteLabelLifecycleStateForTest,
|
|
_setSatelliteTrackingRefreshOutcomeForTest,
|
|
_setTrackedSatelliteRefreshStateForTest,
|
|
_trackedFrameCartesianForTest,
|
|
_trackIssForTest,
|
|
createIssOverlayEntry,
|
|
} from './satellites.js';
|
|
import satellitesLayer from './satellites.js';
|
|
import { createTrackedOverlayEntry } from './trackedReadout.js';
|
|
|
|
const L1 = '1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927';
|
|
const L2 = '2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537';
|
|
|
|
test('partial and dense catalogs do not make an early false missing decision', async () => {
|
|
_setSatelliteTrackingRefreshOutcomeForTest({
|
|
status: 'partial',
|
|
failedGroups: ['stations.txt'],
|
|
});
|
|
const partial = await satellitesLayer.resolveTrackingRestoreTarget(987654);
|
|
assert.equal(partial.status, 'source-unavailable');
|
|
assert.match(partial.reason, /partial CelesTrak catalog/);
|
|
|
|
let releaseDense;
|
|
const densePromise = new Promise((resolve) => { releaseDense = resolve; });
|
|
_setSatelliteTrackingRefreshOutcomeForTest({
|
|
status: 'accepted',
|
|
catalog: 'dense',
|
|
densePromise,
|
|
});
|
|
let settled = false;
|
|
const resolution = satellitesLayer.resolveTrackingRestoreTarget(987654)
|
|
.then((result) => { settled = true; return result; });
|
|
await Promise.resolve();
|
|
assert.equal(settled, false, 'dense absence must wait for the dense catalog');
|
|
releaseDense({ status: 'ready' });
|
|
assert.equal((await resolution).status, 'missing');
|
|
_setSatelliteTrackingRefreshOutcomeForTest({ status: 'accepted', catalog: 'core' });
|
|
});
|
|
|
|
test('satellite first update aborts every parallel catalog request', async () => {
|
|
const realFetch = globalThis.fetch;
|
|
const observedSignals = [];
|
|
globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => {
|
|
observedSignals.push(options.signal);
|
|
options.signal?.addEventListener('abort', () => {
|
|
reject(new DOMException('aborted', 'AbortError'));
|
|
}, { once: true });
|
|
});
|
|
try {
|
|
const controller = new AbortController();
|
|
const work = satellitesLayer.update({}, { signal: controller.signal });
|
|
await Promise.resolve();
|
|
assert.ok(observedSignals.length > 1);
|
|
controller.abort();
|
|
await assert.rejects(work, { name: 'AbortError' });
|
|
assert.equal(observedSignals.every((signal) => signal?.aborted), true);
|
|
} finally {
|
|
globalThis.fetch = realFetch;
|
|
}
|
|
});
|
|
|
|
test('selected satellite params survive delayed arrival and yield to newer explicit intent', () => {
|
|
const point = {
|
|
position: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 420_000),
|
|
show: true,
|
|
pixelSize: 12,
|
|
color: Cesium.Color.RED,
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: 0,
|
|
};
|
|
const viewer = {
|
|
entities: new Cesium.EntityCollection(),
|
|
trackedEntity: undefined,
|
|
scene: {
|
|
frameState: { frameNumber: 1 },
|
|
primitives: { remove() {} },
|
|
},
|
|
};
|
|
_setSatelliteLabelLifecycleStateForTest({
|
|
viewer,
|
|
satrec: twoline2satrec(L1, L2),
|
|
point,
|
|
});
|
|
try {
|
|
assert.doesNotThrow(() => satellitesLayer.setParams({ selectedSatTrackingId: 25544 }));
|
|
assert.equal(satellitesLayer.getParams().selectedSatTrackingId, 25544);
|
|
assert.equal(viewer.trackedEntity?.gevTrackedId, 'satellites:25544');
|
|
|
|
satellitesLayer.stopTracking();
|
|
_removeSatelliteTrackingCandidateForTest(25544);
|
|
satellitesLayer.setParams(
|
|
{ selectedSatTrackingId: 25544 },
|
|
{ origin: 'share-restore' },
|
|
);
|
|
assert.equal(_pendingSatelliteTrackingRestoreForTest(), 25544);
|
|
_setSatelliteLabelLifecycleStateForTest({
|
|
viewer,
|
|
satrec: twoline2satrec(L1, L2),
|
|
point,
|
|
preservePending: true,
|
|
});
|
|
assert.equal(_applyPendingSatelliteTrackingRestoreForTest(), true);
|
|
assert.equal(satellitesLayer.getParams().selectedSatTrackingId, 25544);
|
|
|
|
satellitesLayer.setParams(
|
|
{ selectedSatTrackingId: 99999 },
|
|
{ origin: 'share-restore' },
|
|
);
|
|
assert.equal(_pendingSatelliteTrackingRestoreForTest(), 99999);
|
|
assert.equal(satellitesLayer.trackById(25544, { origin: 'user' }), true);
|
|
assert.equal(_pendingSatelliteTrackingRestoreForTest(), null, 'new user selection cancels stale restore');
|
|
_applyPendingSatelliteTrackingRestoreForTest();
|
|
assert.equal(satellitesLayer.getParams().selectedSatTrackingId, 25544);
|
|
|
|
satellitesLayer.stopTracking();
|
|
_removeSatelliteTrackingCandidateForTest(25544);
|
|
satellitesLayer.setParams(
|
|
{ selectedSatTrackingId: 25544 },
|
|
{ origin: 'local-restore' },
|
|
);
|
|
assert.equal(_pendingSatelliteTrackingRestoreForTest(), 25544);
|
|
satellitesLayer.stopTracking({ origin: 'user' });
|
|
assert.equal(_pendingSatelliteTrackingRestoreForTest(), null, 'explicit clear cancels stale restore');
|
|
_setSatelliteLabelLifecycleStateForTest({
|
|
viewer,
|
|
satrec: twoline2satrec(L1, L2),
|
|
point,
|
|
preservePending: true,
|
|
});
|
|
_applyPendingSatelliteTrackingRestoreForTest();
|
|
assert.equal(satellitesLayer.getParams().selectedSatTrackingId, null);
|
|
|
|
satellitesLayer.setParams(
|
|
{ selectedSatTrackingId: 50003 },
|
|
{ origin: 'share-restore' },
|
|
);
|
|
assert.equal(_pendingSatelliteTrackingRestoreForTest(), 50003);
|
|
satellitesLayer.setParams({ showPoints: true }, { origin: 'tool' });
|
|
assert.equal(
|
|
_pendingSatelliteTrackingRestoreForTest(),
|
|
null,
|
|
'a newer explicit non-selection option cancels stale restore',
|
|
);
|
|
_applyPendingSatelliteTrackingRestoreForTest();
|
|
assert.equal(satellitesLayer.getParams().selectedSatTrackingId, null);
|
|
} finally {
|
|
_clearSatelliteLabelLifecycleForTest();
|
|
}
|
|
});
|
|
|
|
test('a tracked docked cluster consolidates its companions onto one card', () => {
|
|
// ISS and everything berthed to it are separate real tracks at one position,
|
|
// so their ambient labels stack underneath the tracked card. Owner ruling:
|
|
// consolidate them as secondary info on that card, and suppress only those
|
|
// members — never unrelated satellites that merely happen to be nearby.
|
|
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
|
|
const point = { position: new Cesium.Cartesian3() };
|
|
const viewer = { camera: null, scene: { frameState: { frameNumber: 1 } } };
|
|
let nowMs = Date.UTC(2008, 8, 20, 12, 30);
|
|
// The tracked satellite moves ~7.6 km/s, so neighbours are defined RELATIVE to
|
|
// wherever it is at scan time rather than pinned to a stale sample.
|
|
const near = (metres) => ({
|
|
get position() {
|
|
const tracked = _trackedFrameCartesianForTest();
|
|
return new Cesium.Cartesian3(tracked.x + metres, tracked.y, tracked.z);
|
|
},
|
|
});
|
|
|
|
_setTrackedSatelliteRefreshStateForTest({
|
|
noradId: 25544,
|
|
name: 'ISS (ZARYA)',
|
|
satrec: twoline2satrec(L1, L2),
|
|
entity,
|
|
point,
|
|
viewer,
|
|
now: () => nowMs,
|
|
neighbours: [
|
|
{ noradId: 55555, name: 'PROGRESS-MS 34', point: near(120) }, // berthed
|
|
{ noradId: 55556, name: 'SOYUZ-MS 12', point: near(-90) }, // berthed
|
|
{ noradId: 99999, name: 'UNRELATED SAT', point: near(50_000) }, // 50 km away
|
|
],
|
|
});
|
|
|
|
_runSatellitePreRenderForTest();
|
|
// The cluster scan is throttled, so advance past its interval before the frame
|
|
// that must observe the cluster.
|
|
nowMs += 2000;
|
|
viewer.scene.frameState.frameNumber = 2;
|
|
_runSatellitePreRenderForTest();
|
|
|
|
const details = entity.gevLabelModel.details;
|
|
// Class leads the detail block; the altitude line follows it, and the
|
|
// consolidated companions stay last.
|
|
assert.equal(details[0], 'STATION · ISS', 'the class line names what this is');
|
|
assert.match(details[1], /NORAD 25544$/, 'the altitude line is unchanged');
|
|
assert.equal(details[2], 'DOCKED · PROGRESS-MS 34 · +1',
|
|
'companions are consolidated, named, and counted on the tracked card');
|
|
|
|
// Scoping is encoded in that string: THREE neighbours were seeded but only two
|
|
// are counted (the named one plus "+1"). The satellite 50 km away is not part
|
|
// of the cluster and keeps its own identity and label — the owner's explicit
|
|
// constraint that unrelated nearby satellites are never suppressed.
|
|
assert.doesNotMatch(details[2], /UNRELATED/);
|
|
assert.doesNotMatch(details[2], /\+2/);
|
|
});
|
|
test('satellite pre-render refreshes the tracked altitude on each propagated frame', () => {
|
|
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
|
|
const point = { position: new Cesium.Cartesian3() };
|
|
const viewer = {
|
|
camera: null,
|
|
scene: { frameState: { frameNumber: 1 } },
|
|
};
|
|
const epochs = [
|
|
Date.UTC(2008, 8, 20, 12, 30),
|
|
Date.UTC(2008, 8, 20, 13, 0),
|
|
];
|
|
let epochIndex = 0;
|
|
_setTrackedSatelliteRefreshStateForTest({
|
|
noradId: 25544,
|
|
name: 'ISS (ZARYA)',
|
|
satrec: twoline2satrec(L1, L2),
|
|
entity,
|
|
point,
|
|
viewer,
|
|
now: () => epochs[epochIndex],
|
|
});
|
|
|
|
_runSatellitePreRenderForTest();
|
|
assert.equal(entity.gevLabelModel.title, 'ISS (ZARYA)');
|
|
assert.equal(entity.gevLabelModel.details[0], 'STATION · ISS');
|
|
assert.equal(entity.gevLabelModel.details[1], '353 km · NORAD 25544');
|
|
|
|
epochIndex = 1;
|
|
viewer.scene.frameState.frameNumber = 2;
|
|
_runSatellitePreRenderForTest();
|
|
assert.equal(entity.gevLabelModel.details[1], '366 km · NORAD 25544');
|
|
assert.equal(entity.gevLabelModel.details[0], 'STATION · ISS',
|
|
'the class line survives an altitude-only republish');
|
|
});
|
|
|
|
test('ISS ambient and tracked lifecycle uses cached host entries with no native labels or duplicate text', () => {
|
|
const calls = [];
|
|
const overlayHost = {
|
|
setEntries: (...args) => calls.push(['entries', ...args]),
|
|
setVisible: (...args) => calls.push(['visible', ...args]),
|
|
clearSource: (...args) => calls.push(['clear', ...args]),
|
|
};
|
|
const point = {
|
|
position: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 420_000),
|
|
show: true,
|
|
pixelSize: 12,
|
|
color: Cesium.Color.RED,
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: 0,
|
|
};
|
|
const viewer = {
|
|
entities: new Cesium.EntityCollection(),
|
|
trackedEntity: undefined,
|
|
scene: {
|
|
frameState: { frameNumber: 1 },
|
|
primitives: { remove() {} },
|
|
},
|
|
};
|
|
let cleaned = false;
|
|
_setSatelliteLabelLifecycleStateForTest({
|
|
viewer,
|
|
satrec: twoline2satrec(L1, L2),
|
|
point,
|
|
overlayHost,
|
|
});
|
|
try {
|
|
const ambientPublication = calls.find(([type]) => type === 'entries');
|
|
assert.ok(ambientPublication);
|
|
assert.equal(ambientPublication[1], 'satellites-iss');
|
|
assert.deepEqual(ambientPublication[3], ISS_OVERLAY_SOURCE_OPTIONS);
|
|
const ambient = ambientPublication[2][0];
|
|
assert.equal(ambient.title, 'ISS');
|
|
assert.equal(ambient.position(), point.position, 'ISS getter reads the existing point cache');
|
|
assert.equal(point.label, undefined, 'ISS point primitive carries no label graphic');
|
|
|
|
const trackedEntity = _trackIssForTest();
|
|
assert.ok(trackedEntity);
|
|
assert.equal(viewer.entities.values.length, 1, 'runtime guard requires a real tracked entity');
|
|
assert.equal(trackedEntity.label, undefined);
|
|
assert.ok(trackedEntity.point, 'tracked dot remains on the entity for camera framing');
|
|
const trackedEntry = createTrackedOverlayEntry(trackedEntity);
|
|
assert.ok(trackedEntry);
|
|
assert.equal(trackedEntry.id, 'satellites:25544');
|
|
assert.equal(trackedEntry.protected, true);
|
|
assert.equal(trackedEntry.paintLane, 'tracked');
|
|
assert.equal(
|
|
calls.filter(([type, sourceId]) => type === 'entries' && sourceId === 'satellites-iss').length,
|
|
2,
|
|
'tracking may refresh before ownership changes but must not publish a second ISS entry',
|
|
);
|
|
assert.deepEqual(calls.slice(-2), [
|
|
['clear', 'satellites-iss'],
|
|
['visible', 'satellites-iss', false],
|
|
], 'tracked ISS suppresses ambient text so the tracked card is the only text surface');
|
|
|
|
_clearSatelliteLabelLifecycleForTest();
|
|
cleaned = true;
|
|
assert.equal(viewer.entities.values.length, 0);
|
|
assert.equal(
|
|
calls.filter(([type, sourceId]) => type === 'entries' && sourceId === 'satellites-iss').length,
|
|
3,
|
|
'untracking republishes exactly one persistent ISS ambient entry before teardown',
|
|
);
|
|
} finally {
|
|
if (!cleaned) _clearSatelliteLabelLifecycleForTest();
|
|
}
|
|
});
|
|
|
|
test('ISS overlay entry retains the native distance scale and source text', () => {
|
|
const position = new Cesium.Cartesian3(1, 2, 3);
|
|
const entry = createIssOverlayEntry(() => position);
|
|
assert.equal(entry.title, 'ISS');
|
|
assert.equal(entry.position(), position);
|
|
assert.deepEqual(entry.distanceScale, {
|
|
near: 1_000_000,
|
|
nearValue: 1,
|
|
far: 30_000_000,
|
|
farValue: 0.4,
|
|
});
|
|
assert.equal(entry.edgeFade, 'keyhole');
|
|
assert.equal(entry.horizonCull, true);
|
|
});
|