Merge pull request #492 from bilawalsidhu/refactor/construct-application-layers

refactor: construct application layers from supplied sources
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-09-14 14:24:48 -07:00
commit 310809e8f1
53 files changed
+1146 -394

No files matched your search

+2
View File
@@ -91,6 +91,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Construct application layers from explicit sources, with standalone provider selection and catalog-owned aircraft classification; controls and voice queries use those instances.
- Let application data and controls receive the same explicit layer catalog; keep standalone defaults and registration-before-restoration ordering.
- Expose existing FIRMS CSV parsing and UTC time-window helpers through a portable package export, with shared contract fixtures.
+21
View File
@@ -409,3 +409,24 @@ that schema requires a corresponding codec change.
`standalone/catalog` selects the existing page-scoped default instances and metadata.
Reusable data setup imports no standalone layer defaults. The current compatibility
source setters remain available while callers migrate to instance construction.
### Layer construction
`application/layers` constructs the current catalog from explicit source objects
and an application AbortSignal. Small `src/app/layers` modules wire existing scene
services into each family factory. Standalone provider selection lives in
`src/standalone/layerSources.js`. The construction export has its own checked
dependency graph, excluding standalone setup and compatibility layer instances.
Both aircraft layers share the catalog's classification registry; launches use
its satellites and Contacts uses its aircraft, vessels and installations. Data
registration, controls and voice actions read those same instances. Destruction
remains the manager's responsibility; classification also observes application
abort when startup has not reached registration. Scene engines remain page-owned,
so this change does not introduce multiple simultaneous viewers.
Direct `src/data` compatibility entries retain their old defaults and testing
exports. Browser regression probes use the registered instance's testing surface
to avoid accidentally inspecting an unused compatibility instance. Existing source
setters apply only to compatibility instances; the normal application supplies
its sources at construction.
+8
View File
@@ -1,5 +1,13 @@
# God's Eye View Current State
Application startup constructs fresh layer instances from explicit source objects.
Standalone composition selects the existing providers. Controls, launch orbit
lookups, Contacts voice answers and ISS pass queries use the registered catalog.
Aircraft classification is catalog-owned and cancelled with application teardown;
scene engines still support one application per page. Compatibility data entrypoints
remain for direct callers, but normal startup does not configure global sources.
Application data setup receives an ordered layer catalog and serialization metadata.
Controls use the same instances. The standalone composition selects the existing
default catalog, and restoration starts only after all registrations are sealed.
+2 -1
View File
@@ -193,6 +193,7 @@
"node": "./build/application-html.js"
},
"./application/catalog": "./src/app/catalog.js",
"./standalone/catalog": "./src/standalone/catalog.js"
"./standalone/catalog": "./src/standalone/catalog.js",
"./application/layers": "./src/app/constructCatalog.js"
}
}
+2 -1
View File
@@ -246,7 +246,8 @@ if [[ ! -f "$SOURCE_ROOT/src/data/cctv.js" ]]; then
exit 1
fi
if ! grep -q "^[[:space:]]*cctvLayer," "$SOURCE_ROOT/src/standalone/catalog.js"; then
if ! grep -q "return createApplicationCatalog(" "$SOURCE_ROOT/src/standalone/catalog.js" || \
! grep -q "^[[:space:]]*createApplicationCctv({" "$SOURCE_ROOT/src/app/constructCatalog.js"; then
echo "error: CCTV layer not wired in src/standalone/catalog.js"
exit 1
fi
+20 -1
View File
@@ -558,5 +558,24 @@
"src/toolProjectRoot.test.mjs",
"src/app/catalog.js",
"src/standalone/catalog.js",
"src/app/catalog.test.mjs"
"src/app/catalog.test.mjs",
"src/app/layers/militaryInstallations.js",
"src/app/layers/alprCameras.js",
"src/app/layers/traffic.js",
"src/app/layers/submarineCables.js",
"src/app/layers/militaryFlights.js",
"src/app/layers/bikeshare.js",
"src/app/layers/flights.js",
"src/app/layers/radio.js",
"src/app/layers/earthquakes.js",
"src/app/layers/cctv.js",
"src/app/layers/firms.js",
"src/app/layers/satellites.js",
"src/app/layers/militaryAwareness.js",
"src/app/layers/rocketLaunches.js",
"src/app/layers/overlayHost.js",
"src/app/layers/aisLiveVessels.js",
"src/app/constructCatalog.js",
"src/standalone/layerSources.js",
"src/app/constructCatalog.test.mjs"
]
+342 -1
View File
@@ -1503,7 +1503,25 @@
"src/weatherEffectsMath.js",
"src/worldFocus.js",
"src/app/catalog.js",
"src/standalone/catalog.js"
"src/standalone/catalog.js",
"src/app/layers/militaryInstallations.js",
"src/app/layers/alprCameras.js",
"src/app/layers/traffic.js",
"src/app/layers/submarineCables.js",
"src/app/layers/militaryFlights.js",
"src/app/layers/bikeshare.js",
"src/app/layers/flights.js",
"src/app/layers/radio.js",
"src/app/layers/earthquakes.js",
"src/app/layers/cctv.js",
"src/app/layers/firms.js",
"src/app/layers/satellites.js",
"src/app/layers/militaryAwareness.js",
"src/app/layers/rocketLaunches.js",
"src/app/layers/overlayHost.js",
"src/app/layers/aisLiveVessels.js",
"src/app/constructCatalog.js",
"src/standalone/layerSources.js"
],
"external": [
"@mapbox/vector-tile",
@@ -1518,5 +1536,328 @@
"exports": ["./application/catalog"],
"modules": ["src/app/catalog.js"],
"external": []
},
"application-layer-construction": {
"exports": ["./application/layers"],
"modules": [
"src/annotations/annotationResolver.js",
"src/app/catalog.js",
"src/app/constructCatalog.js",
"src/app/layers/aisLiveVessels.js",
"src/app/layers/alprCameras.js",
"src/app/layers/bikeshare.js",
"src/app/layers/cctv.js",
"src/app/layers/earthquakes.js",
"src/app/layers/firms.js",
"src/app/layers/flights.js",
"src/app/layers/militaryAwareness.js",
"src/app/layers/militaryFlights.js",
"src/app/layers/militaryInstallations.js",
"src/app/layers/overlayHost.js",
"src/app/layers/radio.js",
"src/app/layers/rocketLaunches.js",
"src/app/layers/satellites.js",
"src/app/layers/submarineCables.js",
"src/app/layers/traffic.js",
"src/app/sourceSlot.js",
"src/cctvFocusRequest.js",
"src/celestialRing.js",
"src/cockpitMath.js",
"src/data/aircraftClass.js",
"src/data/aircraftIcons.js",
"src/data/aircraftMeta.js",
"src/data/aircraftNearbyPolicy.js",
"src/data/aircraftRecession.js",
"src/data/cctvCards.js",
"src/data/cctvFootprint.js",
"src/data/cctvGizmo.js",
"src/data/cctvLod.js",
"src/data/cctvViewshed.js",
"src/data/cockpitAirLod.js",
"src/data/cockpitContactDot.js",
"src/data/contactMatch.js",
"src/data/contextStore.js",
"src/data/dataCredits.js",
"src/data/detectionCohort.js",
"src/data/detectionDraw.js",
"src/data/detectionPolicy.js",
"src/data/fireAnchors.js",
"src/data/firmsAdapt.js",
"src/data/firmsLabels.js",
"src/data/flowMatch.js",
"src/data/focusDeemphasis.js",
"src/data/geoid.js",
"src/data/groundFloor.js",
"src/data/groundSnap.js",
"src/data/iconOrientation.js",
"src/data/infrastructure.js",
"src/data/installationFeedback.js",
"src/data/issPass.js",
"src/data/labelArbiter.js",
"src/data/layerState.js",
"src/data/localGeojson.js",
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js",
"src/data/local_data/natural_earth/marine.json",
"src/data/local_data/natural_earth/regions.json",
"src/data/local_data/neighborhoods/san-francisco.json",
"src/data/meshFloorSampler.js",
"src/data/militaryAwarenessEngine.js",
"src/data/militaryInstallationData.js",
"src/data/modelVisualAnchor.js",
"src/data/motionModel.js",
"src/data/naturalEarthRegions.js",
"src/data/neighborhoodPolygons.js",
"src/data/pickRegistry.js",
"src/data/radioCountry.js",
"src/data/renderAltitude.js",
"src/data/retryableLoad.js",
"src/data/routePlausible.js",
"src/data/satelliteClass.js",
"src/data/scenePick.js",
"src/data/spriteOrder.js",
"src/data/terrainHeights.js",
"src/data/tomtomTiles.js",
"src/data/tr3bRegistry.js",
"src/data/trackedCamera.js",
"src/data/trackedModelRegime.js",
"src/data/trackedReadout.js",
"src/data/trackingClickGesture.js",
"src/data/trafficBounds.js",
"src/data/trafficFlowStyle.js",
"src/data/trafficPresetStyle.js",
"src/data/trafficQueue.js",
"src/data/trailRenderer.js",
"src/data/vesselLabels.js",
"src/keylessGeocoder.js",
"src/layers/aircraft/classification.js",
"src/layers/alpr/index.js",
"src/layers/alpr/model.js",
"src/layers/alpr/policy.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/source.js",
"src/layers/awareness/controls.js",
"src/layers/awareness/dependencies.js",
"src/layers/awareness/focus.js",
"src/layers/awareness/history.js",
"src/layers/awareness/index.js",
"src/layers/awareness/ingestion.js",
"src/layers/awareness/lifecycle.js",
"src/layers/awareness/model.js",
"src/layers/awareness/navigation.js",
"src/layers/awareness/panel.js",
"src/layers/awareness/policy.js",
"src/layers/awareness/queries.js",
"src/layers/awareness/rendering.js",
"src/layers/awareness/state.js",
"src/layers/awareness/subject.js",
"src/layers/awareness/testing.js",
"src/layers/bikeshare/controls.js",
"src/layers/bikeshare/index.js",
"src/layers/bikeshare/ingestion.js",
"src/layers/bikeshare/lifecycle.js",
"src/layers/bikeshare/model.js",
"src/layers/bikeshare/policy.js",
"src/layers/bikeshare/queries.js",
"src/layers/bikeshare/registry.js",
"src/layers/bikeshare/rendering.js",
"src/layers/bikeshare/selection.js",
"src/layers/bikeshare/source.js",
"src/layers/bikeshare/state.js",
"src/layers/bikeshare/testing.js",
"src/layers/bikeshare/viewport.js",
"src/layers/cctv/calibration.js",
"src/layers/cctv/cards.js",
"src/layers/cctv/catalog.js",
"src/layers/cctv/controls.js",
"src/layers/cctv/frames.js",
"src/layers/cctv/geometry.js",
"src/layers/cctv/geometryQueue.js",
"src/layers/cctv/ground.js",
"src/layers/cctv/health.js",
"src/layers/cctv/hover.js",
"src/layers/cctv/index.js",
"src/layers/cctv/ingestion.js",
"src/layers/cctv/lifecycle.js",
"src/layers/cctv/model.js",
"src/layers/cctv/navigation.js",
"src/layers/cctv/policy.js",
"src/layers/cctv/presentation.js",
"src/layers/cctv/projection.js",
"src/layers/cctv/rendering.js",
"src/layers/cctv/selection.js",
"src/layers/cctv/source.js",
"src/layers/cctv/state.js",
"src/layers/cctv/testing.js",
"src/layers/earthquakes/index.js",
"src/layers/earthquakes/model.js",
"src/layers/earthquakes/source.js",
"src/layers/firms/anchors.js",
"src/layers/firms/cards.js",
"src/layers/firms/index.js",
"src/layers/firms/ingestion.js",
"src/layers/firms/lifecycle.js",
"src/layers/firms/model.js",
"src/layers/firms/policy.js",
"src/layers/firms/queries.js",
"src/layers/firms/rendering.js",
"src/layers/firms/selection.js",
"src/layers/firms/source.js",
"src/layers/firms/state.js",
"src/layers/firms/viewport.js",
"src/layers/flights/controller.js",
"src/layers/flights/enrichment.js",
"src/layers/flights/evidence.js",
"src/layers/flights/index.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js",
"src/layers/flights/motion.js",
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
"src/layers/installations/controls.js",
"src/layers/installations/index.js",
"src/layers/installations/ingestion.js",
"src/layers/installations/lifecycle.js",
"src/layers/installations/model.js",
"src/layers/installations/policy.js",
"src/layers/installations/rendering.js",
"src/layers/installations/selection.js",
"src/layers/installations/source.js",
"src/layers/installations/state.js",
"src/layers/installations/viewport.js",
"src/layers/launches/camera.js",
"src/layers/launches/controls.js",
"src/layers/launches/index.js",
"src/layers/launches/ingestion.js",
"src/layers/launches/launchPad.js",
"src/layers/launches/lifecycle.js",
"src/layers/launches/model.js",
"src/layers/launches/orbitRendering.js",
"src/layers/launches/overlays.js",
"src/layers/launches/panel.js",
"src/layers/launches/paths.js",
"src/layers/launches/policy.js",
"src/layers/launches/policyHelpers.js",
"src/layers/launches/rendering.js",
"src/layers/launches/replay.js",
"src/layers/launches/selection.js",
"src/layers/launches/source.js",
"src/layers/launches/state.js",
"src/layers/launches/testing.js",
"src/layers/military/controller.js",
"src/layers/military/index.js",
"src/layers/military/ingestion.js",
"src/layers/military/lifecycle.js",
"src/layers/military/motion.js",
"src/layers/military/policy.js",
"src/layers/military/queries.js",
"src/layers/military/rendering.js",
"src/layers/military/state.js",
"src/layers/military/testing.js",
"src/layers/military/tracking.js",
"src/layers/radio/catalogModel.js",
"src/layers/radio/categories.js",
"src/layers/radio/clustering.js",
"src/layers/radio/controls.js",
"src/layers/radio/index.js",
"src/layers/radio/ingestion.js",
"src/layers/radio/interaction.js",
"src/layers/radio/labels.js",
"src/layers/radio/lifecycle.js",
"src/layers/radio/model.js",
"src/layers/radio/navigation.js",
"src/layers/radio/playback.js",
"src/layers/radio/policy.js",
"src/layers/radio/presentation.js",
"src/layers/radio/queries.js",
"src/layers/radio/rendering.js",
"src/layers/radio/selection.js",
"src/layers/radio/source.js",
"src/layers/radio/state.js",
"src/layers/radio/tuning.js",
"src/layers/radio/tuningNoise.js",
"src/layers/radio/volume.js",
"src/layers/satellites/catalog.js",
"src/layers/satellites/controls.js",
"src/layers/satellites/index.js",
"src/layers/satellites/ingestion.js",
"src/layers/satellites/interaction.js",
"src/layers/satellites/labels.js",
"src/layers/satellites/lifecycle.js",
"src/layers/satellites/orbits.js",
"src/layers/satellites/policy.js",
"src/layers/satellites/rendering.js",
"src/layers/satellites/source.js",
"src/layers/satellites/state.js",
"src/layers/satellites/testing.js",
"src/layers/satellites/tracking.js",
"src/layers/submarineCables/geometry.js",
"src/layers/submarineCables/index.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/layers/submarineCables/lifecycle.js",
"src/layers/submarineCables/overlay.js",
"src/layers/submarineCables/policy.js",
"src/layers/submarineCables/rendering.js",
"src/layers/submarineCables/state.js",
"src/layers/submarineCables/surface.js",
"src/layers/traffic/animation.js",
"src/layers/traffic/controls.js",
"src/layers/traffic/flow.js",
"src/layers/traffic/flowDecode.js",
"src/layers/traffic/flowSource.js",
"src/layers/traffic/index.js",
"src/layers/traffic/ingestion.js",
"src/layers/traffic/lifecycle.js",
"src/layers/traffic/model.js",
"src/layers/traffic/policy.js",
"src/layers/traffic/rendering.js",
"src/layers/traffic/source.js",
"src/layers/traffic/state.js",
"src/layers/traffic/style.js",
"src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js",
"src/layers/vessels/cards.js",
"src/layers/vessels/evidence.js",
"src/layers/vessels/index.js",
"src/layers/vessels/ingestion.js",
"src/layers/vessels/lifecycle.js",
"src/layers/vessels/policy.js",
"src/layers/vessels/queries.js",
"src/layers/vessels/rendering.js",
"src/layers/vessels/selection.js",
"src/layers/vessels/state.js",
"src/layers/vessels/store.js",
"src/layers/vessels/testing.js",
"src/layers/vessels/tracking.js",
"src/locations.js",
"src/navigationPolicy.js",
"src/overlays/worldOverlay.js",
"src/overlays/worldOverlayDraw.js",
"src/overlays/worldOverlayTokens.js",
"src/renderGovernor.js",
"src/search/defaults.js",
"src/search/geospatial.js",
"src/search/google.js",
"src/search/http.js",
"src/search/placeSearch.js",
"src/services/application.js",
"src/services/requests.js",
"src/ui/panelRailGeometry.js",
"src/ui/radioTunerModel.js",
"src/worldFocus.js"
],
"external": [
"cesium",
"egm96-universal",
"satellite.js",
"@mapbox/vector-tile",
"pbf",
"mgrs"
]
}
}
+5 -21
View File
@@ -3813,27 +3813,11 @@ async function main() {
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
const v = window.__godsEyeView.viewer;
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
// `.module` is the layer OBJECT (the default export), not the module
// namespace, so the handoff seam is not on it. Reach the namespace the
// same way this group reaches groundFloor's: offer the URL the app
// itself loaded (Vite serves an edited file as `…?t=<hmr stamp>`, and
// the plain path would hand back a second, unrelated instance whose
// module state the app never touches), then prove identity by requiring
// its default export to BE the live layer object.
let ns = null;
const urls = [...new Set([
...performance.getEntriesByType('resource').map((e) => e.name)
.filter((n) => /\/src\/data\/flights\.js(\?|$)/.test(n)).reverse(),
`${window.__gevQaSourceBase || '/src'}/data/flights.js`,
])];
for (const url of urls) {
let mod; try { mod = await import(/* @vite-ignore */ url); } catch { continue; }
if (mod?.default === fl && typeof mod._driveFleetModelHandoffForTest === 'function') {
ns = mod;
break;
}
}
if (!ns) return { skipped: `the app's own flights module was not reachable (tried ${urls.length})` };
// Test the registered instance directly, including catalogs constructed
// with their own sources. Never import a second compatibility instance.
const ns = fl.testing;
if (typeof ns?._driveFleetModelHandoffForTest !== 'function')
return { error: "the registered flights instance has no handoff test seam" };
// Use a scenario-owned contact so its groundSnap entry is provably cold;
// earlier display-floor cases intentionally exercise aaa097's cache.
const holdIcao = 'aaa098';
+130
View File
@@ -0,0 +1,130 @@
import { createLayerCatalog } from './catalog.js';
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
import { createApplicationFlights } from './layers/flights.js';
import { createApplicationMilitary } from './layers/militaryFlights.js';
import { createApplicationVessels } from './layers/aisLiveVessels.js';
import { createApplicationCctv } from './layers/cctv.js';
import { createApplicationRadio } from './layers/radio.js';
import { createApplicationTraffic } from './layers/traffic.js';
import { createApplicationBikeshare } from './layers/bikeshare.js';
import { createApplicationInstallations } from './layers/militaryInstallations.js';
import { createApplicationSatellites } from './layers/satellites.js';
import { createApplicationLaunches } from './layers/rocketLaunches.js';
import { createApplicationAlpr } from './layers/alprCameras.js';
import { createApplicationAwareness } from './layers/militaryAwareness.js';
import { createApplicationFirms } from './layers/firms.js';
import { createApplicationEarthquakes } from './layers/earthquakes.js';
import { createApplicationCables } from './layers/submarineCables.js';
import { createInfrastructureLayers } from '../data/infrastructure.js';
import { localGeoJsonServices } from '../data/localGeojson.js';
const SOURCE_METHODS = Object.freeze({
flights: ['getSnapshot'],
military: ['getSnapshot'],
vessels: ['getSnapshot'],
cctv: ['getCatalog', 'getHealth', 'getFrameUrl', 'getMediaUrl'],
radio: ['getDirectory', 'recordClick'],
traffic: [
'requestRoads',
'getStatus',
'fetchFlowForBounds',
'getFlowSessionStats',
'resetFlowTileCache',
],
bikeshare: ['getStations'],
installations: ['getMappedSites', 'searchNearby'],
satellites: ['readGroup'],
launches: ['getLaunches', 'getActiveTle'],
alpr: ['fetch'],
firms: ['getSnapshot'],
earthquakes: ['getSnapshot'],
cables: ['fetch'],
});
/** Construct the current catalog without choosing any source provider.
* Scene engines remain page-owned; layers and classification have this app's lifetime.
* The manager owns layer destruction, while abort releases classification even if startup fails.
*/
export function createApplicationCatalog({
sources,
signal,
metadata = LAYER_STATE_REGISTRY,
vesselOptions,
resolveAsset,
}) {
if (!signal?.addEventListener)
throw new TypeError('An application lifetime signal is required');
signal.throwIfAborted();
for (const [name, methods] of Object.entries(SOURCE_METHODS)) {
if (
methods.some((method) => typeof sources?.[name]?.[method] !== 'function')
)
throw new TypeError(`Invalid catalog source: ${name}`);
}
const militaryRegistry = createMilitaryRegistry();
const dispose = () => {
signal.removeEventListener('abort', dispose);
militaryRegistry.dispose();
};
signal.addEventListener('abort', dispose, { once: true });
try {
militaryRegistry.configureSource(sources.military, { signal });
const flights = createApplicationFlights({
source: sources.flights,
militaryRegistry,
resolveAsset,
});
const military = createApplicationMilitary({
source: sources.military,
militaryRegistry,
resolveAsset,
});
const vessels = createApplicationVessels({
source: sources.vessels,
options: vesselOptions,
});
const installations = createApplicationInstallations({
source: sources.installations,
});
const satellites = createApplicationSatellites({
source: sources.satellites,
});
const catalog = createLayerCatalog(
[
flights,
military,
createApplicationEarthquakes({ source: sources.earthquakes }),
createApplicationAlpr({ source: sources.alpr }),
satellites,
createApplicationLaunches({ source: sources.launches, satellites }),
createApplicationTraffic({ source: sources.traffic }),
createApplicationCctv({ source: sources.cctv }),
createApplicationRadio({ source: sources.radio }),
createApplicationBikeshare({ source: sources.bikeshare }),
vessels,
installations,
createApplicationAwareness({
flights,
military,
vessels,
installations,
}),
...createInfrastructureLayers(localGeoJsonServices),
createApplicationCables({ source: sources.cables }),
createApplicationFirms({
id: 'local-firms',
name: 'FIRMS Active Fires',
icon: '▲',
source: 'NASA FIRMS · LIVE',
feed: sources.firms,
}),
],
metadata,
);
return Object.freeze({ ...catalog, militaryRegistry });
} catch (error) {
dispose();
throw error;
}
}
+77
View File
@@ -0,0 +1,77 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApplicationCatalog } from './constructCatalog.js';
import { createStandaloneLayerSources } from '../standalone/layerSources.js';
import { catalogControlServices } from './catalog.js';
function fixtureSources(ids, calls) {
const sources = createStandaloneLayerSources();
sources.military = {
getSnapshot: async () => {
throw new Error('Use identity acquisition');
},
async getIdentities(_query, { signal }) {
calls.push(signal);
return ids;
},
};
return sources;
}
test('catalogs construct distinct layers and classification from their supplied source', async (t) => {
const a = new AbortController();
const b = new AbortController();
t.after(() => {
a.abort();
b.abort();
});
const callsA = [];
const callsB = [];
const first = createApplicationCatalog({
sources: fixtureSources(['abc123'], callsA),
signal: a.signal,
});
const second = createApplicationCatalog({
sources: fixtureSources(['def456'], callsB),
signal: b.signal,
});
assert.equal(first.layers.length, 17);
assert.deepEqual(
first.layers.map(({ id }) => id),
second.layers.map(({ id }) => id),
);
for (const layer of first.layers)
assert.notEqual(layer, second.get(layer.id));
assert.equal(
catalogControlServices(first).satellitesLayer,
first.get('satellites'),
);
assert.equal(callsA.length, 0, 'construction must not acquire');
await first.militaryRegistry.refreshMilitaryRegistryIfStale();
await second.militaryRegistry.refreshMilitaryRegistryIfStale();
assert.equal(first.militaryRegistry.isMilitaryIcao('abc123'), true);
assert.equal(first.militaryRegistry.isMilitaryIcao('def456'), false);
assert.equal(second.militaryRegistry.isMilitaryIcao('def456'), true);
a.abort();
assert.equal(callsA[0].aborted, true);
assert.equal(first.militaryRegistry.isMilitaryIcao('abc123'), false);
assert.equal(callsB[0].aborted, false);
assert.equal(second.militaryRegistry.isMilitaryIcao('def456'), true);
});
test('invalid or already cancelled construction fails before classification can acquire', () => {
const lifetime = new AbortController();
assert.throws(
() => createApplicationCatalog({ sources: {}, signal: lifetime.signal }),
/catalog source/,
);
lifetime.abort();
assert.throws(
() =>
createApplicationCatalog({
sources: createStandaloneLayerSources(),
signal: lifetime.signal,
}),
{ name: 'AbortError' },
);
});
+1 -1
View File
@@ -49,5 +49,5 @@ export function createApplicationData({
dataManager.buildTogglePanel(document.getElementById('data-toggles'));
styleManager.attachDataManager(dataManager);
return { dataManager };
return { dataManager, catalog };
}
+31
View File
@@ -0,0 +1,31 @@
import { createVesselLayer } from '../../layers/vessels/index.js';
import * as context from '../../data/contextStore.js';
import * as trails from '../../data/trailRenderer.js';
import * as labels from '../../data/detectionDraw.js';
import * as picking from '../../data/pickRegistry.js';
import * as overlay from '../../overlays/worldOverlay.js';
import * as geoid from '../../data/geoid.js';
import * as sprites from '../../data/spriteOrder.js';
import * as focus from '../../data/focusDeemphasis.js';
import * as worldFocus from '../../worldFocus.js';
import * as render from '../../renderGovernor.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationVessels({ source, options = {} }) {
return createVesselLayer({
source,
options,
services: {
context,
trails,
labels,
picking,
overlay,
geoid,
sprites,
focus,
worldFocus,
render,
},
});
}
+13
View File
@@ -0,0 +1,13 @@
import { createAlprCamerasLayer } from '../../layers/alpr/index.js';
import * as render from '../../renderGovernor.js';
import * as context from '../../data/contextStore.js';
import * as picking from '../../data/pickRegistry.js';
import * as groundFloor from '../../data/groundFloor.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationAlpr({ source }) {
return createAlprCamerasLayer({
source,
services: { render, context, picking, groundFloor },
});
}
+13
View File
@@ -0,0 +1,13 @@
import { createBikeshareLayer } from '../../layers/bikeshare/index.js';
import * as render from '../../renderGovernor.js';
import * as sprites from '../../data/spriteOrder.js';
import * as picking from '../../data/pickRegistry.js';
import * as overlays from '../../overlays/worldOverlay.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationBikeshare({ source }) {
return createBikeshareLayer({
source,
services: { render, sprites, picking, overlays },
});
}
+30
View File
@@ -0,0 +1,30 @@
import { createCctvLayer } from '../../layers/cctv/index.js';
import * as sprites from '../../data/spriteOrder.js';
import * as activation from '../../cctvFocusRequest.js';
import * as overlays from '../../overlays/worldOverlay.js';
import * as locations from '../../locations.js';
import * as picking from '../../data/pickRegistry.js';
import * as terrain from '../../data/terrainHeights.js';
import * as ground from '../../data/groundFloor.js';
import * as mesh from '../../data/meshFloorSampler.js';
import * as focus from '../../data/focusDeemphasis.js';
import * as render from '../../renderGovernor.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationCctv({ source }) {
return createCctvLayer({
source,
services: {
sprites,
activation,
overlays,
locations,
picking,
terrain,
ground,
mesh,
focus,
render,
},
});
}
+6
View File
@@ -0,0 +1,6 @@
import { createEarthquakesLayer } from '../../layers/earthquakes/index.js';
import { overlayHost } from './overlayHost.js';
/** Wire earthquake observations to the application overlay host. */
export function createApplicationEarthquakes(options) {
return createEarthquakesLayer({ overlayHost, ...options });
}
+38
View File
@@ -0,0 +1,38 @@
import * as Cesium from 'cesium';
import { createFirmsHeatmapLayer as createLayer } from '../../layers/firms/index.js';
import * as render from '../../renderGovernor.js';
import * as sprites from '../../data/spriteOrder.js';
import * as context from '../../data/contextStore.js';
import * as picking from '../../data/pickRegistry.js';
import * as anchors from '../../data/fireAnchors.js';
import * as overlays from '../../overlays/worldOverlay.js';
import * as focus from '../../worldFocus.js';
export const firmsServices = {
render,
sprites,
context,
picking,
anchors,
overlays,
focus,
};
/** Bind a supplied fire feed to the application scene service owners. */
export function createApplicationFirms(options) {
return createLayer({
...options,
icon: options.icon ?? '▲',
source: options.source ?? 'NASA FIRMS',
feed: options.feed,
services: firmsServices,
overlayHost: options.overlayHost ?? {
setEntries: overlays.setOverlayEntries,
setVisible: overlays.setOverlaySourceVisible,
clearSource: overlays.clearOverlaySource,
hitTest: overlays.hitTestWorldOverlay,
},
screenSpaceEventHandlerFactory:
options.screenSpaceEventHandlerFactory ??
((viewer) => new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)),
});
}
+47
View File
@@ -0,0 +1,47 @@
import { createCivilFlightLayer } from '../../layers/flights/index.js';
import * as picking from '../../data/pickRegistry.js';
import * as sprites from '../../data/spriteOrder.js';
import * as trails from '../../data/trailRenderer.js';
import * as aircraftPresentation from '../../data/tr3bRegistry.js';
import * as camera from '../../data/trackedCamera.js';
import * as labels from '../../data/detectionDraw.js';
import * as groundFloor from '../../data/groundFloor.js';
import * as meshFloor from '../../data/meshFloorSampler.js';
import * as geoid from '../../data/geoid.js';
import * as focus from '../../data/focusDeemphasis.js';
import * as readout from '../../data/trackedReadout.js';
import * as context from '../../data/contextStore.js';
import * as render from '../../renderGovernor.js';
import * as groundSnap from '../../data/groundSnap.js';
import * as recession from '../../data/aircraftRecession.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationFlights({
source,
militaryRegistry,
resolveAsset = (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
}) {
return createCivilFlightLayer({
source,
resolveAsset,
services: {
picking,
sprites,
trails,
aircraftPresentation,
camera,
militaryRegistry,
labels,
groundFloor,
meshFloor,
geoid,
focus,
readout,
context,
render,
groundSnap,
recession,
},
});
}
+23
View File
@@ -0,0 +1,23 @@
import { createAwarenessLayer } from '../../layers/awareness/index.js';
import * as navigation from '../../navigationPolicy.js';
import * as geometry from '../../celestialRing.js';
import * as render from '../../renderGovernor.js';
/** Bind Contacts to the same catalog instances used for rendering. */
export function createApplicationAwareness({
flights,
military,
vessels,
installations,
}) {
return createAwarenessLayer({
services: {
flights,
military,
vessels,
installations,
navigation,
geometry,
render,
},
});
}
+47
View File
@@ -0,0 +1,47 @@
import { createMilitaryFlightLayer } from '../../layers/military/index.js';
import * as picking from '../../data/pickRegistry.js';
import * as sprites from '../../data/spriteOrder.js';
import * as trails from '../../data/trailRenderer.js';
import * as aircraftPresentation from '../../data/tr3bRegistry.js';
import * as camera from '../../data/trackedCamera.js';
import * as labels from '../../data/detectionDraw.js';
import * as groundFloor from '../../data/groundFloor.js';
import * as meshFloor from '../../data/meshFloorSampler.js';
import * as geoid from '../../data/geoid.js';
import * as focus from '../../data/focusDeemphasis.js';
import * as readout from '../../data/trackedReadout.js';
import * as context from '../../data/contextStore.js';
import * as render from '../../renderGovernor.js';
import * as groundSnap from '../../data/groundSnap.js';
import * as recession from '../../data/aircraftRecession.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationMilitary({
source,
militaryRegistry,
resolveAsset = (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
}) {
return createMilitaryFlightLayer({
source,
resolveAsset,
services: {
picking,
sprites,
trails,
aircraftPresentation,
camera,
militaryRegistry,
labels,
groundFloor,
meshFloor,
geoid,
focus,
readout,
context,
render,
groundSnap,
recession,
},
});
}
+14
View File
@@ -0,0 +1,14 @@
import { createInstallationsLayer } from '../../layers/installations/index.js';
import * as render from '../../renderGovernor.js';
import * as context from '../../data/contextStore.js';
import * as ground from '../../data/groundFloor.js';
import * as anchors from '../../data/fireAnchors.js';
import * as picking from '../../data/pickRegistry.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationInstallations({ source }) {
return createInstallationsLayer({
source,
services: { render, context, ground, anchors, picking },
});
}
+10
View File
@@ -0,0 +1,10 @@
import {
clearOverlaySource,
setOverlayEntries,
setOverlaySourceVisible,
} from '../../overlays/worldOverlay.js';
export const overlayHost = Object.freeze({
clearSource: clearOverlaySource,
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
});
+14
View File
@@ -0,0 +1,14 @@
import { createRadioLayer } from '../../layers/radio/index.js';
import * as ground from '../../data/groundFloor.js';
import * as picking from '../../data/pickRegistry.js';
import * as overlays from '../../overlays/worldOverlay.js';
import * as globe from '../../celestialRing.js';
import * as render from '../../renderGovernor.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationRadio({ source }) {
return createRadioLayer({
source,
services: { ground, picking, overlays, globe, render },
});
}
+12
View File
@@ -0,0 +1,12 @@
import { createRocketLaunchesLayer } from '../../layers/launches/index.js';
import * as geometry from '../../celestialRing.js';
import * as overlays from '../../overlays/worldOverlay.js';
import * as render from '../../renderGovernor.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationLaunches({ source, satellites }) {
return createRocketLaunchesLayer({
source,
services: { satellites, geometry, overlays, render },
});
}
+24
View File
@@ -0,0 +1,24 @@
import { createSatellitesLayer } from '../../layers/satellites/index.js';
import * as picking from '../../data/pickRegistry.js';
import * as focus from '../../data/focusDeemphasis.js';
import * as readout from '../../data/trackedReadout.js';
import * as overlays from '../../overlays/worldOverlay.js';
import * as context from '../../data/contextStore.js';
import * as render from '../../renderGovernor.js';
import * as layerState from '../../data/layerState.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationSatellites({ source }) {
return createSatellitesLayer({
source,
services: {
picking,
focus,
readout,
overlays,
context,
render,
layerState,
},
});
}
+13
View File
@@ -0,0 +1,13 @@
import * as Cesium from 'cesium';
import { createSubmarineCableLayer } from '../../layers/submarineCables/index.js';
import { overlayHost } from './overlayHost.js';
/** Wire cable source geometry to the application overlay host. */
export function createApplicationCables(options) {
return createSubmarineCableLayer({
overlayHost,
screenSpaceEventHandlerFactory: (canvas) =>
new Cesium.ScreenSpaceEventHandler(canvas),
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
...options,
});
}
+11
View File
@@ -0,0 +1,11 @@
import { createTrafficLayer } from '../../layers/traffic/index.js';
import * as credits from '../../data/dataCredits.js';
import * as render from '../../renderGovernor.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationTraffic({ source }) {
return createTrafficLayer({
source,
services: { credits, render },
});
}
+4 -25
View File
@@ -1,17 +1,8 @@
import { createVesselLayer } from '../layers/vessels/index.js';
import { createAisStreamSource } from '../sources/live/standalone.js';
import * as context from './contextStore.js';
import * as trails from './trailRenderer.js';
import * as labels from './detectionDraw.js';
import * as picking from './pickRegistry.js';
import * as overlay from '../overlays/worldOverlay.js';
import * as geoid from './geoid.js';
import * as sprites from './spriteOrder.js';
import * as focus from './focusDeemphasis.js';
import * as worldFocus from '../worldFocus.js';
import * as render from '../renderGovernor.js';
import { createApplicationVessels } from '../app/layers/aisLiveVessels.js';
const aisLiveVesselsLayer = createVesselLayer({
import { createAisStreamSource } from '../sources/live/standalone.js';
const aisLiveVesselsLayer = createApplicationVessels({
source: createAisStreamSource({
apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live',
}),
@@ -19,18 +10,6 @@ const aisLiveVesselsLayer = createVesselLayer({
maxRows: import.meta.env?.VITE_AIS_LIVE_MAX_ROWS,
maxLabels: import.meta.env?.VITE_AIS_LIVE_LABEL_MAX_ROWS,
},
services: {
context,
trails,
labels,
picking,
overlay,
geoid,
sprites,
focus,
worldFocus,
render,
},
});
export { AIS_FIRST_CONNECT_GRACE_MS } from '../layers/vessels/policy.js';
export const deriveAisFeedError = aisLiveVesselsLayer.deriveAisFeedError;
+3 -10
View File
@@ -1,12 +1,6 @@
import { createApplicationAlpr } from '../app/layers/alprCameras.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import {
createAlprCamerasLayer,
createOverpassAlprSource,
} from '../layers/alpr/index.js';
import * as render from '../renderGovernor.js';
import * as context from './contextStore.js';
import * as picking from './pickRegistry.js';
import * as groundFloor from './groundFloor.js';
import { createOverpassAlprSource } from '../layers/alpr/index.js';
export * from '../layers/alpr/index.js';
const slot = createSourceSlot(
createOverpassAlprSource(),
@@ -14,7 +8,6 @@ const slot = createSourceSlot(
'ALPR source',
);
export const configureAlprSource = slot.configure;
export default createAlprCamerasLayer({
export default createApplicationAlpr({
source: slot.source,
services: { render, context, picking, groundFloor },
});
+2 -7
View File
@@ -1,10 +1,6 @@
import { createApplicationBikeshare } from '../app/layers/bikeshare.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createBikeshareSource } from '../layers/bikeshare/source.js';
import { createBikeshareLayer } from '../layers/bikeshare/index.js';
import * as render from '../renderGovernor.js';
import * as sprites from './spriteOrder.js';
import * as picking from './pickRegistry.js';
import * as overlays from '../overlays/worldOverlay.js';
const sourceSlot = createSourceSlot(
createBikeshareSource(),
@@ -12,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Bikeshare source',
);
export const configureBikeshareSource = sourceSlot.configure;
const layer = createBikeshareLayer({
const layer = createApplicationBikeshare({
source: sourceSlot.source,
services: { render, sprites, picking, overlays },
});
export const createBikeshareSelectedOverlayEntry =
layer.createBikeshareSelectedOverlayEntry;
+2 -24
View File
@@ -1,16 +1,6 @@
import { createApplicationCctv } from '../app/layers/cctv.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createCctvSource } from '../layers/cctv/source.js';
import { createCctvLayer } from '../layers/cctv/index.js';
import * as sprites from './spriteOrder.js';
import * as activation from '../cctvFocusRequest.js';
import * as overlays from '../overlays/worldOverlay.js';
import * as locations from '../locations.js';
import * as picking from './pickRegistry.js';
import * as terrain from './terrainHeights.js';
import * as ground from './groundFloor.js';
import * as mesh from './meshFloorSampler.js';
import * as focus from './focusDeemphasis.js';
import * as render from '../renderGovernor.js';
const sourceSlot = createSourceSlot(
createCctvSource(),
@@ -18,20 +8,8 @@ const sourceSlot = createSourceSlot(
'Cctv source',
);
export const configureCctvSource = sourceSlot.configure;
const layer = createCctvLayer({
const layer = createApplicationCctv({
source: sourceSlot.source,
services: {
sprites,
activation,
overlays,
locations,
picking,
terrain,
ground,
mesh,
focus,
render,
},
});
export const calibrationPatchMovesAnchor = layer.calibrationPatchMovesAnchor;
export const migrateRangeScaleForFloor = layer.migrateRangeScaleForFloor;
+4 -15
View File
@@ -1,22 +1,11 @@
import {
createEarthquakesLayer as createLayer,
createUsgsEarthquakeSource,
} from '../layers/earthquakes/index.js';
import {
clearOverlaySource,
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
import { createUsgsEarthquakeSource } from '../layers/earthquakes/index.js';
import { createApplicationEarthquakes } from '../app/layers/earthquakes.js';
export * from '../layers/earthquakes/index.js';
/** Wire the standalone source and application overlay owner. */
export function createEarthquakesLayer({
source = createUsgsEarthquakeSource(),
overlayHost = {
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
clearSource: clearOverlaySource,
},
...options
} = {}) {
return createLayer({ source, overlayHost });
return createApplicationEarthquakes({ source, ...options });
}
export default createEarthquakesLayer();
+4 -33
View File
@@ -1,33 +1,16 @@
import { createApplicationFirms, firmsServices } from '../app/layers/firms.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import * as Cesium from 'cesium';
import {
createFirmsHeatmapLayer as createLayer,
createFirmsHelpers,
createFirmsSource,
} from '../layers/firms/index.js';
import * as render from '../renderGovernor.js';
import * as sprites from './spriteOrder.js';
import * as context from './contextStore.js';
import * as picking from './pickRegistry.js';
import * as anchors from './fireAnchors.js';
import * as overlays from '../overlays/worldOverlay.js';
import * as focus from '../worldFocus.js';
const services = {
render,
sprites,
context,
picking,
anchors,
overlays,
focus,
};
const sourceSlot = createSourceSlot(
createFirmsSource(),
['getSnapshot'],
'Fire source',
);
export const configureFirmsSource = sourceSlot.configure;
const helpers = createFirmsHelpers({ services });
const helpers = createFirmsHelpers({ services: firmsServices });
export const mapAnalystRecord = helpers.mapAnalystRecord;
export const fireCullPosition = helpers.fireCullPosition;
export const applyHorizonCull = helpers.applyHorizonCull;
@@ -36,22 +19,10 @@ export const buildFireCard = helpers.buildFireCard;
export const buildCellCard = helpers.buildCellCard;
export const applyFirmsOverlayPolicy = helpers.applyFirmsOverlayPolicy;
/** Supply the local source and the application's scene service owners. */
/** Retain the existing default feed and helper exports for direct callers. */
export function createFirmsHeatmapLayer(options) {
return createLayer({
return createApplicationFirms({
...options,
icon: options.icon ?? '▲',
source: options.source ?? 'NASA FIRMS',
feed: options.feed ?? sourceSlot.source,
services,
overlayHost: options.overlayHost ?? {
setEntries: overlays.setOverlayEntries,
setVisible: overlays.setOverlaySourceVisible,
clearSource: overlays.clearOverlaySource,
hitTest: overlays.hitTestWorldOverlay,
},
screenSpaceEventHandlerFactory:
options.screenSpaceEventHandlerFactory ??
((viewer) => new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)),
});
}
+5 -38
View File
@@ -1,44 +1,11 @@
import { createCivilFlightLayer } from '../layers/flights/index.js';
import { createOpenSkySource } from '../sources/live/standalone.js';
import * as picking from './pickRegistry.js';
import * as sprites from './spriteOrder.js';
import * as trails from './trailRenderer.js';
import * as aircraftPresentation from './tr3bRegistry.js';
import * as camera from './trackedCamera.js';
import * as militaryRegistry from './militaryRegistry.js';
import * as labels from './detectionDraw.js';
import * as groundFloor from './groundFloor.js';
import * as meshFloor from './meshFloorSampler.js';
import * as geoid from './geoid.js';
import * as focus from './focusDeemphasis.js';
import * as readout from './trackedReadout.js';
import * as context from './contextStore.js';
import * as render from '../renderGovernor.js';
import * as groundSnap from './groundSnap.js';
import * as recession from './aircraftRecession.js';
import { createApplicationFlights } from '../app/layers/flights.js';
const flightsLayer = createCivilFlightLayer({
import { createOpenSkySource } from '../sources/live/standalone.js';
import * as militaryRegistry from './militaryRegistry.js';
const flightsLayer = createApplicationFlights({
source: createOpenSkySource(),
resolveAsset: (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
services: {
picking,
sprites,
trails,
aircraftPresentation,
camera,
militaryRegistry,
labels,
groundFloor,
meshFloor,
geoid,
focus,
readout,
context,
render,
groundSnap,
recession,
},
});
export { TRACKED_MODEL_MAX_PX } from '../layers/flights/policy.js';
export const _floorGroundedDisplayPositionForTest =
+2 -10
View File
@@ -1,22 +1,14 @@
import { createAwarenessLayer } from '../layers/awareness/index.js';
import { createApplicationAwareness } from '../app/layers/militaryAwareness.js';
import flights from './flights.js';
import military from './militaryFlights.js';
import vessels from './aisLiveVessels.js';
import installations from './militaryInstallations.js';
import * as navigation from '../navigationPolicy.js';
import * as geometry from '../celestialRing.js';
import * as render from '../renderGovernor.js';
const layer = createAwarenessLayer({
services: {
const layer = createApplicationAwareness({
flights,
military,
vessels,
installations,
navigation,
geometry,
render,
},
});
export const contextTargetFlyToAllowed = layer.contextTargetFlyToAllowed;
export const awarenessClearMatchesSubject = layer.awarenessClearMatchesSubject;
+5 -38
View File
@@ -1,44 +1,11 @@
import { createMilitaryFlightLayer } from '../layers/military/index.js';
import { createAdsbLolSource } from '../sources/live/standalone.js';
import * as picking from './pickRegistry.js';
import * as sprites from './spriteOrder.js';
import * as trails from './trailRenderer.js';
import * as aircraftPresentation from './tr3bRegistry.js';
import * as camera from './trackedCamera.js';
import * as militaryRegistry from './militaryRegistry.js';
import * as labels from './detectionDraw.js';
import * as groundFloor from './groundFloor.js';
import * as meshFloor from './meshFloorSampler.js';
import * as geoid from './geoid.js';
import * as focus from './focusDeemphasis.js';
import * as readout from './trackedReadout.js';
import * as context from './contextStore.js';
import * as render from '../renderGovernor.js';
import * as groundSnap from './groundSnap.js';
import * as recession from './aircraftRecession.js';
import { createApplicationMilitary } from '../app/layers/militaryFlights.js';
const militaryFlightsLayer = createMilitaryFlightLayer({
import { createAdsbLolSource } from '../sources/live/standalone.js';
import * as militaryRegistry from './militaryRegistry.js';
const militaryFlightsLayer = createApplicationMilitary({
source: createAdsbLolSource(),
resolveAsset: (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
services: {
picking,
sprites,
trails,
aircraftPresentation,
camera,
militaryRegistry,
labels,
groundFloor,
meshFloor,
geoid,
focus,
readout,
context,
render,
groundSnap,
recession,
},
});
export { TRACKED_MODEL_MAX_PX } from '../layers/military/policy.js';
export const _setTrackedMilitaryRefreshStateForTest =
+3 -11
View File
@@ -1,13 +1,6 @@
import { createApplicationInstallations } from '../app/layers/militaryInstallations.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import {
createInstallationsLayer,
createInstallationSource,
} from '../layers/installations/index.js';
import * as render from '../renderGovernor.js';
import * as context from './contextStore.js';
import * as ground from './groundFloor.js';
import * as anchors from './fireAnchors.js';
import * as picking from './pickRegistry.js';
import { createInstallationSource } from '../layers/installations/index.js';
const sourceSlot = createSourceSlot(
createInstallationSource(),
@@ -15,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Installation source',
);
export const configureInstallationSource = sourceSlot.configure;
const layer = createInstallationsLayer({
const layer = createApplicationInstallations({
source: sourceSlot.source,
services: { render, context, ground, anchors, picking },
});
export const approximateSurfaceDistanceM = layer.approximateSurfaceDistanceM;
export const classifyGoogleMilitaryPlace = layer.classifyGoogleMilitaryPlace;
+2 -8
View File
@@ -1,11 +1,6 @@
import { createApplicationRadio } from '../app/layers/radio.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createRadioSource } from '../layers/radio/source.js';
import { createRadioLayer } from '../layers/radio/index.js';
import * as ground from './groundFloor.js';
import * as picking from './pickRegistry.js';
import * as overlays from '../overlays/worldOverlay.js';
import * as globe from '../celestialRing.js';
import * as render from '../renderGovernor.js';
const sourceSlot = createSourceSlot(
createRadioSource(),
@@ -13,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Radio source',
);
export const configureRadioSource = sourceSlot.configure;
const layer = createRadioLayer({
const layer = createApplicationRadio({
source: sourceSlot.source,
services: { ground, picking, overlays, globe, render },
});
export const radioGlobeLabel = layer.radioGlobeLabel;
export const createRadioSelectedOverlayEntry =
+4 -9
View File
@@ -1,12 +1,7 @@
import { createApplicationLaunches } from '../app/layers/rocketLaunches.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import {
createRocketLaunchesLayer,
createLaunchSource,
} from '../layers/launches/index.js';
import { createLaunchSource } from '../layers/launches/index.js';
import * as satellites from './satellites.js';
import * as geometry from '../celestialRing.js';
import * as overlays from '../overlays/worldOverlay.js';
import * as render from '../renderGovernor.js';
const sourceSlot = createSourceSlot(
createLaunchSource(),
@@ -14,9 +9,9 @@ const sourceSlot = createSourceSlot(
'Launch source',
);
export const configureLaunchSource = sourceSlot.configure;
const layer = createRocketLaunchesLayer({
const layer = createApplicationLaunches({
source: sourceSlot.source,
services: { satellites, geometry, overlays, render },
satellites,
});
export const satelliteParamsForSpaceMissions =
layer.satelliteParamsForSpaceMissions;
+3 -13
View File
@@ -1,15 +1,6 @@
import { createApplicationSatellites } from '../app/layers/satellites.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import {
createSatellitesLayer,
createSatelliteSource,
} from '../layers/satellites/index.js';
import * as picking from './pickRegistry.js';
import * as focus from './focusDeemphasis.js';
import * as readout from './trackedReadout.js';
import * as overlays from '../overlays/worldOverlay.js';
import * as context from './contextStore.js';
import * as render from '../renderGovernor.js';
import * as layerState from './layerState.js';
import { createSatelliteSource } from '../layers/satellites/index.js';
const sourceSlot = createSourceSlot(
createSatelliteSource(),
@@ -17,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Satellite source',
);
export const configureSatelliteSource = sourceSlot.configure;
const layer = createSatellitesLayer({
const layer = createApplicationSatellites({
source: sourceSlot.source,
services: { picking, focus, readout, overlays, context, render, layerState },
});
export const satelliteVisualsVisible = layer.satelliteVisualsVisible;
export const satelliteCatalogModeChanged = layer.satelliteCatalogModeChanged;
+4 -20
View File
@@ -1,26 +1,10 @@
import * as Cesium from 'cesium';
import {
createSubmarineCableLayer,
createCableOverlayPublisher as createPublisher,
} from '../layers/submarineCables/index.js';
import { createApplicationCables } from '../app/layers/submarineCables.js';
import { overlayHost } from '../app/layers/overlayHost.js';
import { createCableOverlayPublisher as createPublisher } from '../layers/submarineCables/index.js';
import { createBundledCableSource } from '../layers/submarineCables/bundledSource.js';
import {
clearOverlaySource,
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
const overlayHost = Object.freeze({
clearSource: clearOverlaySource,
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
});
export function createTeleGeographySubmarineCableLayer(options = {}) {
return createSubmarineCableLayer({
return createApplicationCables({
source: createBundledCableSource(),
overlayHost,
screenSpaceEventHandlerFactory: (canvas) =>
new Cesium.ScreenSpaceEventHandler(canvas),
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
...options,
});
}
@@ -631,6 +631,8 @@ test('the depth decision is the dated Option-2 host migration, superseding Optio
test('cables create no native labels and no per-frame geometry callbacks', () => {
const source = [
'../../data/telegeographySubmarineCables.js',
'../../app/layers/submarineCables.js',
'../../app/layers/overlayHost.js',
'rendering.js',
'overlay.js',
'surface.js',
+2 -5
View File
@@ -1,8 +1,6 @@
import { createApplicationTraffic } from '../app/layers/traffic.js';
import { createSourceSlot } from '../app/sourceSlot.js';
import { createTrafficSource } from '../layers/traffic/source.js';
import { createTrafficLayer } from '../layers/traffic/index.js';
import * as credits from './dataCredits.js';
import * as render from '../renderGovernor.js';
const sourceSlot = createSourceSlot(
createTrafficSource(),
@@ -14,9 +12,8 @@ const sourceSlot = createSourceSlot(
},
);
export const configureTrafficSource = sourceSlot.configure;
const layer = createTrafficLayer({
const layer = createApplicationTraffic({
source: sourceSlot.source,
services: { credits, render },
});
export const getTrafficTimingDiagnostics = layer.getTrafficTimingDiagnostics;
export const deriveTrafficFlowError = layer.deriveTrafficFlowError;
+4 -1
View File
@@ -22,7 +22,10 @@ async function launch(overrides = {}, dotenv = '', omitCctv = false) {
}
await fs.mkdir(path.join(root, 'src', 'standalone'), { recursive: true });
const catalog = await fs.readFile(new URL('./standalone/catalog.js', import.meta.url), 'utf8');
await fs.writeFile(path.join(root, 'src', 'standalone', 'catalog.js'), omitCctv ? catalog.replace(/^\s*cctvLayer,\s*$/m, '') : catalog);
await fs.writeFile(path.join(root, 'src', 'standalone', 'catalog.js'), catalog);
await fs.mkdir(path.join(root, 'src', 'app'), { recursive: true });
const assembly = await fs.readFile(new URL('./app/constructCatalog.js', import.meta.url), 'utf8');
await fs.writeFile(path.join(root, 'src', 'app', 'constructCatalog.js'), omitCctv ? assembly.replace(/^\s*createApplicationCctv\(.*$/m, '') : assembly);
await fs.mkdir(path.join(root, 'node_modules'));
await fs.symlink(fileURLToPath(new URL('.', import.meta.resolve('vite/package.json'))), path.join(root, 'node_modules', 'vite'), 'dir');
await fs.writeFile(path.join(root, '.env'), dotenv);
+2 -1
View File
@@ -23,9 +23,10 @@ export function createStandaloneApplication({
const loadingScreen = document.getElementById('loading-screen');
const loaderStatus = loadingScreen.querySelector('.loader-status');
let placeSearch;
const catalog = createStandaloneCatalog();
let catalog;
return createApplication({
createScene: (context) => {
catalog = createStandaloneCatalog({ signal: context.signal });
placeSearch = createStandalonePlaceSearch({
...geospatial,
resolveApiKey: () => googleApiKey,
+20 -37
View File
@@ -1,40 +1,23 @@
import flightsLayer from '../data/flights.js';
import militaryFlightsLayer from '../data/militaryFlights.js';
import alprCamerasLayer from '../data/alprCameras.js';
import earthquakesLayer from '../data/earthquakes.js';
import satellitesLayer from '../data/satellites.js';
import rocketLaunchesLayer from '../data/rocketLaunches.js';
import trafficLayer from '../data/traffic.js';
import cctvLayer from '../data/cctv.js';
import radioLayer from '../data/radio.js';
import bikeshareLayer from '../data/bikeshare.js';
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
import militaryInstallationsLayer from '../data/militaryInstallations.js';
import militaryAwarenessLayer from '../data/militaryAwareness.js';
import localDataLayers from '../data/localLayers.js';
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
import { createApplicationCatalog } from '../app/constructCatalog.js';
import { createStandaloneLayerSources } from './layerSources.js';
export { createStandaloneReferenceSources } from './layerSources.js';
import { createLayerCatalog } from '../app/catalog.js';
/** Create fresh layer instances using the existing standalone source choices. */
export function createStandaloneCatalog({
signal = new AbortController().signal,
} = {}) {
return createApplicationCatalog({
sources: createStandaloneLayerSources(),
signal,
vesselOptions: {
maxRows: import.meta.env?.VITE_AIS_LIVE_MAX_ROWS,
maxLabels: import.meta.env?.VITE_AIS_LIVE_LABEL_MAX_ROWS,
},
});
}
/** Select the existing standalone instances and current persistence schema. */
export function createStandaloneCatalog() {
return createLayerCatalog(
[
flightsLayer,
militaryFlightsLayer,
earthquakesLayer,
alprCamerasLayer,
satellitesLayer,
rocketLaunchesLayer,
trafficLayer,
cctvLayer,
radioLayer,
bikeshareLayer,
aisLiveVesselsLayer,
militaryInstallationsLayer,
militaryAwarenessLayer,
...localDataLayers,
],
LAYER_STATE_REGISTRY,
);
// Direct compatibility callers share one catalog; application startup supplies its own.
let compatibilityCatalog;
export function getStandaloneCatalog() {
return (compatibilityCatalog ||= createStandaloneCatalog());
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { createApplicationControls } from '../app/controls.js';
import { createStandaloneCatalog } from './catalog.js';
import { getStandaloneCatalog } from './catalog.js';
export function createStandaloneControls(options) {
return createApplicationControls({
catalog: createStandaloneCatalog(),
catalog: options?.catalog ?? getStandaloneCatalog(),
...options,
});
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { createApplicationData } from '../app/data.js';
import { createStandaloneCatalog } from './catalog.js';
import { getStandaloneCatalog } from './catalog.js';
export function createStandaloneData(options) {
return createApplicationData({
catalog: createStandaloneCatalog(),
catalog: options?.catalog ?? getStandaloneCatalog(),
...options,
});
}
+45
View File
@@ -0,0 +1,45 @@
import {
createOpenSkySource,
createAdsbLolSource,
createAisStreamSource,
} from '../sources/live/standalone.js';
import { createCctvSource } from '../layers/cctv/source.js';
import { createRadioSource } from '../layers/radio/source.js';
import { createTrafficSource } from '../layers/traffic/source.js';
import { createBikeshareSource } from '../layers/bikeshare/source.js';
import { createInstallationSource } from '../layers/installations/index.js';
import { createSatelliteSource } from '../layers/satellites/index.js';
import { createLaunchSource } from '../layers/launches/index.js';
import { createOverpassAlprSource } from '../layers/alpr/index.js';
import { createFirmsSource } from '../layers/firms/index.js';
import { createUsgsEarthquakeSource } from '../layers/earthquakes/source.js';
import { createBundledCableSource } from '../layers/submarineCables/bundledSource.js';
/** Existing reference feeds, usable independently of live source selection. */
export function createStandaloneReferenceSources() {
return {
earthquakes: createUsgsEarthquakeSource(),
cables: createBundledCableSource(),
};
}
/** Select standalone providers without starting their acquisition. */
export function createStandaloneLayerSources() {
return {
...createStandaloneReferenceSources(),
flights: createOpenSkySource(),
military: createAdsbLolSource(),
vessels: createAisStreamSource({
apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live',
}),
cctv: createCctvSource(),
radio: createRadioSource(),
traffic: createTrafficSource(),
bikeshare: createBikeshareSource(),
installations: createInstallationSource(),
satellites: createSatelliteSource(),
launches: createLaunchSource(),
alpr: createOverpassAlprSource(),
firms: createFirmsSource(),
};
}
+15 -2
View File
@@ -1,2 +1,15 @@
/** Compatibility entry for the application's UI composition. */
export { StyleManager } from './ui/composition.js';
import { StyleManager as ApplicationControls } from './ui/composition.js';
import { catalogControlServices } from './app/catalog.js';
import { getStandaloneCatalog } from './standalone/catalog.js';
/** Compatibility entry for direct standalone control construction. */
export class StyleManager extends ApplicationControls {
constructor(viewer, options = {}) {
super(viewer, {
...options,
services: {
...catalogControlServices(getStandaloneCatalog()),
...options.services,
},
});
}
}
-22
View File
@@ -40,18 +40,7 @@ import {
setDetectionStyle,
setDetectionTuning,
} from '../data/detection.js';
import trafficLayer from '../data/traffic.js';
import flightsLayer from '../data/flights.js';
import militaryFlightsLayer from '../data/militaryFlights.js';
import { isTr3b, toggleTr3b } from '../data/tr3bRegistry.js';
import satellitesLayer from '../data/satellites.js';
import cctvLayer from '../data/cctv.js';
import radioLayer from '../data/radio.js';
import bikeshareLayer from '../data/bikeshare.js';
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
import militaryAwarenessLayer from '../data/militaryAwareness.js';
import militaryInstallationsLayer from '../data/militaryInstallations.js';
import rocketLaunchesLayer from '../data/rocketLaunches.js';
import {
cachedGroundFloor,
cachedMeshFloor,
@@ -114,19 +103,8 @@ export class StyleManager extends ApplicationShell {
resumeDetection,
setDetectionStyle,
setDetectionTuning,
trafficLayer,
flightsLayer,
militaryFlightsLayer,
isTr3b,
toggleTr3b,
satellitesLayer,
cctvLayer,
radioLayer,
bikeshareLayer,
aisLiveVesselsLayer,
militaryAwarenessLayer,
militaryInstallationsLayer,
rocketLaunchesLayer,
cachedGroundFloor,
cachedMeshFloor,
GROUND_FLOOR_LIFT_M,
+16 -20
View File
@@ -8,15 +8,10 @@ import {
getSelectedEntityContext,
isContextRecordActive,
} from '../data/contextStore.js';
import { getNextIssPass } from '../data/satellites.js';
import { CCTV_FOCUS_RESULT } from '../data/cctv.js';
import { CCTV_FOCUS_RESULT } from '../layers/cctv/index.js';
import { contextModeWord } from '../contextModePolicy.js';
import { createAnalystEngine } from '../data/analystEngine.js';
import { layerFeedState } from '../data/manager.js';
import militaryAwarenessLayer, {
collectAircraftProximityWindow,
contactsWindowFromSnapshot,
} from '../data/militaryAwareness.js';
import { initCameraVerbs, moveCamera, flyRoute, interruptCameraMotion, adjustOrbitRange } from '../cameraVerbs.js';
import { cachedGroundFloor, warmGroundFloor } from '../data/groundFloor.js';
import { isPickedWorldPosition } from '../data/scenePick.js';
@@ -612,7 +607,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
};
}
const contactsWindow = ['contacts', 'flights'].includes(mode)
? activeContactsWindow()
? activeContactsWindow(dataManager)
: null;
return {
...withContextModeVocabulary(result),
@@ -792,7 +787,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
}
if (name === 'next_iss_pass') {
return nextIssPass(viewer, args);
return nextIssPass(viewer, dataManager, args);
}
if (name === 'analyst_query') {
@@ -2007,7 +2002,7 @@ function compassDir(azDeg) {
return COMPASS_16[Math.round(((((azDeg % 360) + 360) % 360)) / 22.5) % 16];
}
function nextIssPass(viewer, args) {
function nextIssPass(viewer, dataManager, args) {
let latDeg = Number.isFinite(args.latitude) ? args.latitude : null;
let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null;
if (latDeg == null || lonDeg == null) {
@@ -2017,7 +2012,7 @@ function nextIssPass(viewer, args) {
lonDeg = Cesium.Math.toDegrees(carto.longitude);
}
const minElevDeg = Number.isFinite(args.minElevationDeg) ? args.minElevationDeg : 10;
const result = getNextIssPass({ latDeg, lonDeg, minElevDeg });
const result = dataManager?.layers?.get('satellites')?.module?.getNextIssPass?.({ latDeg, lonDeg, minElevDeg }) ?? { status: 'no-tle' };
if (result.status === 'no-tle') {
return {
ok: false,
@@ -2337,7 +2332,7 @@ function getCurrentViewState(viewer, styleManager, dataManager, sceneDirector =
...withContextModeVocabulary(styleManager.getContextModeState()),
// The numbers on the operator's Contacts panel, so a window/count
// question can be answered from what they are looking at.
...(activeContactsWindow() ? { contactsWindow: activeContactsWindow() } : {}),
...(activeContactsWindow(dataManager) ? { contactsWindow: activeContactsWindow(dataManager) } : {}),
}
: null,
cockpit: typeof styleManager.getCockpitState === 'function'
@@ -2408,19 +2403,19 @@ async function getEntityContext(viewer, dataManager, styleManager, args = {}, se
* @param {object} result The general engine's result, reused for scope text.
* @returns {object|null} A unified-count payload, or null.
*/
function aircraftProximityWindowForQuery(args, result) {
function aircraftProximityWindowForQuery(dataManager, args, result) {
const scope = args?.scope;
if (String(scope?.kind || '').toLowerCase() !== 'radius') return null;
const layers = Array.isArray(args.layers) ? args.layers : [];
if (!layers.some((layer) => layer === 'flights' || layer === 'military')) return null;
const snapshot = militaryAwarenessLayer.getContextSnapshot?.();
const snapshot = dataManager?.layers?.get('military-awareness')?.module?.getContextSnapshot?.();
const subject = snapshot?.subject;
if (!subject?.position) return null;
// An explicit centre only qualifies when it IS the subject; otherwise the
// operator asked about somewhere else and must get that answer.
if (scope.center && !centerMatchesSubject(scope.center, subject.position)) return null;
const radiusM = Number.isFinite(scope.km) ? scope.km * 1000 : (snapshot.radiusM || 250_000);
const window = collectAircraftProximityWindow(subject.position, { radiusM, subject });
const window = dataManager.layers.get('military-awareness').module.collectAircraftProximityWindow(subject.position, { radiusM, subject });
if (!window) return null;
const label = subject.label || subject.id || 'the selected contact';
const radiusKm = Math.round(radiusM / 1000);
@@ -2459,7 +2454,7 @@ function aircraftProximityWindowForQuery(args, result) {
military: window.military.length,
aircraft: window.aircraft,
},
...(activeContactsWindow() ? { contactsWindow: activeContactsWindow() } : {}),
...(activeContactsWindow(dataManager) ? { contactsWindow: activeContactsWindow(dataManager) } : {}),
};
}
@@ -3235,9 +3230,10 @@ const VIEWPORT_LOADED_LAYERS = new Set(['flights']);
* diverge no matter which surface asks.
* @returns {object|null} Panel-equivalent window counts.
*/
function activeContactsWindow() {
function activeContactsWindow(dataManager) {
try {
return contactsWindowFromSnapshot(militaryAwarenessLayer.getContextSnapshot?.());
const layer = dataManager?.layers?.get('military-awareness')?.module;
return layer?.contactsWindowFromSnapshot?.(layer.getContextSnapshot?.()) ?? null;
} catch {
return null;
}
@@ -3263,7 +3259,7 @@ function analystProviders(viewer, dataManager, { recordLimitByLayer = null, plac
* @returns {{lat: number, lon: number, label: string|null}|null} Subject centre.
*/
getContextSubject() {
const snapshot = militaryAwarenessLayer.getContextSnapshot?.();
const snapshot = dataManager?.layers?.get('military-awareness')?.module?.getContextSnapshot?.();
const subject = snapshot?.subject;
if (!subject?.position) return null;
const carto = Cesium.Cartographic.fromCartesian(subject.position);
@@ -3343,10 +3339,10 @@ async function runAnalystQuery(viewer, dataManager, args = {}, placeSearch = una
// differ. The generic record/scope engine still owns explicit regions and
// arbitrary points — only "how many aircraft around <this contact>" is
// unified, because that is the question the panel is already answering.
const entityWindow = aircraftProximityWindowForQuery(args, result);
const entityWindow = aircraftProximityWindowForQuery(dataManager, args, result);
if (entityWindow) return entityWindow;
const contactsWindow = activeContactsWindow();
const contactsWindow = activeContactsWindow(dataManager);
const aircraftQueried = (result.coverage?.layersQueried || [])
.some((l) => l.layerKey === 'flights' || l.layerKey === 'military');
// Both numbers, and which one answers the question. The window counts have
+33 -15
View File
@@ -2838,7 +2838,7 @@ async function withAwareness(harness, run) {
: originals.cartoFrom(value)
);
try {
return await run();
return await run(awareness);
} finally {
awareness.getContextSnapshot = originals.snapshot;
flightsLayer.getNearby = originals.flightsNearby;
@@ -2847,7 +2847,7 @@ async function withAwareness(harness, run) {
}
}
function analystRunner() {
function analystRunner(awareness) {
const flights = {
id: 'flights',
// Deliberately a DIFFERENT population from the proximity window: this is
@@ -2869,7 +2869,7 @@ function analystRunner() {
viewer,
styleManager: {},
dataManager: {
layers: new Map([['flights', { module: flights }]]),
layers: new Map([['flights', { module: flights }], ['military-awareness', { module: awareness }]]),
isEnabled: (id) => id === 'flights',
getAll: () => [{ id: 'flights', name: 'Live Flights', enabled: true, stats: { count: 1 } }],
},
@@ -2883,8 +2883,8 @@ test('front5: a nearby ask centres on the Contacts SUBJECT, not the selected dat
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}`, distance: 1000 * i })),
military: Array.from({ length: 5 }, (_, i) => ({ id: `M${i}`, icao24: `m${i}`, distance: 500 * i })),
});
await withAwareness(harness, async () => {
const runner = analystRunner();
await withAwareness(harness, async (awareness) => {
const runner = analystRunner(awareness);
const result = await runner('analyst_query', {
layers: ['flights', 'military'],
// The centre the model reached for in the field: the selected datacenter.
@@ -2913,13 +2913,13 @@ test('front5: the spoken count and the panel window are ONE number by constructi
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })),
military: Array.from({ length: 5 }, (_, i) => ({ id: `M${i}`, icao24: `m${i}` })),
});
await withAwareness(harness, async () => {
await withAwareness(harness, async (awareness) => {
// What the PANEL computes for this subject...
const panel = collectAircraftProximityWindow(harness.snapshot.subject.position, {
subject: harness.snapshot.subject,
});
// ...and what VOICE answers for the same subject.
const spoken = await analystRunner()('analyst_query', {
const spoken = await analystRunner(awareness)('analyst_query', {
layers: ['flights', 'military'],
scope: { kind: 'radius', km: 250 },
});
@@ -2936,8 +2936,8 @@ test('front5: the spoken count and the panel window are ONE number by constructi
test('front5: Contacts active with NO subject falls back to the view, not an empty panel', async () => {
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
const harness = awarenessSubjectHarness({ subject: null });
await withAwareness(harness, async () => {
const result = await analystRunner()('analyst_query', {
await withAwareness(harness, async (awareness) => {
const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights'],
scope: { kind: 'radius', km: 250 },
});
@@ -2954,8 +2954,8 @@ test('front5: an explicit region still uses the region engine while Contacts is
subject: { id: 'a1b2c3', label: 'N546PC' },
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })),
});
await withAwareness(harness, async () => {
const result = await analystRunner()('analyst_query', {
await withAwareness(harness, async (awareness) => {
const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights'],
scope: { kind: 'region', name: 'Texas' },
});
@@ -2988,8 +2988,8 @@ test('front5: the box DIAGONAL is not the subject — 1.32 km away is somewhere
// separation is 1.32 km. This is the case the coordinator flagged: a centre
// far enough to be a different place, slipping through on the diagonal.
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
await withAwareness(subjectWindowHarness(), async () => {
const result = await analystRunner()('analyst_query', {
await withAwareness(subjectWindowHarness(), async (awareness) => {
const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights', 'military'],
scope: { kind: 'radius', km: 250, center: { lat: 29.9 + 0.009, lon: -97.9 + 0.009 } },
});
@@ -3009,8 +3009,8 @@ test('front5: 0.99 km due EAST is the subject, though a degree box rejects it',
// 0.99 km — inside 1 km — yet over the 0.01 box threshold. A box would send
// the operator a different, smaller number for a centre that IS the contact.
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
await withAwareness(subjectWindowHarness(), async () => {
const result = await analystRunner()('analyst_query', {
await withAwareness(subjectWindowHarness(), async (awareness) => {
const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights', 'military'],
scope: { kind: 'radius', km: 250, center: { lat: 29.9, lon: -97.9 + 0.0103 } },
});
@@ -3164,3 +3164,21 @@ test('ALPR common names toggle only the registered camera layer through the norm
}
}
});
test('ISS voice lookup uses the registered satellite instance', async () => {
const calls = [];
const viewer = {
clock: { onTick: { addEventListener: () => () => {} } },
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
camera: { moveEnd: { addEventListener() {} } },
};
const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager: {
layers: new Map([['satellites', { module: { getNextIssPass(query) {
calls.push(query);
return { status: 'none' };
} } }]]),
} });
const result = await runner('next_iss_pass', { latitude: 30, longitude: -97, minElevationDeg: 15 });
assert.deepEqual(calls, [{ latDeg: 30, lonDeg: -97, minElevDeg: 15 }]);
assert.match(result.error, /No ISS pass above 15/);
});