mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor: supply application catalogs explicitly
This commit is contained in:
1 parent
fad93f22f8
commit
c3de679c61
16 files changed
+288
-50
No files matched your search
@@ -0,0 +1,58 @@
|
||||
const CONTROL_LAYER_IDS = Object.freeze({
|
||||
trafficLayer: 'traffic',
|
||||
flightsLayer: 'flights',
|
||||
militaryFlightsLayer: 'military',
|
||||
satellitesLayer: 'satellites',
|
||||
cctvLayer: 'cctv',
|
||||
radioLayer: 'radio',
|
||||
bikeshareLayer: 'bikeshare',
|
||||
aisLiveVesselsLayer: 'ais-live-vessels',
|
||||
militaryAwarenessLayer: 'military-awareness',
|
||||
militaryInstallationsLayer: 'military-installations',
|
||||
rocketLaunchesLayer: 'rocket-launches',
|
||||
});
|
||||
|
||||
/** Capture the ordered application instances and their serialization metadata. */
|
||||
export function createLayerCatalog(layers, metadata) {
|
||||
if (!Array.isArray(layers) || !Array.isArray(metadata))
|
||||
throw new TypeError(
|
||||
'Layer instances and serialization metadata are required',
|
||||
);
|
||||
const byId = new Map();
|
||||
for (const layer of layers) {
|
||||
if (typeof layer?.id !== 'string' || !layer.id || byId.has(layer.id))
|
||||
throw new TypeError(`Invalid or duplicate catalog layer: ${layer?.id}`);
|
||||
byId.set(layer.id, layer);
|
||||
}
|
||||
const ids = new Set();
|
||||
for (const entry of metadata) {
|
||||
if (!byId.has(entry?.id) || ids.has(entry.id))
|
||||
throw new TypeError(
|
||||
`Unmatched or duplicate catalog metadata: ${entry?.id}`,
|
||||
);
|
||||
ids.add(entry.id);
|
||||
}
|
||||
if (ids.size !== byId.size)
|
||||
throw new TypeError('Catalog metadata is incomplete');
|
||||
return Object.freeze({
|
||||
layers: Object.freeze([...layers]),
|
||||
metadata: Object.freeze(
|
||||
metadata.map((entry) => Object.freeze({ ...entry })),
|
||||
),
|
||||
get: (id) => byId.get(id),
|
||||
});
|
||||
}
|
||||
|
||||
/** Bind the current control surface to the exact instances registered by the app. */
|
||||
export function catalogControlServices(catalog) {
|
||||
if (!catalog?.get)
|
||||
throw new TypeError('An application layer catalog is required');
|
||||
return Object.fromEntries(
|
||||
Object.entries(CONTROL_LAYER_IDS).map(([role, id]) => {
|
||||
const layer = catalog.get(id);
|
||||
if (!layer)
|
||||
throw new TypeError(`Control layer missing from catalog: ${id}`);
|
||||
return [role, layer];
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createLayerCatalog, catalogControlServices } from './catalog.js';
|
||||
import { createApplicationData } from './data.js';
|
||||
|
||||
const metadata = (layers) =>
|
||||
layers.map(({ id }) => ({ id, disposition: 'enabled-only' }));
|
||||
|
||||
test('catalog captures membership and metadata while preserving the supplied instances', () => {
|
||||
const first = { id: 'first' };
|
||||
const layers = [first];
|
||||
const schema = metadata(layers);
|
||||
const catalog = createLayerCatalog(layers, schema);
|
||||
layers.push({ id: 'later' });
|
||||
schema[0].disposition = 'invalid';
|
||||
assert.deepEqual(catalog.layers, [first]);
|
||||
assert.equal(catalog.get('first'), first);
|
||||
assert.equal(catalog.get('later'), undefined);
|
||||
assert.equal(catalog.metadata[0].disposition, 'enabled-only');
|
||||
assert.throws(() => catalog.layers.push({ id: 'later' }), TypeError);
|
||||
});
|
||||
|
||||
test('duplicate, missing and extra metadata fail before application registration', () => {
|
||||
const layers = [{ id: 'first' }];
|
||||
assert.throws(
|
||||
() => createLayerCatalog([...layers, ...layers], metadata(layers)),
|
||||
/duplicate/,
|
||||
);
|
||||
assert.throws(() => createLayerCatalog(layers, []), /incomplete/);
|
||||
assert.throws(
|
||||
() => createLayerCatalog(layers, metadata([{ id: 'other' }])),
|
||||
/Unmatched/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createLayerCatalog(layers, [...metadata(layers), ...metadata(layers)]),
|
||||
/duplicate/,
|
||||
);
|
||||
});
|
||||
|
||||
test('data setup seals the caller catalog before controls can restore and drains its exact instances', async (t) => {
|
||||
const previous = globalThis.document;
|
||||
globalThis.document = { getElementById: () => null };
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete globalThis.document;
|
||||
else globalThis.document = previous;
|
||||
});
|
||||
const attached = [];
|
||||
const layers = [
|
||||
{
|
||||
id: 'one',
|
||||
attachDataManager: (manager) => attached.push([...manager.layers.keys()]),
|
||||
},
|
||||
{ id: 'two' },
|
||||
];
|
||||
const releases = [];
|
||||
let observed;
|
||||
const { dataManager } = createApplicationData({
|
||||
scene: { viewer: {} },
|
||||
controls: {
|
||||
styleManager: {
|
||||
attachDataManager(manager) {
|
||||
assert.equal(manager.registrationsFinalized, true);
|
||||
assert.equal(manager.layers.get('one').module, layers[0]);
|
||||
},
|
||||
},
|
||||
},
|
||||
catalog: createLayerCatalog(layers, metadata(layers)),
|
||||
defer: (release) => releases.push(release),
|
||||
onData: (manager) => {
|
||||
observed = manager;
|
||||
},
|
||||
});
|
||||
assert.equal(observed, dataManager);
|
||||
assert.deepEqual(attached, [['one', 'two']]);
|
||||
assert.deepEqual([...dataManager.layers.keys()], ['one', 'two']);
|
||||
assert.throws(() => dataManager.register({ id: 'late' }), /finalized/);
|
||||
for (const release of releases.reverse()) await release();
|
||||
assert.equal(dataManager.layers.size, 0);
|
||||
});
|
||||
|
||||
test('controls bind catalog instances rather than similarly named defaults', () => {
|
||||
const ids = [
|
||||
'traffic',
|
||||
'flights',
|
||||
'military',
|
||||
'satellites',
|
||||
'cctv',
|
||||
'radio',
|
||||
'bikeshare',
|
||||
'ais-live-vessels',
|
||||
'military-awareness',
|
||||
'military-installations',
|
||||
'rocket-launches',
|
||||
];
|
||||
const layers = ids.map((id) => ({ id }));
|
||||
const catalog = createLayerCatalog(layers, metadata(layers));
|
||||
const services = catalogControlServices(catalog);
|
||||
assert.equal(services.flightsLayer, layers[1]);
|
||||
assert.equal(services.aisLiveVesselsLayer, layers[7]);
|
||||
assert.equal(new Set(Object.values(services)).size, layers.length);
|
||||
assert.throws(
|
||||
() => catalogControlServices(createLayerCatalog([], [])),
|
||||
/Control layer missing/,
|
||||
);
|
||||
});
|
||||
Vendored
+3
-1
@@ -1,3 +1,4 @@
|
||||
import { catalogControlServices } from './catalog.js';
|
||||
import { StyleManager } from '../ui/composition.js';
|
||||
import { flyToAustin } from '../camera.js';
|
||||
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
||||
@@ -8,12 +9,13 @@ export function createApplicationControls({
|
||||
loaderStatus,
|
||||
Controls = StyleManager,
|
||||
services,
|
||||
catalog,
|
||||
placeSearch,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||
const styleManager = new Controls(viewer, {
|
||||
services,
|
||||
services: { ...services, ...catalogControlServices(catalog) },
|
||||
mapStackController,
|
||||
placeSearch,
|
||||
});
|
||||
|
||||
+7
-36
@@ -1,24 +1,9 @@
|
||||
import { DataLayerManager } from '../data/manager.js';
|
||||
import flightsLayer from '../data/flights.js';
|
||||
import militaryFlightsLayer from '../data/militaryFlights.js';
|
||||
import alprCamerasLayer from '../data/alprCameras.js';
|
||||
import earthquakesLayer from '../data/earthquakes.js';
|
||||
import satellitesLayer from '../data/satellites.js';
|
||||
import rocketLaunchesLayer from '../data/rocketLaunches.js';
|
||||
import trafficLayer from '../data/traffic.js';
|
||||
import cctvLayer from '../data/cctv.js';
|
||||
import radioLayer from '../data/radio.js';
|
||||
import bikeshareLayer from '../data/bikeshare.js';
|
||||
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
|
||||
import militaryInstallationsLayer from '../data/militaryInstallations.js';
|
||||
import militaryAwarenessLayer from '../data/militaryAwareness.js';
|
||||
import localDataLayers from '../data/localLayers.js';
|
||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||
|
||||
/** Register the application layer catalog before allowing state restoration. */
|
||||
export function createApplicationData({
|
||||
scene: { viewer },
|
||||
controls: { styleManager },
|
||||
catalog,
|
||||
allowQaRegistration,
|
||||
onData,
|
||||
defer,
|
||||
@@ -35,26 +20,12 @@ export function createApplicationData({
|
||||
);
|
||||
});
|
||||
onData?.(dataManager);
|
||||
dataManager.register(flightsLayer);
|
||||
dataManager.register(militaryFlightsLayer);
|
||||
dataManager.register(earthquakesLayer);
|
||||
dataManager.register(alprCamerasLayer);
|
||||
dataManager.register(satellitesLayer);
|
||||
dataManager.register(rocketLaunchesLayer);
|
||||
rocketLaunchesLayer.attachDataManager(dataManager);
|
||||
dataManager.register(trafficLayer);
|
||||
dataManager.register(cctvLayer);
|
||||
dataManager.register(radioLayer);
|
||||
dataManager.register(bikeshareLayer);
|
||||
dataManager.register(aisLiveVesselsLayer);
|
||||
dataManager.register(militaryInstallationsLayer);
|
||||
dataManager.register(militaryAwarenessLayer);
|
||||
militaryAwarenessLayer.attachDataManager(dataManager);
|
||||
for (const layer of localDataLayers) {
|
||||
dataManager.register(layer);
|
||||
}
|
||||
// Restoration starts only after the complete production registry is sealed.
|
||||
dataManager.finalizeRegistrations(LAYER_STATE_REGISTRY);
|
||||
if (!catalog?.layers || !catalog?.metadata)
|
||||
throw new TypeError('An application layer catalog is required');
|
||||
for (const layer of catalog.layers) dataManager.register(layer);
|
||||
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
||||
// Restoration starts only after the caller's complete registry is sealed.
|
||||
dataManager.finalizeRegistrations(catalog.metadata);
|
||||
if (allowQaRegistration) {
|
||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||
if (targetManager !== dataManager)
|
||||
|
||||
Reference in new issue
Block a user