refactor: construct application layers from supplied sources

This commit is contained in:
Sameh Khamis committed 2026-09-14 14:20:01 -07:00
1 parent cd704d0e9b
commit 8160ac5fe4
53 files changed
+1152 -400

No files matched your search

+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)),
});
}
+6 -39
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,
},
militaryRegistry,
});
export { TRACKED_MODEL_MAX_PX } from '../layers/flights/policy.js';
export const _floorGroundedDisplayPositionForTest =
+6 -14
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: {
flights,
military,
vessels,
installations,
navigation,
geometry,
render,
},
const layer = createApplicationAwareness({
flights,
military,
vessels,
installations,
});
export const contextTargetFlyToAllowed = layer.contextTargetFlyToAllowed;
export const awarenessClearMatchesSubject = layer.awarenessClearMatchesSubject;
+6 -39
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,
},
militaryRegistry,
});
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/);
});