Files
gods-eye-view/src/app/layers/displayRehearsal.test.mjs
T
German Khokhlov a2905eb700 Move the engine switch protocol from the layers into the scene binding
attachRenderer takes one context { engine, signal, viewRestored, ... } and
bindCatalog no longer takes a viewer; no layer module receives Cesium's
viewer any more, and a rehearsal check fails if one reads it.

The handoff between engines is one typed object (layers, camera owners,
selection, director) created when the old scene leaves, replacing the
session's handoffLayers, restoringView and selectionContext. viewRestored
resolves where the camera is restored, and the nine layers that kept
their own restoring-view flag wait on it instead. After attach the
binding applies a disabled layer's visibility, registers the picks each
module declares, and resumes the camera owners, so engineSwitch.js no
longer names directions or satellites.
2026-11-10 16:33:29 +04:00

717 lines
19 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { LAYER_MANIFEST } from './renderers.js';
const CONTRACT_PRIMITIVES = ['movers', 'lines', 'ground', 'imagery', 'meshes'];
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import * as geo from '../../geo/index.js';
import { rgbaFromCss } from '../../layers/shared/color.js';
import { aircraftAtlasLayout } from '../../layers/aircraft/render/display.js';
import { vesselAtlasLayout } from '../../layers/vessels/render/display.js';
import { transitTestAtlas } from '../../layers/transit/render/testAtlas.mjs';
import { transitIcon } from '../../data/transitIcons.js';
import { createBikeshareLayer } from '../../layers/bikeshare/index.js';
import { createRadioLayer } from '../../layers/radio/index.js';
import { createDisplayHarness } from '../../layers/installations/render/displayHarness.mjs';
import { BOX } from '../../layers/recentImagery/testDoubles.mjs';
const noop = () => {};
const settle = () => new Promise((resolve) => setImmediate(resolve));
const at = (lon = 0, lat = 0, height = 5) =>
geo.cartesian3.fromDegrees(lon, lat, height);
const color = rgbaFromCss('#5ef08a');
const ring = [
[0, 0],
[1, 0],
[1, 1],
[0, 0],
];
const atlas = (layout) => async () => ({
canvas: { width: layout.width, height: layout.height },
cells: layout.cells,
});
const story = () => ({
name: 'story',
entities: {
values: [
{
id: 'point',
position: at(85, 28),
point: { color, pixelSize: 8 },
},
],
},
});
// Small records from the displays' parity tests; transport and image decoding
// are supplied so this rehearsal needs neither a DOM canvas nor a network.
const cases = {
'bhote-koshi-2026': async (make) => {
const d = make();
await d.loadImages(BOX, ['pre.webp', 'post.webp'], 'Vantor');
d.draw(story());
return { hide: () => d.setVisible(false) };
},
'bhote-koshi-locator': (make) => {
const data = story();
make({ data });
return {
hide: () => {
data.show = false;
},
};
},
flights: async (make) => {
const d = make({ drawAtlas: atlas(aircraftAtlasLayout()) });
await d.whenReady();
d.fleet.attach();
d.fleet.add({ id: 'flight', position: at(), show: true, color });
return { hide: () => d.fleet.setVisible(false) };
},
military: async (make) => cases.flights(make),
'local-adsb': async (make) => {
const d = make({ drawAtlas: atlas(aircraftAtlasLayout()) });
await d.whenReady();
d.attach();
d.setVisible(true);
d.markers.add({
id: 'local',
position: () => at(),
rotation: () => 0,
show: () => true,
});
return { hide: () => d.setVisible(false), detach: () => d.detach() };
},
earthquakes: (make) => {
const d = make();
d.replace([
{
stableId: 'quake',
lon: 0,
lat: 0,
baseRadius: 1000,
color,
fillAlpha: 0.4,
},
]);
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
'air-quality': (make) => {
const d = make();
d.replace([{ id: 'station', lon: 0, lat: 0, color: '#5ef08a' }], 1000);
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
'fire-perimeters': (make) => {
const d = make();
d.replace([{ id: 'area', containedPct: 20, polygons: [[ring]] }]);
d.onClick(noop);
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
'drone-airspace': (make) => {
const d = make();
d.replace([{ id: 'zone', typeCode: 'R', polygons: [[ring]] }]);
d.onClick(noop);
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
'gnss-integrity': (make) => {
const d = make();
d.replace(
[{ west: 30, south: 50, east: 30.5, north: 50.5, level: 'low' }],
{ low: 0.3 },
);
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
's2-ghostmaps': async (make) => {
const d = make();
const paint = d.draw(
'kinetic-30d',
new Map([
[
1,
{
style: { polyColor: [0, 0, 1, 0.6] },
geometries: [{ type: 'polygon', outer: ring, holes: [] }],
},
],
]),
'#00d4ff',
() => '',
);
await paint.ready;
return { hide: () => paint.destroy() };
},
'alpr-cameras': (make, engine, services) => {
const record = { id: 'alpr:0', latitude: 30, longitude: -97, osmId: 0 };
const state = {
enabled: true,
selectedId: null,
lastAnchorSampleAt: 0,
renderRevision: 0,
records: [record],
recordById: new Map([[record.id, record]]),
};
const d = make({
state,
services,
source: {
label: 'Fixture',
attribution: {
name: 'Fixture cameras',
text: 'Fixture cameras',
href: 'https://example.com/cameras',
},
},
});
engine.camera.setView({ destination: at(-97, 30, 2000) });
d.init({ onMove: noop });
d.setVisible(true);
d.renderRecords();
return { hide: () => d.setVisible(false) };
},
'street-level': (make) => {
const d = make();
const lines = d.sequenceLines(
[
{
id: 'a',
parts: [
[
[-121.49, 38.58],
[-121.48, 38.58],
],
],
},
],
{
id: 'coverage:14/1/2',
color: '#00ff00',
alpha: 0.92,
widthPx: 2.5,
pickPrefix: 'mly:seq:',
order: 0,
},
);
return { hide: () => lines.destroy() };
},
satellites: (make) => {
const d = make();
const points = d.createPoints();
points.add({ id: '1', position: at(), color, pixelSize: 5, show: true });
const click = d.listenClick(noop);
return {
hide: () => {
points.show = false;
},
detach: () => click.destroy(),
};
},
'rocket-launches': (make) => {
const d = make();
d.setVisible(true);
d.addPoint({ id: 'launch', position: at(), sizePx: 8, color });
d.onClick(noop);
return { hide: () => d.setVisible(false) };
},
traffic: (make) => {
const d = make();
d.drawDots([
{ point: { position: at(), pixelSize: 5, show: true, color } },
]);
d.setHeat(
'jam',
[
{
coords: [
[0, 0],
[1, 1],
],
},
],
{ color: color.rgba(), widthPx: 3 },
);
return { hide: () => d.show(false) };
},
cctv: (make) => {
const d = make({
state: { _recordById: new Map(), _projectionEntities: [] },
});
d.attach();
d.createMarkers();
const marker = d.createMarker({ id: 'a' }, at());
marker.show = true;
d.bindClick(noop);
return { hide: () => d.setMarkersVisible(false) };
},
radio: (make, engine, services) => {
let context;
const layer = createRadioLayer({
services,
source: {},
rendererFactory(value) {
context = value;
return make(value);
},
});
layer.attachRenderer({ engine });
const renderer = context.parts.renderer;
const station = {
id: '00000000-0000-4000-8000-000000000000',
name: 'Station 0',
lat: 0,
lon: 0,
tags: ['news'],
};
context.state._stationById.set(station.id, station);
context.state._renderById.set(
station.id,
renderer.addStation(station, at(), '#44adff'),
);
renderer.setVisible(true);
return {
hide: () => renderer.setVisible(false),
destroy: () => layer.detachRenderer(),
};
},
transit: async (make) => {
const d = make({ state: {}, services: {}, drawAtlas: transitTestAtlas });
d.attach();
const [markers] = d.createMarkers();
markers.show = true;
await d.whenReady();
markers.add({
id: 'bus',
position: at(),
image: transitIcon('bus'),
width: 20,
height: 20,
color,
rotation: 0,
show: true,
});
return {
hide: () => {
markers.show = false;
},
};
},
bikeshare: (make, engine, services) => {
let display;
const layer = createBikeshareLayer({
services,
source: { getStations: async () => ({ data: { stations: [] } }) },
rendererFactory(value) {
display = make(value);
return display;
},
});
layer.init(null, { engine });
display.show();
display.ensureCityPoints(
'austin-capmetro',
new Map([
[
'central',
{
stationId: 'central',
name: 'Central',
lat: 30.2672,
lon: -97.7431,
capacity: 10,
},
],
]),
);
return { hide: () => layer.disable(), destroy: () => layer.destroy() };
},
directions: (make, engine) => {
const d = make();
d.setMarker('A', { lat: 0, lon: 0 });
d.setRoute({
type: 'LineString',
coordinates: [
[0, 0],
[1, 1],
],
});
const off = engine.input.on('click', noop, { pick: true });
d.show(true);
return { hide: () => d.show(false), detach: off };
},
'recent-imagery': (make) => {
const d = make();
d.rebind(d.resolveHost());
assert.equal(
d.showSlot(
'a',
{ key: 'S30:2026-09-18', product: 'S30', day: '2026-09-18' },
BOX,
),
true,
);
return { hide: () => d.hideSlot('a') };
},
'osm-places': (make) => {
const d = make();
d.replace([{ id: 'node/1', lat: 0, lon: 0, color: '#5ef08a' }]);
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
'ais-live-vessels': async (make) => {
const d = make({ drawAtlas: atlas(vesselAtlasLayout()) });
await d.whenReady();
d.fleet.attach({ show: true });
d.fleet.add({
id: { mmsi: '123456789' },
position: at(),
show: true,
scale: 0.6,
rotation: 0,
});
return { hide: () => d.fleet.setVisible(false) };
},
'military-installations': (make, engine, services) => {
const h = createDisplayHarness(() => ({ engine, destroy: noop }), {
context: services.context,
displayFactory: (_engine, options) => make(options),
});
h.start();
h.setRecords([
{
id: 'site',
name: 'Site',
latitude: 0,
longitude: 0,
category: 'military',
sources: [],
},
]);
h.parts.display.renderRecords();
return {
hide: () => h.parts.display.disable(),
destroy: () => h.destroy(),
};
},
'military-awareness': (make) => {
const d = make();
const rim = d.createRim(at());
const pulses = d.createPulses(at());
pulses.draw({ radii: [0.5], alphas: [0.5], level: 1 });
return {
hide: () => pulses.draw({ radii: [], alphas: [], level: 0 }),
detach: () => {
rim.destroy();
pulses.destroy();
},
};
},
wind: (make) => {
const d = make({
createCanvas: ({ width, height }) => ({ width, height }),
});
d.attach();
d.setField({
model: 'gfs',
grid: { nx: 8, ny: 5, lo1: 0, la1: 90, dx: 45, dy: 45 },
u: Array.from({ length: 40 }, (_, i) => 5 + (i % 7)),
v: Array.from({ length: 40 }, (_, i) => (i % 3) - 1),
});
d.setOptions({ overlay: 'speed' });
d.start();
return { hide: () => d.stop() };
},
'weather-radar': async (make) => weather(make, 'radar'),
'weather-satellite': async (make) => weather(make, 'clouds-regional'),
'weather-lightning': async (make) => weather(make, 'lightning'),
'weather-cyclones': async (make, engine, services) => {
const d = make({ overlayHost: services.overlayHost });
await d.setSnapshot({
storms: [
{
id: 'ep152026',
name: 'Fifteen-E',
position: { longitude: 179, latitude: 15 },
forecastPoints: [],
track: {
type: 'MultiLineString',
coordinates: [
[
[179, 15],
[-179, 16],
],
],
},
},
],
});
return { hide: () => d.setSnapshot({ storms: [] }) };
},
'local-datacenters': (make, engine, services) =>
infrastructure(make, services, 'local-datacenters'),
'local-dams': (make, engine, services) =>
infrastructure(make, services, 'local-dams'),
'telegeography-submarine-cables': (make, engine, services) => {
const d = make({ overlayHost: services.overlayHost });
d.show({
cables: {
features: [
{
type: 'Feature',
id: 'cable',
properties: { name: 'Cable' },
geometry: {
type: 'LineString',
coordinates: [
[0, 0],
[1, 1],
],
},
},
],
},
landingPoints: { features: [] },
});
return { hide: () => d.hide() };
},
'local-firms': (make) => {
const d = make();
d.addCell({ west: 30, south: 50, east: 30.5, north: 50.5, color });
d.setVisible(true);
return { hide: () => d.setVisible(false) };
},
};
async function weather(make, product) {
const d = make();
const time = '2026-11-07T12:00:00.000Z';
assert.equal(
await d.setFrame(
{
schemaVersion: 1,
product,
bounds: { west: -130, south: 20, east: -60, north: 55 },
times: [time],
latest: time,
tileSize: 256,
maxLevel: 6,
tilingScheme: 'geographic',
},
time,
),
true,
);
return { hide: () => d.setHidden(true), destroy: () => d.clear() };
}
function infrastructure(make, services, id) {
const d = make({
config: {
id,
name: 'Runtime Dam',
source: 'fixture',
color: '#5ef08a',
labels: false,
},
services,
});
const geometry = { type: 'Polygon', coordinates: [ring] };
d.show({
features: [
{
type: 'Feature',
id: 'real-dam',
properties: { name: 'Runtime Dam' },
geometry,
},
],
records: [
{
id: 'real-dam',
lon: 0.5,
lat: 0.5,
properties: { name: 'Runtime Dam' },
geometry,
},
],
});
return { hide: () => d.hide() };
}
const skips = {
'wildlife-sightings':
'The layer draws itself through the contract; no manifest display factory.',
'sea-marks':
'The overlay controller draws imagery itself; no manifest display factory.',
'ski-pistes':
'The overlay controller draws imagery itself; no manifest display factory.',
};
function listenerCounts(engine) {
return [engine.camera, engine.frames].flatMap((part) =>
Object.values(part)
.filter((value) => value && typeof value.addEventListener === 'function')
.map((event) => event.numberOfListeners),
);
}
function servicesFixture() {
const entries = new Map();
const contexts = new Map();
const owners = new Map();
const overlayHost = {
setEntries: (id, value) => entries.set(id, value),
setVisible: noop,
clearSource: (id) => entries.delete(id),
};
const context = {
getSelectedEntityContext: () => null,
clearSelectedEntityContextForLayer: noop,
selectEntityContext: noop,
registerEntityContext: (_entity, metadata) =>
contexts.set(metadata.id, metadata),
removeEntityContextsForLayer: (id) => {
for (const [key, value] of contexts)
if (value.layerId === id) contexts.delete(key);
},
};
return {
entries,
contexts,
owners,
context,
overlayHost,
render: { governorRequestRender: noop },
groundFloor: { cachedGroundFloor: () => 150 },
ground: { cachedGroundFloor: () => 20, warmGroundFloor: noop },
globe: { getKeyholeGeometry: () => ({ radius: 500 }) },
overlays: {
setEntries: overlayHost.setEntries,
clearSource: overlayHost.clearSource,
setVisible: noop,
},
picking: {
registerPickOwner: (id, fn) => owners.set(id, fn),
unregisterPickOwner: (id) => owners.delete(id),
resolvePickId: (pick) => pick?.id,
isOwnedByOtherLayer: () => false,
},
...context,
};
}
test('every manifest display draws and releases its resources on a third engine', async (t) => {
assert.deepEqual(
Object.keys(cases).sort(),
Object.entries(LAYER_MANIFEST)
.filter(([, entry]) => entry.display)
.map(([id]) => id)
.sort(),
);
assert.deepEqual(
Object.keys(skips).sort(),
Object.entries(LAYER_MANIFEST)
.filter(([, entry]) => !entry.display)
.map(([id]) => id)
.sort(),
);
const previousWindow = globalThis.window;
globalThis.window = new EventTarget();
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
});
for (const [id, entry] of Object.entries(LAYER_MANIFEST)) {
await t.test(id, { skip: skips[id] }, async () => {
const engine = createFakeEngine({ kind: 'rehearsal' });
engine.extensions = {};
engine.fake.script('sampleHeight', 0);
engine.fake.script('terrainHeight', 0);
const before = listenerCounts(engine);
const credits = [...engine.fake.credits];
const inputs = new Set();
const on = engine.input.on;
engine.input.on = (...args) => {
const off = on(...args);
const token = {};
inputs.add(token);
return () => {
inputs.delete(token);
off();
};
};
const services = servicesFixture();
const factory = await entry.display();
let display;
let lifecycle;
const make = (options = {}) => {
assert.equal(display, undefined, 'one factory call per manifest entry');
return (display = factory(engine, options));
};
try {
lifecycle = (await cases[id](make, engine, services)) ?? {};
await settle();
engine.fake.frame();
assert.ok(
CONTRACT_PRIMITIVES.some((name) =>
engine[name]
.list()
.some(
(item) =>
item.count > 0 ||
name === 'imagery' ||
name === 'lines' ||
item.kind === 'pulse-field',
),
),
'the fixture draws nonempty geometry or imagery',
);
lifecycle.hide?.();
lifecycle.detach?.();
if (lifecycle.destroy) lifecycle.destroy();
else display.destroy();
await settle();
engine.fake.frame();
for (const name of CONTRACT_PRIMITIVES)
assert.deepEqual(
engine[name].list(),
[],
`${name} released before engine destruction`,
);
assert.deepEqual(
listenerCounts(engine),
before,
'frame and camera listeners released',
);
assert.equal(inputs.size, 0, 'input subscriptions released');
assert.deepEqual(engine.fake.credits, credits, 'credits released');
assert.equal(
engine.input.cameraInputsEnabled(),
true,
'camera input restored',
);
assert.equal(
engine.render.isContinuous(),
false,
'continuous rendering released',
);
assert.equal(engine.camera.following(), null, 'no retained follow');
assert.ok(
[...services.entries.values()].every(
(entries) => entries.length === 0,
),
'overlay entries released',
);
assert.equal(services.contexts.size, 0, 'context records released');
assert.equal(services.owners.size, 0, 'pick owners released');
} finally {
lifecycle?.destroy?.();
display?.destroy?.();
engine.destroy();
}
});
}
});