Measure the heights the surface-bound layers place their points at

qa-surface-heights loads deterministic fixtures for import pins,
infrastructure bases, ALPR markers, directions A/B, bikeshare stations
and Bhote Koshi story points, waits for the app to settle on each engine
and records every placed point through the mover contract. --compare
fails when a point moved by more than 0.1 m on terrain; --tiles checks
the points land on the synthetic tileset's 50 m block.
This commit is contained in:
German Khokhlov committed 2026-11-10 16:42:50 +04:00
1 parent d51e4140b7
commit dd56cba2f3
3 files changed
+859

No files matched your search

+333
View File
@@ -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;
});
}
+166
View File
@@ -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);
});
+360
View File
@@ -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();
}
}