From 9bce41d3c6a83125ac496ad3b6abeea6ca200f3b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 9 Nov 2026 01:26:01 -0800 Subject: [PATCH] Read the mesh floor where transit vehicles and grounded flights actually are With readings kept per point, the floor a vehicle gets depends on which point is probed and where it is read. Transit offered the sampler the first point of each vehicle's path in a cell, usually its oldest fix there, so a vehicle that stayed in one cell never had its current spot probed. It now offers the latest point of the path in each cell, and the newest fix in the vehicle's own cell (the selected vehicle's history is walked newest first). The grounded-flight display clamp read the floor at its sticky cell's centre, which now answers from the reading nearest the centre, not the contact. It reads at the contact's own position, held inside the sticky cell so boundary hysteresis still applies. --- docs/CURRENT-STATE.md | 17 ++-- src/data/flights.test.mjs | 35 +++++++++ src/layers/flights/motion.js | 29 +++++-- src/layers/transit/height.js | 16 +++- src/layers/transit/lifecycle.test.mjs | 108 ++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 13 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5e7c36be2..0739cf5e1 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1360,13 +1360,16 @@ the same way; a cell keeps at most sixteen). A read answers from the cell's reading nearest the asked point, so a viaduct and the street beside it in one cell keep their own heights; two readings within about 5 m of each other keep the lower. A spot refused only because the cell's DEM prior was cold is tried -again later. Before this, one probe at the cell's centre latched for the -session, so a centre on a roof lifted everything in the cell (a Haymarket bus -drew 23.5 m above the street, on the garage roof) and a centre in the river put -Red Line trains on the Longfellow Bridge 6–9 m under the deck. Residual: the -probe is vertical, so a vehicle directly under a deck or a roof overhang reads -the structure above it, and a vehicle between readings takes the nearest one, so -its floor steps where the nearest reading changes. +again later. Transit offers the sampler the latest point of each vehicle's path +in a cell (its newest fix in its own cell), and the grounded-flight display +clamp reads at the contact, held inside its sticky cell. Before this, one probe +at the cell's centre latched for the session, so a centre on a roof lifted +everything in the cell (a Haymarket bus drew 23.5 m above the street, on the +garage roof) and a centre in the river put Red Line trains on the Longfellow +Bridge 6–9 m under the deck. Residual: the probe is vertical, so a vehicle +directly under a deck or a roof overhang reads the structure above it, and a +vehicle between readings takes the nearest one, so its floor steps where the +nearest reading changes. ## Vessel components and sources diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index 7fe197c30..1d6007e8f 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -20,9 +20,11 @@ import { setMilitaryLayerActive } from './militaryRegistry.js'; import { GROUND_FLOOR_LIFT_M, reportMeshFloorCell, + reportValidatedMeshFloorCell, setMeshFloorPreferred, _clearMeshFloorCellsForTest, } from './groundFloor.js'; +import { resolveEllipsoidalGround } from './terrainHeights.js'; import { _setTrackedOverlayHostForTest, destroyTrackedReadout, @@ -1181,6 +1183,39 @@ test('display floor: a grounded contact that drifted onto a higher cell is lifte assert.ok(Math.abs(Cesium.Math.toDegrees(c.longitude) + 97.6604) < 1e-6); }); +test('display floor: reads the reading nearest the contact, not the cell centre', async () => { + _clearDisplayFloorStateForTest(); + _clearMeshFloorCellsForTest(); + setMeshFloorPreferred(true); + const realFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response(JSON.stringify({ results: [{ ellipsoid: 120 }] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + try { + await resolveEllipsoidalGround([{ lat: 30.317, lon: -97.717 }]); + } finally { + globalThis.fetch = realFetch; + } + // A hangar roof over the cell's centre; the apron where the contact is. + assert.equal(reportValidatedMeshFloorCell(30.317, -97.717, 142), true); + assert.equal(reportValidatedMeshFloorCell(30.3173, -97.7173, 121), true); + const pos = Cesium.Cartesian3.fromDegrees(-97.7173, 30.3173, 100); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + ); + const c = _floorCarto(out); + assert.ok( + Math.abs(c.height - (121 + GROUND_FLOOR_LIFT_M)) < 0.05, + `height ${c.height} should be the apron's 121 + lift`, + ); + _clearMeshFloorCellsForTest(); + _clearDisplayFloorStateForTest(); +}); + test('display floor: an AIRBORNE contact over the same cell is never lifted', () => { _clearDisplayFloorStateForTest(); _clearMeshFloorCellsForTest(); diff --git a/src/layers/flights/motion.js b/src/layers/flights/motion.js index a14c8c4ac..c09522153 100644 --- a/src/layers/flights/motion.js +++ b/src/layers/flights/motion.js @@ -37,6 +37,22 @@ import { DISPLAY_CORRIDOR_FAIR_SHARE, } from './policy.js'; +/** Half a coarse floor cell (0.001°), less a margin so rounding keeps a + * clamped coordinate inside the cell. */ +const HALF_CELL_DEG = 0.00049; + +/** + * A coordinate held within half a cell of the cell's centre (the short way + * round for longitude), so a read lands in the given cell. + * @param {number} deg @param {number} centreDeg + * @returns {number} + */ +function clampToCell(deg, centreDeg) { + let d = deg - centreDeg; + d -= 360 * Math.round(d / 360); + return centreDeg + Math.max(-HALF_CELL_DEG, Math.min(HALF_CELL_DEG, d)); +} + export function createMotion({ flightState, services, @@ -865,12 +881,15 @@ export function createMotion({ ); // Boundary hysteresis: a position jittering across a cell edge would flip // floors at fleet-tick rate (see stickyFloorCell). - const cell = stickyFloorCell( - Cesium.Math.toDegrees(carto.latitude), - Cesium.Math.toDegrees(carto.longitude), - state?.cell, + const latDeg = Cesium.Math.toDegrees(carto.latitude); + const lonDeg = Cesium.Math.toDegrees(carto.longitude); + const cell = stickyFloorCell(latDeg, lonDeg, state?.cell); + // A cell holds rendered-surface readings at the points they were taken, + // so the floor is read where the contact is, held inside its sticky cell. + const floor = cachedGroundFloor( + clampToCell(latDeg, cell.lat), + clampToCell(lonDeg, cell.lon), ); - const floor = cachedGroundFloor(cell.lat, cell.lon); const next = state || { cell, in: new Cesium.Cartesian3(), diff --git a/src/layers/transit/height.js b/src/layers/transit/height.js index 2e2945a88..027dc7d41 100644 --- a/src/layers/transit/height.js +++ b/src/layers/transit/height.js @@ -124,7 +124,14 @@ export function createHeight({ state, services, parts }) { const cells = new Map(); const collect = (point, answer) => { if (!onGround) return; - if (!cells.has(answer.cell)) + // A cell keeps the last point offered in it: the path is walked + // oldest first, so that is where the vehicle is now or about to be, + // and the mesh sampler reads the surface there. + const held = cells.get(answer.cell); + if (held) { + held.lat = point.lat; + held.lon = point.lon; + } else cells.set(answer.cell, { lat: point.lat, lon: point.lon, @@ -136,6 +143,13 @@ export function createHeight({ state, services, parts }) { const newest = entry.fixes.at(-1); const answer = requestHeight(newest.lat, newest.lon, entry); entry.heightCell = answer.cell; + // The selected vehicle's path also offers its history, newest segment + // first; the vehicle's own cell is read where its newest fix is. + const own = cells.get(answer.cell); + if (own) { + own.lat = newest.lat; + own.lon = newest.lon; + } if (answer.height !== null) { entry.heightM = answer.height; entry.heightResolved = true; diff --git a/src/layers/transit/lifecycle.test.mjs b/src/layers/transit/lifecycle.test.mjs index e36d87de8..fee9a60ed 100644 --- a/src/layers/transit/lifecycle.test.mjs +++ b/src/layers/transit/lifecycle.test.mjs @@ -2938,6 +2938,114 @@ test('mesh refinement reaches all 100 retained DEM cells within five poll budget assert.equal(probed.size, 100); }); +test('mesh refinement probes the latest point the path offers in a cell', async (t) => { + const { createHeight } = await import('./height.js'); + // Three points along one cell's stretch of path, oldest first. + const path = [ + { lat: 42.3611, lon: -71.0751 }, + { lat: 42.3612, lon: -71.0753 }, + { lat: 42.3614, lon: -71.0754 }, + ]; + const entry = { key: 'train', to: path[0], fixes: [path.at(-1)] }; + const probed = []; + const state = { + _enabled: true, + _selectedKey: null, + _visible: new Set([entry]), + _vehicles: new Map([[entry.key, entry]]), + _floorCursor: 0, + _heightDirty: new Set(), + _viewer: { camera: { positionCartographic: { height: 100 } } }, + }; + const height = createHeight({ + state, + services: { + render: { governorRequestRender() {} }, + ground: { + coarseFloorCoord: (lat, lon) => ({ + lat: Number(lat.toFixed(3)), + lon: Number(lon.toFixed(3)), + }), + cachedGroundFloor: () => 10, + GROUND_FLOOR_LIFT_M: 1.5, + }, + mesh: { + sampleMeshFloorCells(scene, points) { + probed.push(...points.map((p) => [p.lat, p.lon])); + }, + }, + }, + parts: { + trails: { + prepareEntry(e, collect) { + if (collect) + for (const point of path) + collect(point, height.requestHeight(point.lat, point.lon)); + }, + }, + rendering: { requestVisibility() {} }, + }, + }); + t.after(() => height.clear()); + height.anchorFloors(); + assert.deepEqual(probed, [[42.3614, -71.0754]]); +}); + +test('the selected vehicle is probed at its newest fix in its own cell', async (t) => { + const { createHeight } = await import('./height.js'); + // The selected path offers its active corridor first, then its history + // newest segment first, so the last offer in the cell is an older point. + const newest = { lat: 42.3614, lon: -71.0754 }; + const offers = [ + newest, + { lat: 42.3612, lon: -71.0753 }, + { lat: 42.3611, lon: -71.0751 }, + ]; + const entry = { key: 'train', to: offers[0], fixes: [newest] }; + const probed = []; + const state = { + _enabled: true, + _selectedKey: entry.key, + _visible: new Set([entry]), + _vehicles: new Map([[entry.key, entry]]), + _floorCursor: 0, + _heightDirty: new Set(), + _viewer: { camera: { positionCartographic: { height: 100 } } }, + }; + const height = createHeight({ + state, + services: { + render: { governorRequestRender() {} }, + ground: { + coarseFloorCoord: (lat, lon) => ({ + lat: Number(lat.toFixed(3)), + lon: Number(lon.toFixed(3)), + }), + cachedGroundFloor: () => 10, + GROUND_FLOOR_LIFT_M: 1.5, + }, + mesh: { + sampleMeshFloorCells(scene, points) { + probed.push(...points.map((p) => [p.lat, p.lon])); + }, + }, + }, + parts: { + trails: { + prepareEntry(e, collect) { + if (collect) + for (const point of offers) + collect(point, height.requestHeight(point.lat, point.lon)); + }, + }, + rendering: { requestVisibility() {} }, + }, + }); + t.after(() => height.clear()); + height.anchorFloors(); + assert.deepEqual(probed, [[42.3614, -71.0754]]); +}); + test('selection churn releases past geometry on every retained vehicle', async (t) => { const app = harness(t, { floorAt: () => 10 }); const { mergeHistory, seek } = await import('../../data/contactPlayback.js');