mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat(app): layer catalog per engine
- src/app/layers/renderers.js: the layer renderer registry
(registerLayerRenderers(engineKind, { [layerId]: factory }),
rendererFor(layerId, engine), layerAvailableOn(layerId, engineKind)).
Every application layer is registered for Cesium as CESIUM_BUILTIN (it
still draws itself until its render/cesium.js); Navara factories come with
the layer tasks. On it, the stable availability API phase 4 injects into
its layer list: createLayerAvailability(engineKind) →
{ engine, isAvailable(id), available(), unavailable() } with { id, name }
rows (src/app/layers/directory.js holds the ids and names in catalog
order; a test keeps it equal to the constructed catalog).
- createApplicationCatalog({ engineKind, availability }) constructs only the
layers the engine can show, in the same order (shared instances made
once, on demand); the catalog carries `availability` and the others'
metadata (`unavailableMetadata`). Cesium builds the same 30 layers in the
same order as before; Navara builds none yet (no Navara renderers) - the
standalone app now asks the catalog for it instead of making an empty one.
- catalogControlServices: a role whose layer the engine leaves out gets an
empty stand-in (absentLayerControlServices folded in); any other missing
layer is still a wiring error.
- LayerLifecycle(viewer, { engine }): every module call (init, enable,
update, disable, destroy) gets { signal, engine } as its second argument;
viewer is null on Navara.
- Links: a link that turns on layers this engine leaves out keeps them in
the durable state (re-saved links carry them; back on Cesium they turn
on) and activates nothing; a notice names them ("NOT ON THIS ENGINE · Kept
in the link: …"). The Navara notice lists the engine's data layers from
the registry.
- The dev launcher's CCTV wiring check matches the new catalog form.
This commit is contained in:
1 parent
e1be2eac19
commit
b0cbed081b
23 files changed
+780
-100
No files matched your search
@@ -250,7 +250,7 @@ if [[ ! -f "$SOURCE_ROOT/src/data/cctv.js" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! grep -q "return createApplicationCatalog(" "$SOURCE_ROOT/src/standalone/catalog.js" || \
|
if ! grep -q "return createApplicationCatalog(" "$SOURCE_ROOT/src/standalone/catalog.js" || \
|
||||||
! grep -q "^[[:space:]]*createApplicationCctv({" "$SOURCE_ROOT/src/app/constructCatalog.js"; then
|
! grep -q "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
|
||||||
|
|||||||
@@ -1096,5 +1096,10 @@
|
|||||||
"src/globe/navara/followGestures.js",
|
"src/globe/navara/followGestures.js",
|
||||||
"src/globe/navara/followGestures.test.mjs",
|
"src/globe/navara/followGestures.test.mjs",
|
||||||
"src/globe/cpuPicks.test.mjs",
|
"src/globe/cpuPicks.test.mjs",
|
||||||
"scripts/qa-overlay-engines.mjs"
|
"scripts/qa-overlay-engines.mjs",
|
||||||
|
"src/app/layers/directory.js",
|
||||||
|
"src/app/layers/renderers.js",
|
||||||
|
"src/app/layers/renderers.test.mjs",
|
||||||
|
"src/data/lifecycleEngine.test.mjs",
|
||||||
|
"src/ui/shareRestorationEngine.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -702,7 +702,8 @@
|
|||||||
"src/geo/scaleToGeodeticSurface.js",
|
"src/geo/scaleToGeodeticSurface.js",
|
||||||
"src/geo/check.js",
|
"src/geo/check.js",
|
||||||
"src/geo/debug.js",
|
"src/geo/debug.js",
|
||||||
"src/geo/developerError.js"
|
"src/geo/developerError.js",
|
||||||
|
"src/app/layers/directory.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -2583,7 +2584,9 @@
|
|||||||
"src/globe/navara/movers/modelMover.js",
|
"src/globe/navara/movers/modelMover.js",
|
||||||
"src/globe/navara/movers/index.js",
|
"src/globe/navara/movers/index.js",
|
||||||
"src/globe/surfaceDetail.js",
|
"src/globe/surfaceDetail.js",
|
||||||
"src/globe/navara/followGestures.js"
|
"src/globe/navara/followGestures.js",
|
||||||
|
"src/app/layers/directory.js",
|
||||||
|
"src/app/layers/renderers.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -3152,7 +3155,9 @@
|
|||||||
"src/geo/developerError.js",
|
"src/geo/developerError.js",
|
||||||
"src/time/clock.js",
|
"src/time/clock.js",
|
||||||
"src/globe/follow.js",
|
"src/globe/follow.js",
|
||||||
"src/globe/cesium/follow.js"
|
"src/globe/cesium/follow.js",
|
||||||
|
"src/app/layers/directory.js",
|
||||||
|
"src/app/layers/renderers.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -3279,7 +3284,8 @@
|
|||||||
"src/loadingFeedback.js",
|
"src/loadingFeedback.js",
|
||||||
"src/navigationPolicy.js",
|
"src/navigationPolicy.js",
|
||||||
"src/ui/shareRestoration.js",
|
"src/ui/shareRestoration.js",
|
||||||
"src/ui/uiLifetime.js"
|
"src/ui/uiLifetime.js",
|
||||||
|
"src/app/layers/directory.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-3
@@ -46,16 +46,25 @@ export function createLayerCatalog(layers, metadata) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bind the current control surface to the exact instances registered by the app. */
|
/**
|
||||||
|
* Bind the current control surface to the exact instances registered by the
|
||||||
|
* app. A layer the catalog leaves out on purpose (another engine's: in
|
||||||
|
* `catalog.unavailableMetadata`) gets an empty stand-in, so the shell's
|
||||||
|
* optional calls (`layer.method?.()`) do nothing; any other missing layer is
|
||||||
|
* a wiring error.
|
||||||
|
*/
|
||||||
export function catalogControlServices(catalog) {
|
export function catalogControlServices(catalog) {
|
||||||
if (!catalog?.get)
|
if (!catalog?.get)
|
||||||
throw new TypeError('An application layer catalog is required');
|
throw new TypeError('An application layer catalog is required');
|
||||||
|
const omitted = new Set(
|
||||||
|
(catalog.unavailableMetadata ?? []).map(({ id }) => id),
|
||||||
|
);
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(CONTROL_LAYER_IDS).map(([role, id]) => {
|
Object.entries(CONTROL_LAYER_IDS).map(([role, id]) => {
|
||||||
const layer = catalog.get(id);
|
const layer = catalog.get(id);
|
||||||
if (!layer)
|
if (layer) return [role, layer];
|
||||||
|
if (omitted.has(id)) return [role, {}];
|
||||||
throw new TypeError(`Control layer missing from catalog: ${id}`);
|
throw new TypeError(`Control layer missing from catalog: ${id}`);
|
||||||
return [role, layer];
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,3 +190,40 @@ test('data setup without a viewer registers an empty catalog and no sonar scene'
|
|||||||
assert.equal(dataManager.registrationsFinalized, true);
|
assert.equal(dataManager.registrationsFinalized, true);
|
||||||
for (const release of releases.reverse()) await release();
|
for (const release of releases.reverse()) await release();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a catalog for an engine gives stand-ins only for the layers it leaves out', () => {
|
||||||
|
const flights = { id: 'flights' };
|
||||||
|
const catalog = {
|
||||||
|
...createLayerCatalog(
|
||||||
|
[flights],
|
||||||
|
[{ id: 'flights', disposition: 'enabled-only' }],
|
||||||
|
),
|
||||||
|
unavailableMetadata: Object.keys({
|
||||||
|
traffic: 1,
|
||||||
|
military: 1,
|
||||||
|
satellites: 1,
|
||||||
|
cctv: 1,
|
||||||
|
'street-level': 1,
|
||||||
|
radio: 1,
|
||||||
|
bikeshare: 1,
|
||||||
|
transit: 1,
|
||||||
|
'ais-live-vessels': 1,
|
||||||
|
'military-awareness': 1,
|
||||||
|
'military-installations': 1,
|
||||||
|
'rocket-launches': 1,
|
||||||
|
'local-adsb': 1,
|
||||||
|
}).map((id) => ({ id })),
|
||||||
|
};
|
||||||
|
const services = catalogControlServices(catalog);
|
||||||
|
assert.equal(services.flightsLayer, flights);
|
||||||
|
assert.deepEqual(services.trafficLayer, {});
|
||||||
|
assert.notEqual(services.trafficLayer, services.militaryFlightsLayer);
|
||||||
|
const missing = {
|
||||||
|
...catalog,
|
||||||
|
unavailableMetadata: catalog.unavailableMetadata.slice(1),
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => catalogControlServices(missing),
|
||||||
|
/Control layer missing from catalog: traffic/,
|
||||||
|
);
|
||||||
|
});
|
||||||
+163
-48
@@ -4,6 +4,8 @@ import { createWeatherLayer } from '../layers/weather/index.js';
|
|||||||
import { createCyclonesLayer } from '../layers/cyclones/index.js';
|
import { createCyclonesLayer } from '../layers/cyclones/index.js';
|
||||||
import { createWindLayer } from '../layers/wind/index.js';
|
import { createWindLayer } from '../layers/wind/index.js';
|
||||||
import { createLayerCatalog } from './catalog.js';
|
import { createLayerCatalog } from './catalog.js';
|
||||||
|
import { createLayerAvailability } from './layers/renderers.js';
|
||||||
|
import { APPLICATION_LAYER_DIRECTORY } from './layers/directory.js';
|
||||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||||
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
|
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
|
||||||
import { createApplicationFlights } from './layers/flights.js';
|
import { createApplicationFlights } from './layers/flights.js';
|
||||||
@@ -47,6 +49,10 @@ export const APPLICATION_LAYER_METADATA = Object.freeze([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
/** Construct the current catalog without choosing any source provider.
|
/** Construct the current catalog without choosing any source provider.
|
||||||
|
* `engineKind` ('cesium' | 'navara'): only the layers that engine can show
|
||||||
|
* (src/app/layers/renderers.js) are constructed, in the same order; the
|
||||||
|
* catalog keeps the others' metadata (`unavailableMetadata`) and carries
|
||||||
|
* the engine's `availability`.
|
||||||
* Scene engines remain page-owned; layers and classification have this app's lifetime.
|
* 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.
|
* The manager owns layer destruction, while abort releases classification even if startup fails.
|
||||||
*/
|
*/
|
||||||
@@ -59,6 +65,8 @@ export function createApplicationCatalog({
|
|||||||
resolveAsset,
|
resolveAsset,
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
|
engineKind = 'cesium',
|
||||||
|
availability = createLayerAvailability(engineKind),
|
||||||
}) {
|
}) {
|
||||||
if (!signal?.addEventListener)
|
if (!signal?.addEventListener)
|
||||||
throw new TypeError('An application lifetime signal is required');
|
throw new TypeError('An application lifetime signal is required');
|
||||||
@@ -79,89 +87,171 @@ export function createApplicationCatalog({
|
|||||||
try {
|
try {
|
||||||
if (sourceComposition.isConfigured('military'))
|
if (sourceComposition.isConfigured('military'))
|
||||||
militaryRegistry.configureSource(sources.military, { signal });
|
militaryRegistry.configureSource(sources.military, { signal });
|
||||||
const flights = createApplicationFlights({
|
// Shared instances (awareness and launches read others) are made once,
|
||||||
|
// when the first layer that needs them is built.
|
||||||
|
const once = (create) => {
|
||||||
|
let value;
|
||||||
|
return () => (value ??= create());
|
||||||
|
};
|
||||||
|
const flights = once(() =>
|
||||||
|
createApplicationFlights({
|
||||||
surface,
|
surface,
|
||||||
source: sources.flights,
|
source: sources.flights,
|
||||||
militaryRegistry,
|
militaryRegistry,
|
||||||
resolveAsset,
|
resolveAsset,
|
||||||
});
|
}),
|
||||||
const military = createApplicationMilitary({
|
);
|
||||||
|
const military = once(() =>
|
||||||
|
createApplicationMilitary({
|
||||||
surface,
|
surface,
|
||||||
source: sources.military,
|
source: sources.military,
|
||||||
militaryRegistry,
|
militaryRegistry,
|
||||||
resolveAsset,
|
resolveAsset,
|
||||||
});
|
}),
|
||||||
const vessels = createApplicationVessels({
|
);
|
||||||
|
const vessels = once(() =>
|
||||||
|
createApplicationVessels({
|
||||||
source: sources.vessels,
|
source: sources.vessels,
|
||||||
options: vesselOptions,
|
options: vesselOptions,
|
||||||
});
|
}),
|
||||||
const installations = createApplicationInstallations({
|
);
|
||||||
|
const installations = once(() =>
|
||||||
|
createApplicationInstallations({
|
||||||
surface,
|
surface,
|
||||||
source: sources.installations,
|
source: sources.installations,
|
||||||
});
|
}),
|
||||||
const satellites = createApplicationSatellites({
|
);
|
||||||
|
const satellites = once(() =>
|
||||||
|
createApplicationSatellites({
|
||||||
source: sources.satellites,
|
source: sources.satellites,
|
||||||
});
|
}),
|
||||||
const catalog = createLayerCatalog(
|
);
|
||||||
|
// Catalog order: [layer id(s), construction]. Only the layers the
|
||||||
|
// engine can show are constructed (renderers.js).
|
||||||
|
const entries = [
|
||||||
|
['bhote-koshi-2026', () => createBhoteKoshiEventLayer()],
|
||||||
[
|
[
|
||||||
createBhoteKoshiEventLayer(),
|
'bhote-koshi-locator',
|
||||||
|
() =>
|
||||||
createBhoteKoshiLocatorLayer({
|
createBhoteKoshiLocatorLayer({
|
||||||
boundaryResolver: nepalBoundaryResolver,
|
boundaryResolver: nepalBoundaryResolver,
|
||||||
}),
|
}),
|
||||||
flights,
|
],
|
||||||
military,
|
['flights', flights],
|
||||||
|
['military', military],
|
||||||
|
[
|
||||||
|
'local-adsb',
|
||||||
|
() =>
|
||||||
createApplicationLocalAdsb({
|
createApplicationLocalAdsb({
|
||||||
surface,
|
surface,
|
||||||
enrichment: sources.flights,
|
enrichment: sources.flights,
|
||||||
displayParams: () => flights.getParams(),
|
displayParams: () => flights().getParams(),
|
||||||
...(resolveAsset ? { resolveAsset } : {}),
|
...(resolveAsset ? { resolveAsset } : {}),
|
||||||
}),
|
}),
|
||||||
createApplicationEarthquakes({ source: sources.earthquakes }),
|
],
|
||||||
|
[
|
||||||
|
'earthquakes',
|
||||||
|
() => createApplicationEarthquakes({ source: sources.earthquakes }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'fire-perimeters',
|
||||||
|
() =>
|
||||||
createApplicationFirePerimeters({
|
createApplicationFirePerimeters({
|
||||||
source: sources['fire-perimeters'],
|
source: sources['fire-perimeters'],
|
||||||
}),
|
}),
|
||||||
createApplicationAlpr({ surface, source: sources.alpr }),
|
],
|
||||||
|
[
|
||||||
|
'alpr-cameras',
|
||||||
|
() => createApplicationAlpr({ surface, source: sources.alpr }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'street-level',
|
||||||
|
() =>
|
||||||
createApplicationStreetLevel({
|
createApplicationStreetLevel({
|
||||||
surface,
|
surface,
|
||||||
sources: { mapillary: sources.mapillary },
|
sources: { mapillary: sources.mapillary },
|
||||||
providers: streetLevelProviders,
|
providers: streetLevelProviders,
|
||||||
}),
|
}),
|
||||||
satellites,
|
],
|
||||||
createApplicationLaunches({ source: sources.launches, satellites }),
|
['satellites', satellites],
|
||||||
createApplicationTraffic({ source: sources.traffic, surface }),
|
[
|
||||||
createApplicationCctv({ surface, source: sources.cctv }),
|
'rocket-launches',
|
||||||
createApplicationRadio({ surface, source: sources.radio }),
|
() =>
|
||||||
createApplicationTransit({ surface, source: sources.transit }),
|
createApplicationLaunches({
|
||||||
createApplicationBikeshare({ source: sources.bikeshare }),
|
source: sources.launches,
|
||||||
createApplicationDirections({ source: sources.directions }),
|
satellites: satellites(),
|
||||||
createApplicationRecentImagery({ source: sources['recent-imagery'] }),
|
}),
|
||||||
vessels,
|
],
|
||||||
installations,
|
[
|
||||||
|
'traffic',
|
||||||
|
() => createApplicationTraffic({ source: sources.traffic, surface }),
|
||||||
|
],
|
||||||
|
['cctv', () => createApplicationCctv({ surface, source: sources.cctv })],
|
||||||
|
[
|
||||||
|
'radio',
|
||||||
|
() => createApplicationRadio({ surface, source: sources.radio }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'transit',
|
||||||
|
() => createApplicationTransit({ surface, source: sources.transit }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bikeshare',
|
||||||
|
() => createApplicationBikeshare({ source: sources.bikeshare }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'directions',
|
||||||
|
() => createApplicationDirections({ source: sources.directions }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'recent-imagery',
|
||||||
|
() =>
|
||||||
|
createApplicationRecentImagery({
|
||||||
|
source: sources['recent-imagery'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
['ais-live-vessels', vessels],
|
||||||
|
['military-installations', installations],
|
||||||
|
[
|
||||||
|
'military-awareness',
|
||||||
|
() =>
|
||||||
createApplicationAwareness({
|
createApplicationAwareness({
|
||||||
flights,
|
flights: flights(),
|
||||||
military,
|
military: military(),
|
||||||
vessels,
|
vessels: vessels(),
|
||||||
installations,
|
installations: installations(),
|
||||||
}),
|
}),
|
||||||
createWindLayer({ feed: sources.wind, clock: weatherClock }),
|
],
|
||||||
|
[
|
||||||
|
'wind',
|
||||||
|
() => createWindLayer({ feed: sources.wind, clock: weatherClock }),
|
||||||
|
],
|
||||||
|
...['weather-radar', 'weather-satellite', 'weather-lightning'].map(
|
||||||
|
(id) => [
|
||||||
|
id,
|
||||||
|
() =>
|
||||||
createWeatherLayer({
|
createWeatherLayer({
|
||||||
feed: sources.weather,
|
feed: sources.weather,
|
||||||
id: 'weather-radar',
|
id,
|
||||||
clock: weatherClock,
|
clock: weatherClock,
|
||||||
}),
|
}),
|
||||||
createWeatherLayer({
|
],
|
||||||
feed: sources.weather,
|
),
|
||||||
id: 'weather-satellite',
|
[
|
||||||
clock: weatherClock,
|
'weather-cyclones',
|
||||||
}),
|
() => createCyclonesLayer({ feed: sources.cyclones }),
|
||||||
createWeatherLayer({
|
],
|
||||||
feed: sources.weather,
|
[
|
||||||
id: 'weather-lightning',
|
['local-datacenters', 'local-dams'],
|
||||||
clock: weatherClock,
|
() => createInfrastructureLayers(localGeoJsonServices),
|
||||||
}),
|
],
|
||||||
createCyclonesLayer({ feed: sources.cyclones }),
|
[
|
||||||
...createInfrastructureLayers(localGeoJsonServices),
|
'telegeography-submarine-cables',
|
||||||
createApplicationCables({ source: sources.cables }),
|
() => createApplicationCables({ source: sources.cables }),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'local-firms',
|
||||||
|
() =>
|
||||||
createApplicationFirms({
|
createApplicationFirms({
|
||||||
surface,
|
surface,
|
||||||
id: 'local-firms',
|
id: 'local-firms',
|
||||||
@@ -171,12 +261,37 @@ export function createApplicationCatalog({
|
|||||||
feed: sources.firms,
|
feed: sources.firms,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
metadata,
|
];
|
||||||
|
const available = (id) => availability.isAvailable(id);
|
||||||
|
const layers = [];
|
||||||
|
for (const [ids, create] of entries) {
|
||||||
|
const wanted = [ids].flat().filter(available);
|
||||||
|
if (!wanted.length) continue;
|
||||||
|
for (const layer of [create()].flat())
|
||||||
|
if (wanted.includes(layer.id)) layers.push(layer);
|
||||||
|
}
|
||||||
|
const catalog = createLayerCatalog(
|
||||||
|
layers,
|
||||||
|
metadata.filter(({ id }) => available(id)),
|
||||||
);
|
);
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...catalog,
|
...catalog,
|
||||||
|
engineKind,
|
||||||
|
// Layers this engine cannot show: their metadata, so links keep their
|
||||||
|
// state (data.js), and the stable availability API (renderers.js).
|
||||||
|
unavailableMetadata: Object.freeze(
|
||||||
|
metadata.filter(({ id }) => !available(id)),
|
||||||
|
),
|
||||||
|
availability,
|
||||||
getSourceAvailability: sourceComposition.createAvailabilityLookup(
|
getSourceAvailability: sourceComposition.createAvailabilityLookup(
|
||||||
catalog.layers,
|
catalog.layers,
|
||||||
|
{
|
||||||
|
omitted: new Set(
|
||||||
|
APPLICATION_LAYER_DIRECTORY.map(({ id }) => id).filter(
|
||||||
|
(id) => !available(id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
militaryRegistry,
|
militaryRegistry,
|
||||||
surface,
|
surface,
|
||||||
|
|||||||
@@ -191,3 +191,54 @@ test('Mapillary can be omitted or replaced without changing catalog membership',
|
|||||||
/Mapillary source/,
|
/Mapillary source/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the catalog per engine: Cesium as before, Navara only what it can show', async (t) => {
|
||||||
|
const { APPLICATION_LAYER_DIRECTORY } = await import('./layers/directory.js');
|
||||||
|
const { createLayerAvailability, createRendererRegistry } =
|
||||||
|
await import('./layers/renderers.js');
|
||||||
|
const controller = new AbortController();
|
||||||
|
t.after(() => controller.abort());
|
||||||
|
const build = (options) =>
|
||||||
|
createApplicationCatalog({
|
||||||
|
sources: createStandaloneLayerSources(),
|
||||||
|
signal: controller.signal,
|
||||||
|
surface: fixtureSurface(controller.signal),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const cesium = build({});
|
||||||
|
// The directory is the catalog Cesium builds: ids, names and order.
|
||||||
|
assert.deepEqual(
|
||||||
|
cesium.layers.map(({ id, name }) => ({ id, name })),
|
||||||
|
APPLICATION_LAYER_DIRECTORY.map(({ id, name }) => ({ id, name })),
|
||||||
|
);
|
||||||
|
assert.equal(cesium.engineKind, 'cesium');
|
||||||
|
assert.deepEqual(cesium.unavailableMetadata, []);
|
||||||
|
assert.equal(cesium.availability.unavailable().length, 0);
|
||||||
|
|
||||||
|
const navara = build({ engineKind: 'navara' });
|
||||||
|
assert.equal(navara.layers.length, 0, 'no Navara renderers yet');
|
||||||
|
assert.equal(navara.unavailableMetadata.length, cesium.metadata.length);
|
||||||
|
assert.equal(navara.availability.engine, 'navara');
|
||||||
|
|
||||||
|
// With Navara renderers for two layers, only those are constructed, in
|
||||||
|
// catalog order, with their metadata.
|
||||||
|
const registry = createRendererRegistry();
|
||||||
|
registry.register('navara', {
|
||||||
|
satellites: () => ({}),
|
||||||
|
flights: () => ({}),
|
||||||
|
});
|
||||||
|
const subset = build({
|
||||||
|
engineKind: 'navara',
|
||||||
|
availability: createLayerAvailability('navara', { registry }),
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
subset.layers.map(({ id }) => id),
|
||||||
|
['flights', 'satellites'],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
subset.metadata.map(({ id }) => id),
|
||||||
|
['flights', 'satellites'],
|
||||||
|
);
|
||||||
|
assert.ok(subset.unavailableMetadata.some(({ id }) => id === 'military'));
|
||||||
|
assert.equal(subset.get('military'), undefined);
|
||||||
|
});
|
||||||
Vendored
+4
-8
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { catalogControlServices } from './catalog.js';
|
||||||
absentLayerControlServices,
|
|
||||||
catalogControlServices,
|
|
||||||
} from './catalog.js';
|
|
||||||
import { StyleManager } from '../ui/composition.js';
|
import { StyleManager } from '../ui/composition.js';
|
||||||
import { flyToAustin } from '../camera.js';
|
import { flyToAustin } from '../camera.js';
|
||||||
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
||||||
@@ -24,10 +21,9 @@ export function createApplicationControls({
|
|||||||
searchAndFlyTo: operations.searchAndFlyTo,
|
searchAndFlyTo: operations.searchAndFlyTo,
|
||||||
fetchRegionalBrief: (...args) =>
|
fetchRegionalBrief: (...args) =>
|
||||||
operations.requests.regional.getBrief(...args),
|
operations.requests.regional.getBrief(...args),
|
||||||
// No viewer (the Navara engine): no data layers, empty stand-ins.
|
// The catalog's layers; the ones this engine cannot show (Navara)
|
||||||
...(viewer
|
// are empty stand-ins.
|
||||||
? catalogControlServices(catalog)
|
...catalogControlServices(catalog),
|
||||||
: absentLayerControlServices()),
|
|
||||||
},
|
},
|
||||||
requestServices: operations.requests,
|
requestServices: operations.requests,
|
||||||
// The page's globe engine: the shell's overlay host runs on it.
|
// The page's globe engine: the shell's overlay host runs on it.
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,7 @@ import { LayerPresentation } from './layerPresentation.js';
|
|||||||
import { createCyberSonarScene } from '../cyberSonarScene.js';
|
import { createCyberSonarScene } from '../cyberSonarScene.js';
|
||||||
/** Register the application layer catalog before allowing state restoration. */
|
/** Register the application layer catalog before allowing state restoration. */
|
||||||
export function createApplicationData({
|
export function createApplicationData({
|
||||||
scene: { viewer, mapStackController },
|
scene: { viewer, engine, mapStackController },
|
||||||
controls: { styleManager },
|
controls: { styleManager },
|
||||||
catalog,
|
catalog,
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
@@ -11,7 +11,9 @@ export function createApplicationData({
|
|||||||
defer,
|
defer,
|
||||||
}) {
|
}) {
|
||||||
// Initialize data layer manager
|
// Initialize data layer manager
|
||||||
|
// The modules get { signal, engine } on every call (viewer null on Navara).
|
||||||
const dataManager = new LayerLifecycle(viewer, {
|
const dataManager = new LayerLifecycle(viewer, {
|
||||||
|
engine,
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
getSourceAvailability: catalog?.getSourceAvailability,
|
getSourceAvailability: catalog?.getSourceAvailability,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// The application's layers in catalog order, by id and display name: what
|
||||||
|
// the catalog builds on Cesium. Plain data, so the availability of a layer
|
||||||
|
// on an engine (renderers.js) can be told without constructing it.
|
||||||
|
// constructCatalog.test.mjs keeps it equal to the constructed catalog.
|
||||||
|
|
||||||
|
export const APPLICATION_LAYER_DIRECTORY = Object.freeze(
|
||||||
|
[
|
||||||
|
['bhote-koshi-2026', 'Bhote Koshi Flood'],
|
||||||
|
['bhote-koshi-locator', 'Bhote Koshi Locator'],
|
||||||
|
['flights', 'Live Flights'],
|
||||||
|
['military', 'Military Flights'],
|
||||||
|
['local-adsb', 'Local ADS-B'],
|
||||||
|
['earthquakes', 'Earthquakes (24h)'],
|
||||||
|
['fire-perimeters', 'Fire Perimeters'],
|
||||||
|
['alpr-cameras', 'ALPR Cameras'],
|
||||||
|
['street-level', 'Street Level'],
|
||||||
|
['satellites', 'Satellites'],
|
||||||
|
['rocket-launches', 'Space Missions (30d)'],
|
||||||
|
['traffic', 'Street Traffic'],
|
||||||
|
['cctv', 'CCTV'],
|
||||||
|
['radio', 'Radio'],
|
||||||
|
['transit', 'Transit'],
|
||||||
|
['bikeshare', 'Bikeshare'],
|
||||||
|
['directions', 'Directions'],
|
||||||
|
['recent-imagery', 'Recent Imagery'],
|
||||||
|
['ais-live-vessels', 'Live AIS Vessels'],
|
||||||
|
['military-installations', 'Mapped Installations'],
|
||||||
|
['military-awareness', 'Global Context'],
|
||||||
|
['wind', 'Wind'],
|
||||||
|
['weather-radar', 'Rain radar'],
|
||||||
|
['weather-satellite', 'Satellite clouds'],
|
||||||
|
['weather-lightning', 'Lightning density'],
|
||||||
|
['weather-cyclones', 'Cyclone advisories'],
|
||||||
|
['local-datacenters', 'Datacenters'],
|
||||||
|
['local-dams', 'Dams'],
|
||||||
|
['telegeography-submarine-cables', 'Submarine Cables'],
|
||||||
|
['local-firms', 'FIRMS Active Fires'],
|
||||||
|
].map(([id, name]) => Object.freeze({ id, name })),
|
||||||
|
);
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// Which engine draws which layer: the layer renderer registry (plan, "layer
|
||||||
|
// renderer interface") and the layer availability API built on it.
|
||||||
|
//
|
||||||
|
// A renderer factory `(engine, options) => renderer` draws one layer on one
|
||||||
|
// engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium
|
||||||
|
// layers still draw themselves until their render/cesium.js is extracted
|
||||||
|
// (the *a tasks): they are registered as CESIUM_BUILTIN. Cesium entries are
|
||||||
|
// registered here, statically (main chunk); Navara factories are registered
|
||||||
|
// by the Navara mode after its import() (src/app/navaraMode.js).
|
||||||
|
//
|
||||||
|
// ## Layer availability (stable API, used by phase 4's layer list)
|
||||||
|
//
|
||||||
|
// import { createLayerAvailability } from './app/layers/renderers.js';
|
||||||
|
// const availability = createLayerAvailability(engine.kind);
|
||||||
|
// availability.engine // 'cesium' | 'navara'
|
||||||
|
// availability.isAvailable(id) // can this engine show layer `id`?
|
||||||
|
// availability.available() // [{ id, name }] in catalog order
|
||||||
|
// availability.unavailable() // [{ id, name }] the catalog leaves out
|
||||||
|
//
|
||||||
|
// The object is frozen; available() and unavailable() return fresh arrays.
|
||||||
|
// It reads the registry when called, so a Navara factory registered later
|
||||||
|
// (after the Navara mode loads) shows up. `name` is the layer's display name
|
||||||
|
// (src/app/layers/directory.js). Phase 4 injects it as its `availability`
|
||||||
|
// provider (`{ engine, unavailable() }` is the part it reads).
|
||||||
|
|
||||||
|
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
||||||
|
|
||||||
|
const ENGINE_KINDS = new Set(['cesium', 'navara']);
|
||||||
|
|
||||||
|
/** The layer draws itself on Cesium (no separate renderer yet). */
|
||||||
|
export const CESIUM_BUILTIN = Object.freeze({ builtin: 'cesium' });
|
||||||
|
|
||||||
|
const isFactory = (value) =>
|
||||||
|
typeof value === 'function' || value === CESIUM_BUILTIN;
|
||||||
|
|
||||||
|
/** A registry of layer renderers per engine kind. */
|
||||||
|
export function createRendererRegistry() {
|
||||||
|
const byEngine = new Map();
|
||||||
|
const entries = (engineKind) => byEngine.get(engineKind) ?? new Map();
|
||||||
|
return {
|
||||||
|
/** Adds `{ [layerId]: factory }` for an engine kind; one per layer. */
|
||||||
|
register(engineKind, factories) {
|
||||||
|
if (typeof engineKind !== 'string' || !engineKind)
|
||||||
|
throw new TypeError('registerLayerRenderers needs an engine kind');
|
||||||
|
let map = byEngine.get(engineKind);
|
||||||
|
if (!map) byEngine.set(engineKind, (map = new Map()));
|
||||||
|
for (const [layerId, factory] of Object.entries(factories ?? {})) {
|
||||||
|
if (!isFactory(factory))
|
||||||
|
throw new TypeError(
|
||||||
|
`Layer renderer for ${layerId} on ${engineKind} is not a factory`,
|
||||||
|
);
|
||||||
|
if (map.has(layerId))
|
||||||
|
throw new Error(
|
||||||
|
`Layer ${layerId} already has a renderer on ${engineKind}`,
|
||||||
|
);
|
||||||
|
map.set(layerId, factory);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The renderer of a layer on `engine` (factory(engine, options)), or null
|
||||||
|
* when the engine has none or the layer still draws itself (Cesium).
|
||||||
|
*/
|
||||||
|
rendererFor(layerId, engine, options = {}) {
|
||||||
|
const factory = entries(engine?.kind).get(layerId);
|
||||||
|
return typeof factory === 'function' ? factory(engine, options) : null;
|
||||||
|
},
|
||||||
|
layerAvailableOn: (layerId, engineKind) => entries(engineKind).has(layerId),
|
||||||
|
availableIds: (engineKind) => [...entries(engineKind).keys()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultRegistry = createRendererRegistry();
|
||||||
|
|
||||||
|
/** Registers layer renderers on the application's registry. */
|
||||||
|
export const registerLayerRenderers = (engineKind, factories) =>
|
||||||
|
defaultRegistry.register(engineKind, factories);
|
||||||
|
/** The application registry's renderer of a layer on an engine, or null. */
|
||||||
|
export const rendererFor = (layerId, engine, options) =>
|
||||||
|
defaultRegistry.rendererFor(layerId, engine, options);
|
||||||
|
/** Whether the application can show a layer on an engine kind. */
|
||||||
|
export const layerAvailableOn = (layerId, engineKind) =>
|
||||||
|
defaultRegistry.layerAvailableOn(layerId, engineKind);
|
||||||
|
|
||||||
|
// Every application layer draws itself on Cesium today.
|
||||||
|
registerLayerRenderers(
|
||||||
|
'cesium',
|
||||||
|
Object.fromEntries(
|
||||||
|
APPLICATION_LAYER_DIRECTORY.map(({ id }) => [id, CESIUM_BUILTIN]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} LayerAvailability
|
||||||
|
* @property {'cesium' | 'navara'} engine
|
||||||
|
* @property {(layerId: string) => boolean} isAvailable
|
||||||
|
* @property {() => { id: string, name: string }[]} available
|
||||||
|
* @property {() => { id: string, name: string }[]} unavailable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layers an engine kind can show (stable API; see the module comment).
|
||||||
|
* @param {'cesium' | 'navara'} engineKind
|
||||||
|
* @param {{ registry?: object, directory?: { id: string, name: string }[] }} [options]
|
||||||
|
* Test seams: the registry and the layer directory.
|
||||||
|
* @returns {LayerAvailability}
|
||||||
|
*/
|
||||||
|
export function createLayerAvailability(
|
||||||
|
engineKind,
|
||||||
|
{ registry = defaultRegistry, directory = APPLICATION_LAYER_DIRECTORY } = {},
|
||||||
|
) {
|
||||||
|
if (!ENGINE_KINDS.has(engineKind))
|
||||||
|
throw new TypeError(`Unknown engine kind for layers: ${engineKind}`);
|
||||||
|
const isAvailable = (layerId) =>
|
||||||
|
registry.layerAvailableOn(layerId, engineKind);
|
||||||
|
const rows = (wanted) =>
|
||||||
|
directory
|
||||||
|
.filter(({ id }) => isAvailable(id) === wanted)
|
||||||
|
.map(({ id, name }) => ({ id, name }));
|
||||||
|
return Object.freeze({
|
||||||
|
engine: engineKind,
|
||||||
|
isAvailable,
|
||||||
|
available: () => rows(true),
|
||||||
|
unavailable: () => rows(false),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
CESIUM_BUILTIN,
|
||||||
|
createLayerAvailability,
|
||||||
|
createRendererRegistry,
|
||||||
|
layerAvailableOn,
|
||||||
|
} from './renderers.js';
|
||||||
|
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
||||||
|
|
||||||
|
test('a registry knows which engine renders which layer', () => {
|
||||||
|
const registry = createRendererRegistry();
|
||||||
|
const made = [];
|
||||||
|
registry.register('navara', {
|
||||||
|
flights: (engine, options) => {
|
||||||
|
made.push([engine.kind, options]);
|
||||||
|
return { destroy() {} };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
registry.register('cesium', {
|
||||||
|
flights: CESIUM_BUILTIN,
|
||||||
|
satellites: CESIUM_BUILTIN,
|
||||||
|
});
|
||||||
|
assert.equal(registry.layerAvailableOn('flights', 'navara'), true);
|
||||||
|
assert.equal(registry.layerAvailableOn('satellites', 'navara'), false);
|
||||||
|
assert.equal(registry.layerAvailableOn('satellites', 'cesium'), true);
|
||||||
|
assert.equal(registry.layerAvailableOn('flights', 'other'), false);
|
||||||
|
const renderer = registry.rendererFor(
|
||||||
|
'flights',
|
||||||
|
{ kind: 'navara' },
|
||||||
|
{ tint: 1 },
|
||||||
|
);
|
||||||
|
assert.ok(renderer);
|
||||||
|
assert.deepEqual(made, [['navara', { tint: 1 }]]);
|
||||||
|
// A Cesium layer that still renders itself has no separate renderer.
|
||||||
|
assert.equal(registry.rendererFor('flights', { kind: 'cesium' }), null);
|
||||||
|
assert.equal(registry.rendererFor('satellites', { kind: 'navara' }), null);
|
||||||
|
assert.deepEqual(registry.availableIds('cesium').sort(), [
|
||||||
|
'flights',
|
||||||
|
'satellites',
|
||||||
|
]);
|
||||||
|
assert.throws(() => registry.register('navara', { x: 42 }), TypeError);
|
||||||
|
assert.throws(
|
||||||
|
() => registry.register('navara', { flights: () => ({}) }),
|
||||||
|
/already/,
|
||||||
|
'one renderer per layer and engine',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the default registry: every layer on Cesium, none yet on Navara', () => {
|
||||||
|
for (const { id } of APPLICATION_LAYER_DIRECTORY) {
|
||||||
|
assert.equal(layerAvailableOn(id, 'cesium'), true, id);
|
||||||
|
assert.equal(layerAvailableOn(id, 'navara'), false, id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the layer availability API: engine, isAvailable, available, unavailable', () => {
|
||||||
|
const registry = createRendererRegistry();
|
||||||
|
registry.register('navara', { flights: () => ({}) });
|
||||||
|
const directory = [
|
||||||
|
{ id: 'flights', name: 'Live Flights' },
|
||||||
|
{ id: 'satellites', name: 'Satellites' },
|
||||||
|
];
|
||||||
|
const availability = createLayerAvailability('navara', {
|
||||||
|
registry,
|
||||||
|
directory,
|
||||||
|
});
|
||||||
|
assert.equal(availability.engine, 'navara');
|
||||||
|
assert.equal(availability.isAvailable('flights'), true);
|
||||||
|
assert.equal(availability.isAvailable('satellites'), false);
|
||||||
|
assert.equal(availability.isAvailable('unknown'), false);
|
||||||
|
assert.deepEqual(availability.available(), [
|
||||||
|
{ id: 'flights', name: 'Live Flights' },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(availability.unavailable(), [
|
||||||
|
{ id: 'satellites', name: 'Satellites' },
|
||||||
|
]);
|
||||||
|
// Fresh copies: a caller cannot change the answer.
|
||||||
|
availability.unavailable().push({ id: 'x', name: 'x' });
|
||||||
|
assert.equal(availability.unavailable().length, 1);
|
||||||
|
assert.ok(Object.isFrozen(availability));
|
||||||
|
const cesium = createLayerAvailability('cesium');
|
||||||
|
assert.equal(cesium.unavailable().length, 0);
|
||||||
|
assert.equal(cesium.available().length, APPLICATION_LAYER_DIRECTORY.length);
|
||||||
|
assert.throws(() => createLayerAvailability('vulkan'), TypeError);
|
||||||
|
});
|
||||||
+19
-5
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
|
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
|
||||||
import { isEmbeddedInline } from './embed.js';
|
import { isEmbeddedInline } from './embed.js';
|
||||||
|
import { createLayerAvailability } from './layers/renderers.js';
|
||||||
import {
|
import {
|
||||||
getRenderGovernorDiagnostics,
|
getRenderGovernorDiagnostics,
|
||||||
governorRequestRender,
|
governorRequestRender,
|
||||||
@@ -16,11 +17,21 @@ import {
|
|||||||
const loadNavaraGlobeModule = async () =>
|
const loadNavaraGlobeModule = async () =>
|
||||||
(await import('../globe/navara/index.js')).createNavaraGlobe;
|
(await import('../globe/navara/index.js')).createNavaraGlobe;
|
||||||
|
|
||||||
/** The shell's notice in the experimental Navara mode (label and detail). */
|
/**
|
||||||
export const NAVARA_MODE_NOTICE = Object.freeze({
|
* The shell's notice in the experimental Navara mode (label and detail): the
|
||||||
|
* data layers this engine shows (its catalog, from the renderer registry)
|
||||||
|
* and what it does not have.
|
||||||
|
* @param {import('./layers/renderers.js').LayerAvailability} availability
|
||||||
|
*/
|
||||||
|
export function navaraModeNotice(availability) {
|
||||||
|
const names = availability.available().map(({ name }) => name);
|
||||||
|
return Object.freeze({
|
||||||
label: 'Experimental Navara engine',
|
label: 'Experimental Navara engine',
|
||||||
detail: 'Layers, cockpit, director and voice are unavailable',
|
detail: names.length
|
||||||
|
? `Data layers: ${names.join(', ')}; the other layers, cockpit, director and voice are unavailable`
|
||||||
|
: 'No data layers on this engine yet; cockpit, director and voice are unavailable',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data credits for what the shell uses without data layers: place search,
|
* Data credits for what the shell uses without data layers: place search,
|
||||||
@@ -131,9 +142,12 @@ export function createViewerlessTools({
|
|||||||
};
|
};
|
||||||
document.addEventListener('visibilitychange', syncVisibility);
|
document.addEventListener('visibilitychange', syncVisibility);
|
||||||
defer(() => document.removeEventListener('visibilitychange', syncVisibility));
|
defer(() => document.removeEventListener('visibilitychange', syncVisibility));
|
||||||
styleManager._showGlobalStatusNotice?.(NAVARA_MODE_NOTICE.label, {
|
const notice = navaraModeNotice(
|
||||||
|
data.catalog?.availability ?? createLayerAvailability('navara'),
|
||||||
|
);
|
||||||
|
styleManager._showGlobalStatusNotice?.(notice.label, {
|
||||||
state: 'retry',
|
state: 'retry',
|
||||||
detail: NAVARA_MODE_NOTICE.detail,
|
detail: notice.detail,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
});
|
});
|
||||||
window.__godsEyeView = {
|
window.__godsEyeView = {
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ export function resolveCatalogSources(supplied = {}) {
|
|||||||
return {
|
return {
|
||||||
sources,
|
sources,
|
||||||
isConfigured: (name) => !missing.has(name),
|
isConfigured: (name) => !missing.has(name),
|
||||||
createAvailabilityLookup(layers) {
|
/** `omitted`: layer ids the catalog leaves out on purpose (another
|
||||||
|
* engine's layers); any other missing consumer is a wiring error. */
|
||||||
|
createAvailabilityLookup(layers, { omitted = new Set() } = {}) {
|
||||||
const byId = new Map(layers.map((layer) => [layer.id, layer]));
|
const byId = new Map(layers.map((layer) => [layer.id, layer]));
|
||||||
const availability = new Map();
|
const availability = new Map();
|
||||||
for (const [name, { layers: ids }] of Object.entries(
|
for (const [name, { layers: ids }] of Object.entries(
|
||||||
@@ -93,7 +95,10 @@ export function resolveCatalogSources(supplied = {}) {
|
|||||||
)) {
|
)) {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const layer = byId.get(id);
|
const layer = byId.get(id);
|
||||||
if (!layer) throw new Error(`Missing source consumer: ${id}`);
|
if (!layer) {
|
||||||
|
if (omitted.has(id)) continue;
|
||||||
|
throw new Error(`Missing source consumer: ${id}`);
|
||||||
|
}
|
||||||
availability.set(
|
availability.set(
|
||||||
id,
|
id,
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createApplicationTools } from './tools.js';
|
import { createApplicationTools } from './tools.js';
|
||||||
import { NAVARA_MODE_NOTICE } from './navaraMode.js';
|
import { navaraModeNotice } from './navaraMode.js';
|
||||||
|
import {
|
||||||
|
createLayerAvailability,
|
||||||
|
createRendererRegistry,
|
||||||
|
} from './layers/renderers.js';
|
||||||
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
import {
|
import {
|
||||||
_resetRenderGovernorForTest,
|
_resetRenderGovernorForTest,
|
||||||
@@ -66,19 +70,27 @@ test('without a viewer the tools run the governor on the engine, show the notice
|
|||||||
assert.equal(chrome.length, 1);
|
assert.equal(chrome.length, 1);
|
||||||
assert.equal(chrome[0].initializeWelcome, null, 'no first-run welcome');
|
assert.equal(chrome[0].initializeWelcome, null, 'no first-run welcome');
|
||||||
assert.equal(chrome[0].styleManager, styleManager);
|
assert.equal(chrome[0].styleManager, styleManager);
|
||||||
|
const notice = navaraModeNotice(createLayerAvailability('navara'));
|
||||||
assert.deepEqual(notices, [
|
assert.deepEqual(notices, [
|
||||||
{
|
{
|
||||||
label: NAVARA_MODE_NOTICE.label,
|
label: notice.label,
|
||||||
options: {
|
options: {
|
||||||
state: 'retry',
|
state: 'retry',
|
||||||
detail: NAVARA_MODE_NOTICE.detail,
|
detail: notice.detail,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert.match(
|
assert.match(
|
||||||
`${NAVARA_MODE_NOTICE.label}: ${NAVARA_MODE_NOTICE.detail}`,
|
`${notice.label}: ${notice.detail}`,
|
||||||
/^Experimental Navara engine: layers, cockpit, director and voice are unavailable$/i,
|
/^Experimental Navara engine: no data layers on this engine yet; cockpit, director and voice are unavailable$/i,
|
||||||
|
);
|
||||||
|
// With Navara renderers, the notice names the layers it shows.
|
||||||
|
const registry = createRendererRegistry();
|
||||||
|
registry.register('navara', { satellites: () => ({}), flights: () => ({}) });
|
||||||
|
assert.equal(
|
||||||
|
navaraModeNotice(createLayerAvailability('navara', { registry })).detail,
|
||||||
|
'Data layers: Live Flights, Satellites; the other layers, cockpit, director and voice are unavailable',
|
||||||
);
|
);
|
||||||
// The governor drives the engine's continuous rendering.
|
// The governor drives the engine's continuous rendering.
|
||||||
assert.equal(isRenderGovernorInstalled(engine), true);
|
assert.equal(isRenderGovernorInstalled(engine), true);
|
||||||
|
|||||||
@@ -2793,3 +2793,49 @@ test('explicit provider switches reach durable state without adopting unrelated
|
|||||||
coordinator.destroy();
|
coordinator.destroy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a link through an engine without some layers keeps their state and activates nothing', async () => {
|
||||||
|
// The Navara catalog: only satellites registered.
|
||||||
|
const manager = new DataLayerManager({});
|
||||||
|
const enabled = [];
|
||||||
|
manager.register(
|
||||||
|
fakeLayer('satellites', { enable: () => (enabled.push('satellites'), true) }),
|
||||||
|
);
|
||||||
|
manager.finalizeRegistrations(
|
||||||
|
LAYER_STATE_REGISTRY.filter(({ id }) => id === 'satellites'),
|
||||||
|
);
|
||||||
|
const shared = normalizeLayerState({
|
||||||
|
enabledLayerIds: ['flights', 'ais-live-vessels', 'satellites'],
|
||||||
|
});
|
||||||
|
const sink = shareSink();
|
||||||
|
const coordinator = new LayerStateCoordinator(manager, sink, {
|
||||||
|
storage: memoryStorage(),
|
||||||
|
});
|
||||||
|
const results = await coordinator.start({
|
||||||
|
shareLayerState: shared,
|
||||||
|
shareCreatedAtMs: 1_000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(enabled, ['satellites'], 'only the registered layer runs');
|
||||||
|
const missing = results
|
||||||
|
.filter(({ phase, targetEnabled }) => phase === 'missing' && targetEnabled)
|
||||||
|
.map(({ layerId }) => layerId)
|
||||||
|
.sort();
|
||||||
|
assert.deepEqual(missing, ['ais-live-vessels', 'flights']);
|
||||||
|
// Re-saved (the link the user copies on Navara): the state is all there.
|
||||||
|
assert.deepEqual(
|
||||||
|
sink.provider().enabledLayerIds,
|
||||||
|
['ais-live-vessels', 'flights', 'satellites'],
|
||||||
|
);
|
||||||
|
// An explicit change of a layer that is here keeps the others too.
|
||||||
|
await manager.setEnabled('satellites', false, { origin: 'user' });
|
||||||
|
assert.deepEqual(sink.provider().enabledLayerIds, [
|
||||||
|
'ais-live-vessels',
|
||||||
|
'flights',
|
||||||
|
]);
|
||||||
|
// And back on Cesium the link turns them on.
|
||||||
|
const decoded = decodeLayerStateParams(
|
||||||
|
new URLSearchParams(encode(sink.provider())),
|
||||||
|
);
|
||||||
|
assert.deepEqual(decoded.enabledLayerIds, ['ais-live-vessels', 'flights']);
|
||||||
|
coordinator.destroy();
|
||||||
|
});
|
||||||
+55
-13
@@ -1,3 +1,4 @@
|
|||||||
|
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||||
import { cancelCameraArrival } from './cameraArrival.js';
|
import { cancelCameraArrival } from './cameraArrival.js';
|
||||||
|
|
||||||
function cloneLayerParams(value) {
|
function cloneLayerParams(value) {
|
||||||
@@ -74,12 +75,20 @@ function refreshFailureFromStats(stats, label) {
|
|||||||
* for real-time data overlays on the CesiumJS globe.
|
* for real-time data overlays on the CesiumJS globe.
|
||||||
*/
|
*/
|
||||||
export class LayerLifecycle {
|
export class LayerLifecycle {
|
||||||
|
/**
|
||||||
|
* @param {object | null} viewer The Cesium viewer (null on Navara).
|
||||||
|
* @param {{ engine?: import('../globe/contract.js').GlobeEngine,
|
||||||
|
* allowQaRegistration?: boolean, getSourceAvailability?: Function }} [options]
|
||||||
|
* `engine` (default: the viewer's registered engine): every module call
|
||||||
|
* gets `{ signal, engine }` as its second argument.
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
viewer,
|
viewer,
|
||||||
{ allowQaRegistration = false, getSourceAvailability } = {},
|
{ allowQaRegistration = false, getSourceAvailability, engine } = {},
|
||||||
) {
|
) {
|
||||||
this._catalogSourceAvailability = getSourceAvailability;
|
this._catalogSourceAvailability = getSourceAvailability;
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
|
this.engine = engine ?? engineFromViewer(viewer) ?? null;
|
||||||
this._activityListeners = new Set();
|
this._activityListeners = new Set();
|
||||||
this.layers = new Map(); // id → { module, enabled, initialized, intervalId, lifecycleState, lifecycleUncertain }
|
this.layers = new Map(); // id → { module, enabled, initialized, intervalId, lifecycleState, lifecycleUncertain }
|
||||||
this._listeners = new Set();
|
this._listeners = new Set();
|
||||||
@@ -92,6 +101,11 @@ export class LayerLifecycle {
|
|||||||
this._qaLayerIds = new Set();
|
this._qaLayerIds = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The second argument of every layer module call: `{ signal, engine }`. */
|
||||||
|
_moduleContext(signal = null) {
|
||||||
|
return { signal, engine: this.engine };
|
||||||
|
}
|
||||||
|
|
||||||
register(layerModule) {
|
register(layerModule) {
|
||||||
if (this._registrationsFinalized) {
|
if (this._registrationsFinalized) {
|
||||||
throw new Error('Data-layer registrations are finalized');
|
throw new Error('Data-layer registrations are finalized');
|
||||||
@@ -292,7 +306,10 @@ export class LayerLifecycle {
|
|||||||
let result;
|
let result;
|
||||||
let failure = null;
|
let failure = null;
|
||||||
try {
|
try {
|
||||||
result = await entry.module.update(this.viewer, { signal });
|
result = await entry.module.update(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(signal),
|
||||||
|
);
|
||||||
// Even a rejected/partial update may have changed the data exposed by
|
// Even a rejected/partial update may have changed the data exposed by
|
||||||
// the layer. Publish before classifying its result so observers see it.
|
// the layer. Publish before classifying its result so observers see it.
|
||||||
this._publishActivity({ type: 'data-updated', layerId });
|
this._publishActivity({ type: 'data-updated', layerId });
|
||||||
@@ -739,7 +756,9 @@ export class LayerLifecycle {
|
|||||||
let compensated = false;
|
let compensated = false;
|
||||||
let compensationError = null;
|
let compensationError = null;
|
||||||
try {
|
try {
|
||||||
compensated = (await entry.module.enable(this.viewer)) !== false;
|
compensated =
|
||||||
|
(await entry.module.enable(this.viewer, this._moduleContext())) !==
|
||||||
|
false;
|
||||||
if (!compensated) {
|
if (!compensated) {
|
||||||
compensationError = lifecycleRejectedError(
|
compensationError = lifecycleRejectedError(
|
||||||
layerId,
|
layerId,
|
||||||
@@ -757,7 +776,10 @@ export class LayerLifecycle {
|
|||||||
if (!compensated) {
|
if (!compensated) {
|
||||||
try {
|
try {
|
||||||
cleanupConfirmed =
|
cleanupConfirmed =
|
||||||
(await entry.module.disable(this.viewer)) !== false;
|
(await entry.module.disable(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(),
|
||||||
|
)) !== false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Data] ${layerId} cancelled-disable final cleanup error:`,
|
`[Data] ${layerId} cancelled-disable final cleanup error:`,
|
||||||
@@ -805,7 +827,10 @@ export class LayerLifecycle {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const disabled = await entry.module.disable(this.viewer, { signal });
|
const disabled = await entry.module.disable(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(signal),
|
||||||
|
);
|
||||||
if (disabled === false)
|
if (disabled === false)
|
||||||
throw lifecycleRejectedError(layerId, 'disable');
|
throw lifecycleRejectedError(layerId, 'disable');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -854,7 +879,7 @@ export class LayerLifecycle {
|
|||||||
// where an asynchronous enable finishes after this callback.
|
// where an asynchronous enable finishes after this callback.
|
||||||
try {
|
try {
|
||||||
abortCleanup = Promise.resolve(
|
abortCleanup = Promise.resolve(
|
||||||
entry.module.disable(this.viewer),
|
entry.module.disable(this.viewer, this._moduleContext()),
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Data] ${layerId} cancelled-enable cleanup error:`,
|
`[Data] ${layerId} cancelled-enable cleanup error:`,
|
||||||
@@ -883,7 +908,8 @@ export class LayerLifecycle {
|
|||||||
let cleanupConfirmed = false;
|
let cleanupConfirmed = false;
|
||||||
try {
|
try {
|
||||||
cleanupConfirmed =
|
cleanupConfirmed =
|
||||||
(await entry.module.disable(this.viewer)) !== false;
|
(await entry.module.disable(this.viewer, this._moduleContext())) !==
|
||||||
|
false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Data] ${layerId} cancelled-enable final cleanup error:`,
|
`[Data] ${layerId} cancelled-enable final cleanup error:`,
|
||||||
@@ -924,7 +950,8 @@ export class LayerLifecycle {
|
|||||||
let cleanupConfirmed = false;
|
let cleanupConfirmed = false;
|
||||||
try {
|
try {
|
||||||
cleanupConfirmed =
|
cleanupConfirmed =
|
||||||
(await entry.module.disable(this.viewer)) !== false;
|
(await entry.module.disable(this.viewer, this._moduleContext())) !==
|
||||||
|
false;
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Data] ${layerId} failed-enable cleanup error:`,
|
`[Data] ${layerId} failed-enable cleanup error:`,
|
||||||
@@ -950,7 +977,10 @@ export class LayerLifecycle {
|
|||||||
if (!entry.initialized) {
|
if (!entry.initialized) {
|
||||||
this._setVisibilityIntentPhase(entry, intentEpoch, 'init');
|
this._setVisibilityIntentPhase(entry, intentEpoch, 'init');
|
||||||
try {
|
try {
|
||||||
const initialized = await entry.module.init(this.viewer, { signal });
|
const initialized = await entry.module.init(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(signal),
|
||||||
|
);
|
||||||
if (initialized === false)
|
if (initialized === false)
|
||||||
throw lifecycleRejectedError(layerId, 'init');
|
throw lifecycleRejectedError(layerId, 'init');
|
||||||
entry.initialized = true;
|
entry.initialized = true;
|
||||||
@@ -990,7 +1020,10 @@ export class LayerLifecycle {
|
|||||||
entry.lifecycleUncertain = false;
|
entry.lifecycleUncertain = false;
|
||||||
this._setVisibilityIntentPhase(entry, intentEpoch, 'enable');
|
this._setVisibilityIntentPhase(entry, intentEpoch, 'enable');
|
||||||
try {
|
try {
|
||||||
const enabled = await entry.module.enable(this.viewer, { signal });
|
const enabled = await entry.module.enable(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(signal),
|
||||||
|
);
|
||||||
if (enabled === false) throw lifecycleRejectedError(layerId, 'enable');
|
if (enabled === false) throw lifecycleRejectedError(layerId, 'enable');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (signal?.aborted || isAbortError(e)) {
|
if (signal?.aborted || isAbortError(e)) {
|
||||||
@@ -1006,7 +1039,10 @@ export class LayerLifecycle {
|
|||||||
// First update immediately
|
// First update immediately
|
||||||
this._setVisibilityIntentPhase(entry, intentEpoch, 'update');
|
this._setVisibilityIntentPhase(entry, intentEpoch, 'update');
|
||||||
try {
|
try {
|
||||||
const updated = await entry.module.update(this.viewer, { signal });
|
const updated = await entry.module.update(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(signal),
|
||||||
|
);
|
||||||
if (updated === false) throw lifecycleRejectedError(layerId, 'update');
|
if (updated === false) throw lifecycleRejectedError(layerId, 'update');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (signal?.aborted || isAbortError(e)) {
|
if (signal?.aborted || isAbortError(e)) {
|
||||||
@@ -2146,7 +2182,10 @@ export class LayerLifecycle {
|
|||||||
entry.latestQueuedAbsoluteIntent = null;
|
entry.latestQueuedAbsoluteIntent = null;
|
||||||
if (entry.enabled) {
|
if (entry.enabled) {
|
||||||
try {
|
try {
|
||||||
const disabled = await entry.module.disable(this.viewer);
|
const disabled = await entry.module.disable(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(),
|
||||||
|
);
|
||||||
if (disabled === false)
|
if (disabled === false)
|
||||||
throw lifecycleRejectedError(layerId, 'destroy-disable');
|
throw lifecycleRejectedError(layerId, 'destroy-disable');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -2165,7 +2204,10 @@ export class LayerLifecycle {
|
|||||||
}
|
}
|
||||||
if (typeof entry.module.destroy === 'function') {
|
if (typeof entry.module.destroy === 'function') {
|
||||||
try {
|
try {
|
||||||
const destroyed = await entry.module.destroy(this.viewer);
|
const destroyed = await entry.module.destroy(
|
||||||
|
this.viewer,
|
||||||
|
this._moduleContext(),
|
||||||
|
);
|
||||||
if (destroyed === false)
|
if (destroyed === false)
|
||||||
throw lifecycleRejectedError(layerId, 'destroy');
|
throw lifecycleRejectedError(layerId, 'destroy');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { LayerLifecycle } from './lifecycle.js';
|
||||||
|
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
|
||||||
|
|
||||||
|
// A layer module that records the second argument of every lifecycle call.
|
||||||
|
function recordingLayer(id) {
|
||||||
|
const calls = [];
|
||||||
|
const record = (name) => (_viewer, context) => {
|
||||||
|
calls.push([name, context]);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
calls,
|
||||||
|
module: {
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
icon: '',
|
||||||
|
source: 'test',
|
||||||
|
init: record('init'),
|
||||||
|
enable: record('enable'),
|
||||||
|
update: record('update'),
|
||||||
|
disable: record('disable'),
|
||||||
|
destroy: record('destroy'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('every layer module call gets { signal, engine }, also with no viewer', async () => {
|
||||||
|
const engine = createFakeEngine({ kind: 'navara' });
|
||||||
|
const lifecycle = new LayerLifecycle(null, { engine });
|
||||||
|
assert.equal(lifecycle.engine, engine);
|
||||||
|
const layer = recordingLayer('probe');
|
||||||
|
lifecycle.register(layer.module);
|
||||||
|
lifecycle.finalizeRegistrations([
|
||||||
|
{ id: 'probe', disposition: 'enabled-only', token: 'z' },
|
||||||
|
]);
|
||||||
|
await lifecycle.setEnabled('probe', true, { origin: 'user' });
|
||||||
|
await lifecycle.setEnabled('probe', false, { origin: 'user' });
|
||||||
|
await lifecycle.destroyAll();
|
||||||
|
const names = layer.calls.map(([name]) => name);
|
||||||
|
for (const name of ['init', 'enable', 'disable', 'destroy'])
|
||||||
|
assert.ok(names.includes(name), `${name} was called: ${names}`);
|
||||||
|
for (const [name, context] of layer.calls) {
|
||||||
|
assert.equal(context?.engine, engine, `${name} got the engine`);
|
||||||
|
assert.ok('signal' in context, `${name} got a signal slot`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -28,7 +28,7 @@ async function launch(overrides = {}, dotenv = '', omitCctv = false) {
|
|||||||
await fs.writeFile(path.join(root, 'src', 'standalone', 'catalog.js'), catalog);
|
await fs.writeFile(path.join(root, 'src', 'standalone', 'catalog.js'), catalog);
|
||||||
await fs.mkdir(path.join(root, 'src', 'app'), { recursive: true });
|
await fs.mkdir(path.join(root, 'src', 'app'), { recursive: true });
|
||||||
const assembly = await fs.readFile(new URL('./app/constructCatalog.js', import.meta.url), 'utf8');
|
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.writeFile(path.join(root, 'src', 'app', 'constructCatalog.js'), omitCctv ? assembly.replace(/^.*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);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { createStandaloneControls } from './controls.js';
|
|||||||
import { createStandaloneData } from './data.js';
|
import { createStandaloneData } from './data.js';
|
||||||
import { createStandaloneTools } from './tools.js';
|
import { createStandaloneTools } from './tools.js';
|
||||||
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
||||||
import { createLayerCatalog } from '../app/catalog.js';
|
|
||||||
import { resolveEngineKind } from '../app/engineChoice.js';
|
import { resolveEngineKind } from '../app/engineChoice.js';
|
||||||
|
|
||||||
// The existing controls and layer catalog contain page-scoped state.
|
// The existing controls and layer catalog contain page-scoped state.
|
||||||
@@ -63,13 +62,11 @@ export function createStandaloneApplication({
|
|||||||
loaderStatus,
|
loaderStatus,
|
||||||
engineKind,
|
engineKind,
|
||||||
});
|
});
|
||||||
// The experimental Navara engine runs without data layers: an empty
|
// The catalog of this engine: on Navara only the layers with a Navara
|
||||||
// catalog with the same interface.
|
// renderer (src/app/layers/renderers.js; none yet), the others' state
|
||||||
if (engineKind === 'navara') {
|
// kept for links.
|
||||||
catalog = createLayerCatalog([], []);
|
|
||||||
return scene;
|
|
||||||
}
|
|
||||||
catalog = createStandaloneCatalog({
|
catalog = createStandaloneCatalog({
|
||||||
|
engineKind,
|
||||||
sources,
|
sources,
|
||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
nepalBoundaryResolver: (signal) =>
|
nepalBoundaryResolver: (signal) =>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export { createStandaloneReferenceSources } from './layerSources.js';
|
|||||||
/** Create fresh layer instances using the existing standalone source choices. */
|
/** Create fresh layer instances using the existing standalone source choices. */
|
||||||
export function createStandaloneCatalog({
|
export function createStandaloneCatalog({
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
|
engineKind = 'cesium',
|
||||||
sources = {},
|
sources = {},
|
||||||
streetLevelProviders,
|
streetLevelProviders,
|
||||||
signal = new AbortController().signal,
|
signal = new AbortController().signal,
|
||||||
@@ -16,6 +17,7 @@ export function createStandaloneCatalog({
|
|||||||
}),
|
}),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return createApplicationCatalog({
|
return createApplicationCatalog({
|
||||||
|
engineKind,
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
surface,
|
surface,
|
||||||
sources: createStandaloneLayerSources(sources),
|
sources: createStandaloneLayerSources(sources),
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { LayerStateCoordinator } from '../data/layerStateCoordinator.js';
|
import { LayerStateCoordinator } from '../data/layerStateCoordinator.js';
|
||||||
|
import { APPLICATION_LAYER_DIRECTORY } from '../app/layers/directory.js';
|
||||||
|
|
||||||
|
/** Names of the layers a link turned on that this engine does not have. */
|
||||||
|
export function layersNotOnThisEngine(results) {
|
||||||
|
return results
|
||||||
|
.filter(({ phase, targetEnabled }) => phase === 'missing' && targetEnabled)
|
||||||
|
.map(
|
||||||
|
({ layerId }) =>
|
||||||
|
APPLICATION_LAYER_DIRECTORY.find(({ id }) => id === layerId)?.name ??
|
||||||
|
layerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
import { stampInitialShareGesture } from '../navigationPolicy.js';
|
import { stampInitialShareGesture } from '../navigationPolicy.js';
|
||||||
import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
@@ -159,11 +171,26 @@ export class ShareRestoration {
|
|||||||
{ clearSelection: true },
|
{ clearSelection: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
void this._layerStateRestorePromise.then(() => {
|
void this._layerStateRestorePromise.then((results) => {
|
||||||
this.syncModels3d(this._layerStateCoordinator?.getDurableState());
|
this.syncModels3d(this._layerStateCoordinator?.getDurableState());
|
||||||
|
this._noticeLayersNotOnThisEngine(results);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* A link that turns on layers this engine's catalog leaves out (Navara):
|
||||||
|
* they stay off here, their state stays in the link, and the notice names
|
||||||
|
* them.
|
||||||
|
*/
|
||||||
|
_noticeLayersNotOnThisEngine(results) {
|
||||||
|
if (this._disposed || !Array.isArray(results)) return;
|
||||||
|
const names = layersNotOnThisEngine(results);
|
||||||
|
if (!names.length) return;
|
||||||
|
this.showStatus('NOT ON THIS ENGINE', {
|
||||||
|
state: 'retry',
|
||||||
|
detail: `Kept in the link: ${names.join(', ')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
cancelSelection() {
|
cancelSelection() {
|
||||||
if (
|
if (
|
||||||
this._hasShareState &&
|
this._hasShareState &&
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { layersNotOnThisEngine } from './shareRestoration.js';
|
||||||
|
|
||||||
|
test('the notice names the link layers this engine leaves out', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
layersNotOnThisEngine([
|
||||||
|
{ layerId: 'flights', phase: 'missing', targetEnabled: true },
|
||||||
|
{ layerId: 'ais-live-vessels', phase: 'missing', targetEnabled: true },
|
||||||
|
{ layerId: 'traffic', phase: 'missing', targetEnabled: false },
|
||||||
|
{ layerId: 'satellites', phase: 'settled', targetEnabled: true },
|
||||||
|
]),
|
||||||
|
['Live Flights', 'Live AIS Vessels'],
|
||||||
|
);
|
||||||
|
assert.deepEqual(layersNotOnThisEngine([]), []);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user