Files
gods-eye-view/src/data/infrastructure.test.mjs
T
Sameh Khamis d51ea4007b Ask for heights through one service per scene
About 20 sites called scene.sampleHeight, globe.getHeight,
scene.clampToHeight or the MostDetailed variants directly, each with its own
idea of a miss (undefined, NaN, a throw). They now ask heightsFor(scene) in
src/queries/heights.js:

- now(point, { against, exclude }) answers from what is loaded, or null;
  against is 'surface' (what is drawn) or 'terrain' (loaded terrain only).
- clampNow(position) moves a position onto the drawn surface, or null.
- batch() and clampBatch() wait for the most detailed tiles: null per
  missed point, a rejection when the whole query fails.
- supported() reports { surface, clamp } without allocating.

Cesium still answers now() synchronously, so nothing changes on screen and
nothing is slower: in the live app 1,000 calls cost the same through the
service as direct (terrain 0.5-0.7 ms, surface 2-6 ms a call, run-to-run
noise larger than the difference). Every site already treated a miss as
"hold the last value or fall back"; a renderer that can only answer from a
cache or asynchronously can now plug in at one place.

- A Bhote Koshi evidence point whose terrain sample misses stays at its
  fallback elevation instead of reading the miss as sea level.
- The mesh floor sampler skips a scene without depth-texture support
  instead of spending its budget on probes that throw.
- The world annotation probe's trace reads hit/missed.
- A source scan forbids direct height calls outside the service.

The service is exported as ./queries/heights and documented in
docs/APPLICATION.md.
2026-11-04 09:24:30 -08:00

217 lines
6.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import * as Cesium from 'cesium';
import { createInfrastructureLayers } from 'gods-eye-view/infrastructure';
import { createLocalGeoJsonLayer } from 'gods-eye-view/infrastructure/geojson';
import { fakeInput } from '../input/testDoubles.mjs';
function services() {
const records = new Map();
const credited = [];
let selection;
return {
records,
credited,
showOsmCredit: (viewer) => credited.push(viewer),
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
registerEntityContext(entity, metadata) {
records.set(metadata.id, { entity, ...metadata });
},
selectEntityContext(entity) {
selection = entity;
},
clearSelectedEntityContextForLayer() {
selection = undefined;
},
removeEntityContextsForLayer(id) {
for (const [key, record] of records)
if (record.layerId === id) records.delete(key);
},
governorRequestRender() {},
selection: () => selection,
};
}
test('package exports import without an application, DOM, fetch, or timers', () => {
const result = spawnSync(
process.execPath,
[
'--input-type=module',
'-e',
`
globalThis.fetch = () => { throw new Error('unexpected fetch'); };
globalThis.setTimeout = () => { throw new Error('unexpected timer'); };
for (const key of ['window', 'document']) {
delete globalThis[key];
}
await import('gods-eye-view/infrastructure');
await import('gods-eye-view/infrastructure/geojson');
await import('gods-eye-view/infrastructure/lod');
`,
],
{ cwd: new URL('../..', import.meta.url), encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
});
test('infrastructure factory preserves identity and creates independent state without loading', (t) => {
t.mock.method(globalThis, 'fetch', () => {
throw new Error('factory must not fetch');
});
const first = createInfrastructureLayers(services());
const second = createInfrastructureLayers(services());
assert.deepEqual(
first.map(({ id, name, source }) => ({ id, name, source })),
[
{ id: 'local-datacenters', name: 'Datacenters', source: 'Local' },
{ id: 'local-dams', name: 'Dams', source: 'USACE' },
],
);
first.forEach((layer, index) => {
assert.notEqual(layer, second[index]);
layer.destroy();
assert.deepEqual(second[index].getStats(), {
count: 0,
lastUpdate: null,
error: null,
});
second[index].destroy();
});
});
test('dataset URLs still name the complete bundled sources', () => {
for (const [file, count] of [
['datacenters', 4351],
['dams', 704],
]) {
const lines = readFileSync(
new URL(`./local_data/${file}/${file}.geojsonl`, import.meta.url),
'utf8',
)
.split('\n')
.filter((line) => line.trim());
assert.equal(lines.length, count);
}
});
test('two viewers use their supplied contexts and dispose independently', async (t) => {
t.mock.method(globalThis, 'fetch', async () => ({
ok: true,
text: async () =>
JSON.stringify({
type: 'Feature',
id: 'same-id',
properties: { name: 'Dam' },
geometry: {
type: 'Polygon',
coordinates: [
[
[0, 0],
[0.01, 0],
[0, 0.01],
[0, 0],
],
],
},
}),
}));
const hosts = [services(), services()];
const instances = hosts.map((host) => {
const sources = new Cesium.DataSourceCollection();
const layer = createLocalGeoJsonLayer(
{
id: 'local-dams',
name: 'Dams',
osmDerived: true,
color: '#0088ff',
url: '/dams.geojsonl',
},
host,
);
const viewer = {
dataSources: sources,
scene: {
canvas: {},
preRender: new Cesium.Event(),
requestRender() {},
screenSpaceCameraController: { enableInputs: true },
pick: () => ({ id: sources.get(0).entities.values[0] }),
},
camera: { moveEnd: new Cesium.Event(), flyTo() {} },
};
const input = fakeInput(viewer);
return { layer, viewer, click: () => input.click({ x: 0, y: 0 }) };
});
t.after(() =>
instances.forEach(({ layer, viewer }) => layer.destroy(viewer)),
);
await Promise.all(instances.map(({ layer, viewer }) => layer.enable(viewer)));
assert.equal(hosts[0].records.size, 1);
assert.deepEqual(
hosts.map((host) => host.credited),
instances.map(({ viewer }) => [viewer]),
);
assert.equal(hosts[1].records.size, 1);
instances[0].click();
assert.ok(hosts[0].selection());
assert.equal(hosts[1].selection(), undefined);
instances[0].layer.destroy(instances[0].viewer);
assert.equal(hosts[0].records.size, 0);
assert.equal(hosts[1].records.size, 1);
assert.equal(instances[1].viewer.dataSources.length, 1);
});
test('consumer build includes only infrastructure code and resolves assets under a non-root base', async () => {
const { build } = await import('vite');
const { fileURLToPath } = await import('node:url');
const result = await build({
configFile: false,
logLevel: 'silent',
base: '/example/',
build: {
write: false,
assetsInlineLimit: 0,
rollupOptions: {
input: fileURLToPath(
import.meta.resolve('gods-eye-view/infrastructure'),
),
external: ['cesium'],
preserveEntrySignatures: 'strict',
},
},
});
const output = result.output;
const entry = output.find((item) => item.type === 'chunk' && item.isEntry);
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'dispatcher.js',
'heights.js',
'infrastructure.js',
'infrastructureData.js',
'infrastructureOverlayEntry.js',
'inputOwnership.js',
'localGeojsonCore.js',
'localGeojsonLod.js',
'pickRegistry.js',
'scenePick.js',
'sensorPick.js',
]);
assert.deepEqual(
entry.imports,
['cesium'],
'the viewer supplies the same Cesium dependency',
);
for (const name of ['datacenters', 'dams']) {
const asset = output.find(
(item) => item.type === 'asset' && item.fileName.includes(name),
);
assert.ok(asset, `${name} must be emitted`);
assert.ok(
entry.code.includes(`/example/${asset.fileName}`),
`${name} must retain the consumer base path`,
);
}
});