Files
gods-eye-view/scripts/qa-transit.mjs
T
Bilawal Sidhu 76c20be277 feat(transit): live public-transit vehicles from open GTFS-Realtime feeds (#587)
Supersedes #218 by @spcpza, ported onto the current component layout with fixes from review.

Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied.

Contributor: @spcpza. His original commit is the base of this branch, and his own re-port onto main (2026-09-15, `5b69011`) was consulted while finishing the height work. That port reads every vehicle's surface from the app's shared ground-floor system (`cachedGroundFloor` / `warmGroundFloor` / `GROUND_FLOOR_LIFT_M`) rather than sampling the scene per layer, keeps a vehicle hidden until its cell answers, warms a bounded batch per poll on a timer instead of per frame, and renders at the ellipsoid above the altitude gate. That is the sounder primitive — one floor system for the app, with the rooftop-versus-street question already settled against a datum-correct DEM prior — and this branch's height work was rebuilt on it, replacing a bespoke cell cache, ground prior and rooftop rule written here. Credit for that direction is his.

Future work (not in this PR), both written up in docs/CURRENT-STATE.md:

- Readable route names. The realtime feeds carry only `route_id`; `route_short_name` / `route_long_name` live in each operator's static GTFS `routes.txt`, and headsigns in `trips.txt`. Doing it properly wants a server-side metadata loader beside the transit proxy with fixed registered static sources, bounded indexes built once and persisted, a slow conditional refresh, and a versioned route dictionary the browser caches. Shipped here: ids are never rewritten, and a cut one carries an ellipsis instead of reading as a typo.
- Below-ground rendering. Subway vehicles are drawn on the surface directly above themselves, because the feeds carry no depth and a train nobody can see is not worth drawing; the card says so plainly. Real treatment wants OSM tunnel geometry per city and a ghosted sprite tethered to the surface. The owner recorded it and left it.

Round four and after (2026-09-15). The owner tested round three under FLIR and NVG, and then asked for the design to be taken further: elegant in performance, beautiful on camera, trails with a cache for public transport, Cesium-native, and a core other moving layers can adopt. Codex Astra reviewed the plan (it confirmed the speed fault: a glide started from the drawn position at receipt time but spanned the gap between report times, so with MBTA reporting every ~10 s against a 15 s poll every second glide ran at 1.5x; it rejected a queued-segment patch), then wrote the design, then implemented it in commit-sized passes with adversarial review between them and real-browser harness runs by the integrator.

What this PR now contains beyond the contributor's original port:

- Delayed playback in a contact-agnostic core (`src/data/contactPlayback.js`): each vehicle keeps a typed, bounded fix history and a display clock that runs at one second per second through it, a feed-derived lag behind real time, holding at its newest fix rather than extrapolating and never sprinting to catch up. Marker, course, motion word, card and trail all read the same displayed sample. The clock has an explicit time-offset and seek API so a rewind is a later feature, not a rewrite; flights and vessels are untouched.
- A selected-vehicle trail (`src/data/contactTrailRenderer.js`, `src/layers/transit/trails.js`): the retained history drawn with batched Cesium primitives, clipped at display time so the head is exactly the sprite, faded by age, in the mode colour (white under sensor presets), on the same surface path as the marker.
- Bounded proxy-side history (`server/providers/transitHistory.js`): fifteen minutes and at most 128 fixes per vehicle with hard byte ceilings, MBTA only for now (opt-in per feed), served by a small endpoint that never fetches upstream, so a selected trail survives a browser reload while the server keeps running.
- Scene work bounded to what is visible and moving, an occluder, cached DETECT candidates, holds on the render loop only while something visible moves.
- Sensor presets: white-hot haloed sprites under NVG/FLIR/noir (black-hot inverts), saturated mode colours under CRT, per-mode bracket tiers in every theme, the selected card keeping its mode colour.
- A discriminating browser harness (`scripts/qa-transit.mjs`) that samples postRender frames, reads the real card anchor, and measures fleet budgets at 800 and 3,000 vehicles.

Not built, recorded as follow-ups: curated route display names (lowest priority per the owner), fleet-wide tails, route snapping, a rewind UI, adoption of the playback core by flights and vessels.

Verified at merge (2026-09-15 morning): unit suite 3,896 passing, build clean, format and boundary gates clean, tracking regression 109/109, browser harness keyless 200 pass / 0 fail (13 sensor checks unexercised because the live view offered fewer than six isolated sprites; every judged sprite passed), keyed on Google 3D tiles 198 pass with the per-frame allocation gate as the only failure. Follow-ups recorded: the keyed build's playback sampler still allocates per frame (CPU and frame time stay within budget; the 3,000-vehicle metro view sits at a 27.6 ms frame interval against a 25 ms budget), the shared detection overlay's per-paint allocation, friendlier route names, fleet-wide tails, a rewind UI, and adopting the playback core for vessels and flights.
Co-authored-by: spcpza <[email protected]>
2026-09-15 05:18:30 -05:00

1952 lines
73 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import {
boundPageEvaluations,
reportTransitVisibility,
restoreRetainedTransit,
sampleTransitPixels,
readTransitFleetStyle,
} from './qa-transit-browser.mjs';
import {
BOSTON,
chooseLiveCities,
selectStyle,
detectOn,
} from './qa-transit-controls.mjs';
import {
runFleetBudgets,
runBostonMatrix,
runBostonLive,
} from './qa-transit-scenes.mjs';
/**
* Browser proof for the Transit layer against a running app.
*
* npm run qa:transit (defaults to http://localhost:4173)
* QA_BASE_URL=http://localhost:4305 node scripts/qa-transit.mjs
*
* Drives the real layer toggle and the real CONTEXT chooser, watches the real
* /api/transit traffic, proves vehicles move between polls in the SCENE (not
* just in the JSON), captures street-level shots in two cities, and proves that
* turning the layer off stops every request and leaves nothing rendered.
*
* Screenshots land in the gitignored qa-shots/ directory unless QA_SHOTS says
* otherwise. Needs a running server; the feeds are keyless, so a keyless boot
* exercises the same paths.
*/
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import puppeteer from 'puppeteer';
import { createTransitHistory } from '../server/providers/transitHistory.js';
import { DETECTION_THEME_MAP } from '../src/overlays/worldOverlayTokens.js';
import { transitModeTier } from '../src/data/transitPresetStyle.js';
import {
reduceAnchor,
reduceTrailHead,
reduceScenario,
reduceSensorContrast,
reduceScriptedMotion,
reduceMotion,
reduceFleet,
} from '../src/layers/transit/qaMetrics.js';
const BASE = process.env.QA_BASE_URL || 'http://localhost:4173';
const SHOTS = process.env.QA_SHOTS || 'qa-shots/transit';
const TAG = process.env.QA_TAG || 'keyed';
/** Which sections to run: all, or a comma list of cities,playback,disable,context. */
const SECTIONS = new Set((process.env.QA_SECTIONS || 'all').split(','));
const runs = (section) => SECTIONS.has('all') || SECTIONS.has(section);
const CITIES = chooseLiveCities();
let failures = 0;
let unexercised = 0;
/**
* A check has three outcomes. PASS and FAIL are what they say. UNEXERCISED
* means the run could not put the claim to the test — a parked fleet has no
* motion to judge — and it is reported as its own word rather than as a
* pass, because a pass that proved nothing is how a broken interpolation
* sailed through this harness once.
*/
const check = (
name,
passed,
detail = '',
{ unexercised: idle = false } = {},
) => {
const word = idle ? 'UNEXERCISED' : passed ? 'PASS' : 'FAIL';
console.log(`[${word}] ${name}${detail ? ` — ${detail}` : ''}`);
if (idle) {
unexercised += 1;
failures += 1;
} else if (!passed) failures += 1;
};
const renderingSource = await readFile(
new URL('../src/layers/transit/rendering.js', import.meta.url),
'utf8',
);
const frameSource = renderingSource
.split('function onPreRender()')[1]
.split('const occluder')[0];
check(
'frame pass does not scan the fleet or copy the moving set',
!/_vehicles\.values|\[\.\.\.state\._moving\]/.test(frameSource),
);
const palette = {
bus: '#5EF08A',
tram: '#FFC24A',
subway: '#FF4538',
rail: '#D9A6FF',
ferry: '#5FD6FF',
unknown: '#D8DDE5',
};
check(
'all preset bracket themes preserve the six mode colours',
Object.values(DETECTION_THEME_MAP).every((theme) =>
Object.entries(palette).every(
([mode, color]) =>
theme.tiers[transitModeTier(mode)].toUpperCase() === color,
),
),
);
check(
'all required sections enabled',
SECTIONS.has('all'),
'Partial runs are diagnostic and cannot pass acceptance.',
);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** Hex → [r,g,b]. */
const rgbOf = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
const rgbDistance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
/**
* Rendered pixels of the scene and of the detection surface around each
* visible transit sprite: centre and darkest/brightest ring luma from the
* WebGL canvas (the app preserves its drawing buffer), and the dominant
* opaque colour painted on the detection surface in a bracket-sized window.
* Sprites closer than 44 px to another sprite are skipped so a ring is not
* read off a neighbour.
* @param {import('puppeteer').Page} page
* @returns {Promise<Array<{key:string, mode:string, centre:number, ringMin:number, ringMax:number, bracket:number[]|null, x:number, y:number}>>}
*/
const sampleRendered = (page) => sampleTransitPixels(page, palette);
/**
* The rendered position of every VISIBLE transit billboard, read back through
* Cesium. Visible only: off-screen vehicles take their newest fix directly, so
* including them would let hidden vehicles snapping stand in for motion a
* person can see.
* @param {import('puppeteer').Page} page
* @returns {Promise<Array<{key: string, lat: number, lon: number}>>}
*/
/**
* Window-space position and billboard rotation of every visible transit
* sprite. Orientation is a screen-space claim, so it has to be checked in
* screen space.
* @param {import('puppeteer').Page} page
* @returns {Promise<Array<{key: string, x: number, y: number, rotation: number}>>}
*/
async function sampleScreen(page) {
return page.evaluate(() => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const scene = app.viewer.scene;
const canvas = scene.canvas;
// The app exposes no `window.Cesium`, so the projection is done here in
// plain numbers: Cesium matrices are column-major arrays of 16 doubles,
// and window coordinates are the standard clip -> NDC -> viewport walk.
const view = scene.camera.viewMatrix;
const proj = scene.camera.frustum.projectionMatrix;
const mul = (m, v) => [
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14] * v[3],
m[3] * v[0] + m[7] * v[1] + m[11] * v[2] + m[15] * v[3],
];
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const out = [];
for (const entry of state._vehicles.values()) {
if (!entry.marker || entry.marker.show === false) continue;
const p = entry.marker.position;
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
if (!(clip[3] > 0)) continue;
const ndcX = clip[0] / clip[3];
const ndcY = clip[1] / clip[3];
out.push({
entityCount: app.viewer.entities.values.length,
key: entry.key,
x: (ndcX * 0.5 + 0.5) * width,
y: (1 - (ndcY * 0.5 + 0.5)) * height,
rotation: entry.marker.rotation,
pollSeq: entry.pollSeq,
segment:
entry.from && entry.to ? `${entry.from.t}-${entry.to.t}` : null,
});
if (out.length >= 200) break;
}
return out;
});
}
async function sampleVisible(page) {
return page.evaluate(() => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const Cartographic = app.viewer.camera.positionCartographic.constructor;
return [...state._vehicles.values()]
.filter((entry) => entry.marker && entry.marker.show !== false)
.slice(0, 200)
.map((entry) => {
const carto = Cartographic.fromCartesian(entry.marker.position);
return {
key: entry.key,
lat: (carto.latitude * 180) / Math.PI,
lon: (carto.longitude * 180) / Math.PI,
gliding: state._moving.has(entry),
// Which poll and which displayed segment this reading belongs to:
// two readings that straddle a poll or a segment change are not
// evidence about interpolation, whatever the elapsed time says.
pollSeq: entry.pollSeq,
segment:
entry.from && entry.to ? `${entry.from.t}-${entry.to.t}` : null,
};
});
});
}
// Accelerate the storage clock without collecting a live feed in the background.
const historyProbe = createTransitHistory({
sweep: false,
storage: { bytes: 0, keys: 0, stores: new Set() },
limits: { feedBytes: 128000, processBytes: 128000 },
});
const retainedSizes = [];
for (let now = 0; now <= 7200000; now += 15000) {
historyProbe.ingest(
{ id: 'probe', historyRetention: true, defaultMode: 'bus' },
Array.from({ length: 12 }, (_, i) => ({
id: String(i),
timestamp: now / 1000,
lat: 42,
lon: -71,
})),
now,
);
if (now >= 900000)
retainedSizes.push(historyProbe.diagnostics().allocatedBytes);
}
check(
'two-hour proxy history allocation plateaus',
Math.max(...retainedSizes) === Math.min(...retainedSizes) &&
Math.max(...retainedSizes) <= 128000,
`${retainedSizes.length} warmed samples, ${Math.max(...retainedSizes)} allocated bytes`,
);
historyProbe.clear();
await mkdir(SHOTS, { recursive: true });
const browser = await puppeteer.launch({
headless: false,
protocolTimeout: 150000,
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
args: [
'--no-sandbox',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1920,1080',
'--enable-precise-memory-info',
],
});
const page = await browser.newPage();
boundPageEvaluations(page);
await page.setViewport({ width: 1920, height: 1080 });
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
page.on('console', (message) => {
if (message.text().startsWith('TRANSIT_SAMPLER')) console.log(message.text());
});
const transitRequests = [];
page.on('request', (request) => {
if (request.url().includes('/api/transit')) {
transitRequests.push({ at: Date.now(), url: request.url() });
}
});
const shot = (name, caption) =>
page
.screenshot({ path: `${SHOTS}/${name}`, type: 'jpeg', quality: 82 })
.then(() => {
console.log(` shot ${name} — ${caption}`);
});
const view = (city) =>
`${BASE}/?welcome=0#lat=${city.lat}&lon=${city.lon}&alt=600&heading=${city.heading}&pitch=-45`;
try {
await page.goto(view(BOSTON), { waitUntil: 'domcontentloaded' });
// Ready when the catalog is registered and its rows exist. (The manager's
// private panel handle moved into a presentation facade; waiting on it here
// was waiting for something that no longer exists.)
await page.waitForFunction(
() =>
Boolean(window.__godsEyeView?.dataManager?.layers?.size) &&
Boolean(document.querySelector('[data-layer-id="transit"]')),
{ timeout: 90_000 },
);
await wait(6_000);
// The Data Layers panel starts collapsed; a user opens it before toggling.
const expanded = await page.$eval(
'#data-panel .panel-collapse-btn',
(button) => {
if (button.getAttribute('aria-expanded') === 'true') return true;
button.click();
return false;
},
);
if (!expanded) await wait(800);
check(
'the Data Layers panel opens from its own header button',
await page.$eval(
'#data-panel .panel-collapse-btn',
(b) => b.getAttribute('aria-expanded') === 'true',
),
);
// Record what the layer hands the shared overlay host, so the card check can
// quote the operator's own words rather than assert an internal key was set.
await page.evaluate(() => {
window.__gevTransitCards = new Map();
const layer = window.__godsEyeView.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const host = state.DEFAULT_OVERLAY_HOST;
layer._setTransitOverlayHostForTest({
setEntries(sourceId, entries, options) {
for (const entry of entries || []) {
window.__gevTransitCards.set(entry.id, {
title: entry.title,
details: entry.details,
});
}
return host.setEntries(sourceId, entries, options);
},
setVisible: host.setVisible,
clearSource(sourceId) {
window.__gevTransitCards.clear();
return host.clearSource(sourceId);
},
});
});
const row = '[data-layer-id="transit"]';
const hasRow = await page.$(row);
check('the Transit layer has a row in the layer panel', Boolean(hasRow));
await page.$eval(row, (element) =>
element.scrollIntoView({ block: 'center' }),
);
await wait(400);
const before = transitRequests.length;
await page.click(`${row} .data-toggle-btn`);
await page.waitForFunction(
() => window.__godsEyeView.dataManager.isEnabled('transit'),
{ timeout: 20_000 },
);
check(
'clicking the row button enables the layer and starts real traffic',
transitRequests.length > before,
`${transitRequests.length - before} request(s)`,
);
await page.evaluate(() => {
window.__transitRenderErrors = [];
window.__godsEyeView.viewer.scene.renderError.addEventListener(
(scene, error) => window.__transitRenderErrors.push(error.message),
);
});
for (const city of runs('cities') ? CITIES : []) {
console.log(`\n== ${city.label} ==`);
// City-wide first: enough altitude to see the fleet as a fleet.
await page.evaluate((c) => {
const camera = window.__godsEyeView.viewer.camera;
const Cartographic = camera.positionCartographic.constructor;
camera.setView({
destination: Cartographic.toCartesian(
Cartographic.fromDegrees(c.lon, c.lat - 0.03, 3_500),
),
orientation: { heading: 0, pitch: (-40 * Math.PI) / 180, roll: 0 },
});
}, city);
await page.waitForFunction(
() =>
(window.__godsEyeView.dataManager.layers
.get('transit')
.module.getStats().count || 0) > 0,
{ timeout: 90_000 },
);
await wait(9_000);
const fleet = await page.evaluate(() => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
return {
count: layer._transitStateForTest()._vehicles.size,
stats: layer.getStats(),
};
});
check(
`${city.label}: vehicles are rendered`,
fleet.count >= 20,
`${fleet.count} vehicles, coverage "${fleet.stats.coverage}"`,
);
await shot(
`${TAG}-${city.id}-00-fleet.jpg`,
`${city.label} from 3.5 km — ${fleet.count} vehicles across the metro`,
);
// Street level: 600 m up, 45 degrees down, framed on a real vehicle so the
// shot shows what a user sees when they drop in on one.
const framed = await page.evaluate((c) => {
const app = window.__godsEyeView;
const camera = app.viewer.camera;
const Cartographic = camera.positionCartographic.constructor;
const state = app.dataManager.layers
.get('transit')
.module._transitStateForTest();
let best = null;
for (const entry of state._vehicles.values()) {
const d = Math.hypot(entry.to.lat - c.lat, entry.to.lon - c.lon);
if (!best || d < best.d)
best = { d, lat: entry.to.lat, lon: entry.to.lon, key: entry.key };
}
if (!best) return null;
// Camera 600 m back along the heading, 600 m up, looking 45 degrees down:
// the vehicle lands in the middle of the frame.
const heading = (c.heading * Math.PI) / 180;
const lat = best.lat - (600 * Math.cos(heading)) / 111_320;
const lon =
best.lon -
(600 * Math.sin(heading)) /
(111_320 * Math.cos((best.lat * Math.PI) / 180));
camera.setView({
destination: Cartographic.toCartesian(
Cartographic.fromDegrees(lon, lat, 600),
),
orientation: { heading, pitch: (-45 * Math.PI) / 180, roll: 0 },
});
return best;
}, city);
check(
`${city.label}: a vehicle to fly to`,
Boolean(framed),
framed?.key || 'none',
);
await wait(9_000);
const first = await page.evaluate(() => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const Cartographic = app.viewer.camera.positionCartographic.constructor;
return {
count: state._vehicles.size,
stats: layer.getStats(),
// The RENDERED primitive position, read back through Cesium — not the
// fix the layer is aiming at. A layer that never moved a point would
// still show two different `entry.to` values, so comparing those would
// prove the data changed, not that anything on screen did.
// VISIBLE markers only. Off-screen vehicles take their newest fix
// directly — that is the whole point of the view gate — so sampling
// them would let a layer whose visible interpolation is broken pass
// this check on the strength of hidden vehicles snapping.
sample: [...state._vehicles.values()]
.filter((entry) => entry.marker && entry.marker.show !== false)
.slice(0, 200)
.map((entry) => {
const carto = Cartographic.fromCartesian(entry.marker.position);
return {
key: entry.key,
lat: (carto.latitude * 180) / Math.PI,
lon: (carto.longitude * 180) / Math.PI,
};
}),
};
});
// Click a vehicle that is actually on screen, recomputing its position
// immediately before each attempt — these dots are gliding.
let selected = null;
for (let attempt = 0; attempt < 6 && !selected; attempt += 1) {
const target = await page.evaluate((skip) => {
const app = window.__godsEyeView;
const state = app.dataManager.layers
.get('transit')
.module._transitStateForTest();
const scene = app.viewer.scene;
const seen = [];
for (const entry of state._vehicles.values()) {
const screen = scene.cartesianToCanvasCoordinates(
entry.marker.position,
);
if (
screen &&
screen.x > 360 &&
screen.x < scene.canvas.clientWidth - 120 &&
screen.y > 120 &&
screen.y < scene.canvas.clientHeight - 160
) {
seen.push({
key: entry.key,
x: Math.round(screen.x),
y: Math.round(screen.y),
});
}
}
return seen[skip] || null;
}, attempt);
if (!target) break;
await page.mouse.click(target.x, target.y);
await wait(900);
selected = await page.evaluate(() => {
const app = window.__godsEyeView;
const key = app.dataManager.layers
.get('transit')
.module._transitStateForTest()._selectedKey;
if (!key) return null;
// A selected key is bookkeeping. The claim is that the user SEES a
// card, and these cards are painted onto the shared overlay CANVAS
// rather than built from DOM nodes — so the proof is the operator's own
// words the layer published, plus pixels that actually landed there.
const published = window.__gevTransitCards?.get(key) || null;
const canvas = document.getElementById('world-overlay-canvas');
let painted = 0;
if (canvas?.width) {
const context = canvas.getContext('2d', { willReadFrequently: true });
const pixels = context.getImageData(
0,
0,
canvas.width,
canvas.height,
).data;
for (let i = 3; i < pixels.length; i += 64) {
if (pixels[i] > 8) painted += 1;
}
}
return { key, published, painted };
});
if (selected && (!selected.published || selected.painted === 0)) {
selected = null;
}
}
const cardLines = selected
? [selected.published.title, ...(selected.published.details || [])]
: [];
check(
`${city.label}: clicking a vehicle raises a card with its details on screen`,
cardLines.length >= 3 &&
/Route |Vehicle /.test(cardLines[0] || '') &&
cardLines.some((line) => /Reported /.test(line)) &&
selected.painted > 0,
selected
? `${selected.key} — "${cardLines.slice(0, 4).join(' / ')}" over ${selected.painted} painted overlay samples`
: 'no vehicle card became visible',
);
// DETECT is on by default at DENSE. Prove the overlay actually boxes and
// labels transit contacts rather than ignoring them, which is what the
// owner found: the layer drew dots and the sensor overlay looked past them.
const detect = await page.evaluate(() => {
const gev = window.__godsEyeView;
const layer = gev.dataManager.layers.get('transit').module;
const objects = layer.getDetectableObjects({ maxCount: 200 });
const button = document.getElementById('detection-toggle');
return {
mode: (button?.textContent || '').replace(/\s+/g, ' ').trim(),
count: objects.length,
sample: objects.slice(0, 3).map((o) => `${o.id}/${o.metric || ''}`),
types: [...new Set(objects.map((o) => o.type))],
labelled: objects.filter((o) => o.id && o.metric).length,
skipped: objects.filter((o) => o.skipLabel).length,
};
});
// What the host DOES with these is pinned by the detection-host unit test,
// which drives the real painter and reads the route and mode back off the
// canvas. Here the claim is narrower and fully attributable: the layer
// offers DETECT properly formed contacts, which is precisely what it used
// to offer none of.
check(
`${city.label}: the layer offers DETECT real contacts, not bare dots`,
detect.count > 0 &&
detect.types.length === 1 &&
detect.types[0] === 'VEH' &&
detect.labelled === detect.count,
`${detect.count} contacts, all labelled, e.g. ${detect.sample.join(', ')} (DETECT ${detect.mode})`,
);
// The glyphs themselves: modes must not all be one shape.
const sprites = await page.evaluate(() => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
const images = new Map();
for (const entry of state._vehicles.values()) {
if (!entry.marker?.show) continue;
// Compare the WHOLE data URI: every glyph shares a long base64 prefix,
// so a truncated comparison says they are all the same picture.
images.set(entry.mode, String(entry.marker.image || ''));
}
return {
modes: [...images.keys()],
distinct: new Set(images.values()).size,
glyphBytes: [...images.values()].map((uri) => uri.length),
billboards: [...state._vehicles.values()].filter((e) => e.marker)
.length,
shown: state._shownCount,
};
});
check(
`${city.label}: vehicles draw as mode sprites, not plain dots`,
sprites.distinct === sprites.modes.length && sprites.modes.length > 0,
`${sprites.modes.join(', ')} → ${sprites.distinct} distinct glyph(s) of sizes ${sprites.glyphBytes.join('/')}; ${sprites.shown} shown of ${sprites.billboards}`,
);
await shot(
`${TAG}-${city.id}-01-vehicles.jpg`,
`${city.label} at 600 m, 45° pitch — ${first.count} vehicles in the layer, selected card on the one in frame`,
);
// ---- Rendered appearance under the sensor presets (first active live city) --------
// In-scene sprites pass through the post-FX chain; what matters is what
// reaches the screen. DETECT on, then each preset the owner tested:
// pixels of the sprite core and its ring from the WebGL canvas, the
// colour painted for its bracket on the detection surface, and one
// screenshot per preset for the diary.
if (city.id === CITIES[0].id) {
console.log('\n== rendered appearance ==');
const profile = await detectOn(page);
check(
'DETECT reaches the dense profile through its own button',
profile === 'DENSE',
profile,
);
const themeFor = (style) =>
DETECTION_THEME_MAP[style] || DETECTION_THEME_MAP._default;
const presets = [
{ name: 'normal', style: 'normal', params: {}, hot: null },
{
name: 'thermal-whot',
style: 'thermal',
params: { 'WHOT/BHOT': 0, Ironbow: 0 },
hot: 'white',
},
{
name: 'thermal-bhot',
style: 'thermal',
params: { 'WHOT/BHOT': 1, Ironbow: 0 },
hot: 'black',
},
{
name: 'thermal-ironbow',
style: 'thermal',
params: { 'WHOT/BHOT': 0, Ironbow: 1 },
hot: 'white',
},
{
name: 'surveillance',
style: 'surveillance',
params: {},
hot: 'white',
},
{ name: 'noir', style: 'noir', params: {}, hot: 'white' },
{ name: 'retro', style: 'retro', params: {}, hot: null },
];
await selectStyle(page, 'normal');
const baseline = await page.evaluate(readTransitFleetStyle, {
resetSelection: true,
});
for (const preset of presets) {
const how = await selectStyle(page, preset.style, preset.params);
await wait(1_200);
const styling = await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStylingForTest(),
);
const rendered = await sampleRendered(page);
await shot(
`${TAG}-boston-style-${preset.name}.jpg`,
`${city.label} at street level under ${preset.name}, DETECT on — ${rendered.length} isolated sprites sampled (style set via ${how})`,
);
const modes = new Set(rendered.map((r) => r.mode));
if (preset.hot) {
const contrast = reduceSensorContrast(
rendered,
preset.hot,
preset.style,
);
check(
`${preset.name}: sprite cores have the expected sensor contrast`,
contrast.pass,
JSON.stringify(contrast),
{ unexercised: rendered.length < 6 },
);
check(
`${preset.name}: the layer styled its sprites for ${preset.style}`,
styling.stylePreset === preset.style,
JSON.stringify(styling),
);
}
// Brackets: the detection surface paints each contact in its tier
// colour, which must be the mode's colour for this theme.
const theme = themeFor(preset.style);
const matched = rendered.filter(
(r) =>
r.bracket &&
rgbDistance(
r.bracket,
rgbOf(theme.tiers[transitModeTier(r.mode)]),
) < 48,
);
const byMode = new Map();
for (const r of rendered) if (r.bracket) byMode.set(r.mode, r.bracket);
const modeColours = [...byMode.values()];
let distinct = true;
for (let i = 0; i < modeColours.length; i += 1)
for (let j = i + 1; j < modeColours.length; j += 1)
if (rgbDistance(modeColours[i], modeColours[j]) < 40)
distinct = false;
check(
`${preset.name}: brackets carry the mode colour (${[...modes].join(', ') || 'no modes'})`,
rendered.length >= 6 &&
matched.length >= Math.ceil(rendered.length * 0.75) &&
(byMode.size < 2 || distinct),
rendered.length === 0
? 'no isolated sprite in frame to sample'
: `${matched.length} of ${rendered.length} within tolerance of their tier; ${byMode.size} mode(s) painted${byMode.size >= 2 ? (distinct ? ', distinguishable' : ', NOT distinguishable') : ''}; misses=${JSON.stringify(
rendered
.filter((r) => !matched.includes(r))
.map((r) => ({
key: r.key,
sampled: r.bracket,
tier: transitModeTier(r.mode),
expected: rgbOf(theme.tiers[transitModeTier(r.mode)]),
})),
)}`,
{ unexercised: rendered.length < 6 },
);
const clickTarget = rendered[0];
if (clickTarget) {
await page.mouse.click(clickTarget.x, clickTarget.y);
await wait(100);
const picked = await page.evaluate((key) => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
return {
selected: state._selectedKey === key,
pickOwner: state._lastPickForTest,
};
}, clickTarget.key);
check(
`${preset.name}: mouse click selects the displayed sprite`,
picked.selected,
JSON.stringify(picked),
);
await page.keyboard.press('Escape');
} else
check(
`${preset.name}: mouse click selects the displayed sprite`,
false,
'No verified unobscured sprite',
{ unexercised: true },
);
check(
`${preset.name}: the icon raster cache stays bounded`,
styling.iconCacheSize <= 6 * 2 * 3,
`${styling.iconCacheSize} rasters`,
);
}
// Selection under FLIR keeps the sprite white; leaving FLIR restores it.
await selectStyle(page, 'thermal', { 'WHOT/BHOT': 0, Ironbow: 0 });
const target = (await sampleRendered(page))[0];
if (target) {
await page.mouse.click(target.x, target.y);
await wait(800);
const underFlir = await page.evaluate((key) => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const entry = state._vehicles.get(key);
return {
selected: state._selectedKey === key,
color: entry?.marker.color.toCssColorString(),
width: entry?.marker.width,
pickOwner: (() => {
const p = window.__godsEyeView.viewer.scene.pick(
entry.marker.computeScreenSpacePosition(
window.__godsEyeView.viewer.scene,
),
);
return {
id: typeof p?.id === 'string' ? p.id : null,
primitiveId:
typeof p?.primitive?.id === 'string' ? p.primitive.id : null,
collection:
entry.markerCollection === state._animatedMarkers
? 'animated'
: 'stationary',
};
})(),
};
}, target.key);
check(
'clicking a sprite under FLIR selects it and keeps it white-hot',
underFlir.selected && underFlir.color === 'rgb(255,255,255)',
JSON.stringify(underFlir),
);
await page.keyboard.press('Escape');
await wait(500);
} else {
check(
'clicking a sprite under FLIR selects it and keeps it white-hot',
false,
'no isolated sprite to click',
{ unexercised: true },
);
}
await selectStyle(page, 'normal');
const restored = await page.evaluate(readTransitFleetStyle, {
key: baseline?.key,
});
check(
'returning to normal restores the shipped sprite exactly',
Boolean(restored && baseline) &&
!restored.selected &&
!baseline.selected &&
restored.color === baseline.color &&
restored.width === baseline.width &&
restored.image === baseline.image,
restored && baseline
? `${baseline.key}: selected ${baseline.selected}→${restored.selected}, ${baseline.color}/${baseline.width}px → ${restored.color}/${restored.width}px; raster equal ${baseline.image === restored.image}`
: 'the baseline vehicle left the fleet',
{ unexercised: !(restored && baseline) },
);
}
// Motion acceptance uses known report-time playback even when live fleets park.
await selectStyle(page, 'normal');
await page.evaluate((city) => {
const app = window.__godsEyeView,
camera = app.viewer.camera;
const C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(C.fromDegrees(city.lon, city.lat, 600)),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, city);
await wait(2000);
await page.evaluate(
(city) =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._loadTransitFleetForTest(16, city, 25),
city,
);
await wait(1000);
const motionFirst = await sampleVisible(page);
await wait(30_000);
const second = await page.evaluate(() => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const Cartographic = app.viewer.camera.positionCartographic.constructor;
return {
count: state._vehicles.size,
// The RENDERED primitive position, read back through Cesium — not the
// fix the layer is aiming at. A layer that never moved a point would
// still show two different `entry.to` values, so comparing those would
// prove the data changed, not that anything on screen did.
// VISIBLE markers only. Off-screen vehicles take their newest fix
// directly — that is the whole point of the view gate — so sampling
// them would let a layer whose visible interpolation is broken pass
// this check on the strength of hidden vehicles snapping.
sample: [...state._vehicles.values()]
.filter((entry) => entry.marker && entry.marker.show !== false)
.slice(0, 200)
.map((entry) => {
const carto = Cartographic.fromCartesian(entry.marker.position);
return {
key: entry.key,
lat: (carto.latitude * 180) / Math.PI,
lon: (carto.longitude * 180) / Math.PI,
};
}),
};
});
const beforeById = new Map(motionFirst.map((v) => [v.key, v]));
let moved = 0;
let matched = 0;
for (const v of second.sample) {
const was = beforeById.get(v.key);
if (!was) continue;
matched += 1;
const dLat = (v.lat - was.lat) * 111_320;
const dLon =
(v.lon - was.lon) * 111_320 * Math.cos((v.lat * Math.PI) / 180);
if (Math.hypot(dLat, dLon) > 5) moved += 1;
}
check(
`${city.label} deterministic fixture: the same vehicles are still drawn 30 s later, in new places`,
matched >= 5 && moved >= 3,
`${matched} same visible ids, ${moved} whose RENDERED point moved more than 5 m`,
);
// Between polls, not across them. Positions changing over thirty seconds
// only proves new data arrived; a layer that snapped every fifteen seconds
// and drew nothing in between would pass that. Two reads two seconds apart
// fall inside one poll interval, so anything that moves is being
// INTERPOLATED on screen.
const glideA = await sampleVisible(page);
await wait(2_000);
const glideB = await sampleVisible(page);
const glideBefore = new Map(glideA.map((v) => [v.key, v]));
let glided = 0;
let glideMatched = 0;
let straddled = 0;
// Only vehicles the LAYER says are mid-segment can testify here, and only
// readings from the SAME poll and the same displayed segment: a pair that
// straddles a poll could have snapped, and would pass as motion.
let claimed = 0;
for (const v of glideB) {
const was = glideBefore.get(v.key);
if (!was) continue;
glideMatched += 1;
if (!(was.gliding || v.gliding)) continue;
if (was.pollSeq !== v.pollSeq || was.segment !== v.segment) {
straddled += 1;
continue;
}
claimed += 1;
const dLat = (v.lat - was.lat) * 111_320;
const dLon =
(v.lon - was.lon) * 111_320 * Math.cos((v.lat * Math.PI) / 180);
if (Math.hypot(dLat, dLon) > 1) glided += 1;
}
check(
`${city.label} deterministic fixture: a vehicle the layer says is mid-segment moves on screen between polls`,
claimed > 0 && glided >= 1,
claimed === 0
? `nothing was mid-segment inside one poll across ${glideMatched} visible vehicles (${straddled} pairs straddled a poll)`
: `${glided} of ${claimed} mid-segment vehicles moved within one poll and one segment (${straddled} straddling pairs discarded)`,
{ unexercised: claimed === 0 },
);
// Orientation, measured the only way that settles it: where the sprite
// points against where the sprite actually travelled on screen. The
// projection is perspective-exact, so an off-centre vehicle at an oblique
// pitch must agree too — that is precisely the case the orthographic
// approximation got wrong by twenty-odd degrees.
//
// Only sprites that visibly moved can testify, and a keyless Austin view
// holds a handful of them; the floor is what that view can honestly
// supply, and the accuracy bar does the work.
const headingA = await sampleScreen(page);
await wait(6_000);
const headingB = await sampleScreen(page);
const wasById = new Map(headingA.map((v) => [v.key, v]));
let aimed = 0;
let checked = 0;
let worstDeg = 0;
for (const now of headingB) {
const was = wasById.get(now.key);
if (!was) continue;
// A poll in between could have placed it; that is not travel.
if (was.pollSeq !== now.pollSeq) continue;
const dx = now.x - was.x;
const dy = now.y - was.y;
// Only vehicles that visibly travelled can testify about direction.
if (Math.hypot(dx, dy) < 6) continue;
checked += 1;
// The sprite is authored nose-up and rotated CCW, so at rotation r its
// nose points along (-sin r, -cos r) in window coordinates.
const noseX = -Math.sin(now.rotation);
const noseY = -Math.cos(now.rotation);
const travel = Math.hypot(dx, dy);
const cosErr = (noseX * dx + noseY * dy) / travel;
const errDeg =
(Math.acos(Math.max(-1, Math.min(1, cosErr))) * 180) / Math.PI;
if (errDeg <= 35) aimed += 1;
else worstDeg = Math.max(worstDeg, errDeg);
}
check(
`${city.label} deterministic fixture: sprites point the way they are travelling on screen`,
checked > 0 && aimed >= Math.ceil(checked * 0.8),
checked === 0
? 'no sprite travelled far enough on screen inside one poll to testify'
: `${aimed} of ${checked} moving sprites within 35° of their screen travel` +
(worstDeg ? `, worst miss ${Math.round(worstDeg)}°` : ''),
{ unexercised: checked === 0 },
);
await shot(
`${TAG}-${city.id}-02-thirty-seconds-later.jpg`,
`${city.label} deterministic fixture 30 s later — ${second.count} vehicles, ${moved}/${matched} moved`,
);
}
check(
'vehicle selection never raises a Cesium render error',
(await page.evaluate(() => window.__transitRenderErrors || [])).length ===
0,
JSON.stringify(
await page.evaluate(() => window.__transitRenderErrors || []),
),
);
if (runs('history')) {
console.log('\n== retained history reload ==');
const recovery = await browser.newPage();
boundPageEvaluations(recovery);
try {
await recovery.goto(view(BOSTON), { waitUntil: 'domcontentloaded' });
await recovery.waitForFunction(
() => window.__godsEyeView?.dataManager?.layers?.has('transit'),
{ timeout: 90000 },
);
// Leave enough time for two snapshots, priming and reload in one minute.
if (Date.now() % 60000 > 20000)
await wait(60000 - (Date.now() % 60000) + 50);
const first = await recovery.evaluate(async () =>
(
await fetch('/api/transit/vehicles/mbta', {
signal: AbortSignal.timeout(10000),
})
).json(),
);
await wait(16000);
const retained = await recovery.evaluate(async (prior) => {
const current = await (
await fetch('/api/transit/vehicles/mbta', {
signal: AbortSignal.timeout(10000),
})
).json();
const candidates = current.vehicles.filter((v) =>
prior.vehicles.some((p) => p.id === v.id),
);
// Prime the newest fixes from this exact feed, not evenly spaced stale IDs.
const primedAt = Date.now();
const picked = candidates
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
.slice(0, 12);
const histories = await Promise.all(
picked.map(async (vehicle) => {
const response = await fetch(
`/api/transit/trail/mbta/${encodeURIComponent(vehicle.id)}`,
{ signal: AbortSignal.timeout(10000) },
);
if (!response.ok)
throw new Error(`Trail prime HTTP ${response.status}`);
return response.json();
}),
);
return histories
.filter((history) => history.fixes?.length >= 2)
.map((history) => ({ ...history, feedId: 'mbta', primedAt }));
}, first);
await recovery.reload({ waitUntil: 'domcontentloaded' });
await recovery.waitForFunction(
() => window.__godsEyeView?.dataManager?.layers?.has('transit'),
{ timeout: 90000 },
);
await recovery.evaluate(() => {
window.__transitRenderErrors = [];
window.__godsEyeView.viewer.scene.renderError.addEventListener(
(scene, error) => window.__transitRenderErrors.push(error.message),
);
});
const restored = await recovery.evaluate(
restoreRetainedTransit,
retained,
);
if (restored.key) {
await recovery.waitForFunction(
() =>
!window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitPartsForTest()
.trails.requestDiagnostics().pending,
{ timeout: 20000 },
);
await wait(4000);
const proof = await recovery.evaluate(
({ key, oldest }) => {
const layer =
window.__godsEyeView.dataManager.layers.get('transit').module;
const entry = layer._transitStateForTest()._vehicles.get(key),
parts = layer._transitPartsForTest();
if (!entry) return { gone: true };
const segments = entry.trailSegments?.length || 0;
const renderErrors = window.__transitRenderErrors || [];
const count = entry.track.count,
restoredOldest = entry.fixes[0].t;
const before = parts.trails.requestDiagnostics().abortCount;
parts.selection.selectVehicle(key);
parts.selection.clearSelection();
return {
count,
segments,
renderErrors,
restoredOldest,
oldest,
aborted: parts.trails.requestDiagnostics().abortCount > before,
};
},
{ key: restored.key, oldest: restored.oldest },
);
check(
'reload restores observed history and selection churn aborts its request',
proof.count >= 2 &&
proof.restoredOldest <= proof.oldest &&
proof.aborted,
proof.gone
? 'Selected primed vehicle left during recovery'
: JSON.stringify(proof),
{ unexercised: proof.gone === true },
);
if (!proof.gone)
check(
'reloaded MBTA trail prepares geometry without a Cesium render error',
proof.segments > 0 && proof.renderErrors.length === 0,
JSON.stringify(proof),
{
unexercised:
proof.segments === 0 && proof.renderErrors.length === 0,
},
);
} else
check('reload restores observed history', false, restored.error, {
unexercised: restored.unexercised === true,
});
} finally {
await recovery.close();
await page.bringToFront();
}
}
// ---- Deterministic playback and card anchor (synthetic feed) -----------
// The live feeds prove the layer against reality; they cannot prove it
// against a script. For the next two minutes the MBTA response is replaced
// at the network layer by six scripted vehicles near the Boston view —
// straight travel, a corner, a genuine stop, a ninety-second silence, a
// late packet and a feed that only refreshes every thirty seconds — and
// every rendered frame is sampled.
if (runs('playback')) {
console.log('\n== deterministic playback ==');
await page.evaluate((c) => {
const camera = window.__godsEyeView.viewer.camera;
const Cartographic = camera.positionCartographic.constructor;
camera.setView({
destination: Cartographic.toCartesian(
Cartographic.fromDegrees(c.lon, c.lat + 0.005, 2400),
),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, BOSTON);
await page.evaluate(() => {
window.__godsEyeView.dataManager.layers
.get('transit')
.module._setTransitFixtureFloorsForTest(
['straight', 'corner', 'stop', 'gap', 'late', 'dup'].map(
(id) => `mbta:sim-${id}`,
),
20,
);
});
const ORIGIN = { lat: BOSTON.lat, lon: BOSTON.lon };
const M_LAT = 1 / 111_320;
const M_LON = 1 / (111_320 * Math.cos((ORIGIN.lat * Math.PI) / 180));
const at = (eastM, northM) => ({
lat: ORIGIN.lat + northM * M_LAT,
lon: ORIGIN.lon + eastM * M_LON,
});
const simStart = Date.now();
const reportTime = (elapsedS, cadenceS) =>
Math.floor(elapsedS / cadenceS) * cadenceS;
const SIM = {
straight: { speed: 10 },
corner: { speed: 8, turnAt: 60 },
stop: { speed: 8, stopFrom: 30, stopTo: 75 },
gap: { speed: 6, silentFrom: 40, silentTo: 130 },
late: { speed: 8, lateFrom: 50, lateTo: 60 },
dup: { speed: 8, cadence: 30 },
};
const synthetic = (nowMs) => {
const elapsed = (nowMs - simStart) / 1000;
const vehicles = [];
const push = (id, pos, tS) =>
vehicles.push({
id,
lat: pos.lat,
lon: pos.lon,
bearing: null,
speedMps: null,
timestamp: Math.round(simStart / 1000 + tS),
timestampSource: 'vehicle',
routeId: id.toUpperCase(),
tripId: null,
directionId: null,
label: null,
stopId: null,
status: null,
occupancy: null,
});
{
const t = reportTime(elapsed, 10);
push('sim-straight', at(-220, SIM.straight.speed * t), t);
}
{
const t = reportTime(elapsed, 10);
const n = Math.min(t, SIM.corner.turnAt) * SIM.corner.speed;
const e = Math.max(0, t - SIM.corner.turnAt) * SIM.corner.speed;
push('sim-corner', at(110 + e, n), t);
}
{
const t = reportTime(elapsed, 10);
const moved =
t <= SIM.stop.stopFrom
? t
: t <= SIM.stop.stopTo
? SIM.stop.stopFrom
: SIM.stop.stopFrom + (t - SIM.stop.stopTo);
push('sim-stop', at(-110, moved * SIM.stop.speed), t);
}
{
const t0 = reportTime(elapsed, 10);
const t =
t0 >= SIM.gap.silentFrom && t0 < SIM.gap.silentTo
? SIM.gap.silentFrom
: t0;
push('sim-gap', at(220, t * SIM.gap.speed), t);
}
{
const t0 = reportTime(elapsed, 10);
const t =
elapsed >= SIM.late.lateFrom && elapsed < SIM.late.lateTo
? t0 - 30
: t0;
push('sim-late', at(330, t * SIM.late.speed), t);
}
{
const t = reportTime(elapsed, SIM.dup.cadence);
push('sim-dup', at(-330, t * SIM.dup.speed), t);
}
return {
feedId: 'mbta',
name: 'MBTA',
fetchedAt: nowMs,
feedTimestamp: null,
version: '2.0',
entityCount: vehicles.length,
truncated: false,
count: vehicles.length,
vehicles,
};
};
let intercepting = true;
await page.setRequestInterception(true);
const onRequest = (request) => {
if (
intercepting &&
request.url().includes('/api/transit/vehicles/mbta')
) {
const now = Date.now();
request.respond({
status: 200,
contentType: 'application/json',
headers: { 'x-transit-contact': String(now) },
body: JSON.stringify(synthetic(now)),
});
return;
}
request.continue();
};
page.on('request', onRequest);
// Wait for the scripted fleet to replace the live one.
await page.waitForFunction(
() => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
return state._vehicles.has('mbta:sim-straight');
},
{ timeout: 60_000 },
);
const simState = () =>
page.evaluate(() => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
const now = Date.now();
return ['straight', 'corner', 'stop', 'gap', 'late', 'dup']
.map((k) => {
const e = state._vehicles.get(`mbta:sim-${k}`);
if (!e) return `${k}:absent`;
return `${k}:${e.marker ? (e.marker.show === false ? 'hidden' : 'shown') : 'nomarker'}/lag${((now - e.playT) / 1000).toFixed(0)}/${state._moving.has(e) ? 'mv' : 'idle'}/fx${e.fixes.length}/r${e.resets || 0}`;
})
.join(' ');
});
console.log(` scripted fleet before trace: ${await simState()}`);
await reportTransitVisibility(page, 'scripted visibility before trace');
// Sample every rendered frame for two minutes.
const trace = await page.evaluate(
(durationMs) =>
new Promise((resolve) => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const Cartographic =
app.viewer.camera.positionCartographic.constructor;
const keys = [
'sim-straight',
'sim-corner',
'sim-stop',
'sim-gap',
'sim-late',
'sim-dup',
].map((k) => `mbta:${k}`);
const out = Object.fromEntries(keys.map((k) => [k, []]));
const words = Object.fromEntries(keys.map((k) => [k, []]));
const t0 = performance.now();
let lastWordAt = -1e9;
let remove;
const timer = setTimeout(() => {
remove?.();
resolve({ out, words });
}, durationMs + 1000);
const tick = () => {
const now = performance.now();
const wall = Date.now();
for (const key of keys) {
const entry = state._vehicles.get(key);
if (!entry?.marker?.show) continue;
const carto = Cartographic.fromCartesian(entry.marker.position);
out[key].push([
now - t0,
(carto.latitude * 180) / Math.PI,
(carto.longitude * 180) / Math.PI,
entry.playT,
entry.resets || 0,
state._moving.has(entry) ? 1 : 0,
entry.courseDeg,
wall,
entry.track.count,
entry.sample.segmentSpeedMps,
entry.sample.fromSeq,
entry.marker.position.x,
entry.marker.position.y,
entry.marker.position.z,
entry.sample.toSeq,
entry.sample.phase,
entry.clocks.monoNowMs,
]);
}
if (now - lastWordAt > 1_000) {
lastWordAt = now;
for (const contact of layer.getDetectableObjects()) {
if (words[contact.sourceId])
words[contact.sourceId].push([now - t0, contact.metric]);
}
}
if (now - t0 >= durationMs) {
clearTimeout(timer);
remove();
resolve({ out, words });
}
};
remove = app.viewer.scene.postRender.addEventListener(tick);
}),
120_000,
);
console.log(` scripted fleet after trace: ${await simState()}`);
await reportTransitVisibility(page, 'scripted visibility after trace');
const metres = (a, b) =>
Math.hypot(
(b[1] - a[1]) * 111_320,
(b[2] - a[2]) * 111_320 * Math.cos((a[1] * Math.PI) / 180),
);
const judge = (key) => reduceScenario(trace, key);
// Preserve every frame and both bracket ends; never join moving rows across a hold.
const speedEvidence = Object.entries(trace.out).map(([key, rows]) => ({
key,
...reduceScriptedMotion(key, rows),
}));
await writeFile(
`${SHOTS}/${TAG}-scripted-frames.json`,
JSON.stringify(trace),
);
for (const evidence of speedEvidence)
check(
`${evidence.key}: ordinary postRender frames follow the displayed segment within 2%`,
evidence.pass,
JSON.stringify(evidence),
);
const scenarioCheck = (name, conditions, detail) =>
check(
name,
Object.values(conditions).every(Boolean),
`${detail}; conditions=${JSON.stringify(conditions)}; failed=${JSON.stringify(Object.keys(conditions).filter((k) => !conditions[k]))}`,
);
const straight = judge('straight');
scenarioCheck(
'straight travel is played back continuously at its own speed',
{
'straight.rows > 3_000': straight.rows > 3_000,
'straight.total > 300': straight.total > 300,
'straight.moved > straight.rows * 0.4':
straight.moved > straight.rows * 0.4,
'straight.worstJump <= SIM.straight.speed * 1.02 + 0.1':
straight.worstJump <= SIM.straight.speed * 1.02 + 0.1,
'straight.speedWorst >= SIM.straight.speed * 0.98':
straight.speedWorst >= SIM.straight.speed * 0.98,
'straight.speedWorst <= SIM.straight.speed * 1.02':
straight.speedWorst <= SIM.straight.speed * 1.02,
'straight.wallFaster === 0': straight.wallFaster === 0,
'straight.resets === 0': straight.resets === 0,
},
`${straight.rows} frames (${straight.keys}), ${straight.total.toFixed(0)} m drawn, ${straight.moved} moving frames, worst per-frame ${straight.worstJump.toFixed(1)} m/s, peak segment speed ${straight.speedWorst.toFixed(2)} m/s vs ${SIM.straight.speed}, ${straight.wallFaster} frames faster than real time, ${straight.resets} resets`,
);
scenarioCheck(
'the display lag is bounded and never below the feed floor',
{
'straight.lagValid': straight.lagValid,
'straight.lagMin >= 24': straight.lagMin >= 24,
'straight.lagMax <= 215': straight.lagMax <= 215,
},
`lag ${straight.lagMin.toFixed(1)}–${straight.lagMax.toFixed(1)} s`,
);
const corner = judge('corner');
const cornerRows = trace.out['mbta:sim-corner'] || [];
const courseLate = cornerRows
.slice(-300)
.map((r) => r[6])
.filter(Number.isFinite);
const courseEarly = cornerRows
.filter((r) => r[5] === 1)
.slice(0, 300)
.map((r) => r[6])
.filter(Number.isFinite);
const mean = (xs) =>
xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : NaN;
scenarioCheck(
'a corner is turned on the displayed segment: north first, east once the turn is played',
{
'courseEarly.length > 0': courseEarly.length > 0,
'courseLate.length > 0': courseLate.length > 0,
'Math.abs(mean(courseEarly)) < 12': Math.abs(mean(courseEarly)) < 12,
'Math.abs(mean(courseLate) - 90) < 15':
Math.abs(mean(courseLate) - 90) < 15,
'corner.worstJump <= SIM.corner.speed * 1.5 + 2':
corner.worstJump <= SIM.corner.speed * 1.5 + 2,
'corner.resets === 0': corner.resets === 0,
},
`early course ${mean(courseEarly).toFixed(1)}°, late course ${mean(courseLate).toFixed(1)}°, worst per-frame ${corner.worstJump.toFixed(1)} m/s`,
);
const stopWords = (trace.words['mbta:sim-stop'] || []).map((w) => w[1]);
const stopRows = trace.out['mbta:sim-stop'] || [];
const stopStill = stopRows.filter(
(r, i) => i > 0 && metres(stopRows[i - 1], r) < 0.01,
).length;
scenarioCheck(
'a genuine stop reads STOPPED while the marker stands still, then moves on',
{
'stopWords.some((w) => /STOPPED/.test(w))': stopWords.some((w) =>
/STOPPED/.test(w),
),
'stopStill > 600': stopStill > 600,
"judge('stop').total > 200": judge('stop').total > 200,
"judge('stop').resets === 0": judge('stop').resets === 0,
},
`words seen: ${[...new Set(stopWords)].join(' | ')}; ${stopStill} still frames; ${judge('stop').total.toFixed(0)} m drawn`,
);
const gap = judge('gap');
scenarioCheck(
'a ninety-second silence holds at the newest fix — no extrapolation, no reset — and resumes',
{
'gap.resets === 0': gap.resets === 0,
'gap.worstJump <= SIM.gap.speed * 1.5 + 2':
gap.worstJump <= SIM.gap.speed * 1.5 + 2,
'gap.total > 100': gap.total > 100,
},
`${gap.total.toFixed(0)} m drawn, worst per-frame ${gap.worstJump.toFixed(1)} m/s, ${gap.resets} resets`,
);
const late = judge('late');
scenarioCheck(
'a late packet is refused: no backward motion, no reset',
{
'late.backwards === 0': late.backwards === 0,
'late.resets === 0': late.resets === 0,
'late.total > 200': late.total > 200,
},
`${late.backwards} backward frames, ${late.resets} resets, ${late.total.toFixed(0)} m drawn`,
);
const dup = judge('dup');
scenarioCheck(
'a feed that refreshes every thirty seconds plays at the fix speed, not in bursts',
{
'dup.speedWorst <= SIM.dup.speed * 1.06':
dup.speedWorst <= SIM.dup.speed * 1.06,
'dup.worstJump <= SIM.dup.speed * 1.5 + 2':
dup.worstJump <= SIM.dup.speed * 1.5 + 2,
'dup.total > 200': dup.total > 200,
'dup.resets === 0': dup.resets === 0,
},
`peak segment speed ${dup.speedWorst.toFixed(2)} m/s vs ${SIM.dup.speed}, worst per-frame ${dup.worstJump.toFixed(1)} m/s, ${dup.total.toFixed(0)} m drawn`,
);
// ---- Card anchor synchronisation ---------------------------------------
console.log('\n== card anchor ==');
await page.evaluate(() => {
const app = window.__godsEyeView,
camera = app.viewer.camera;
const entry = app.dataManager.layers
.get('transit')
.module._transitStateForTest()
._vehicles.get('mbta:sim-straight');
if (!entry) return;
const C = camera.positionCartographic.constructor;
const at = C.fromCartesian(entry.marker.position);
at.height = 600;
camera.setView({
destination: C.toCartesian(at),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
});
await wait(1500);
await reportTransitVisibility(page, 'scripted visibility before click');
const target = await page.evaluate(() => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
const entry = state._vehicles.get('mbta:sim-straight');
if (!entry?.marker?.show) return null;
const scene = app.viewer.scene;
const canvas = scene.canvas;
const view = scene.camera.viewMatrix;
const proj = scene.camera.frustum.projectionMatrix;
const mul = (m, v) => [
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14] * v[3],
m[3] * v[0] + m[7] * v[1] + m[11] * v[2] + m[15] * v[3],
];
const p = entry.marker.position;
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
const x = ((clip[0] / clip[3]) * 0.5 + 0.5) * canvas.clientWidth;
const y = (1 - ((clip[1] / clip[3]) * 0.5 + 0.5)) * canvas.clientHeight;
if (
!(clip[3] > 0) ||
x < 0 ||
y < 0 ||
x >= canvas.clientWidth ||
y >= canvas.clientHeight
)
return null;
const picked = scene.pick({ x, y });
return {
entityCount: app.viewer.entities.values.length,
key: entry.key,
x,
y,
pickKey: picked?.id ?? picked?.primitive?.id ?? null,
};
});
let anchor = null;
if (target) {
check(
'the scripted sprite owns its screen-space pick',
target.pickKey === target.key,
JSON.stringify(target),
);
await page.mouse.click(target.x, target.y);
await wait(600);
anchor = await page.evaluate(
async (key, durationMs) => {
const overlay = await import('/src/overlays/worldOverlay.js');
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
if (state._selectedKey !== key)
return {
selected: false,
selection: {
expected: key,
actual: state._selectedKey,
handler: !!state._clickHandler,
lastPick: state._lastPickForTest ?? null,
pointerOwner: (
await import('/src/data/inputOwnership.js')
).pointerOwner(),
},
};
const scene = app.viewer.scene;
const canvas = scene.canvas;
const mul = (m, v) => [
m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12] * v[3],
m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13] * v[3],
m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14] * v[3],
m[3] * v[0] + m[7] * v[1] + m[11] * v[2] + m[15] * v[3],
];
const frames = [];
const publicationsBefore = state._cardPublications || 0;
let headSamples = 0,
headErrorPx = 0,
bodyMutation = 0,
previousTrail = null;
const entityCount = app.viewer.entities.values.length;
return new Promise((resolve) => {
const t0 = performance.now();
const tick = () => {
const entry = state._vehicles.get(key);
const rect = overlay.getOverlayPaintRect('transit-selected', key);
if (entry?.marker) {
const view = scene.camera.viewMatrix;
const proj = scene.camera.frustum.projectionMatrix;
const p = entry.marker.position;
const clip = mul(proj, mul(view, [p.x, p.y, p.z, 1]));
frames.push({
t: performance.now() - t0,
mx: ((clip[0] / clip[3]) * 0.5 + 0.5) * canvas.clientWidth,
my:
(1 - ((clip[1] / clip[3]) * 0.5 + 0.5)) *
canvas.clientHeight,
markerVisible: entry.marker.show !== false,
worldFinite: [p.x, p.y, p.z].every(Number.isFinite),
cardPresent: !!rect,
surfaceReady: entry.surfaceReady,
ax: rect?.anchorX ?? null,
ay: rect?.anchorY ?? null,
poll: entry.pollSeq,
moving: state._moving.has(entry) ? 1 : 0,
});
}
const trail = layer._transitPartsForTest().trails.diagnostics();
if (trail && entry?.marker) {
if (
previousTrail &&
previousTrail.revision === trail.revision &&
previousTrail.body !== trail.body
)
bodyMutation++;
previousTrail = trail;
if (trail.head.show && trail.head.positions.length) {
const project = (p) => {
const clip = mul(
scene.camera.frustum.projectionMatrix,
mul(scene.camera.viewMatrix, [p.x, p.y, p.z, 1]),
);
return [
((clip[0] / clip[3]) * canvas.clientWidth) / 2,
((clip[1] / clip[3]) * canvas.clientHeight) / 2,
];
};
const h = project(trail.head.positions.at(-1)),
m = project(entry.marker.position);
headErrorPx = Math.max(
headErrorPx,
Math.hypot(h[0] - m[0], h[1] - m[1]),
);
headSamples++;
}
}
if (performance.now() - t0 >= durationMs) finish();
};
let remove;
const finish = () => {
clearTimeout(timer);
remove?.();
resolve({
selected: true,
frames,
textPublishes:
(state._cardPublications || 0) - publicationsBefore,
headSamples,
headErrorPx,
bodyMutation,
entityCount,
});
};
const timer = setTimeout(finish, durationMs + 1000);
remove = scene.postRender.addEventListener(tick);
});
},
target.key,
20_000,
);
}
check(
'selected trail head stays within 1 CSS px of the marker, without entity additions or body rebuilds between revisions',
reduceTrailHead(anchor, target).pass,
JSON.stringify(reduceTrailHead(anchor, target)),
);
if (anchor?.selected) {
await writeFile(
`${SHOTS}/${TAG}-anchor-frames.json`,
JSON.stringify(anchor.frames),
);
const evidence = reduceAnchor(anchor.frames);
check(
'actual painted host anchor stays within 1 CSS px of the marker',
evidence.pass,
JSON.stringify(evidence),
);
check(
'card text publication stays throttled while the anchor runs at frame rate',
anchor.textPublishes > 0 && anchor.textPublishes <= 20_000 / 250 + 3,
`${anchor.textPublishes} text publications in 20 s`,
);
} else {
check(
'the card anchor moves in lockstep with the sprite on every frame, across polls and a settle',
false,
target
? 'the click did not select the scripted vehicle'
: 'no scripted vehicle on screen',
{ unexercised: true },
);
}
// Removal: the scripted vehicle leaves the feed; after two missed polls its
// card must go with it.
intercepting = false;
await page.evaluate(() =>
window.__godsEyeView.dataManager.layers
.get('transit')
.module._setTransitFixtureFloorsForTest([], null),
);
page.off('request', onRequest);
await page.setRequestInterception(false);
await page
.waitForFunction(
() => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
return !state._vehicles.has('mbta:sim-straight');
},
{ timeout: 60_000 },
)
.catch(() => {});
const afterRemoval = await page.evaluate(() => {
const state = window.__godsEyeView.dataManager.layers
.get('transit')
.module._transitStateForTest();
return {
gone: !state._vehicles.has('mbta:sim-straight'),
selected: state._selectedKey,
};
});
check(
'a removed vehicle takes its card with it',
afterRemoval.gone && afterRemoval.selected !== 'mbta:sim-straight',
JSON.stringify(afterRemoval),
);
await page.evaluate((c) => {
const camera = window.__godsEyeView.viewer.camera;
const Cartographic = camera.positionCartographic.constructor;
camera.setView({
destination: Cartographic.toCartesian(
Cartographic.fromDegrees(c.lon, c.lat, 600),
),
orientation: {
heading: (c.heading * Math.PI) / 180,
pitch: (-45 * Math.PI) / 180,
roll: 0,
},
});
}, CITIES.at(-1));
await wait(4_000);
}
const sceneQA = {
page,
check,
wait,
shots: SHOTS,
tag: TAG,
detectOn,
selectStyle,
sampleRendered,
};
if (runs('budgets')) await runFleetBudgets(sceneQA);
if (runs('matrix')) await runBostonMatrix(sceneQA);
if (runs('boston-live')) await runBostonLive(sceneQA);
// Turning it off must stop the traffic and leave nothing behind.
console.log('\n== disable ==');
await page.$eval(row, (element) =>
element.scrollIntoView({ block: 'center' }),
);
await page.click(`${row} .data-toggle-btn`);
await page.waitForFunction(
() => !window.__godsEyeView.dataManager.isEnabled('transit'),
{ timeout: 20_000 },
);
const afterDisable = transitRequests.length;
const teardown = await page.evaluate(() => {
const layer = window.__godsEyeView.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
return {
vehicles: state._vehicles.size,
primitives:
(state._markers?.length || 0) + (state._animatedMarkers?.length || 0),
visible: state._visible.size,
moving: state._moving.size,
detect: state._detectCache,
timers: [
state._visibilityTimer,
state._maintenanceTimer,
state._floorTimer,
state._cameraDebounceTimer,
],
trailPending: layer._transitPartsForTest().trails.requestDiagnostics()
.pending,
shown: state._markers ? state._markers.show : null,
inFlight: state._inFlight.size,
activeFeeds: state._activeFeeds.size,
holds: window.__godsEyeView.getRenderGovernorDiagnostics().holds,
};
});
check(
'disable leaves no vehicles and no point primitives',
teardown.vehicles === 0 &&
teardown.primitives === 0 &&
teardown.visible === 0 &&
teardown.moving === 0 &&
teardown.detect === null &&
teardown.timers.every((t) => t === null) &&
!teardown.trailPending &&
!teardown.holds.includes('transit'),
JSON.stringify(teardown),
);
await shot(
`${TAG}-disabled.jpg`,
'Transit off — same view, nothing rendered',
);
await wait(25_000);
check(
'no further transit requests after disable',
transitRequests.length === afterDisable,
`${transitRequests.length - afterDisable} request(s) in 25 s`,
);
// ---- Context modes -------------------------------------------------------
console.log('\n== context modes ==');
// The CONTEXT panel starts collapsed; a user expands it before choosing a mode.
await page.$eval(
'[data-collapse-target="global-context-panel"]',
(button) => {
if (button.getAttribute('aria-expanded') !== 'true') button.click();
},
);
await wait(1_200);
check(
'the CONTEXT panel opens from its own header button',
await page.$eval(
'[data-collapse-target="global-context-panel"]',
(b) => b.getAttribute('aria-expanded') === 'true',
),
);
const isOn = () =>
page.evaluate(() => window.__godsEyeView.dataManager.isEnabled('transit'));
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('transit', true, {
origin: 'user',
}),
);
await wait(6_000);
check('transit is on before entering a Context mode', await isOn());
// CONTACTS
await page.click('#global-context-flights-btn');
await wait(18_000);
const inContacts = {
transit: await isOn(),
vehicles: await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers.get('transit').module.getStats()
.count,
),
};
// Contacts is not a passive overlay: it puts every non-dependency layer away
// for the duration, Transit included, and gives it back on exit.
check(
'Contacts puts Transit away for the duration',
inContacts.transit === false && inContacts.vehicles === 0,
JSON.stringify(inContacts),
);
await page.click('#global-context-flights-btn');
await wait(12_000);
check('leaving Contacts restores Transit', await isOn());
// SPACE MISSIONS
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('transit', false, {
origin: 'user',
}),
);
await wait(2_000);
await page.click('#global-context-missions-btn');
await wait(20_000);
const blocked = await page.evaluate(async () => {
const settled = await window.__godsEyeView.dataManager.setEnabled(
'transit',
true,
{ origin: 'user' },
);
return {
settled,
enabled: window.__godsEyeView.dataManager.isEnabled('transit'),
};
});
check(
'Space Missions refuses to enable Transit while it isolates replay data',
blocked.enabled === false,
JSON.stringify(blocked),
);
await page.click('#global-context-missions-btn');
await wait(12_000);
check(
'leaving Space Missions leaves Transit off, as it was on entry',
(await isOn()) === false,
);
check(
'no uncaught page errors',
pageErrors.length === 0,
pageErrors.join(' | '),
);
console.log(
`\ntotal /api/transit requests this run: ${transitRequests.length}`,
);
check(
'scripted selections never raise a Cesium render error',
(await page.evaluate(() => window.__transitRenderErrors || [])).length ===
0,
JSON.stringify(
await page.evaluate(() => window.__transitRenderErrors || []),
),
);
} catch (error) {
check(
'browser harness completed within its bounded waits',
false,
error.stack || error.message,
);
} finally {
await browser.close();
}
console.log(
failures
? `HARNESS: FAIL (${failures} failed, ${unexercised} unexercised)`
: `HARNESS: PASS (${unexercised} unexercised)`,
);
process.exit(failures || unexercised ? 1 : 0);