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.
This commit is contained in:
Bilawal Sidhu authored and GitHub committed 2026-09-15 13:26:41 -05:00
1 parent cd22ca3559
commit 0bb61f53c6
24 files changed
+1945 -205

No files matched your search

+71 -16
View File
@@ -2450,9 +2450,9 @@ its criteria cannot be silently ignored.
| Traffic | OSM Overpass (+ optional TomTom live flow) | `src/data/traffic.js` | `/api/overpass` + `/api/tomtom` | viewport-driven |
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
| Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh |
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
| Bikeshare 🚲 | GBFS (Lyft + BCycle) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change |
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
| Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static |
| Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static |
@@ -2540,7 +2540,9 @@ What a card needs is a resolved ground anchor: a step whose ground cell has not
answered yet opens nothing rather than a card hanging at sea level. The cells
are re-read for up to 90 seconds, which outlasts a cold terrain round trip;
reroute, CLEAR and disable cancel that.
Transit is off by default, and share links use token `j`. The layer polls
Transit is off by default, and share links use token `j`. The movement panel order
places Transit directly after Street Traffic and before Bike Share. Cameras and
utilities retain their own groups. The layer polls
registered feeds near the camera every 15 seconds, below a 3,000 km altitude
gate. Seven keyless regions are available: Boston, Austin, Minneapolis–St Paul,
Helsinki, the Netherlands, Norway and South East Queensland. Mode silhouettes
@@ -2565,10 +2567,19 @@ clipped to the displayed time. The browser retains up to 15 minutes and 128
fixes per vehicle, under a global 32 MiB history budget. Capacity pressure may
shorten that history. Only the selected vehicle has a trail: completed segments
are batched primitives, and a small moving head ends at the marker. Trail
geometry is bounded to 2,048 prepared vertices; history changes rebuild the
body, while ordinary playback updates the head and segment visibility.
Both normal and depth-fail instance colours follow the age ramp once per display
second or style change. Subdivision crossings update only changed visibility
geometry is bounded to 640 prepared vertices, prioritizing the active corridor
and shortening the oldest geometry first. Active and future marker corridors
retain their own 640-vertex budget; long report intervals use coarser subdivisions
than 25 m instead of dropping an admitted vehicle. Supported scenes use ground polylines
classified against both terrain and 3D tiles, including after map-stack changes,
with per-segment age colours. History changes rebuild the body. The head is one
short draped corridor, clipped to displayed time by a material uniform; only
subdivision crossings rebuild it. Ordinary frames upload no head geometry.
Without ground-polyline support, ordinary primitives retain a mode-coloured
0.55-alpha depth-fail path, including the head, without dimming it to near-black.
Instance attribute updates write back the same typed array returned by Cesium;
its getters return copies, so reading a second time discards a visibility or colour edit.
Normal instance colours follow the age ramp once per display second or style change. Subdivision crossings update only changed visibility
attributes. Deselecting releases selection geometry; marker paths retain only
active and future corridors.
@@ -2586,22 +2597,27 @@ ingestion, eviction uses receipt-ordered queues, and capacity loss is disclosed
when history is recreated.
**Surface and route limitations.** Heights are aligned to work with Google 3D
tiles. Unknown historical surfaces leave gaps in a trail. Sparse reports can
tiles. Draped history is prepared on selection even while marker floors are unresolved;
Cesium resolves its surface. Without ground-polyline support, unknown historical
surfaces leave gaps. A selected body follows layer enablement and vehicle existence,
so an off-screen head does not hide an on-screen tail. Sparse reports can
cut across corners; these paths are not surveyed route geometry. Subway markers
are projected to street level because the feed does not provide depth, and the
selected card explains that choice. Actual street-level views still require
visual inspection.
**Appearance.** Normal fleet frames are 20 px, growing to 30 px on selection;
NVG, thermal and noir frames are 30/36 px. Source rasters are 48/96 px before padding.
External halos are 1/1.5 px in normal and 3/3 px in sensors, calculated from the
CRT, NVG, thermal and noir frames are 26/39 px. Source rasters are 48/96 px before padding.
External halos are 1/1.5 px in normal, 1.25/1.25 px in CRT and 2/2 px in sensors, calculated from the
final unpadded display size. Six modes, two sizes and three profiles cap the
raster cache at 36 variants. Normal mode colours are bus `#5EF08A`, tram
`#FFC24A`, subway `#FF4538`, rail `#D9A6FF`, ferry `#5FD6FF`, and unknown
`#D8DDE5`. Selection keeps the mode colour. Sensor bodies use solid rounded envelopes with at least 60% opaque white coverage
of the padded display footprint, without interior panel lines. Sensor input is opaque white with a
fully opaque black halo; black-hot reverses the contrast through the
thermal effect. Creation, mode changes, selection, deselection, map presets and
`#D8DDE5`. Selection keeps the mode colour. Sensors reuse each normal silhouette's exact
outer path, filled opaque white without interior panel lines, with an opaque
`#05080C` halo. Only the external padding grows; the unpadded silhouette remains
at CRT size. There is no envelope body. Black-hot reverses the contrast and
Ironbow maps the white input to its hottest palette entry through the thermal
effect; sprite code never applies a red tint. Creation, mode changes, selection, deselection, map presets and
Cockpit vision use the same sprite styling function.
Sensor signatures remain in-scene. The thresholds in `qaMetrics.js` require
@@ -2611,14 +2627,17 @@ of its phosphor peak white (0.65992 from RGB 0.16/1/0.22), halo minimum ≤0.25
with the centre minus sampled background reported without gating. Each preset needs at least
six verified, unobscured sprites; insufficient coverage is UNEXERCISED.
The sampler excludes overlapping footprints, including otherwise ineligible
sprites, and verifies pick ownership across all nine sampled core pixels.
sprites, and chooses nine source-raster points in the widest solid white band, avoiding
window/panel gaps. Equal-width tram bands prefer the upper solid body over the
geometric centre; the nine-point spacing and all thresholds remain unchanged.
The sampler then verifies pick ownership at all nine points.
Noir samples also exclude its deliberately vignetted outer field, using shader
settings and screen position rather than observed brightness. Selected-trail
diagnostics report every condition even when selection fails.
Transit brackets use the normal-composite overlay surface: a 1.25 px mode-colour
stroke over 3.25 px dark backing. Other contact themes retain their existing
behavior. The selected trail uses a 2 px core over 4 px backing, mode-coloured
behavior. The selected trail uses a 3 px core over 5 px backing, mode-coloured
in normal and white input in sensors. Alpha falls linearly from 0.80 at the head
to 0.45 at two minutes, 0.20 at ten minutes and 0.08 at fifteen minutes.
@@ -2641,7 +2660,10 @@ heights, and reuses caller-owned sample and segment storage.
collections above CCTV and Bikeshare. Traffic, Bikeshare and Transit share camera
sensitivity claims; the original sensitivity returns after the last release.
Visibility checks combine the camera frustum and ellipsoid occlusion at no more
than 4 Hz; missing map bounds do not admit the globe's far side. Frame work
than 4 Hz; missing map bounds do not admit the globe's far side. Camera arrivals
rebuild an unprepared or cold marker corridor during that visibility sweep,
adopt a loaded surface, and update both heightPending and surfaceReady diagnostics.
A resolved display sample clears its pending-height gate without waiting for a poll. Frame work
visits moving visible vehicles, up to 64 pending height changes, and the selected
head. Rotation visits visible membership at 5 Hz, with selected rotation per
frame. The transit render hold exists only for visible positional or orientation
@@ -2655,6 +2677,15 @@ destruction also releases collections and registrations.
QA_BASE_URL=http://localhost:4305 QA_TAG=keyless node scripts/qa-transit.mjs
```
`QA_BASE_URL=http://localhost:4305 node scripts/qa-transit-recovery.mjs` is the
focused keyless camera-arrival regression. With destination tiles loaded, it
injects nine disclosed moving-bus reports and delays their floor acquisition,
selects the hidden vehicle, then releases the floor and calls camera.setView.
Within two seconds it requires a visible marker, a prepared visible body and
at least six of eight paired off/on trail pixel samples. It uses production
selection, visibility and geometry owners and writes screenshots plus JSON;
it does not establish Google 3D acceptance.
The full run samples completed frames at 1920×1080 CSS pixels, records DPR, and
measures moving fleets of 800 and 3,000 vehicles with 128 fixes each and DETECT
on. Street/metro targets are respectively: transit frame CPU p95 ≤1/4 ms, frame
@@ -2685,6 +2716,30 @@ injected playback independently of live fleet activity. Thermal checks verify
style activation before reading generated controls; DETECT checks set density
and exercise the real OFF/restore button. Cesium render errors fail selection
acceptance.
The `trail-visible` section requires a visible Google 3D tileset. It selects a
scripted straight bus with CapMetro metadata under the isolated `qa-trail-capmetro`
feed ID, with nine retained fixes, runs for 20 seconds at each
300/600/900 m view (55/45/45 degrees), then freezes playback and waits for sustained tile and primitive readiness.
It captures off/on/off/on framebuffer patches at eight distinct projected surface
points outside the padded sprite and selection bracket. `Google 3D selected trail visible at 6 of 8 projected samples`
prints the selected key, fix count, elapsed display time, path length, hit count,
per-point coordinates/hits and renderer strategy. At least six points need a
repeatably changed green core or dark backing at the same pixels. Repeated off
and on channels and their contrast deltas must agree within 8 RGB levels;
background refinement, overlapping samples and unchanged dark roads fail. The dark
backing must reduce summed RGB by at least 40%, and either match needs RGB
distance of at least 40, so a faint depth-fail wash cannot satisfy visibility. Artifacts are
`<tag>-<altitude>m-trail-visible.jpg` and matching JSON. Run the same harness
against the base and patched keyed builds; partial runs remain diagnostic.
Node checks run Cesium's final geometry pipeline, including batch IDs and
position encoding: 1,576 bytes per head subdivision and 2,014,128 bytes for the
1,278-instance body. The body plus a separate 81,792-byte instance reserve totals
2,095,920 bytes, below the 2 MiB body limit; the head remains below its separate
2 KiB/frame geometry limit. Ordinary frames upload no head geometry. These are
pipeline buffer measurements, not measured GPU time or instance-texture uploads. The same 20-second browser interval scopes
WebGL uploads to the trail body/head, forces one body rebuild, reports head CPU
p95/max, and gates body uploads at 2 MiB and head uploads at 2 KiB/frame.
Required empty or unexercised checks fail acceptance. The passing final line is
`HARNESS: PASS (0 unexercised)`. A `QA_SECTIONS` subset is diagnostic only and
cannot claim full acceptance.