mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e7-height-datum' into german/navara-refactor
This commit is contained in:
commit
08d5c86e80
3 files changed
+859
No files matched your search
@@ -0,0 +1,333 @@
|
||||
// Run this checkout's probe against either build; no page-side source imports.
|
||||
// node scripts/qa-surface-heights.mjs http://localhost:4308 --out=heights.json
|
||||
// Add --compare=before.json for the 0.1 m gate, or --tiles for the 50 m block.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import * as geo from '../src/geo/index.js';
|
||||
import {
|
||||
createStrict,
|
||||
localPreview,
|
||||
openPage,
|
||||
waitForEngine,
|
||||
TILES_FIXTURE_PATH,
|
||||
} from './qa-strict-harness.mjs';
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import {
|
||||
center,
|
||||
points,
|
||||
layers,
|
||||
offsets,
|
||||
counts,
|
||||
features,
|
||||
fixtureRoutes,
|
||||
observeSurfaceMovers,
|
||||
settledPositions,
|
||||
} from './qa/surfaceHeights.mjs';
|
||||
|
||||
export function compareHeights(before, after, { tiles = false } = {}) {
|
||||
assert.equal(before.version, 1, 'before snapshot version');
|
||||
assert.equal(after.version, 1, 'after snapshot version');
|
||||
assert.equal(
|
||||
before.surface,
|
||||
tiles ? 'tiles' : 'terrain',
|
||||
'before surface (tiles require --tiles)',
|
||||
);
|
||||
assert.equal(after.surface, before.surface, 'matching surfaces');
|
||||
assert.equal(before.fixture, after.fixture, 'matching fixtures');
|
||||
const reports = [];
|
||||
for (const engine of new Set([
|
||||
'cesium',
|
||||
'navara',
|
||||
...Object.keys(before.engines),
|
||||
...Object.keys(after.engines),
|
||||
])) {
|
||||
const left = before.engines[engine] ?? {},
|
||||
right = after.engines[engine] ?? {};
|
||||
for (const layer of new Set([
|
||||
...layers,
|
||||
...Object.keys(left),
|
||||
...Object.keys(right),
|
||||
])) {
|
||||
const a = left[layer] ?? {},
|
||||
b = right[layer] ?? {};
|
||||
const missing = Object.keys(a).filter((id) => !Object.hasOwn(b, id));
|
||||
const extra = Object.keys(b).filter((id) => !Object.hasOwn(a, id));
|
||||
const invalid = [];
|
||||
let maxAbsM = 0,
|
||||
point = null;
|
||||
for (const id of Object.keys(a)) {
|
||||
if (!Object.hasOwn(b, id)) continue;
|
||||
if (![a[id]?.heightM, b[id]?.heightM].every(Number.isFinite)) {
|
||||
invalid.push(id);
|
||||
continue;
|
||||
}
|
||||
const delta = Math.abs(a[id].heightM - b[id].heightM);
|
||||
if (delta > maxAbsM) {
|
||||
maxAbsM = delta;
|
||||
point = id;
|
||||
}
|
||||
}
|
||||
reports.push({
|
||||
engine,
|
||||
layer,
|
||||
maxAbsM,
|
||||
point,
|
||||
missing,
|
||||
extra,
|
||||
invalid,
|
||||
ok:
|
||||
Object.keys(a).length > 0 &&
|
||||
Object.keys(b).length > 0 &&
|
||||
!missing.length &&
|
||||
!extra.length &&
|
||||
!invalid.length &&
|
||||
maxAbsM <= 0.1 + 1e-9,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ok: reports.every((r) => r.ok), reports };
|
||||
}
|
||||
|
||||
function serialize(rows, layer) {
|
||||
const result = {};
|
||||
for (const row of rows) {
|
||||
const position = geo.cartographic.fromCartesian(row.position);
|
||||
const lon = geo.math.toDegrees(position.longitude),
|
||||
lat = geo.math.toDegrees(position.latitude);
|
||||
let id = row.id;
|
||||
if (layer === 'import') {
|
||||
// Imported sprites have no pick ids; match the fixture's unique coordinates.
|
||||
const point = points.find(
|
||||
(p) => Math.abs(p.lon - lon) < 1e-7 && Math.abs(p.lat - lat) < 1e-7,
|
||||
);
|
||||
assert.ok(point, `Unknown import pin at ${lon},${lat}`);
|
||||
id = point.id;
|
||||
}
|
||||
if (layer === 'directions')
|
||||
id = `${row.index === 0 ? 'a' : 'b'}/${row.setId === 'directions-markers' ? 'disc' : 'letter'}`;
|
||||
assert.ok(!Object.hasOwn(result, id), `Duplicate ${layer} point ${id}`);
|
||||
assert.ok(
|
||||
Number.isFinite(position.height),
|
||||
`Nonfinite ${layer} point ${id}`,
|
||||
);
|
||||
assert.ok(
|
||||
Math.abs(lon - center.lon) < 0.0004 &&
|
||||
Math.abs(lat - center.lat) < 0.0004,
|
||||
`${layer}/${id} outside the block fixture`,
|
||||
);
|
||||
result[id] = { heightM: position.height, lon, lat };
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(result).sort(([a], [b]) => a.localeCompare(b)),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadDisplay(page, layer) {
|
||||
await page.evaluate(
|
||||
async ({ layer, points, features }) => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const enable = async (id) => {
|
||||
if (!(await dataManager.setEnabled(id, true)))
|
||||
throw new Error(`Cannot enable ${id}`);
|
||||
return dataManager.layers.get(id).module;
|
||||
};
|
||||
if (layer === 'import') {
|
||||
const outcomes = await window.__gevGeoImport.importFiles([
|
||||
new File(
|
||||
[JSON.stringify({ type: 'FeatureCollection', features })],
|
||||
'surface-heights.geojson',
|
||||
{ type: 'application/geo+json' },
|
||||
),
|
||||
]);
|
||||
if (outcomes.some((o) => o.error))
|
||||
throw new Error(JSON.stringify(outcomes));
|
||||
} else if (layer === 'infrastructure') {
|
||||
await enable('local-datacenters');
|
||||
await enable('local-dams');
|
||||
} else if (layer === 'directions') {
|
||||
const mod = await enable(layer);
|
||||
mod.placeEndpoint('a', points[0]);
|
||||
mod.placeEndpoint('b', points[2]);
|
||||
} else if (layer === 'story') {
|
||||
const mod = dataManager.layers.get('bhote-koshi-2026').module;
|
||||
mod.setParams({
|
||||
presentation: 'scene-beat',
|
||||
sceneSurface: 'evidence-beat',
|
||||
progress: 0.5,
|
||||
autoPlay: false,
|
||||
});
|
||||
await enable('bhote-koshi-2026');
|
||||
mod.setParams({ progress: 0.5, autoPlay: false });
|
||||
} else await enable(layer === 'alpr' ? 'alpr-cameras' : layer);
|
||||
},
|
||||
{ layer, points, features },
|
||||
);
|
||||
}
|
||||
|
||||
async function unloadDisplay(page, layer) {
|
||||
await page.evaluate(async (layer) => {
|
||||
if (layer === 'import') {
|
||||
for (const file of window.__gevGeoImport.list())
|
||||
window.__gevGeoImport.remove(file.id);
|
||||
} else {
|
||||
const ids =
|
||||
layer === 'infrastructure'
|
||||
? ['local-datacenters', 'local-dams']
|
||||
: [
|
||||
layer === 'story'
|
||||
? 'bhote-koshi-2026'
|
||||
: layer === 'alpr'
|
||||
? 'alpr-cameras'
|
||||
: layer,
|
||||
];
|
||||
for (const id of ids)
|
||||
await window.__godsEyeView.dataManager.setEnabled(id, false);
|
||||
}
|
||||
}, layer);
|
||||
}
|
||||
|
||||
async function main(argv) {
|
||||
const option = (name) =>
|
||||
argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
||||
const base = localPreview(
|
||||
argv.find((a) => !a.startsWith('--')) ??
|
||||
process.env.QA_BASE_URL ??
|
||||
'http://localhost:4308',
|
||||
);
|
||||
const tiles = argv.includes('--tiles');
|
||||
const before = option('compare')
|
||||
? JSON.parse(readFileSync(option('compare'), 'utf8'))
|
||||
: null;
|
||||
const snapshot = {
|
||||
version: 1,
|
||||
fixture: 'surface-heights-v1',
|
||||
surface: tiles ? 'tiles' : 'terrain',
|
||||
engines: {},
|
||||
};
|
||||
if (before) compareHeights(before, snapshot, { tiles });
|
||||
const routes = fixtureRoutes(base.origin, tiles);
|
||||
const strict = createStrict({ origin: base.origin, layers, routes });
|
||||
base.search = '?engine=navara&welcome=0';
|
||||
if (tiles) base.searchParams.set('qaTileset', TILES_FIXTURE_PATH);
|
||||
base.hash = `v=2&lat=${center.lat}&lon=${center.lon}&alt=1100&heading=0&pitch=-90&roll=0&map=${tiles ? 'photoreal' : 'osm'}&l=`;
|
||||
await strict.run({}, async (browser) => {
|
||||
for (const kind of ['navara', 'cesium']) {
|
||||
const { page, errors } = await openPage(browser);
|
||||
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
|
||||
await waitForEngine(page, 'navara');
|
||||
if (kind === 'cesium') await strict.pick(page, kind);
|
||||
await page.evaluate(
|
||||
async ({ destination, tiles }) => {
|
||||
const { engine, styleManager } = window.__godsEyeView;
|
||||
styleManager.orbitController.stop();
|
||||
engine.camera.cancelFlight();
|
||||
await engine.maps.setTerrain({
|
||||
kind: tiles ? 'ellipsoid' : 'keyless',
|
||||
});
|
||||
engine.camera.setView({
|
||||
destination,
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
engine.render.request('qa-surface-heights');
|
||||
},
|
||||
{
|
||||
destination: geo.cartesian3.fromDegrees(center.lon, center.lat, 1100),
|
||||
tiles,
|
||||
},
|
||||
);
|
||||
await whenSettled(page);
|
||||
await page.waitForFunction(
|
||||
({ points, tiles }) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.render.request('qa-surface-heights');
|
||||
return (
|
||||
engine.surface.tilesLoaded() &&
|
||||
engine.maps.photorealActive() === tiles &&
|
||||
points.every((p) => {
|
||||
const place = {
|
||||
longitude: (p.lon * Math.PI) / 180,
|
||||
latitude: (p.lat * Math.PI) / 180,
|
||||
height: 0,
|
||||
};
|
||||
const height = tiles
|
||||
? engine.surface.sampleHeight(place)
|
||||
: engine.surface.terrainHeight(place);
|
||||
return (
|
||||
Number.isFinite(height) &&
|
||||
(tiles ? Math.abs(height - 50) < 0.1 : height > 1)
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
{ timeout: 90000 },
|
||||
{ points, tiles },
|
||||
);
|
||||
await page.evaluate(observeSurfaceMovers);
|
||||
snapshot.engines[kind] = {};
|
||||
for (const layer of layers) {
|
||||
await loadDisplay(page, layer);
|
||||
await page.evaluate(
|
||||
(destination) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
engine.camera.cancelFlight();
|
||||
engine.camera.setView({
|
||||
destination,
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
engine.render.request('qa-surface-heights');
|
||||
},
|
||||
geo.cartesian3.fromDegrees(center.lon, center.lat, 1100),
|
||||
);
|
||||
await whenSettled(page);
|
||||
const rows = await settledPositions(page, layer, counts[layer]);
|
||||
const values = serialize(rows, layer);
|
||||
snapshot.engines[kind][layer] = values;
|
||||
if (tiles)
|
||||
for (const [id, p] of Object.entries(values))
|
||||
assert.ok(
|
||||
Math.abs(p.heightM - (50 + offsets[layer])) <= 0.1,
|
||||
`${kind}/${layer}/${id}: ${p.heightM} m, expected ${50 + offsets[layer]} m`,
|
||||
);
|
||||
await strict.passed(
|
||||
page,
|
||||
layer,
|
||||
`${counts[layer]} committed surface points`,
|
||||
values,
|
||||
);
|
||||
console.log(
|
||||
`PASS ${kind}/${layer}: ${rows.length} points${tiles ? ` at ${50 + offsets[layer]} m (±0.1 m)` : ''}`,
|
||||
);
|
||||
await unloadDisplay(page, layer);
|
||||
}
|
||||
await page.evaluate(() => window.qaSurfaceMovers.restore());
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
}
|
||||
if (option('out')) {
|
||||
writeFileSync(option('out'), JSON.stringify(snapshot, null, 2) + '\n');
|
||||
console.log(`WROTE ${option('out')}`);
|
||||
}
|
||||
if (before) {
|
||||
const comparison = compareHeights(before, snapshot, { tiles });
|
||||
for (const r of comparison.reports)
|
||||
console.log(
|
||||
`${r.ok ? 'PASS' : 'FAIL'} ${r.engine}/${r.layer}: max |Δ|=${r.maxAbsM.toFixed(6)} m; missing=${r.missing.join(',') || 'none'}; extra=${r.extra.join(',') || 'none'}; invalid=${r.invalid.join(',') || 'none'}`,
|
||||
);
|
||||
assert.ok(
|
||||
comparison.ok,
|
||||
'Surface heights differ by more than 0.1 m or the point sets changed',
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`PASS surface heights: 12 engine/layer measurements (${snapshot.surface})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
||||
main(process.argv.slice(2)).catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { compareHeights } from './qa-surface-heights.mjs';
|
||||
import {
|
||||
layers,
|
||||
observeSurfaceMovers,
|
||||
fixtureRoutes,
|
||||
center,
|
||||
} from './qa/surfaceHeights.mjs';
|
||||
import { createFakeEngine } from '../src/globe/testing/fakeEngine.js';
|
||||
import { decodeAlprTile } from '../src/layers/alpr/tileRecords.js';
|
||||
|
||||
const snapshot = (heightM = 50) => ({
|
||||
version: 1,
|
||||
fixture: 'surface-heights-v1',
|
||||
surface: 'terrain',
|
||||
engines: Object.fromEntries(
|
||||
['cesium', 'navara'].map((engine) => [
|
||||
engine,
|
||||
Object.fromEntries(
|
||||
layers.map((layer) => [layer, { a: { heightM }, b: { heightM } }]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
test('comparison accepts identical heights and the inclusive 0.1 m boundary', () => {
|
||||
for (const delta of [0, 0.099, 0.1, -0.1]) {
|
||||
const result = compareHeights(snapshot(), snapshot(50 + delta));
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.reports.length, 12);
|
||||
assert.ok(Math.abs(result.reports[0].maxAbsM - Math.abs(delta)) < 1e-10);
|
||||
}
|
||||
});
|
||||
|
||||
test('comparison reports the largest absolute difference and fails above tolerance', () => {
|
||||
const after = snapshot();
|
||||
after.engines.navara.import.a.heightM = 50.01;
|
||||
after.engines.navara.import.b.heightM = 49.8999;
|
||||
const result = compareHeights(snapshot(), after);
|
||||
assert.equal(result.ok, false);
|
||||
const failures = result.reports.filter((r) => !r.ok);
|
||||
assert.equal(failures.length, 1);
|
||||
assert.equal(failures[0].point, 'b');
|
||||
assert.ok(Math.abs(failures[0].maxAbsM - 0.1001) < 1e-10);
|
||||
});
|
||||
|
||||
test('missing and extra points fail even with identical surviving heights', () => {
|
||||
for (const engine of ['cesium', 'navara'])
|
||||
for (const layer of layers)
|
||||
for (const change of ['missing', 'extra']) {
|
||||
const after = snapshot();
|
||||
if (change === 'missing') delete after.engines[engine][layer].a;
|
||||
else after.engines[engine][layer].extra = { heightM: 50 };
|
||||
const result = compareHeights(snapshot(), after);
|
||||
assert.equal(result.ok, false);
|
||||
const report = result.reports.find(
|
||||
(r) => r.engine === engine && r.layer === layer,
|
||||
);
|
||||
assert.deepEqual(report.missing, change === 'missing' ? ['a'] : []);
|
||||
assert.deepEqual(report.extra, change === 'extra' ? ['extra'] : []);
|
||||
}
|
||||
});
|
||||
|
||||
test('missing engines, layers, empty cohorts and invalid heights cannot pass', () => {
|
||||
for (const change of [
|
||||
(s) => {
|
||||
delete s.engines.navara;
|
||||
},
|
||||
(s) => {
|
||||
delete s.engines.cesium.story;
|
||||
},
|
||||
(s) => {
|
||||
s.engines.navara.alpr = {};
|
||||
},
|
||||
...[null, NaN, Infinity, '50'].map((height) => (s) => {
|
||||
s.engines.cesium.import.a.heightM = height;
|
||||
}),
|
||||
]) {
|
||||
const after = snapshot();
|
||||
change(after);
|
||||
assert.equal(compareHeights(snapshot(), after).ok, false);
|
||||
}
|
||||
const empty = snapshot();
|
||||
empty.engines.cesium.story = {};
|
||||
assert.equal(compareHeights(empty, empty).ok, false);
|
||||
});
|
||||
|
||||
test('tiles require explicit opt-in and cannot be compared with terrain', () => {
|
||||
const tiles = snapshot();
|
||||
tiles.surface = 'tiles';
|
||||
assert.throws(() => compareHeights(tiles, tiles), /tiles require --tiles/);
|
||||
assert.throws(() => compareHeights(snapshot(), tiles), /matching surfaces/);
|
||||
assert.equal(compareHeights(tiles, tiles, { tiles: true }).ok, true);
|
||||
});
|
||||
|
||||
test('the probe reads committed contract positions, snapshots mutable inputs and drops destroyed sets', () => {
|
||||
const engine = createFakeEngine();
|
||||
const original = globalThis.window;
|
||||
globalThis.window = { __godsEyeView: { engine } };
|
||||
try {
|
||||
observeSurfaceMovers();
|
||||
const probe = window.qaSurfaceMovers;
|
||||
const set = engine.movers.createSet({
|
||||
id: 'alpr-cameras',
|
||||
kind: 'point',
|
||||
capacity: 1,
|
||||
point: { sizePx: 5 },
|
||||
});
|
||||
const position = { x: 1, y: 2, z: 3 };
|
||||
set.set(0, { position, show: true, pickId: 'camera' });
|
||||
set.setCount(1);
|
||||
assert.throws(() => probe.read('alpr'), /Uncommitted mover count/);
|
||||
set.commit();
|
||||
position.z = 500;
|
||||
assert.equal(probe.read('alpr')[0].position.z, 3);
|
||||
set.set(0, { position });
|
||||
assert.equal(probe.read('alpr')[0].position.z, 3);
|
||||
set.commit();
|
||||
assert.equal(probe.read('alpr')[0].position.z, 500);
|
||||
assert.equal(probe.read('alpr')[0].id, 'camera');
|
||||
set.destroy();
|
||||
assert.deepEqual(probe.read('alpr'), []);
|
||||
probe.restore();
|
||||
} finally {
|
||||
engine.destroy();
|
||||
if (original === undefined) delete globalThis.window;
|
||||
else globalThis.window = original;
|
||||
}
|
||||
});
|
||||
|
||||
test('fixture routes match both bundled and dev infrastructure URLs and decode real camera tiles', () => {
|
||||
const origin = 'http://localhost:4308';
|
||||
const routes = fixtureRoutes(origin, true);
|
||||
const answer = (url) =>
|
||||
routes.find((r) => new URLPattern(r.pattern).test(url))?.respond({ url });
|
||||
for (const path of [
|
||||
'/assets/datacenters-abc.geojsonl',
|
||||
'/src/data/local_data/dams/dams.geojsonl',
|
||||
]) {
|
||||
const rows = answer(origin + path)
|
||||
.body.split('\n')
|
||||
.map(JSON.parse);
|
||||
assert.equal(rows.length, 3);
|
||||
assert.ok(
|
||||
rows.every(
|
||||
(r) => Math.abs(r.geometry.coordinates[1] - center.lat) < 0.0004,
|
||||
),
|
||||
);
|
||||
}
|
||||
const z = 12,
|
||||
scale = 2 ** z;
|
||||
const x = Math.floor(((center.lon + 180) / 360) * scale);
|
||||
const y = Math.floor(
|
||||
((1 - Math.asinh(Math.tan((center.lat * Math.PI) / 180)) / Math.PI) / 2) *
|
||||
scale,
|
||||
);
|
||||
const tile = answer(
|
||||
`https://tiles.dontgetflocked.com/cameras-us-hourly/${z}/${x}/${y}.mvt`,
|
||||
);
|
||||
const cameras = decodeAlprTile(tile.body, z, x, y);
|
||||
assert.equal(cameras.length, 3);
|
||||
assert.equal(new Set(cameras.map((r) => r.id)).size, 3);
|
||||
assert.ok(cameras.every((r) => Math.abs(r.longitude - center.lon) < 0.0004));
|
||||
assert.equal(answer(origin + '/api/unexpected-feed').status, 503);
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { cameraTile } from '../fixtures/layers/alpr.mjs';
|
||||
import { tilesFixtureRoutes } from '../qa-strict-harness.mjs';
|
||||
|
||||
const { extras } = JSON.parse(
|
||||
readFileSync(new URL('../fixtures/tiles3d/tileset.json', import.meta.url)),
|
||||
);
|
||||
export const center = extras.probes.block50;
|
||||
export const points = [-1, 0, 1].map((n, i) => ({
|
||||
id: `surface-${i}`,
|
||||
lon: center.lon + n * 0.00018,
|
||||
lat: center.lat + n * 0.00018,
|
||||
}));
|
||||
export const layers = [
|
||||
'import',
|
||||
'infrastructure',
|
||||
'alpr',
|
||||
'directions',
|
||||
'bikeshare',
|
||||
'story',
|
||||
];
|
||||
export const offsets = {
|
||||
import: 0,
|
||||
infrastructure: 0,
|
||||
alpr: 1.5,
|
||||
directions: 0,
|
||||
bikeshare: 2,
|
||||
story: 0,
|
||||
};
|
||||
export const counts = {
|
||||
import: 3,
|
||||
infrastructure: 6,
|
||||
alpr: 3,
|
||||
directions: 4,
|
||||
bikeshare: 3,
|
||||
story: 1,
|
||||
};
|
||||
|
||||
export const features = points.map(({ id, lon, lat }) => ({
|
||||
type: 'Feature',
|
||||
id,
|
||||
properties: { name: id },
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
}));
|
||||
|
||||
export function fixtureRoutes(origin, tiles) {
|
||||
const json = (value) => ({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(value),
|
||||
});
|
||||
const event = JSON.parse(
|
||||
readFileSync(
|
||||
new URL(
|
||||
'../../public/events/bhote-koshi-2026/event.json',
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
);
|
||||
event.evidenceSpine = [];
|
||||
event.fieldReports = [];
|
||||
event.reconstruction.corridor = points.map((p, i) => ({
|
||||
...p,
|
||||
chainageM: i * 30,
|
||||
elevationM: 0,
|
||||
}));
|
||||
event.reconstruction.storyFloodWindow = { start: 0, end: 1 };
|
||||
event.imagery.rectangle = {
|
||||
west: center.lon - 0.0004,
|
||||
east: center.lon + 0.0004,
|
||||
south: center.lat - 0.0004,
|
||||
north: center.lat + 0.0004,
|
||||
};
|
||||
const cameras = points.map((p, i) => ({
|
||||
latitude: p.lat,
|
||||
longitude: p.lon,
|
||||
osmId: 9100000 + i,
|
||||
operator: 'Surface fixture',
|
||||
brand: 'Fixture',
|
||||
cameraType: 'fixed',
|
||||
direction: '',
|
||||
}));
|
||||
return [
|
||||
...(tiles ? tilesFixtureRoutes(origin) : []),
|
||||
...['datacenters', 'dams'].map((name) => ({
|
||||
pattern: `${origin}/*${name}*.geojsonl*`,
|
||||
respond: () => ({
|
||||
contentType: 'application/json',
|
||||
body: features
|
||||
.map((f) =>
|
||||
JSON.stringify(
|
||||
name === 'dams'
|
||||
? {
|
||||
...f,
|
||||
geometry: {
|
||||
...f.geometry,
|
||||
coordinates: [
|
||||
f.geometry.coordinates[0],
|
||||
f.geometry.coordinates[1] + 0.0001,
|
||||
],
|
||||
},
|
||||
}
|
||||
: f,
|
||||
),
|
||||
)
|
||||
.join('\n'),
|
||||
}),
|
||||
})),
|
||||
{
|
||||
pattern: `${origin}/events/bhote-koshi-2026/event.json*`,
|
||||
respond: () => json(event),
|
||||
},
|
||||
{
|
||||
pattern: `${origin}/api/terrain/heights*`,
|
||||
respond: ({ url }) =>
|
||||
json({
|
||||
results: new URL(url).searchParams
|
||||
.get('points')
|
||||
.split(';')
|
||||
.map(() => ({ ellipsoid: tiles ? 50 : 150 })),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pattern: `${origin}/api/route*`,
|
||||
respond: () =>
|
||||
json({
|
||||
ok: true,
|
||||
geometry: points.map((p) => [p.lon, p.lat]),
|
||||
distanceM: 60,
|
||||
durationS: 30,
|
||||
steps: [],
|
||||
}),
|
||||
},
|
||||
{
|
||||
pattern: `${origin}/api/gbfs/*`,
|
||||
respond: ({ url }) => {
|
||||
const upstream = decodeURIComponent(
|
||||
new URL(url).pathname.slice('/api/gbfs/'.length),
|
||||
);
|
||||
const information = upstream.includes('station_information');
|
||||
return json({
|
||||
last_updated: 1785153600,
|
||||
ttl: 3600,
|
||||
version: '2.3',
|
||||
data: {
|
||||
stations: points.map((p) =>
|
||||
information
|
||||
? {
|
||||
station_id: p.id,
|
||||
name: p.id,
|
||||
lat: p.lat,
|
||||
lon: p.lon,
|
||||
capacity: 20,
|
||||
}
|
||||
: {
|
||||
station_id: p.id,
|
||||
num_bikes_available: 10,
|
||||
num_docks_available: 10,
|
||||
is_installed: 1,
|
||||
is_renting: 1,
|
||||
is_returning: 1,
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
pattern: 'https://tiles.dontgetflocked.com/*',
|
||||
respond: ({ url }) => {
|
||||
const path = new URL(url).pathname;
|
||||
if (path.endsWith('.json'))
|
||||
return json({
|
||||
tiles: [
|
||||
'https://tiles.dontgetflocked.com/cameras-us-hourly/{z}/{x}/{y}.mvt',
|
||||
],
|
||||
bounds: [-160, 17, -64, 59],
|
||||
});
|
||||
const match = path.match(/\/(\d+)\/(\d+)\/(\d+)\.mvt$/);
|
||||
if (!match)
|
||||
throw new Error(`Unexpected camera fixture request: ${path}`);
|
||||
return {
|
||||
contentType: 'application/x-protobuf',
|
||||
body: cameraTile(...match.slice(1).map(Number), cameras),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
// Auxiliary startup services must never turn this fixture run into live feeds.
|
||||
pattern: `${origin}/api/*`,
|
||||
respond: ({ url }) => {
|
||||
const path = new URL(url).pathname;
|
||||
if (
|
||||
['/api/google/tiles-token', '/api/realtime/debug-log'].includes(path)
|
||||
)
|
||||
return { status: 204 };
|
||||
if (path.endsWith('/status')) return json({ configured: false });
|
||||
return {
|
||||
status: 503,
|
||||
...json({ error: 'Unavailable in surface fixture' }),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Observe committed positions: neither revision's mover registry exposes instances. */
|
||||
export function observeSurfaceMovers() {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const records = new Map();
|
||||
let frames = 0;
|
||||
const offFrame = engine.frames.postRender.addEventListener(() => {
|
||||
frames++;
|
||||
});
|
||||
const create = engine.movers.createSet;
|
||||
const layerOf = (id) => {
|
||||
if (/^geo-import-.*-pins-/.test(id)) return 'import';
|
||||
if (/^local-(datacenters|dams)-pins$/.test(id)) return 'infrastructure';
|
||||
if (id === 'alpr-cameras') return 'alpr';
|
||||
if (/^directions-(markers|marker-letters)$/.test(id)) return 'directions';
|
||||
if (id === 'bikeshare') return 'bikeshare';
|
||||
if (id === 'bhote-koshi-flood-bhote-koshi-surge-front') return 'story';
|
||||
return null;
|
||||
};
|
||||
engine.movers.createSet = function (options) {
|
||||
const set = create.call(this, options);
|
||||
const layer = layerOf(options.id);
|
||||
if (!layer) return set;
|
||||
const record = { layer, set, items: [], committed: [] };
|
||||
records.set(options.id, record);
|
||||
const write = set.set,
|
||||
commit = set.commit,
|
||||
destroy = set.destroy;
|
||||
set.set = function (index, fields) {
|
||||
const result = write.call(this, index, fields);
|
||||
record.items[index] = {
|
||||
...record.items[index],
|
||||
...fields,
|
||||
...(fields.position ? { position: { ...fields.position } } : {}),
|
||||
...(fields.pickId
|
||||
? { pickId: String(fields.pickId.id ?? fields.pickId) }
|
||||
: {}),
|
||||
};
|
||||
return result;
|
||||
};
|
||||
set.commit = function () {
|
||||
const result = commit.call(this);
|
||||
record.committed = record.items
|
||||
.slice(0, set.count())
|
||||
.map((p) => ({ ...p }));
|
||||
return result;
|
||||
};
|
||||
set.destroy = function () {
|
||||
const result = destroy.call(this);
|
||||
records.delete(options.id);
|
||||
return result;
|
||||
};
|
||||
return set;
|
||||
};
|
||||
window.qaSurfaceMovers = {
|
||||
frames: () => frames,
|
||||
read(layer) {
|
||||
const registry = engine.movers.list();
|
||||
return [...records]
|
||||
.filter(([, r]) => r.layer === layer)
|
||||
.flatMap(([id, r]) => {
|
||||
const live = registry.find((s) => s.id === id);
|
||||
if (!live || live.count !== r.committed.length)
|
||||
throw new Error(`Uncommitted mover count: ${id}`);
|
||||
return r.committed.map((p, index) => ({
|
||||
setId: id,
|
||||
index,
|
||||
id: p.pickId ?? String(index),
|
||||
position: p.position,
|
||||
shown: live.shown && p.show !== false,
|
||||
ready: r.set.ready(index),
|
||||
}));
|
||||
});
|
||||
},
|
||||
restore() {
|
||||
offFrame();
|
||||
engine.movers.createSet = create;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** A settled camera alone does not mean asynchronous placement has committed. */
|
||||
export async function settledPositions(page, layer, count) {
|
||||
await page.evaluate(() => {
|
||||
window.qaSurfaceStable = null;
|
||||
});
|
||||
const handle = await page
|
||||
.waitForFunction(
|
||||
(layer, count) => {
|
||||
const { engine, dataManager, surfaceServices } = window.__godsEyeView;
|
||||
engine.render.request('qa-surface-heights');
|
||||
const rows = window.qaSurfaceMovers.read(layer);
|
||||
let ready =
|
||||
engine.surface.tilesLoaded() &&
|
||||
rows.length === count &&
|
||||
rows.every(
|
||||
(p) =>
|
||||
p.shown &&
|
||||
p.ready &&
|
||||
['x', 'y', 'z'].every((axis) =>
|
||||
Number.isFinite(p.position?.[axis]),
|
||||
),
|
||||
);
|
||||
if (layer === 'alpr') {
|
||||
const mod = dataManager.layers.get('alpr-cameras').module;
|
||||
ready &&=
|
||||
!mod.getStats().loading &&
|
||||
mod
|
||||
.getAnalystRecords()
|
||||
.every((p) =>
|
||||
Number.isFinite(
|
||||
surfaceServices.groundFloor.cachedGroundFloor(p.lat, p.lon),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (layer === 'bikeshare')
|
||||
ready &&= !dataManager.layers.get('bikeshare').module.getStats()
|
||||
.loading;
|
||||
const signature = JSON.stringify(
|
||||
rows.map((p) => [
|
||||
p.setId,
|
||||
p.id,
|
||||
p.position &&
|
||||
['x', 'y', 'z'].map((axis) =>
|
||||
Math.round(p.position[axis] * 1000),
|
||||
),
|
||||
]),
|
||||
);
|
||||
const previous = window.qaSurfaceStable;
|
||||
const drawn = window.qaSurfaceMovers.frames();
|
||||
const stable =
|
||||
ready && previous?.signature === signature
|
||||
? previous.frames + (drawn > previous.drawn ? 1 : 0)
|
||||
: 0;
|
||||
window.qaSurfaceStable = { signature, frames: stable, drawn, rows };
|
||||
return stable >= 10 ? rows : false;
|
||||
},
|
||||
{ polling: 'raf', timeout: 90000 },
|
||||
layer,
|
||||
count,
|
||||
)
|
||||
.catch(async (cause) => {
|
||||
const state = await page
|
||||
.evaluate(() => window.qaSurfaceStable)
|
||||
.catch(() => null);
|
||||
throw new Error(
|
||||
`Placement readiness failed for ${layer}: expected ${count} points; ${JSON.stringify(state)}`,
|
||||
{ cause },
|
||||
);
|
||||
});
|
||||
try {
|
||||
return await handle.jsonValue();
|
||||
} finally {
|
||||
await handle.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user