Files
gods-eye-view/scripts/qa-transit-heading.mjs
T
German Khokhlov 0ef29781e5 Fix the review findings across the engines, layers, data and QA
Navara now matches Cesium where the review found it diverging: a flight
holds the camera inputs, camera.changed counts turns as Cesium does,
render requests made during a frame survive it, follow pinches zoom,
ground widths follow the pixel ratio and Cyber sonar reaches glyph caches
and models. On Cesium, a staged imagery swap keeps loading tiles, setView
replaces a running flight, and a cleared weather detail goes away.

The contract gains MoverSet.failed, maps.terrainRevision,
render.maxTextureSize and surface.sampleTerrainMostDetailed on every
engine, so layers retry failed models, key road heights by terrain,
size weather textures to the GPU and place Bhote Koshi cards on detailed
terrain.

Engine switches keep the live view, follows, focus, layer options and
cards: a pick hands over the exact pose, applies no address options and
pans no carried-over card; a history visit still applies its entry.

Layers put GhostMaps pins, cable landings, OSM Places and directions on
the measured surface without per-frame sampling, draw the Cockpit contact
dot, hide disabled CCTV on attach, and keep ALPR running outside secure
contexts. KML imports draw raised polygon fills, rotated and coloured
ground overlays and north-referenced icon headings, and report files
with nothing to draw.

QA scripts read the contract instead of Cesium internals (test:track is
109/109 again), wait for the camera to rest before clicking, and give
cold switches and parked engines realistic time. Docs follow the code.
2026-11-09 21:34:42 +04:00

166 lines
6.0 KiB
JavaScript

import puppeteer from 'puppeteer';
import { blockPaidQaMaps } from './qa-map-policy.mjs';
import { transitQaBase } from './qa-transit-browser.mjs';
import { chromeExecutablePath } from './qa-chrome.mjs';
/** Prove parked and moving headings in the real scene, without live feeds. */
export async function runTransitHeadingRegression(base, check) {
base = transitQaBase(base);
const browser = await puppeteer.launch({
headless: true,
executablePath: chromeExecutablePath(),
args: [
'--no-sandbox',
'--disable-background-timer-throttling',
...(process.platform === 'darwin' ? ['--use-angle=metal'] : []),
'--enable-gpu',
],
});
try {
const page = await browser.newPage();
await blockPaidQaMaps(page);
await page.setViewport({ width: 1280, height: 800 });
await page.goto(
`${base}/?welcome=0#lat=42.3601&lon=-71.0589&alt=1200&heading=0&pitch=-90`,
{ waitUntil: 'domcontentloaded' },
);
await page.waitForFunction(
() => window.__godsEyeView?.dataManager?.layers?.has('transit'),
{ timeout: 90000 },
);
await new Promise((resolve) => setTimeout(resolve, 6000));
await page.evaluate(async () => {
const app = window.__godsEyeView;
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
const layer = app.dataManager.layers.get('transit').module;
layer._loadTransitFleetForTest(4, { lat: 42.3601, lon: -71.0589 }, 100);
const state = layer._transitStateForTest(),
parts = layer._transitPartsForTest();
const { destroyTrack } = await import('/src/data/contactPlayback.js');
const {
initializePlayback,
recordFix,
updatePlayback,
attachFloorToPlace,
} = await import('/src/layers/transit/movement.js');
// The first three are parked: held east, reported east only, unknown.
for (let i = 0; i < 3; i++) {
const entry = state._vehicles.get(`mbta:qa-${i}`);
state._moving.delete(entry);
destroyTrack(entry.track);
initializePlayback(entry, state._historyBudget);
recordFix(entry, {
t: Date.now() - 30000,
lat: entry.record.lat,
lon: entry.record.lon,
});
attachFloorToPlace(entry, entry.record, 20);
updatePlayback(entry, Date.now(), performance.now());
entry.courseDeg = i === 0 ? 90 : null;
entry.record.bearing = i === 1 ? 90 : null;
entry.displayPaths.clear();
parts.rendering.placeSample(entry);
parts.rendering.schedulePlayback(entry);
}
parts.rendering.syncRenderHold();
window.__headingErrors = [];
app.viewer.scene.renderError.addEventListener((scene, error) =>
window.__headingErrors.push(error.message),
);
});
const pose = async (heading) => {
await page.evaluate((heading) => {
const camera = window.__godsEyeView.viewer.camera;
const C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(C.fromDegrees(-71.0589, 42.3601, 1200)),
orientation: { heading, pitch: -Math.PI / 2, roll: 0 },
});
}, heading);
await new Promise((resolve) => setTimeout(resolve, 700));
};
const sample = () =>
page.evaluate(async () => {
const app = window.__godsEyeView,
scene = app.viewer.scene;
const state = app.dataManager.layers
.get('transit')
.module._transitStateForTest();
const mul = (m, v) =>
[0, 1, 2, 3].map(
(r) =>
m[r] * v[0] +
m[r + 4] * v[1] +
m[r + 8] * v[2] +
m[r + 12] * v[3],
);
const project = (p) => {
const clip = mul(
scene.camera.frustum.projectionMatrix,
mul(scene.camera.viewMatrix, [p.x, p.y, p.z, 1]),
);
return {
x: ((clip[0] / clip[3] + 1) * scene.canvas.clientWidth) / 2,
y: ((1 - clip[1] / clip[3]) * scene.canvas.clientHeight) / 2,
};
};
return [0, 1, 2, 3].map((i) => {
const entry = state._vehicles.get(`mbta:qa-${i}`);
const p = project(entry.marker.position);
return {
rotation: entry.marker.rotation,
course: entry.courseDeg,
moving: state._moving.has(entry),
visible: entry.marker.show,
x: p.x,
y: p.y,
};
});
});
await pose(0);
const before = await sample();
await pose(Math.PI / 2);
const after = await sample();
const deltaDeg = (a, b) =>
(Math.atan2(Math.sin(b - a), Math.cos(b - a)) * 180) / Math.PI;
for (const [i, label] of ['held course', 'reported bearing'].entries()) {
const delta = deltaDeg(before[i].rotation, after[i].rotation);
check(
`parked ${label} reprojects through a 90 degree camera orbit`,
before[i].visible &&
after[i].visible &&
!before[i].moving &&
!after[i].moving &&
Math.abs(delta - 90) <= 5,
JSON.stringify({ before: before[i], after: after[i], delta }),
);
}
check(
'unknown parked course remains screen-up',
before[2].rotation === 0 && after[2].rotation === 0,
);
await new Promise((resolve) => setTimeout(resolve, 1200));
const moved = (await sample())[3];
const dx = moved.x - after[3].x,
dy = moved.y - after[3].y;
const travelRotation = Math.atan2(-dx, -dy);
const error = Math.abs(deltaDeg(travelRotation, moved.rotation));
check(
'moving vehicle still points along its rendered travel after orbit',
moved.visible && moved.moving && Math.hypot(dx, dy) > 0.5 && error <= 5,
JSON.stringify({
pixels: Math.hypot(dx, dy),
error,
after: after[3],
moved,
}),
);
check(
'heading regression has no scene render errors',
(await page.evaluate(() => window.__headingErrors)).length === 0,
);
} finally {
await browser.close();
}
}