refactor: own application request operations and surface caches

This commit is contained in:
Sameh Khamis committed 2026-09-14 14:49:04 -07:00
1 parent 310809e8f1
commit ac977bd8bc
53 files changed
+4388 -3317

No files matched your search

+12 -4
View File
@@ -47,6 +47,7 @@ const SOURCE_METHODS = Object.freeze({
* The manager owns layer destruction, while abort releases classification even if startup fails.
*/
export function createApplicationCatalog({
surface,
sources,
signal,
metadata = LAYER_STATE_REGISTRY,
@@ -56,6 +57,9 @@ export function createApplicationCatalog({
if (!signal?.addEventListener)
throw new TypeError('An application lifetime signal is required');
signal.throwIfAborted();
if (!surface?.groundFloor || !surface?.terrain)
throw new TypeError('Application surface services are required');
for (const [name, methods] of Object.entries(SOURCE_METHODS)) {
if (
methods.some((method) => typeof sources?.[name]?.[method] !== 'function')
@@ -71,11 +75,13 @@ export function createApplicationCatalog({
try {
militaryRegistry.configureSource(sources.military, { signal });
const flights = createApplicationFlights({
surface,
source: sources.flights,
militaryRegistry,
resolveAsset,
});
const military = createApplicationMilitary({
surface,
source: sources.military,
militaryRegistry,
resolveAsset,
@@ -85,6 +91,7 @@ export function createApplicationCatalog({
options: vesselOptions,
});
const installations = createApplicationInstallations({
surface,
source: sources.installations,
});
const satellites = createApplicationSatellites({
@@ -95,12 +102,12 @@ export function createApplicationCatalog({
flights,
military,
createApplicationEarthquakes({ source: sources.earthquakes }),
createApplicationAlpr({ source: sources.alpr }),
createApplicationAlpr({ surface, source: sources.alpr }),
satellites,
createApplicationLaunches({ source: sources.launches, satellites }),
createApplicationTraffic({ source: sources.traffic }),
createApplicationCctv({ source: sources.cctv }),
createApplicationRadio({ source: sources.radio }),
createApplicationCctv({ surface, source: sources.cctv }),
createApplicationRadio({ surface, source: sources.radio }),
createApplicationBikeshare({ source: sources.bikeshare }),
vessels,
installations,
@@ -113,6 +120,7 @@ export function createApplicationCatalog({
...createInfrastructureLayers(localGeoJsonServices),
createApplicationCables({ source: sources.cables }),
createApplicationFirms({
surface,
id: 'local-firms',
name: 'FIRMS Active Fires',
icon: '▲',
@@ -122,7 +130,7 @@ export function createApplicationCatalog({
],
metadata,
);
return Object.freeze({ ...catalog, militaryRegistry });
return Object.freeze({ ...catalog, militaryRegistry, surface });
} catch (error) {
dispose();
throw error;
+17 -1
View File
@@ -1,3 +1,4 @@
import { createSurfaceServices } from './surfaceServices.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApplicationCatalog } from './constructCatalog.js';
@@ -30,10 +31,12 @@ test('catalogs construct distinct layers and classification from their supplied
const first = createApplicationCatalog({
sources: fixtureSources(['abc123'], callsA),
signal: a.signal,
surface: fixtureSurface(a.signal),
});
const second = createApplicationCatalog({
sources: fixtureSources(['def456'], callsB),
signal: b.signal,
surface: fixtureSurface(b.signal),
});
assert.equal(first.layers.length, 17);
assert.deepEqual(
@@ -62,7 +65,12 @@ test('catalogs construct distinct layers and classification from their supplied
test('invalid or already cancelled construction fails before classification can acquire', () => {
const lifetime = new AbortController();
assert.throws(
() => createApplicationCatalog({ sources: {}, signal: lifetime.signal }),
() =>
createApplicationCatalog({
sources: {},
signal: lifetime.signal,
surface: fixtureSurface(lifetime.signal),
}),
/catalog source/,
);
lifetime.abort();
@@ -75,3 +83,11 @@ test('invalid or already cancelled construction fails before classification can
{ name: 'AbortError' },
);
});
function fixtureSurface(signal) {
return createSurfaceServices({
terrainSource: { getHeights: async () => [] },
signal,
eventTarget: null,
});
}
+13 -3
View File
@@ -5,7 +5,7 @@ import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
/** Construct the existing controls and camera presentation. */
export function createApplicationControls({
scene: { viewer, mapStackController },
scene: { viewer, mapStackController, operations },
loaderStatus,
Controls = StyleManager,
services,
@@ -15,7 +15,15 @@ export function createApplicationControls({
}) {
// Initialize the style manager (post-processing, HUD, locations, share links)
const styleManager = new Controls(viewer, {
services: { ...services, ...catalogControlServices(catalog) },
services: {
...services,
...operations.surface.controlServices,
searchAndFlyTo: operations.searchAndFlyTo,
fetchRegionalBrief: (...args) =>
operations.requests.regional.getBrief(...args),
...catalogControlServices(catalog),
},
requestServices: operations.requests,
mapStackController,
placeSearch,
});
@@ -26,7 +34,9 @@ export function createApplicationControls({
// clouds use a separate, capped low-resolution GPU pass that never attaches
// Cesium fog or post-process stages and is fully stopped in map mode.
const weatherEffects = null;
const cockpitCloudEffects = initCockpitCloudEffects(viewer);
const cockpitCloudEffects = initCockpitCloudEffects(viewer, {
weatherService: operations.requests.weather,
});
defer(() => cockpitCloudEffects?.destroy());
// If no share link state, do default fly-to Austin
+2 -2
View File
@@ -2,10 +2,10 @@ 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 }) {
export function createApplicationAlpr({ surface, source }) {
const { groundFloor } = surface;
return createAlprCamerasLayer({
source,
services: { render, context, picking, groundFloor },
+2 -4
View File
@@ -4,14 +4,12 @@ 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 }) {
export function createApplicationCctv({ surface, source }) {
const { terrain, groundFloor: ground, meshFloor: mesh } = surface;
return createCctvLayer({
source,
services: {
+2 -4
View File
@@ -4,7 +4,6 @@ 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 = {
@@ -12,19 +11,18 @@ export const firmsServices = {
sprites,
context,
picking,
anchors,
overlays,
focus,
};
/** Bind a supplied fire feed to the application scene service owners. */
export function createApplicationFirms(options) {
export function createApplicationFirms({ surface, ...options }) {
return createLayer({
...options,
icon: options.icon ?? '▲',
source: options.source ?? 'NASA FIRMS',
feed: options.feed,
services: firmsServices,
services: { ...firmsServices, anchors: surface.anchors },
overlayHost: options.overlayHost ?? {
setEntries: overlays.setOverlayEntries,
setVisible: overlays.setOverlaySourceVisible,
+2 -3
View File
@@ -5,23 +5,22 @@ 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({
surface,
source,
militaryRegistry,
resolveAsset = (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
}) {
const { groundFloor, meshFloor, groundSnap } = surface;
return createCivilFlightLayer({
source,
resolveAsset,
+2 -3
View File
@@ -5,23 +5,22 @@ 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({
surface,
source,
militaryRegistry,
resolveAsset = (url) =>
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
}) {
const { groundFloor, meshFloor, groundSnap } = surface;
return createMilitaryFlightLayer({
source,
resolveAsset,
+2 -3
View File
@@ -1,12 +1,11 @@
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 }) {
export function createApplicationInstallations({ surface, source }) {
const { groundFloor: ground, anchors } = surface;
return createInstallationsLayer({
source,
services: { render, context, ground, anchors, picking },
+2 -2
View File
@@ -1,12 +1,12 @@
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 }) {
export function createApplicationRadio({ surface, source }) {
const { groundFloor: ground } = surface;
return createRadioLayer({
source,
services: { ground, picking, overlays, globe, render },
+37
View File
@@ -0,0 +1,37 @@
import { createSurfaceServices } from './surfaceServices.js';
import { createAnnotationResolver } from '../annotations/resolver.js';
import { searchAndFlyTo } from '../locations.js';
/** Assemble application operations from the caller's request services. */
export function createApplicationOperations({ requests, signal, eventTarget }) {
for (const [name, method] of Object.entries({
boundaries: 'query',
terrain: 'getHeights',
regional: 'getBrief',
weather: 'getConditions',
summary: 'summarize',
})) {
if (typeof requests?.[name]?.[method] !== 'function')
throw new TypeError(`Missing application request service: ${name}`);
}
const surface = createSurfaceServices({
terrainSource: requests.terrain,
signal,
eventTarget,
});
const annotationResolver = createAnnotationResolver({
boundarySource: requests.boundaries,
signal,
});
return Object.freeze({
requests,
surface,
annotationResolver,
searchAndFlyTo: (viewer, query, options = {}) =>
searchAndFlyTo(viewer, query, {
...options,
boundaries: requests.boundaries,
recoverNearView: annotationResolver.placesNearViewRecovery,
}),
});
}
+165
View File
@@ -0,0 +1,165 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApplicationOperations } from './operations.js';
function requests(terrain, boundaries = { query: async () => [] }) {
return {
terrain,
boundaries,
regional: { getBrief: async () => ({}) },
weather: { getConditions: async () => ({}) },
summary: { summarize: async () => ({}) },
};
}
test('surface cache and stack listeners belong to the supplied application lifetime', async (t) => {
const a = new AbortController();
const b = new AbortController();
t.after(() => {
a.abort();
b.abort();
});
const target = new EventTarget();
let calls = 0;
const first = createApplicationOperations({
signal: a.signal,
eventTarget: target,
requests: requests({
async getHeights(points) {
calls++;
return points.map(() => ({ ellipsoid: 123 }));
},
}),
});
const second = createApplicationOperations({
signal: b.signal,
eventTarget: target,
requests: requests({
async getHeights(points) {
return points.map(() => ({ ellipsoid: 456 }));
},
}),
});
const point = { lat: 30, lon: -97 };
await first.surface.terrain.resolveEllipsoidalGround([point]);
await first.surface.terrain.resolveEllipsoidalGround([point]);
await second.surface.terrain.resolveEllipsoidalGround([point]);
assert.equal(calls, 1);
assert.equal(first.surface.groundFloor.cachedGroundFloor(30, -97), 123);
assert.equal(second.surface.groundFloor.cachedGroundFloor(30, -97), 456);
target.dispatchEvent(
new CustomEvent('gev:map-stack-changed', {
detail: { activeId: 'esri-imagery' },
}),
);
assert.equal(first.surface.groundFloor.meshFloorPreferred(), false);
a.abort();
assert.equal(first.surface.terrain.cachedEllipsoidalGround(30, -97), null);
target.dispatchEvent(
new CustomEvent('gev:map-stack-changed', {
detail: { activeId: 'photoreal' },
}),
);
assert.equal(
first.surface.groundFloor.meshFloorPreferred(),
false,
'disposed listener cannot update the old owner',
);
assert.equal(second.surface.groundFloor.meshFloorPreferred(), true);
});
test('a terrain reply after application abort cannot refill the cache or become a fallback', async () => {
const lifetime = new AbortController();
let respond;
let receivedSignal;
const operations = createApplicationOperations({
signal: lifetime.signal,
eventTarget: null,
requests: requests({
getHeights(_points, { signal }) {
receivedSignal = signal;
return new Promise((resolve) => {
respond = resolve;
});
},
}),
});
const pending = operations.surface.terrain.resolveEllipsoidalGround([
{ lat: 30, lon: -97 },
]);
lifetime.abort();
respond([{ ellipsoid: 123 }]);
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(receivedSignal.aborted, true);
assert.equal(
operations.surface.terrain.cachedEllipsoidalGround(30, -97),
null,
);
});
test('annotation lookup and cached outlines use the supplied source independently', async (t) => {
const priorWindow = globalThis.window;
globalThis.window = { setTimeout, clearTimeout };
const a = new AbortController();
const b = new AbortController();
t.after(() => {
a.abort();
b.abort();
if (priorWindow === undefined) delete globalThis.window;
else globalThis.window = priorWindow;
});
const calls = [0, 0];
const make = (signal, index) =>
createApplicationOperations({
signal,
eventTarget: null,
requests: requests(
{ getHeights: async () => [] },
{
query: async () => {
calls[index]++;
const delta = index * 0.0001;
return [
{
type: 'way',
id: index + 1,
tags: { building: 'yes', name: 'Source fixture hall' },
geometry: [
{ lat: 30 - 0.0001, lon: -97 - 0.0001 + delta },
{ lat: 30 - 0.0001, lon: -97 + 0.0001 + delta },
{ lat: 30 + 0.0001, lon: -97 + 0.0001 + delta },
{ lat: 30 + 0.0001, lon: -97 - 0.0001 + delta },
{ lat: 30 - 0.0001, lon: -97 - 0.0001 + delta },
],
},
];
},
},
),
});
const first = make(a.signal, 0);
const second = make(b.signal, 1);
const query = {
viewer: {},
target: 'Source fixture hall',
latitude: 30,
longitude: -97,
footprint: true,
entityKind: 'building',
};
const resultA = await first.annotationResolver.resolveAnnotationTarget(query);
const resultB =
await second.annotationResolver.resolveAnnotationTarget(query);
assert.ok(resultA.ring?.length >= 4);
assert.ok(resultB.ring?.length >= 4);
assert.notDeepEqual(resultA.ring, resultB.ring);
const before = calls[0];
const cached = await first.annotationResolver.resolveAnnotationTarget(query);
assert.deepEqual(cached.ring, resultA.ring);
assert.equal(calls[0], before);
a.abort();
await assert.rejects(
first.annotationResolver.resolveAnnotationTarget(query),
{ name: 'AbortError' },
);
});
+7 -1
View File
@@ -1,3 +1,4 @@
import { createApplicationOperations } from './operations.js';
import * as Cesium from 'cesium';
import { createApplicationViewer } from '../app/viewer.js';
import { registerDataCredits } from '../data/dataCredits.js';
@@ -13,6 +14,7 @@ import { describeError } from './errors.js';
/** Construct the application globe using the caller's local configuration. */
export async function createApplicationScene({
requestServices,
googleApiKey,
cesiumToken,
credits,
@@ -22,6 +24,10 @@ export async function createApplicationScene({
signal,
defer,
}) {
const operations = createApplicationOperations({
requests: requestServices,
signal,
});
defer(initLogoGaze());
const previousKey = window.__GOOGLE_MAPS_API_KEY__;
if (googleApiKey) {
@@ -109,5 +115,5 @@ export async function createApplicationScene({
});
signal.throwIfAborted();
return { viewer, tileset, mapStackController };
return { viewer, tileset, mapStackController, operations };
}
+37
View File
@@ -0,0 +1,37 @@
import { createTerrainHeights } from '../services/terrainHeights.js';
import { createGroundFloor } from '../services/groundFloor.js';
import { createMeshFloorSampler } from '../services/meshFloorSampler.js';
import { createGroundSnap } from '../services/groundSnap.js';
import { createFireAnchors } from '../layers/firms/anchors.js';
/** Own the DEM, coarse floors and mesh samples used by one application. */
export function createSurfaceServices({
terrainSource,
signal,
eventTarget = globalThis.window,
}) {
const terrain = createTerrainHeights({ source: terrainSource, signal });
const groundFloor = createGroundFloor({ terrain, signal });
const meshFloor = createMeshFloorSampler({
groundFloor,
signal,
eventTarget,
});
return Object.freeze({
terrain,
groundFloor,
meshFloor,
groundSnap: Object.freeze({
createGroundSnap: () => createGroundSnap({ groundFloor }),
}),
anchors: createFireAnchors(groundFloor),
controlServices: Object.freeze({
cachedGroundFloor: groundFloor.cachedGroundFloor,
cachedMeshFloor: groundFloor.cachedMeshFloor,
GROUND_FLOOR_LIFT_M: groundFloor.GROUND_FLOOR_LIFT_M,
meshFloorPreferred: groundFloor.meshFloorPreferred,
warmGroundFloor: groundFloor.warmGroundFloor,
sampleMeshFloorCells: meshFloor.sampleMeshFloorCells,
}),
});
}
+11 -2
View File
@@ -23,13 +23,18 @@ export function createApplicationTools({
signal,
defer,
}) {
const { viewer, tileset, mapStackController } = scene;
const { viewer, tileset, mapStackController, operations } = scene;
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
const { dataManager } = data;
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager);
defer(() => sceneDirector.destroy());
onSceneDirector?.(sceneDirector);
const annotations = initAnnotations({ viewer, tileset, placeSearch });
const annotations = initAnnotations({
viewer,
tileset,
placeSearch,
resolver: operations.annotationResolver,
});
defer(() => {
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
annotations.destroy();
@@ -95,6 +100,7 @@ export function createApplicationTools({
weatherEffects,
cockpitCloudEffects,
getRenderGovernorDiagnostics,
surfaceServices: operations.surface,
requestRender: governorRequestRender,
};
const debug = window.__godsEyeView;
@@ -103,6 +109,9 @@ export function createApplicationTools({
});
const voiceCommands = initGevVoiceCommands({
...voice,
floorServices: operations.surface.groundFloor,
annotationResolver: operations.annotationResolver,
searchNavigation: operations.searchAndFlyTo,
signal,
placeSearch,
viewer,