Files
gods-eye-view/scripts/qa-transit-browser.mjs
T
Bilawal Sidhu 0bb61f53c6 fix(transit): keep the real silhouettes under sensor presets, make the selected trail visible, put Transit in Movement (#596)
## Changes

Follow-up to #587 after the owner's first look at the merged transit layer on Google 3D tiles.

- **Sensor presets keep the real silhouettes.** Under NVG, thermal (white-hot, black-hot, Ironbow) and noir, vehicles are the same silhouettes as in normal view, filled opaque white with a 2 px dark halo, at the CRT display sizes (26 px fleet, 39 px selected). The solid rounded "signature" bodies are gone.
- **The selected vehicle's trail is visible.** The retained history is now draped onto 3D tiles and terrain (`GroundPolylinePrimitive`, `ClassificationType.BOTH`, the way the traffic heat-lines are drawn), 3 px core over a 5 px dark backing, per-segment age colour, a short draped head clipped to the sprite. Two defects kept it off screen before: Cesium's per-instance attribute getters return copies, so the renderer's show/colour edits were discarded, and a marker did not recover after the camera jumped to it, which hid the trail with it. Trail geometry is now prepared on selection independent of floor resolution and marker visibility; sparse long intervals (rail) keep a bounded marker corridor instead of dropping the interval; classification survives map-source changes.
- **Transit sits in the Movement panel group** between Street Traffic and Bike Share (the panel orders by `PANEL_GROUPS`, not by catalog order; the catalog is moved too).

## How it was verified

- `npm run format:check` (854 files), `npm run check:boundaries`, `npm test` (4,062 passing), `npm run build` clean; tracking regression 109/109 in a real browser on Google 3D tiles.
- Browser harness (`scripts/qa-transit.mjs`), keyed on Google 3D tiles and keyless: keyed 194 pass, with the trail pixel check passing at 300, 600 and 900 m; the remaining keyed failures are the per-frame allocation gate already recorded as a follow-up from #587 (CPU and frame time within budget), four readability lines at threshold edges, and the card-anchor check now counting frames where the selected marker is legitimately off screen; keyless 193 pass with the remaining misses being threshold edges on the thin tram silhouette (centre 0.83–0.88 against 0.85) and one bus ring 0.28 against 0.25 on a pure-white roof.
- New harness checks: `Google 3D selected trail visible at 6 of 8 projected samples` (repeated off/on framebuffer captures at 300/600/900 m, samples outside the sprite footprint, a background-change negative control; fails on main, passes here), a camera-arrival check (select a moving vehicle, `setView` onto it, marker shown and trail body drawn within 2 s), `PANEL_ORDER` pinned, raster tests comparing every sensor raster's mask and aspect ratio to the normal silhouette.
- Direct look on a keyed build: a live CapMetro bus at 30 m/s selected through the layer API draws its draped trail along the highway lane (harness capture `keyed-600m-trail-visible.jpg`).
- Adversarial review by Codex Astra before the last two fix passes (stack-switch classification, marker corridor under the vertex cap, pixel-gate loophole), all three fixed with regressions.

Docs: `docs/CURRENT-STATE.md` and `CHANGELOG.md` updated in this PR. No data-source changes.
2026-09-15 13:26:41 -05:00

666 lines
26 KiB
JavaScript

/** Bound page promises as well as CDP itself; labels identify a stalled section. */
export function boundPageEvaluations(page, defaultTimeoutMs = 30000) {
const evaluate = page.evaluate.bind(page);
page.evaluate = (fn, ...args) => {
// The two long trace collectors pass their duration explicitly.
const duration =
args.find((v) => typeof v === 'number' && v >= 6000 && v <= 120000) || 0;
const timeoutMs = Math.max(
defaultTimeoutMs,
duration ? duration + 5000 : 0,
);
const label = String(fn).slice(0, 120).replace(/\s+/g, ' ');
return evaluate(
async (source, values, timeoutMs, label) => {
let timer;
try {
return await Promise.race([
// Puppeteer callbacks are standalone functions, just as in evaluate.
Promise.resolve().then(() => (0, eval)(`(${source})`)(...values)),
new Promise((_, reject) => {
timer = setTimeout(
() =>
reject(
new Error(
`Transit QA page wait timed out after ${timeoutMs} ms: ${label}`,
),
),
timeoutMs,
);
}),
]);
} finally {
clearTimeout(timer);
}
},
String(fn),
args,
timeoutMs,
label,
);
};
}
export async function reportTransitVisibility(page, label) {
const result = await page.evaluate(() => {
const app = window.__godsEyeView;
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
return {
total: state._vehicles.size,
visible: state._visible.size,
moving: state._moving.size,
held: state._renderHeld,
requestRenderMode: app.viewer.scene.requestRenderMode,
renderLoop: app.viewer.useDefaultRenderLoop,
renderErrors: window.__transitRenderErrors || [],
bounds: state._viewBounds,
hidden: layer._transitVisibilityForTest().filter((e) => !e.shown),
};
});
console.log(` ${label}: ${JSON.stringify(result)}`);
if (result.renderLoop === false || result.renderErrors.length)
throw new Error(
`${label}: Cesium render loop stopped or raised an error: ${JSON.stringify(result.renderErrors)}`,
);
return result;
}
/** Intersect primed histories with the new snapshot; disappearance is availability. */
export async function restoreRetainedTransit(retained) {
if (!retained.length)
return {
error: 'No multi-fix history observed during the probe',
unexercised: true,
};
const app = window.__godsEyeView;
await app.dataManager.setEnabled('transit', true, { source: 'qa' });
const layer = app.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
// enable() schedules proximity discovery; an empty pre-discovery map is not
// evidence that every primed vehicle departed. Wait for this feed's poll.
const deadline = Date.now() + 12000;
while (state._activeFeeds && !state._vehicles.size && Date.now() < deadline)
await new Promise((r) => setTimeout(r, 100));
await layer.update();
if (
retained[0]?.primedAt &&
Math.floor(Date.now() / 60000) !== Math.floor(retained[0].primedAt / 60000)
)
return {
error: 'Reload crossed the priming minute; same-minute proof unavailable',
unexercised: true,
};
const history = retained.find((h) =>
state._vehicles.has(`${h.feedId || 'mbta'}:${h.vehicleId}`),
);
if (!history)
return {
error: `All ${retained.length} primed vehicles left the current snapshot`,
unexercised: true,
};
const entry = state._vehicles.get(
`${history.feedId || 'mbta'}:${history.vehicleId}`,
);
const camera = app.viewer.camera,
C = camera.positionCartographic.constructor;
camera.setView({
destination: C.toCartesian(
C.fromDegrees(entry.record.lon, entry.record.lat, 600),
),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
layer._transitPartsForTest().selection.selectVehicle(entry.key);
return { key: entry.key, oldest: history.oldestT };
}
export async function sampleTransitPixels(page, palette) {
return page.evaluate(async (palette) => {
const app = window.__godsEyeView;
const state = app.dataManager.layers
.get('transit')
.module._transitStateForTest();
// Decode the actual billboard rasters before the paint being judged.
const overlay = window.location
? await import('/src/overlays/worldOverlay.js')
: null;
const rasters = new Map();
await Promise.all(
[
...new Set(
[...state._vehicles.values()]
.map((e) => e.marker?.image)
.filter(Boolean),
),
].map(async (url) => {
const img = new Image();
img.src = url;
await img.decode();
const c = document.createElement('canvas');
c.width = img.width;
c.height = img.height;
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0);
rasters.set(url, {
width: c.width,
height: c.height,
data: ctx.getImageData(0, 0, c.width, c.height).data,
});
}),
);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
remove();
reject(
new Error('Transit pixel readback: no postRender within 5000 ms'),
);
}, 5000);
const remove =
window.__godsEyeView.viewer.scene.postRender.addEventListener(() => {
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;
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 sprites = [];
const footprints = [];
const intersects = (a, b) =>
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y;
const plates =
app.styleManager
?.getDetectionDiagnostics?.()
?.calloutRects?.filter((r) => r.alpha > 0.01) || [];
if (state._selectedKey) {
const card = overlay?.getOverlayPaintRect(
'transit-selected',
state._selectedKey,
);
if (card) plates.push(card);
}
const panels = (overlay?.WORLD_OVERLAY_OCCLUDER_SELECTORS || [])
.flatMap((selector) => [...document.querySelectorAll(selector)])
.filter(
(el) =>
!el.hidden &&
el.checkVisibility({
checkOpacity: true,
checkVisibilityCSS: true,
}),
)
.map((el) => {
const r = el.getBoundingClientRect();
return { x: r.left, y: r.top, w: r.width, h: r.height };
});
const skipped = [];
const style = state._stylePreset;
const uniforms = app.styleManager?.stages?.[style]?.uniforms || {};
const intensity = uniforms.intensity ?? 1;
const pixelation =
uniforms.pixelation ?? (style === 'surveillance' ? 2.5 : 1.5);
const sensor = style === 'surveillance' || style === 'thermal';
// Invert NVG's barrel transform; thermal only quantizes the source.
const screenPoint = (sx, sy) => {
let x = sx,
y = sy;
if (style === 'surveillance') {
const tx = (sx / width) * 2 - 1,
ty = (sy / height) * 2 - 1;
let cx = tx,
cy = ty;
for (let i = 0; i < 16; i++) {
const r2 = cx * cx + cy * cy;
const d =
1 + r2 * intensity * 0.25 + r2 * r2 * intensity * 0.075;
cx = tx / d;
cy = ty / d;
}
x = ((cx + 1) * width) / 2;
y = ((cy + 1) * height) / 2;
}
const offset = sensor
? ((1 + (pixelation - 1) * intensity) * intensity) /
2 /
(canvas.width / width)
: 0;
return { x: x + offset, y: y - offset };
};
const uiBlocked = (x, y) => {
const el = document.elementFromPoint?.(x, y);
return (
el &&
el !== canvas &&
!el.closest?.(
'#world-overlay-root, #world-overlay-detection-surface',
) &&
el.tagName !== 'BODY' &&
el.tagName !== 'HTML'
);
};
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 screen = entry.marker.computeScreenSpacePosition?.(scene) || {
x: ((clip[0] / clip[3]) * 0.5 + 0.5) * width,
y: (1 - ((clip[1] / clip[3]) * 0.5 + 0.5)) * height,
};
const rect = entry.marker.constructor.getScreenSpaceBoundingBox?.(
entry.marker,
screen,
) || { x: screen.x - 16, y: screen.y - 16, width: 32, height: 32 };
const { x, y } = screenPoint(screen.x, screen.y);
const c = Math.abs(Math.cos(entry.marker.rotation || 0)),
sn = Math.abs(Math.sin(entry.marker.rotation || 0));
const w = rect.width * c + rect.height * sn,
h = rect.height * c + rect.width * sn;
const footprint = { x: x - w / 2, y: y - h / 2, w, h };
footprints.push({ key: entry.key, ...footprint });
let reason = null;
if (
rect.x < 0 ||
rect.y < 0 ||
rect.x + rect.width > width ||
rect.y + rect.height > height
)
reason = 'billboard rectangle outside frame';
if (
entry.marker.color?.alpha < 0.95 ||
entry.markerCollection?.show === false
)
reason = 'billboard not opaque/collection hidden';
const raster = rasters.get(entry.marker.image);
if (!raster) reason = 'billboard alpha unavailable';
else if (
raster.data[
(Math.floor(raster.height / 2) * raster.width +
Math.floor(raster.width / 2)) *
4 +
3
] < 242
)
reason = 'billboard centre is transparent';
if (
sensor &&
Math.hypot(
(((x / width) * 2 - 1) * width) / height,
(y / height) * 2 - 1,
) > 0.6
)
reason = 'outside unmasked sensor lens';
if (style === 'noir') {
// Noir deliberately darkens the frame edges. Judge the core in
// its unmasked field, independently of the measured brightness.
const u = x / width,
v = y / height;
const transmission = Math.pow(
16 * u * (1 - u) * v * (1 - v),
0.3 + 0.4 * (uniforms.vignetteAmt ?? 0.5) * intensity,
);
if (1 - intensity + transmission * intensity < 0.9)
reason = 'outside unmasked noir field';
}
if (
[
[x, y],
[rect.x, rect.y],
[rect.x + rect.width, rect.y + rect.height],
[rect.x + rect.width, rect.y],
[rect.x, rect.y + rect.height],
].some(([px, py]) => uiBlocked(px, py))
)
reason = 'UI panel overlaps billboard';
if (panels.some((r) => intersects(footprint, r)))
reason = 'UI panel rectangle overlaps billboard';
if (plates.some((r) => intersects(footprint, r)))
reason = 'DETECT label/card rectangle overlaps billboard';
if (reason) {
skipped.push({ key: entry.key, reason });
continue;
}
if (x < 40 || y < 40 || x > width - 40 || y > height - 40) continue;
sprites.push({
key: entry.key,
mode: entry.mode,
x,
y,
selected: state._selectedKey === entry.key,
screenX: screen.x,
screenY: screen.y,
rect,
footprint,
bracketX: entry.detectContact?._candidateScreenX ?? screen.x,
bracketY: entry.detectContact?._candidateScreenY ?? screen.y,
raster,
rotation: entry.marker.rotation || 0,
halfW: Number.isFinite(entry.marker.width)
? Math.ceil(entry.marker.width / 2) + 2
: 11,
halfH: Number.isFinite(entry.marker.height)
? Math.ceil(entry.marker.height / 2) + 2
: 7,
});
}
const isolated = sprites.filter(
(a) =>
!a.selected &&
footprints.every(
(b) => b.key === a.key || !intersects(a.footprint, b),
),
);
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
const scale = canvas.width / canvas.clientWidth;
const luma = (r, g, b) => (0.299 * r + 0.587 * g + 0.114 * b) / 255;
const readLuma = (cx, cy) => {
const px = Math.floor(cx * scale);
const py = canvas.height - 1 - Math.floor(cy * scale);
const buf = new Uint8Array(4);
gl.readPixels(px, py, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf);
return luma(buf[0], buf[1], buf[2]);
};
const surface = document.getElementById('world-overlay-canvas');
const ctx = surface?.getContext('2d');
// A hidden surface has no client size; nothing to read there.
const sscale =
surface && surface.clientWidth > 0
? surface.width / surface.clientWidth
: 0;
const out = [];
for (const s of isolated) {
if (out.length >= 12) break;
// The normal canvas includes DETECT plates and selected cards.
// A plate over the body invalidates this contact, even if WebGL below is hot.
if (ctx && sscale > 0) {
const body = ctx.getImageData(
Math.floor((s.x - 3) * sscale),
Math.floor((s.y - 3) * sscale),
Math.max(1, Math.ceil(6 * sscale)),
Math.max(1, Math.ceil(6 * sscale)),
).data;
if (body.some((v, i) => i % 4 === 3 && v > 0)) {
skipped.push({
key: s.key,
reason: 'DETECT label/card overlaps core',
});
continue;
}
}
// Pick nine points from a solid patch of the source silhouette,
// before examining framebuffer brightness. Avoid panels/window gaps.
let corePoints = null,
widestBand = -1;
const cosCore = Math.cos(s.rotation),
sinCore = Math.sin(s.rotation);
// A tram's parallel sides have equally wide bands. Prefer its upper
// body band over the geometric centre when those widths tie.
const offsets =
s.mode === 'tram'
? [-0.2, 0.2, -0.1, 0.1, 0]
: [0, -0.1, 0.1, -0.2, 0.2];
for (const offset of offsets) {
const candidates = [];
let solid = true,
bandWidth = Infinity;
// Rank source geometry, never the observed framebuffer brightness.
// A thin tram needs the widest opaque white band, with room for
// the pixel filter on each side of the sampled patch.
for (let dy = -1; dy <= 1; dy++) {
const ry = Math.floor(
((dy + offset * s.rect.height) / s.rect.height + 0.5) *
s.raster.height,
);
const middle = Math.floor(s.raster.width / 2);
const white = (x) =>
x >= 0 &&
x < s.raster.width &&
[0, 1, 2, 3].every(
(c) =>
s.raster.data[(ry * s.raster.width + x) * 4 + c] >= 240,
);
let left = middle,
right = middle;
while (white(left - 1)) left--;
while (white(right + 1)) right++;
bandWidth = Math.min(
bandWidth,
white(middle) ? right - left + 1 : 0,
);
}
for (let dx = -1; dx <= 1; dx++)
for (let dy = -1; dy <= 1; dy++) {
const localY = dy + offset * s.rect.height;
const rx = Math.floor(
(dx / s.rect.width + 0.5) * s.raster.width,
);
const ry = Math.floor(
(localY / s.rect.height + 0.5) * s.raster.height,
);
const i = (ry * s.raster.width + rx) * 4;
if ([0, 1, 2, 3].some((c) => s.raster.data[i + c] < 240))
solid = false;
candidates.push(
screenPoint(
s.screenX + dx * cosCore + localY * sinCore,
s.screenY - dx * sinCore + localY * cosCore,
),
);
}
if (solid && bandWidth > widestBand) {
corePoints = candidates;
widestBand = bandWidth;
}
}
if (!corePoints) {
skipped.push({
key: s.key,
reason: 'no solid silhouette interior for nine samples',
});
continue;
}
// Picking is independent of rendered brightness: an overlapping
// label or sprite must not masquerade as a dim sensor signature.
// Check the whole sampled core, not just its centre pick.
let coreOwned = true;
if (scene.pick) {
const framebuffer = gl.getParameter?.(gl.FRAMEBUFFER_BINDING);
for (const core of corePoints) {
let u = core.x / width,
v = 1 - core.y / height;
if (style === 'surveillance') {
const x = u * 2 - 1,
y = v * 2 - 1,
r2 = x * x + y * y;
const d =
1 + r2 * intensity * 0.25 + r2 * r2 * intensity * 0.075;
u = (x * d + 1) / 2;
v = (y * d + 1) / 2;
}
if (sensor) {
const grid = 1 + (pixelation - 1) * intensity;
u +=
((Math.floor((u * canvas.width) / grid) * grid) /
canvas.width -
u) *
intensity;
v +=
((Math.floor((v * canvas.height) / grid) * grid) /
canvas.height -
v) *
intensity;
}
const picked = scene.pick(
{ x: u * width, y: (1 - v) * height },
1,
1,
);
if (picked?.id !== s.key && picked?.primitive?.id !== s.key)
coreOwned = false;
}
if (gl.bindFramebuffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
}
if (!coreOwned) {
skipped.push({
key: s.key,
reason:
'sampled core is partially obscured or outside the sprite',
});
continue;
}
let centre = 0;
for (const core of corePoints)
centre += readLuma(core.x, core.y) / corePoints.length;
let ringMin = 1;
let ringMax = 0;
// Sample the actual rotated raster halo, not a fixed-radius circle
// that can hit the hot body or terrain after a display-size change.
const raster = s.raster;
const cos = Math.cos(s.rotation),
sin = Math.sin(s.rotation);
for (let ry = 1; ry < raster.height - 1; ry++)
for (let rx = 1; rx < raster.width - 1; rx++) {
const i = (ry * raster.width + rx) * 4;
if (
raster.data[i + 3] < 200 ||
raster.data[i] > 30 ||
raster.data[i + 1] > 30 ||
raster.data[i + 2] > 30
)
continue;
const dx = ((rx + 0.5) / raster.width - 0.5) * s.rect.width;
const dy = ((ry + 0.5) / raster.height - 0.5) * s.rect.height;
const q = screenPoint(
s.screenX + dx * cos + dy * sin,
s.screenY - dx * sin + dy * cos,
);
const v = readLuma(q.x, q.y);
ringMin = Math.min(ringMin, v);
ringMax = Math.max(ringMax, v);
}
let bracket = null;
if (ctx && sscale > 0) {
// Transit brackets follow the padded sprite extent and snapped
// stroke centres. Sample the middle of each straight segment,
// never a colour histogram containing antialiased edges/callouts.
const cx = Math.floor(s.bracketX) + 0.5,
cy = Math.floor(s.bracketY) + 0.5;
const hw = s.halfW,
hh = s.halfH,
mid = Math.max(4, Math.floor(Math.min(hw, hh) * 0.55)) / 2;
const points = [
[cx - hw + mid, cy - hh],
[cx + hw - mid, cy - hh],
[cx - hw + mid, cy + hh],
[cx + hw - mid, cy + hh],
[cx - hw, cy - hh + mid],
[cx + hw, cy - hh + mid],
[cx - hw, cy + hh - mid],
[cx + hw, cy + hh - mid],
];
let bestAlpha = 0;
for (const [index, [x, y]] of points.entries()) {
if (
uiBlocked(x, y) ||
plates.some((r) =>
intersects({ x: x - 1, y: y - 1, w: 2, h: 2 }, r),
)
)
continue;
// A label plate fills both sides of a stroke. Require empty
// pixels beyond its dark backing on the perpendicular axis.
const normal = index < 4 ? [0, 3] : [3, 0];
if (
[-1, 1].some(
(sign) =>
ctx.getImageData(
Math.floor((x + sign * normal[0]) * sscale),
Math.floor((y + sign * normal[1]) * sscale),
1,
1,
).data[3] > 0,
)
)
continue;
const data = ctx.getImageData(
Math.floor(x * sscale),
Math.floor(y * sscale),
1,
1,
).data;
if (data[3] > bestAlpha) {
bestAlpha = data[3];
bracket = [data[0], data[1], data[2]];
}
}
}
out.push({
key: s.key,
mode: s.mode,
verified: true,
rectangle: s.rect,
centre,
background:
[
readLuma(s.x + 25, s.y),
readLuma(s.x - 25, s.y),
readLuma(s.x, s.y + 25),
readLuma(s.x, s.y - 25),
].reduce((a, b) => a + b, 0) / 4,
ringMin,
ringMax,
bracket,
bracketTier: `transit_${s.mode}`,
bracketExpected: palette[s.mode]
? [1, 3, 5].map((i) =>
parseInt(palette[s.mode].slice(i, i + 2), 16),
)
: null,
x: s.x,
y: s.y,
});
}
clearTimeout(timer);
remove();
console.log(
`TRANSIT_SAMPLER ${style}: ${out.length} verified; skipped=${JSON.stringify(skipped)}`,
);
resolve(out);
});
window.__godsEyeView.viewer.scene.requestRender();
});
}, palette);
}
/** A fleet-to-fleet comparison must not inherit the city's selected marker. */
export function readTransitFleetStyle({ key, resetSelection = false } = {}) {
const layer = window.__godsEyeView.dataManager.layers.get('transit').module;
const state = layer._transitStateForTest();
if (resetSelection) layer._transitPartsForTest().selection.clearSelection();
const entry = key
? state._vehicles.get(key)
: [...state._vehicles.values()].find((e) => e.marker?.show);
return entry
? {
key: entry.key,
selected: state._selectedKey === entry.key,
color: entry.marker.color.toCssColorString(),
width: entry.marker.width,
image: entry.marker.image,
}
: null;
}