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
+1152 -400

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] ## [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. - 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. - 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. `standalone/catalog` selects the existing page-scoped default instances and metadata.
Reusable data setup imports no standalone layer defaults. The current compatibility Reusable data setup imports no standalone layer defaults. The current compatibility
source setters remain available while callers migrate to instance construction. 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 # 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. Application data setup receives an ordered layer catalog and serialization metadata.
Controls use the same instances. The standalone composition selects the existing Controls use the same instances. The standalone composition selects the existing
default catalog, and restoration starts only after all registrations are sealed. default catalog, and restoration starts only after all registrations are sealed.
+2 -1
View File
@@ -193,6 +193,7 @@
"node": "./build/application-html.js" "node": "./build/application-html.js"
}, },
"./application/catalog": "./src/app/catalog.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 exit 1
fi 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" echo "error: CCTV layer not wired in src/standalone/catalog.js"
exit 1 exit 1
fi fi
+20 -1
View File
@@ -558,5 +558,24 @@
"src/toolProjectRoot.test.mjs", "src/toolProjectRoot.test.mjs",
"src/app/catalog.js", "src/app/catalog.js",
"src/standalone/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/weatherEffectsMath.js",
"src/worldFocus.js", "src/worldFocus.js",
"src/app/catalog.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": [ "external": [
"@mapbox/vector-tile", "@mapbox/vector-tile",
@@ -1518,5 +1536,328 @@
"exports": ["./application/catalog"], "exports": ["./application/catalog"],
"modules": ["src/app/catalog.js"], "modules": ["src/app/catalog.js"],
"external": [] "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 Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
const v = window.__godsEyeView.viewer; const v = window.__godsEyeView.viewer;
const fl = window.__godsEyeView.dataManager.layers.get('flights').module; const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
// `.module` is the layer OBJECT (the default export), not the module // Test the registered instance directly, including catalogs constructed
// namespace, so the handoff seam is not on it. Reach the namespace the // with their own sources. Never import a second compatibility instance.
// same way this group reaches groundFloor's: offer the URL the app const ns = fl.testing;
// itself loaded (Vite serves an edited file as `…?t=<hmr stamp>`, and if (typeof ns?._driveFleetModelHandoffForTest !== 'function')
// the plain path would hand back a second, unrelated instance whose return { error: "the registered flights instance has no handoff test seam" };
// 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})` };
// Use a scenario-owned contact so its groundSnap entry is provably cold; // Use a scenario-owned contact so its groundSnap entry is provably cold;
// earlier display-floor cases intentionally exercise aaa097's cache. // earlier display-floor cases intentionally exercise aaa097's cache.
const holdIcao = 'aaa098'; 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')); dataManager.buildTogglePanel(document.getElementById('data-toggles'));
styleManager.attachDataManager(dataManager); 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 { createApplicationVessels } from '../app/layers/aisLiveVessels.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';
const aisLiveVesselsLayer = createVesselLayer({ import { createAisStreamSource } from '../sources/live/standalone.js';
const aisLiveVesselsLayer = createApplicationVessels({
source: createAisStreamSource({ source: createAisStreamSource({
apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live', 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, maxRows: import.meta.env?.VITE_AIS_LIVE_MAX_ROWS,
maxLabels: import.meta.env?.VITE_AIS_LIVE_LABEL_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 { AIS_FIRST_CONNECT_GRACE_MS } from '../layers/vessels/policy.js';
export const deriveAisFeedError = aisLiveVesselsLayer.deriveAisFeedError; 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 { createSourceSlot } from '../app/sourceSlot.js';
import { import { createOverpassAlprSource } from '../layers/alpr/index.js';
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';
export * from '../layers/alpr/index.js'; export * from '../layers/alpr/index.js';
const slot = createSourceSlot( const slot = createSourceSlot(
createOverpassAlprSource(), createOverpassAlprSource(),
@@ -14,7 +8,6 @@ const slot = createSourceSlot(
'ALPR source', 'ALPR source',
); );
export const configureAlprSource = slot.configure; export const configureAlprSource = slot.configure;
export default createAlprCamerasLayer({ export default createApplicationAlpr({
source: slot.source, 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 { createSourceSlot } from '../app/sourceSlot.js';
import { createBikeshareSource } from '../layers/bikeshare/source.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( const sourceSlot = createSourceSlot(
createBikeshareSource(), createBikeshareSource(),
@@ -12,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Bikeshare source', 'Bikeshare source',
); );
export const configureBikeshareSource = sourceSlot.configure; export const configureBikeshareSource = sourceSlot.configure;
const layer = createBikeshareLayer({ const layer = createApplicationBikeshare({
source: sourceSlot.source, source: sourceSlot.source,
services: { render, sprites, picking, overlays },
}); });
export const createBikeshareSelectedOverlayEntry = export const createBikeshareSelectedOverlayEntry =
layer.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 { createSourceSlot } from '../app/sourceSlot.js';
import { createCctvSource } from '../layers/cctv/source.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( const sourceSlot = createSourceSlot(
createCctvSource(), createCctvSource(),
@@ -18,20 +8,8 @@ const sourceSlot = createSourceSlot(
'Cctv source', 'Cctv source',
); );
export const configureCctvSource = sourceSlot.configure; export const configureCctvSource = sourceSlot.configure;
const layer = createCctvLayer({ const layer = createApplicationCctv({
source: sourceSlot.source, source: sourceSlot.source,
services: {
sprites,
activation,
overlays,
locations,
picking,
terrain,
ground,
mesh,
focus,
render,
},
}); });
export const calibrationPatchMovesAnchor = layer.calibrationPatchMovesAnchor; export const calibrationPatchMovesAnchor = layer.calibrationPatchMovesAnchor;
export const migrateRangeScaleForFloor = layer.migrateRangeScaleForFloor; export const migrateRangeScaleForFloor = layer.migrateRangeScaleForFloor;
+4 -15
View File
@@ -1,22 +1,11 @@
import { import { createUsgsEarthquakeSource } from '../layers/earthquakes/index.js';
createEarthquakesLayer as createLayer, import { createApplicationEarthquakes } from '../app/layers/earthquakes.js';
createUsgsEarthquakeSource,
} from '../layers/earthquakes/index.js';
import {
clearOverlaySource,
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
export * from '../layers/earthquakes/index.js'; export * from '../layers/earthquakes/index.js';
/** Wire the standalone source and application overlay owner. */ /** Wire the standalone source and application overlay owner. */
export function createEarthquakesLayer({ export function createEarthquakesLayer({
source = createUsgsEarthquakeSource(), source = createUsgsEarthquakeSource(),
overlayHost = { ...options
setEntries: setOverlayEntries,
setVisible: setOverlaySourceVisible,
clearSource: clearOverlaySource,
},
} = {}) { } = {}) {
return createLayer({ source, overlayHost }); return createApplicationEarthquakes({ source, ...options });
} }
export default createEarthquakesLayer(); 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 { createSourceSlot } from '../app/sourceSlot.js';
import * as Cesium from 'cesium';
import { import {
createFirmsHeatmapLayer as createLayer,
createFirmsHelpers, createFirmsHelpers,
createFirmsSource, createFirmsSource,
} from '../layers/firms/index.js'; } 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( const sourceSlot = createSourceSlot(
createFirmsSource(), createFirmsSource(),
['getSnapshot'], ['getSnapshot'],
'Fire source', 'Fire source',
); );
export const configureFirmsSource = sourceSlot.configure; export const configureFirmsSource = sourceSlot.configure;
const helpers = createFirmsHelpers({ services }); const helpers = createFirmsHelpers({ services: firmsServices });
export const mapAnalystRecord = helpers.mapAnalystRecord; export const mapAnalystRecord = helpers.mapAnalystRecord;
export const fireCullPosition = helpers.fireCullPosition; export const fireCullPosition = helpers.fireCullPosition;
export const applyHorizonCull = helpers.applyHorizonCull; export const applyHorizonCull = helpers.applyHorizonCull;
@@ -36,22 +19,10 @@ export const buildFireCard = helpers.buildFireCard;
export const buildCellCard = helpers.buildCellCard; export const buildCellCard = helpers.buildCellCard;
export const applyFirmsOverlayPolicy = helpers.applyFirmsOverlayPolicy; 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) { export function createFirmsHeatmapLayer(options) {
return createLayer({ return createApplicationFirms({
...options, ...options,
icon: options.icon ?? '▲',
source: options.source ?? 'NASA FIRMS',
feed: options.feed ?? sourceSlot.source, 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 { createApplicationFlights } from '../app/layers/flights.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';
const flightsLayer = createCivilFlightLayer({ import { createOpenSkySource } from '../sources/live/standalone.js';
import * as militaryRegistry from './militaryRegistry.js';
const flightsLayer = createApplicationFlights({
source: createOpenSkySource(), source: createOpenSkySource(),
resolveAsset: (url) => militaryRegistry,
`${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 { TRACKED_MODEL_MAX_PX } from '../layers/flights/policy.js';
export const _floorGroundedDisplayPositionForTest = 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 flights from './flights.js';
import military from './militaryFlights.js'; import military from './militaryFlights.js';
import vessels from './aisLiveVessels.js'; import vessels from './aisLiveVessels.js';
import installations from './militaryInstallations.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({ const layer = createApplicationAwareness({
services: { flights,
flights, military,
military, vessels,
vessels, installations,
installations,
navigation,
geometry,
render,
},
}); });
export const contextTargetFlyToAllowed = layer.contextTargetFlyToAllowed; export const contextTargetFlyToAllowed = layer.contextTargetFlyToAllowed;
export const awarenessClearMatchesSubject = layer.awarenessClearMatchesSubject; export const awarenessClearMatchesSubject = layer.awarenessClearMatchesSubject;
+6 -39
View File
@@ -1,44 +1,11 @@
import { createMilitaryFlightLayer } from '../layers/military/index.js'; import { createApplicationMilitary } from '../app/layers/militaryFlights.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';
const militaryFlightsLayer = createMilitaryFlightLayer({ import { createAdsbLolSource } from '../sources/live/standalone.js';
import * as militaryRegistry from './militaryRegistry.js';
const militaryFlightsLayer = createApplicationMilitary({
source: createAdsbLolSource(), source: createAdsbLolSource(),
resolveAsset: (url) => militaryRegistry,
`${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 { TRACKED_MODEL_MAX_PX } from '../layers/military/policy.js';
export const _setTrackedMilitaryRefreshStateForTest = 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 { createSourceSlot } from '../app/sourceSlot.js';
import { import { createInstallationSource } from '../layers/installations/index.js';
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';
const sourceSlot = createSourceSlot( const sourceSlot = createSourceSlot(
createInstallationSource(), createInstallationSource(),
@@ -15,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Installation source', 'Installation source',
); );
export const configureInstallationSource = sourceSlot.configure; export const configureInstallationSource = sourceSlot.configure;
const layer = createInstallationsLayer({ const layer = createApplicationInstallations({
source: sourceSlot.source, source: sourceSlot.source,
services: { render, context, ground, anchors, picking },
}); });
export const approximateSurfaceDistanceM = layer.approximateSurfaceDistanceM; export const approximateSurfaceDistanceM = layer.approximateSurfaceDistanceM;
export const classifyGoogleMilitaryPlace = layer.classifyGoogleMilitaryPlace; 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 { createSourceSlot } from '../app/sourceSlot.js';
import { createRadioSource } from '../layers/radio/source.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( const sourceSlot = createSourceSlot(
createRadioSource(), createRadioSource(),
@@ -13,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Radio source', 'Radio source',
); );
export const configureRadioSource = sourceSlot.configure; export const configureRadioSource = sourceSlot.configure;
const layer = createRadioLayer({ const layer = createApplicationRadio({
source: sourceSlot.source, source: sourceSlot.source,
services: { ground, picking, overlays, globe, render },
}); });
export const radioGlobeLabel = layer.radioGlobeLabel; export const radioGlobeLabel = layer.radioGlobeLabel;
export const createRadioSelectedOverlayEntry = 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 { createSourceSlot } from '../app/sourceSlot.js';
import { import { createLaunchSource } from '../layers/launches/index.js';
createRocketLaunchesLayer,
createLaunchSource,
} from '../layers/launches/index.js';
import * as satellites from './satellites.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( const sourceSlot = createSourceSlot(
createLaunchSource(), createLaunchSource(),
@@ -14,9 +9,9 @@ const sourceSlot = createSourceSlot(
'Launch source', 'Launch source',
); );
export const configureLaunchSource = sourceSlot.configure; export const configureLaunchSource = sourceSlot.configure;
const layer = createRocketLaunchesLayer({ const layer = createApplicationLaunches({
source: sourceSlot.source, source: sourceSlot.source,
services: { satellites, geometry, overlays, render }, satellites,
}); });
export const satelliteParamsForSpaceMissions = export const satelliteParamsForSpaceMissions =
layer.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 { createSourceSlot } from '../app/sourceSlot.js';
import { import { createSatelliteSource } from '../layers/satellites/index.js';
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';
const sourceSlot = createSourceSlot( const sourceSlot = createSourceSlot(
createSatelliteSource(), createSatelliteSource(),
@@ -17,9 +8,8 @@ const sourceSlot = createSourceSlot(
'Satellite source', 'Satellite source',
); );
export const configureSatelliteSource = sourceSlot.configure; export const configureSatelliteSource = sourceSlot.configure;
const layer = createSatellitesLayer({ const layer = createApplicationSatellites({
source: sourceSlot.source, source: sourceSlot.source,
services: { picking, focus, readout, overlays, context, render, layerState },
}); });
export const satelliteVisualsVisible = layer.satelliteVisualsVisible; export const satelliteVisualsVisible = layer.satelliteVisualsVisible;
export const satelliteCatalogModeChanged = layer.satelliteCatalogModeChanged; export const satelliteCatalogModeChanged = layer.satelliteCatalogModeChanged;
+4 -20
View File
@@ -1,26 +1,10 @@
import * as Cesium from 'cesium'; import { createApplicationCables } from '../app/layers/submarineCables.js';
import { import { overlayHost } from '../app/layers/overlayHost.js';
createSubmarineCableLayer, import { createCableOverlayPublisher as createPublisher } from '../layers/submarineCables/index.js';
createCableOverlayPublisher as createPublisher,
} from '../layers/submarineCables/index.js';
import { createBundledCableSource } from '../layers/submarineCables/bundledSource.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 = {}) { export function createTeleGeographySubmarineCableLayer(options = {}) {
return createSubmarineCableLayer({ return createApplicationCables({
source: createBundledCableSource(), source: createBundledCableSource(),
overlayHost,
screenSpaceEventHandlerFactory: (canvas) =>
new Cesium.ScreenSpaceEventHandler(canvas),
mapStackEventTarget: typeof window !== 'undefined' ? window : null,
...options, ...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', () => { test('cables create no native labels and no per-frame geometry callbacks', () => {
const source = [ const source = [
'../../data/telegeographySubmarineCables.js', '../../data/telegeographySubmarineCables.js',
'../../app/layers/submarineCables.js',
'../../app/layers/overlayHost.js',
'rendering.js', 'rendering.js',
'overlay.js', 'overlay.js',
'surface.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 { createSourceSlot } from '../app/sourceSlot.js';
import { createTrafficSource } from '../layers/traffic/source.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( const sourceSlot = createSourceSlot(
createTrafficSource(), createTrafficSource(),
@@ -14,9 +12,8 @@ const sourceSlot = createSourceSlot(
}, },
); );
export const configureTrafficSource = sourceSlot.configure; export const configureTrafficSource = sourceSlot.configure;
const layer = createTrafficLayer({ const layer = createApplicationTraffic({
source: sourceSlot.source, source: sourceSlot.source,
services: { credits, render },
}); });
export const getTrafficTimingDiagnostics = layer.getTrafficTimingDiagnostics; export const getTrafficTimingDiagnostics = layer.getTrafficTimingDiagnostics;
export const deriveTrafficFlowError = layer.deriveTrafficFlowError; 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 }); await fs.mkdir(path.join(root, 'src', 'standalone'), { recursive: true });
const catalog = await fs.readFile(new URL('./standalone/catalog.js', import.meta.url), 'utf8'); 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.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.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); 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 loadingScreen = document.getElementById('loading-screen');
const loaderStatus = loadingScreen.querySelector('.loader-status'); const loaderStatus = loadingScreen.querySelector('.loader-status');
let placeSearch; let placeSearch;
const catalog = createStandaloneCatalog(); let catalog;
return createApplication({ return createApplication({
createScene: (context) => { createScene: (context) => {
catalog = createStandaloneCatalog({ signal: context.signal });
placeSearch = createStandalonePlaceSearch({ placeSearch = createStandalonePlaceSearch({
...geospatial, ...geospatial,
resolveApiKey: () => googleApiKey, resolveApiKey: () => googleApiKey,
+20 -37
View File
@@ -1,40 +1,23 @@
import flightsLayer from '../data/flights.js'; import { createApplicationCatalog } from '../app/constructCatalog.js';
import militaryFlightsLayer from '../data/militaryFlights.js'; import { createStandaloneLayerSources } from './layerSources.js';
import alprCamerasLayer from '../data/alprCameras.js'; export { createStandaloneReferenceSources } from './layerSources.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 { 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. */ // Direct compatibility callers share one catalog; application startup supplies its own.
export function createStandaloneCatalog() { let compatibilityCatalog;
return createLayerCatalog( export function getStandaloneCatalog() {
[ return (compatibilityCatalog ||= createStandaloneCatalog());
flightsLayer,
militaryFlightsLayer,
earthquakesLayer,
alprCamerasLayer,
satellitesLayer,
rocketLaunchesLayer,
trafficLayer,
cctvLayer,
radioLayer,
bikeshareLayer,
aisLiveVesselsLayer,
militaryInstallationsLayer,
militaryAwarenessLayer,
...localDataLayers,
],
LAYER_STATE_REGISTRY,
);
} }
+2 -2
View File
@@ -1,8 +1,8 @@
import { createApplicationControls } from '../app/controls.js'; import { createApplicationControls } from '../app/controls.js';
import { createStandaloneCatalog } from './catalog.js'; import { getStandaloneCatalog } from './catalog.js';
export function createStandaloneControls(options) { export function createStandaloneControls(options) {
return createApplicationControls({ return createApplicationControls({
catalog: createStandaloneCatalog(), catalog: options?.catalog ?? getStandaloneCatalog(),
...options, ...options,
}); });
} }
+2 -2
View File
@@ -1,8 +1,8 @@
import { createApplicationData } from '../app/data.js'; import { createApplicationData } from '../app/data.js';
import { createStandaloneCatalog } from './catalog.js'; import { getStandaloneCatalog } from './catalog.js';
export function createStandaloneData(options) { export function createStandaloneData(options) {
return createApplicationData({ return createApplicationData({
catalog: createStandaloneCatalog(), catalog: options?.catalog ?? getStandaloneCatalog(),
...options, ...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. */ import { StyleManager as ApplicationControls } from './ui/composition.js';
export { StyleManager } 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, setDetectionStyle,
setDetectionTuning, setDetectionTuning,
} from '../data/detection.js'; } 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 { 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 { import {
cachedGroundFloor, cachedGroundFloor,
cachedMeshFloor, cachedMeshFloor,
@@ -114,19 +103,8 @@ export class StyleManager extends ApplicationShell {
resumeDetection, resumeDetection,
setDetectionStyle, setDetectionStyle,
setDetectionTuning, setDetectionTuning,
trafficLayer,
flightsLayer,
militaryFlightsLayer,
isTr3b, isTr3b,
toggleTr3b, toggleTr3b,
satellitesLayer,
cctvLayer,
radioLayer,
bikeshareLayer,
aisLiveVesselsLayer,
militaryAwarenessLayer,
militaryInstallationsLayer,
rocketLaunchesLayer,
cachedGroundFloor, cachedGroundFloor,
cachedMeshFloor, cachedMeshFloor,
GROUND_FLOOR_LIFT_M, GROUND_FLOOR_LIFT_M,
+16 -20
View File
@@ -8,15 +8,10 @@ import {
getSelectedEntityContext, getSelectedEntityContext,
isContextRecordActive, isContextRecordActive,
} from '../data/contextStore.js'; } from '../data/contextStore.js';
import { getNextIssPass } from '../data/satellites.js'; import { CCTV_FOCUS_RESULT } from '../layers/cctv/index.js';
import { CCTV_FOCUS_RESULT } from '../data/cctv.js';
import { contextModeWord } from '../contextModePolicy.js'; import { contextModeWord } from '../contextModePolicy.js';
import { createAnalystEngine } from '../data/analystEngine.js'; import { createAnalystEngine } from '../data/analystEngine.js';
import { layerFeedState } from '../data/manager.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 { initCameraVerbs, moveCamera, flyRoute, interruptCameraMotion, adjustOrbitRange } from '../cameraVerbs.js';
import { cachedGroundFloor, warmGroundFloor } from '../data/groundFloor.js'; import { cachedGroundFloor, warmGroundFloor } from '../data/groundFloor.js';
import { isPickedWorldPosition } from '../data/scenePick.js'; import { isPickedWorldPosition } from '../data/scenePick.js';
@@ -612,7 +607,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
}; };
} }
const contactsWindow = ['contacts', 'flights'].includes(mode) const contactsWindow = ['contacts', 'flights'].includes(mode)
? activeContactsWindow() ? activeContactsWindow(dataManager)
: null; : null;
return { return {
...withContextModeVocabulary(result), ...withContextModeVocabulary(result),
@@ -792,7 +787,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
} }
if (name === 'next_iss_pass') { if (name === 'next_iss_pass') {
return nextIssPass(viewer, args); return nextIssPass(viewer, dataManager, args);
} }
if (name === 'analyst_query') { if (name === 'analyst_query') {
@@ -2007,7 +2002,7 @@ function compassDir(azDeg) {
return COMPASS_16[Math.round(((((azDeg % 360) + 360) % 360)) / 22.5) % 16]; 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 latDeg = Number.isFinite(args.latitude) ? args.latitude : null;
let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null; let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null;
if (latDeg == null || lonDeg == null) { if (latDeg == null || lonDeg == null) {
@@ -2017,7 +2012,7 @@ function nextIssPass(viewer, args) {
lonDeg = Cesium.Math.toDegrees(carto.longitude); lonDeg = Cesium.Math.toDegrees(carto.longitude);
} }
const minElevDeg = Number.isFinite(args.minElevationDeg) ? args.minElevationDeg : 10; 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') { if (result.status === 'no-tle') {
return { return {
ok: false, ok: false,
@@ -2337,7 +2332,7 @@ function getCurrentViewState(viewer, styleManager, dataManager, sceneDirector =
...withContextModeVocabulary(styleManager.getContextModeState()), ...withContextModeVocabulary(styleManager.getContextModeState()),
// The numbers on the operator's Contacts panel, so a window/count // The numbers on the operator's Contacts panel, so a window/count
// question can be answered from what they are looking at. // question can be answered from what they are looking at.
...(activeContactsWindow() ? { contactsWindow: activeContactsWindow() } : {}), ...(activeContactsWindow(dataManager) ? { contactsWindow: activeContactsWindow(dataManager) } : {}),
} }
: null, : null,
cockpit: typeof styleManager.getCockpitState === 'function' 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. * @param {object} result The general engine's result, reused for scope text.
* @returns {object|null} A unified-count payload, or null. * @returns {object|null} A unified-count payload, or null.
*/ */
function aircraftProximityWindowForQuery(args, result) { function aircraftProximityWindowForQuery(dataManager, args, result) {
const scope = args?.scope; const scope = args?.scope;
if (String(scope?.kind || '').toLowerCase() !== 'radius') return null; if (String(scope?.kind || '').toLowerCase() !== 'radius') return null;
const layers = Array.isArray(args.layers) ? args.layers : []; const layers = Array.isArray(args.layers) ? args.layers : [];
if (!layers.some((layer) => layer === 'flights' || layer === 'military')) return null; 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; const subject = snapshot?.subject;
if (!subject?.position) return null; if (!subject?.position) return null;
// An explicit centre only qualifies when it IS the subject; otherwise the // An explicit centre only qualifies when it IS the subject; otherwise the
// operator asked about somewhere else and must get that answer. // operator asked about somewhere else and must get that answer.
if (scope.center && !centerMatchesSubject(scope.center, subject.position)) return null; if (scope.center && !centerMatchesSubject(scope.center, subject.position)) return null;
const radiusM = Number.isFinite(scope.km) ? scope.km * 1000 : (snapshot.radiusM || 250_000); 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; if (!window) return null;
const label = subject.label || subject.id || 'the selected contact'; const label = subject.label || subject.id || 'the selected contact';
const radiusKm = Math.round(radiusM / 1000); const radiusKm = Math.round(radiusM / 1000);
@@ -2459,7 +2454,7 @@ function aircraftProximityWindowForQuery(args, result) {
military: window.military.length, military: window.military.length,
aircraft: window.aircraft, 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. * diverge no matter which surface asks.
* @returns {object|null} Panel-equivalent window counts. * @returns {object|null} Panel-equivalent window counts.
*/ */
function activeContactsWindow() { function activeContactsWindow(dataManager) {
try { try {
return contactsWindowFromSnapshot(militaryAwarenessLayer.getContextSnapshot?.()); const layer = dataManager?.layers?.get('military-awareness')?.module;
return layer?.contactsWindowFromSnapshot?.(layer.getContextSnapshot?.()) ?? null;
} catch { } catch {
return null; return null;
} }
@@ -3263,7 +3259,7 @@ function analystProviders(viewer, dataManager, { recordLimitByLayer = null, plac
* @returns {{lat: number, lon: number, label: string|null}|null} Subject centre. * @returns {{lat: number, lon: number, label: string|null}|null} Subject centre.
*/ */
getContextSubject() { getContextSubject() {
const snapshot = militaryAwarenessLayer.getContextSnapshot?.(); const snapshot = dataManager?.layers?.get('military-awareness')?.module?.getContextSnapshot?.();
const subject = snapshot?.subject; const subject = snapshot?.subject;
if (!subject?.position) return null; if (!subject?.position) return null;
const carto = Cesium.Cartographic.fromCartesian(subject.position); 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 // differ. The generic record/scope engine still owns explicit regions and
// arbitrary points — only "how many aircraft around <this contact>" is // arbitrary points — only "how many aircraft around <this contact>" is
// unified, because that is the question the panel is already answering. // 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; if (entityWindow) return entityWindow;
const contactsWindow = activeContactsWindow(); const contactsWindow = activeContactsWindow(dataManager);
const aircraftQueried = (result.coverage?.layersQueried || []) const aircraftQueried = (result.coverage?.layersQueried || [])
.some((l) => l.layerKey === 'flights' || l.layerKey === 'military'); .some((l) => l.layerKey === 'flights' || l.layerKey === 'military');
// Both numbers, and which one answers the question. The window counts have // 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) : originals.cartoFrom(value)
); );
try { try {
return await run(); return await run(awareness);
} finally { } finally {
awareness.getContextSnapshot = originals.snapshot; awareness.getContextSnapshot = originals.snapshot;
flightsLayer.getNearby = originals.flightsNearby; flightsLayer.getNearby = originals.flightsNearby;
@@ -2847,7 +2847,7 @@ async function withAwareness(harness, run) {
} }
} }
function analystRunner() { function analystRunner(awareness) {
const flights = { const flights = {
id: 'flights', id: 'flights',
// Deliberately a DIFFERENT population from the proximity window: this is // Deliberately a DIFFERENT population from the proximity window: this is
@@ -2869,7 +2869,7 @@ function analystRunner() {
viewer, viewer,
styleManager: {}, styleManager: {},
dataManager: { dataManager: {
layers: new Map([['flights', { module: flights }]]), layers: new Map([['flights', { module: flights }], ['military-awareness', { module: awareness }]]),
isEnabled: (id) => id === 'flights', isEnabled: (id) => id === 'flights',
getAll: () => [{ id: 'flights', name: 'Live Flights', enabled: true, stats: { count: 1 } }], 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 })), 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 })), military: Array.from({ length: 5 }, (_, i) => ({ id: `M${i}`, icao24: `m${i}`, distance: 500 * i })),
}); });
await withAwareness(harness, async () => { await withAwareness(harness, async (awareness) => {
const runner = analystRunner(); const runner = analystRunner(awareness);
const result = await runner('analyst_query', { const result = await runner('analyst_query', {
layers: ['flights', 'military'], layers: ['flights', 'military'],
// The centre the model reached for in the field: the selected datacenter. // 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}` })), flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })),
military: Array.from({ length: 5 }, (_, i) => ({ id: `M${i}`, icao24: `m${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... // What the PANEL computes for this subject...
const panel = collectAircraftProximityWindow(harness.snapshot.subject.position, { const panel = collectAircraftProximityWindow(harness.snapshot.subject.position, {
subject: harness.snapshot.subject, subject: harness.snapshot.subject,
}); });
// ...and what VOICE answers for the same 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'], layers: ['flights', 'military'],
scope: { kind: 'radius', km: 250 }, 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 () => { 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 }; globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
const harness = awarenessSubjectHarness({ subject: null }); const harness = awarenessSubjectHarness({ subject: null });
await withAwareness(harness, async () => { await withAwareness(harness, async (awareness) => {
const result = await analystRunner()('analyst_query', { const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights'], layers: ['flights'],
scope: { kind: 'radius', km: 250 }, 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' }, subject: { id: 'a1b2c3', label: 'N546PC' },
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })), flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })),
}); });
await withAwareness(harness, async () => { await withAwareness(harness, async (awareness) => {
const result = await analystRunner()('analyst_query', { const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights'], layers: ['flights'],
scope: { kind: 'region', name: 'Texas' }, 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 // 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. // far enough to be a different place, slipping through on the diagonal.
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null }; globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
await withAwareness(subjectWindowHarness(), async () => { await withAwareness(subjectWindowHarness(), async (awareness) => {
const result = await analystRunner()('analyst_query', { const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights', 'military'], layers: ['flights', 'military'],
scope: { kind: 'radius', km: 250, center: { lat: 29.9 + 0.009, lon: -97.9 + 0.009 } }, 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 // 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. // the operator a different, smaller number for a centre that IS the contact.
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null }; globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
await withAwareness(subjectWindowHarness(), async () => { await withAwareness(subjectWindowHarness(), async (awareness) => {
const result = await analystRunner()('analyst_query', { const result = await analystRunner(awareness)('analyst_query', {
layers: ['flights', 'military'], layers: ['flights', 'military'],
scope: { kind: 'radius', km: 250, center: { lat: 29.9, lon: -97.9 + 0.0103 } }, 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/);
});