mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 05:12:23 +00:00
Merge pull request #1642 from bilawalsidhu/sameh/transit-floor-fix
Keep vehicles off rooftops and bridge decks by reading the 3D-tile surface where each vehicle is
This commit is contained in:
14 files changed
+521
-34
No files matched your search
@@ -0,0 +1,7 @@
|
||||
- Keep buses and trains off rooftops and out of bridge decks on the photoreal
|
||||
3D tiles: the ground floor probes the rendered mesh where each vehicle is,
|
||||
keeps each reading at the point it was taken (up to sixteen a cell) and
|
||||
answers from the reading nearest the vehicle, instead of one probe at each
|
||||
110 m cell's centre that lifted every vehicle in the cell onto whatever lay
|
||||
there. Transit probes each vehicle's latest point, and grounded flights read
|
||||
the floor at their own position (Sameh Khamis, @samehkhamis, #1642).
|
||||
+25
-1
@@ -1347,6 +1347,30 @@ and snapshot source. The standalone adapter keeps the existing USGS daily feed,
|
||||
M2.5+ filtering, static discs, magnitude labels and analyst records. Disabling or
|
||||
destroying the layer cancels pending work and ignores late results.
|
||||
|
||||
## Rendered-mesh floor readings — November 9, 2026
|
||||
|
||||
On the photoreal stack the shared floor (`cachedGroundFloor`) answers from
|
||||
rendered-mesh readings per coarse cell (0.001°, about 110 × 80 m), sampled by
|
||||
`src/services/meshFloorSampler.js` and gated against the DEM by
|
||||
`reportValidatedMeshFloorCell` in `src/services/groundFloor.js`. The probe goes
|
||||
where the caller's point is (a vehicle or contact), not to the cell's centre;
|
||||
the sampler reads a cell at up to eight spots about 12–17 m or more apart, and
|
||||
each reading that passes the gate is kept at its point (ground snaps add theirs
|
||||
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. 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
|
||||
|
||||
`src/data/aisLiveVessels.js` assembles `createVesselLayer` from the
|
||||
@@ -3148,7 +3172,7 @@ This is the current runtime/source-of-truth snapshot for the project.
|
||||
> `HELD_SNAP_CONTRADICTION_M` (5 m) ABOVE the held value drops the hold and the
|
||||
> contact is COLD again, back to the floored 2D billboard. The rule is
|
||||
> ONE-SIDED on purpose — it targets BURIAL. A cell reading BELOW a real sample
|
||||
> is the floor chain's expected under-read (one-shot latch over ~111 m,
|
||||
> is the floor chain's expected under-read (a few point readings over ~111 m,
|
||||
> neighbours lean lowest, `displayFloorHeightM` only ever raises), not evidence
|
||||
> of anything, and a two-sided cut was disproved on the track rig by a planted
|
||||
> cell 66.7 m below a real sample at the same spot. Accepted residual, stated
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -22,6 +22,7 @@ export const {
|
||||
reportMeshFloorCell,
|
||||
meshFloorSampleWithinPrior,
|
||||
reportValidatedMeshFloorCell,
|
||||
meshFloorPriorReady,
|
||||
cachedMeshFloor,
|
||||
_clearMeshFloorCellsForTest,
|
||||
cachedGroundFloor,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { sampleMeshFloorCells } from './meshFloorSampler.js';
|
||||
import { resolveEllipsoidalGround } from './terrainHeights.js';
|
||||
import {
|
||||
cachedGroundFloor, cachedMeshFloor, setMeshFloorPreferred,
|
||||
_clearMeshFloorCellsForTest,
|
||||
_clearMeshFloorCellsForTest, reportValidatedMeshFloorCell,
|
||||
} from './groundFloor.js';
|
||||
|
||||
/** Distinct site per test — terrainHeights' cache is module-global and warm
|
||||
@@ -137,3 +137,180 @@ test('sampler: a cell just across the antimeridian is still near the viewer', ()
|
||||
sampleMeshFloorCells(scene, [{ lat: -16.81, lon: 179.999 }], { viewerLat: -16.81, viewerLon: -179.999 });
|
||||
assert.equal(calls, 2, 'about 200 m apart, so both cells are probed');
|
||||
});
|
||||
|
||||
test('sampler: the probe goes where the point is, not to the cell centre', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
const asked = [];
|
||||
const scene = fakeScene((carto) => {
|
||||
asked.push(Cesium.Math.toDegrees(carto.longitude));
|
||||
return 122.0;
|
||||
});
|
||||
const point = { lat: cell.lat + 0.0003, lon: cell.lon + 0.0004 };
|
||||
sampleMeshFloorCells(scene, [point], { viewerLat: cell.lat, viewerLon: cell.lon });
|
||||
assert.equal(asked.length, 1);
|
||||
assert.ok(Math.abs(asked[0] - point.lon) < 1e-9, `probed at ${asked[0]}, not ${point.lon}`);
|
||||
});
|
||||
|
||||
test('sampler: a viaduct and the street beside it in one cell keep their own heights', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
// A viaduct deck 9 m up along one line of the cell, the street elsewhere.
|
||||
const deckLat = cell.lat + 0.0003;
|
||||
let calls = 0;
|
||||
const scene = fakeScene((carto) => {
|
||||
calls += 1;
|
||||
return Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0;
|
||||
});
|
||||
const opts = { viewerLat: cell.lat, viewerLon: cell.lon };
|
||||
const onDeck = { lat: deckLat, lon: cell.lon };
|
||||
const street = { lat: cell.lat - 0.0003, lon: cell.lon };
|
||||
sampleMeshFloorCells(scene, [onDeck], opts);
|
||||
sampleMeshFloorCells(scene, [street], opts);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(cachedMeshFloor(onDeck.lat, onDeck.lon + 0.0001), 129.0, 'the train stays on the deck');
|
||||
assert.equal(cachedMeshFloor(street.lat, street.lon - 0.0001), 121.0, 'the bus stays on the street');
|
||||
assert.equal(cachedGroundFloor(street.lat, street.lon), 121.0);
|
||||
sampleMeshFloorCells(scene, [{ lat: deckLat, lon: cell.lon + 0.00001 }], opts);
|
||||
assert.equal(calls, 2, 'a point next to one already probed is not probed again');
|
||||
});
|
||||
|
||||
test('sampler: a roof over the cell centre does not lift a bus on the street', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
// A roof 22 m up covering the cell's centre; the street 30 m west of it.
|
||||
const scene = fakeScene((carto) =>
|
||||
Math.abs(Cesium.Math.toDegrees(carto.longitude) - cell.lon) < 0.0001 ? 142.0 : 121.0);
|
||||
const bus = { lat: cell.lat, lon: cell.lon - 0.0003 };
|
||||
sampleMeshFloorCells(scene, [bus], { viewerLat: cell.lat, viewerLon: cell.lon });
|
||||
assert.equal(cachedGroundFloor(bus.lat, bus.lon), 121.0);
|
||||
});
|
||||
|
||||
test('sampler: a second reading at the same spot keeps the lower one', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
assert.equal(reportValidatedMeshFloorCell(cell.lat, cell.lon, 131.0), true);
|
||||
assert.equal(reportValidatedMeshFloorCell(cell.lat + 0.00002, cell.lon, 122.0), true);
|
||||
assert.equal(cachedMeshFloor(cell.lat, cell.lon), 122.0);
|
||||
assert.equal(reportValidatedMeshFloorCell(cell.lat, cell.lon + 0.00002, 140.0), true);
|
||||
assert.equal(cachedMeshFloor(cell.lat, cell.lon), 122.0, 'a higher one never raises it');
|
||||
});
|
||||
|
||||
test('sampler: a cell with a reading is probed a few times at most', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
let calls = 0;
|
||||
const scene = fakeScene(() => {
|
||||
calls += 1;
|
||||
return 121.0;
|
||||
});
|
||||
const opts = { viewerLat: cell.lat, viewerLon: cell.lon };
|
||||
for (let i = 0; i < 20; i++)
|
||||
sampleMeshFloorCells(
|
||||
scene,
|
||||
[{ lat: cell.lat - 0.0004 + (i % 5) * 0.0002, lon: cell.lon - 0.00045 + Math.floor(i / 5) * 0.0003 }],
|
||||
opts,
|
||||
);
|
||||
assert.equal(calls, 8);
|
||||
});
|
||||
|
||||
test('sampler: a cell with no accepted reading is probed again on every call', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
let calls = 0;
|
||||
const scene = fakeScene(() => {
|
||||
calls += 1;
|
||||
return 138.0;
|
||||
});
|
||||
const opts = { viewerLat: cell.lat, viewerLon: cell.lon };
|
||||
for (let i = 0; i < 3; i++) sampleMeshFloorCells(scene, [cell], opts);
|
||||
assert.equal(calls, 3, 'no DEM prior yet, so nothing is kept and the cell is retried');
|
||||
await seedDem(cell, 120.0);
|
||||
sampleMeshFloorCells(scene, [cell], opts);
|
||||
assert.equal(cachedMeshFloor(cell.lat, cell.lon), 138.0, 'and lands once the prior is warm');
|
||||
});
|
||||
|
||||
test('sampler: a spot whose reading is refused does not use up the cell', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
// A tower 260 m up where the first vehicle waits; the street elsewhere and
|
||||
// a deck 9 m up along one line of the cell.
|
||||
const towerLon = cell.lon + 0.0004;
|
||||
const deckLat = cell.lat + 0.0003;
|
||||
const scene = fakeScene((carto) => {
|
||||
if (Math.abs(Cesium.Math.toDegrees(carto.longitude) - towerLon) < 1e-7) return 380.0;
|
||||
return Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0;
|
||||
});
|
||||
const opts = { viewerLat: cell.lat, viewerLon: cell.lon };
|
||||
for (let i = 0; i < 10; i++) sampleMeshFloorCells(scene, [{ lat: cell.lat, lon: towerLon }], opts);
|
||||
assert.equal(cachedMeshFloor(cell.lat, cell.lon), null, 'the tower is refused every time');
|
||||
sampleMeshFloorCells(scene, [{ lat: cell.lat - 0.0003, lon: cell.lon }], opts);
|
||||
sampleMeshFloorCells(scene, [{ lat: deckLat, lon: cell.lon - 0.0003 }], opts);
|
||||
assert.equal(cachedMeshFloor(deckLat, cell.lon - 0.0003), 129.0, 'the deck is still probed');
|
||||
});
|
||||
|
||||
test('ground floor: a cell keeps at most sixteen readings', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
await seedDem(cell, 120.0);
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const lat = cell.lat - 0.00045 + (i % 8) * 0.0001;
|
||||
const lon = cell.lon - 0.00045 + Math.floor(i / 8) * 0.0002;
|
||||
reportValidatedMeshFloorCell(lat, lon, 121.0 + i);
|
||||
}
|
||||
// Readings 0..15 are kept; reading 39 (at the far corner) is not, so that
|
||||
// spot answers from the nearest kept one.
|
||||
assert.equal(cachedMeshFloor(cell.lat - 0.00045, cell.lon - 0.00045), 121.0);
|
||||
assert.equal(cachedMeshFloor(cell.lat + 0.00025, cell.lon + 0.00035), 121.0 + 15);
|
||||
});
|
||||
|
||||
test('sampler: a spot refused while the DEM was cold is probed again once it is warm', async () => {
|
||||
reset();
|
||||
const cell = nextSite();
|
||||
const deckLat = cell.lat + 0.0003;
|
||||
let calls = 0;
|
||||
const scene = fakeScene((carto) => {
|
||||
calls += 1;
|
||||
return Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0;
|
||||
});
|
||||
const opts = { viewerLat: cell.lat, viewerLon: cell.lon };
|
||||
const onDeck = { lat: deckLat, lon: cell.lon };
|
||||
sampleMeshFloorCells(scene, [onDeck], opts);
|
||||
assert.equal(cachedMeshFloor(onDeck.lat, onDeck.lon), null, 'no prior, nothing kept');
|
||||
await seedDem(cell, 120.0);
|
||||
sampleMeshFloorCells(scene, [{ lat: cell.lat - 0.0003, lon: cell.lon }], opts);
|
||||
sampleMeshFloorCells(scene, [onDeck], opts);
|
||||
assert.equal(calls, 3);
|
||||
assert.equal(cachedMeshFloor(onDeck.lat, onDeck.lon), 129.0);
|
||||
});
|
||||
|
||||
test('sampler: a spot refused while the DEM prior was evicted is probed again', async () => {
|
||||
const { createGroundFloor } = await import('../services/groundFloor.js');
|
||||
const { createMeshFloorSampler } = await import('../services/meshFloorSampler.js');
|
||||
let prior = 120.0;
|
||||
const terrain = {
|
||||
cachedRealEllipsoidalGround: () => prior,
|
||||
resolveEllipsoidalGround: async () => {},
|
||||
};
|
||||
const groundFloor = createGroundFloor({ terrain });
|
||||
const sampler = createMeshFloorSampler({ groundFloor });
|
||||
const cell = { lat: 41.5, lon: -97.66 };
|
||||
const deckLat = cell.lat + 0.0003;
|
||||
const scene = fakeScene((carto) =>
|
||||
Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0);
|
||||
const opts = { viewerLat: cell.lat, viewerLon: cell.lon };
|
||||
const onDeck = { lat: deckLat, lon: cell.lon };
|
||||
sampler.sampleMeshFloorCells(scene, [{ lat: cell.lat - 0.0003, lon: cell.lon }], opts);
|
||||
prior = null; // the cell's DEM entry is evicted; the street reading stays
|
||||
sampler.sampleMeshFloorCells(scene, [onDeck], opts);
|
||||
assert.equal(groundFloor.cachedMeshFloor(onDeck.lat, onDeck.lon), 121.0);
|
||||
prior = 120.0;
|
||||
sampler.sampleMeshFloorCells(scene, [onDeck], opts);
|
||||
assert.equal(groundFloor.cachedMeshFloor(onDeck.lat, onDeck.lon), 129.0);
|
||||
});
|
||||
@@ -448,7 +448,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
|
||||
// Photoreal regime. Sampling is delegated to the shared coarse-cell
|
||||
// sampler. It remains event-driven, one-shot per cell, and keeps its
|
||||
// sampler. It remains event-driven, a few probes per cell, and keeps its
|
||||
// existing acceptance window; CCTV adds no rooftop rejection policy.
|
||||
if (sampleGround && parts.model.projectionTilesReady()) {
|
||||
record.groundMeshSampleRequestCount =
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -300,8 +300,8 @@ export function createFlightSnapshotRenderer({
|
||||
// floor cells collected in the loop (fire-and-forget, single-flight;
|
||||
// read synchronously by NEXT poll's clamp — the military-layer pattern).
|
||||
warmGroundFloor(floorWarmPoints);
|
||||
// Round 4: sample the RENDERED mesh for those same cells (one-shot per
|
||||
// cell, budget-capped, viewer-proximate, google-3d regime only). Own
|
||||
// Round 4: sample the RENDERED mesh for those same cells (a few probes
|
||||
// per cell, budget-capped, viewer-proximate, google-3d regime only). Own
|
||||
// billboards/models are excluded so a vertical probe can't land on an
|
||||
// aircraft instead of the pavement.
|
||||
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
|
||||
|
||||
@@ -228,8 +228,8 @@ export function createMilitarySnapshotRenderer({
|
||||
const bbStale = flightState._billboards.get(icao24);
|
||||
if (bbStale) bbStale.position = liftedPos;
|
||||
}
|
||||
// Round 4: sample the RENDERED mesh for those cells (one-shot per cell,
|
||||
// budget-capped, viewer-proximate, google-3d regime only), excluding
|
||||
// Round 4: sample the RENDERED mesh for those cells (a few probes per
|
||||
// cell, budget-capped, viewer-proximate, google-3d regime only), excluding
|
||||
// this layer's own billboards/models from the probe.
|
||||
{
|
||||
const viewerCarto =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
+69
-12
@@ -418,13 +418,26 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
// is the photogrammetric MESH, which sits above it (measured ~17 m at the
|
||||
// Austin airport apron). DEM-flooring therefore still buried sprites/trails
|
||||
// inside the mesh. These cells hold the RENDERED surface height, sampled
|
||||
// one-shot per coarse cell near the viewer (scene.sampleHeight — the same
|
||||
// at the callers' points, a few per coarse cell near the viewer, each kept
|
||||
// where it was taken and read back from the nearest (scene.sampleHeight — the same
|
||||
// source groundSnap uses, which is why 3D models always looked right) and
|
||||
// reported here by meshFloorSampler.js. Design principle (owner): NEVER
|
||||
// below the visible surface; slightly above is always fine. The DEM stays
|
||||
// the instant global prior/fallback and the sanity gate for samples.
|
||||
/** @type {Map<string, number>} coarse cell key -> rendered-surface height (m, ellipsoidal). */
|
||||
/** @type {Map<string, Array<{lat: number, lon: number, h: number}>>}
|
||||
* coarse cell key -> rendered-surface readings (m, ellipsoidal), each kept
|
||||
* at the point it was probed. A read answers from the reading nearest the
|
||||
* asked point, so a viaduct and the street beside it in one cell keep their
|
||||
* own heights. */
|
||||
const _meshCells = new Map();
|
||||
/** @constant {number} Readings closer than this (degrees, about 5 m) are
|
||||
* one spot: the lower one is kept there. */
|
||||
const MESH_READING_SAME_SPOT_DEG = 0.00005;
|
||||
/** @constant {number} Readings kept per cell. The sampler probes a cell at
|
||||
* most eight times, but ground snaps report at each aircraft's position;
|
||||
* past this many, a reading away from the others is not kept, so a busy
|
||||
* apron cell stays a short list to scan. */
|
||||
const MESH_READINGS_PER_CELL = 16;
|
||||
/** @type {boolean} Whether mesh cells apply — true only in the google-3d
|
||||
* regime (photoreal stack). On globe stacks the rendered surface IS the
|
||||
* terrain provider (Re:Earth on keyless), so the DEM is already correct
|
||||
@@ -444,7 +457,8 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
|
||||
/**
|
||||
* Records a sampled rendered-surface height for the cell containing the
|
||||
* coordinate (one-shot latch — first accepted sample wins).
|
||||
* coordinate, unchecked, when the cell has no reading yet. The sampler goes
|
||||
* through `reportValidatedMeshFloorCell`, which keeps a reading per point.
|
||||
* @param {number} lat @param {number} lon @param {number} heightM
|
||||
*/
|
||||
function reportMeshFloorCell(lat, lon, heightM) {
|
||||
@@ -456,7 +470,7 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
return;
|
||||
const c = coarseFloorCoord(lat, lon);
|
||||
const key = `${c.lat},${c.lon}`;
|
||||
if (!_meshCells.has(key)) _meshCells.set(key, heightM);
|
||||
if (!_meshCells.has(key)) _meshCells.set(key, [{ lat, lon, h: heightM }]);
|
||||
}
|
||||
|
||||
/** Return whether a rendered-mesh sample is plausible against a real DEM prior. */
|
||||
@@ -471,9 +485,11 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
|
||||
/**
|
||||
* Validate and record a rendered-mesh floor through the single shared gate.
|
||||
* Callers must not bypass this with raw `reportMeshFloorCell`: a coarse-LOD,
|
||||
* rooftop, or aircraft hit would otherwise win the session-long first-write
|
||||
* latch and poison every floor consumer in the cell.
|
||||
* Callers must not bypass this with raw `reportMeshFloorCell`: a coarse-LOD
|
||||
* or aircraft hit would otherwise poison every floor consumer in the cell.
|
||||
* The reading is kept at the point it was taken; a second reading at the
|
||||
* same spot (within about 5 m) keeps the lower of the two. A cell keeps at
|
||||
* most MESH_READINGS_PER_CELL readings.
|
||||
* @returns {boolean} Whether the sample passed and was reported.
|
||||
*/
|
||||
function reportValidatedMeshFloorCell(lat, lon, heightM) {
|
||||
@@ -481,13 +497,41 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
const cell = coarseFloorCoord(lat, lon);
|
||||
const prior = cachedRealEllipsoidalGround(cell.lat, cell.lon);
|
||||
if (!meshFloorSampleWithinPrior(heightM, prior)) return false;
|
||||
reportMeshFloorCell(cell.lat, cell.lon, heightM);
|
||||
const key = `${cell.lat},${cell.lon}`;
|
||||
const readings = _meshCells.get(key);
|
||||
if (!readings) {
|
||||
_meshCells.set(key, [{ lat, lon, h: heightM }]);
|
||||
return true;
|
||||
}
|
||||
const same = readings.find(
|
||||
(r) =>
|
||||
Math.abs(r.lat - lat) < MESH_READING_SAME_SPOT_DEG &&
|
||||
Math.abs(r.lon - lon) < MESH_READING_SAME_SPOT_DEG,
|
||||
);
|
||||
if (same) {
|
||||
if (heightM < same.h) same.h = heightM;
|
||||
} else if (readings.length < MESH_READINGS_PER_CELL) {
|
||||
readings.push({ lat, lon, h: heightM });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous mesh-cell read — null when the cell is unsampled OR the
|
||||
* google-3d regime is inactive (globe stacks must floor on the DEM).
|
||||
* Whether the cell holding the point has the real DEM prior that
|
||||
* `reportValidatedMeshFloorCell` judges a reading against.
|
||||
* @param {number} lat @param {number} lon
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function meshFloorPriorReady(lat, lon) {
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false;
|
||||
const c = coarseFloorCoord(lat, lon);
|
||||
return cachedRealEllipsoidalGround(c.lat, c.lon) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous mesh-cell read — the cell's reading nearest the point, or
|
||||
* null when the cell is unsampled OR the google-3d regime is inactive (globe
|
||||
* stacks must floor on the DEM).
|
||||
* @param {number} lat @param {number} lon
|
||||
* @returns {number|null}
|
||||
*/
|
||||
@@ -495,8 +539,20 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
if (!_meshPreferred) return null;
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
||||
const c = coarseFloorCoord(lat, lon);
|
||||
const h = _meshCells.get(`${c.lat},${c.lon}`);
|
||||
return h != null ? h : null;
|
||||
const readings = _meshCells.get(`${c.lat},${c.lon}`);
|
||||
if (!readings) return null;
|
||||
if (readings.length === 1) return readings[0].h;
|
||||
const k = Math.cos((lat * Math.PI) / 180) ** 2;
|
||||
let best = readings[0],
|
||||
bestD = Infinity;
|
||||
for (const r of readings) {
|
||||
const d = (r.lat - lat) ** 2 + k * (r.lon - lon) ** 2;
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = r;
|
||||
}
|
||||
}
|
||||
return best.h;
|
||||
}
|
||||
|
||||
/** Test hook: clears mesh cells (never called at runtime — cells are valid all session). */
|
||||
@@ -662,6 +718,7 @@ export function createGroundFloor({ terrain, signal }) {
|
||||
reportMeshFloorCell,
|
||||
meshFloorSampleWithinPrior,
|
||||
reportValidatedMeshFloorCell,
|
||||
meshFloorPriorReady,
|
||||
cachedMeshFloor,
|
||||
_clearMeshFloorCellsForTest,
|
||||
cachedGroundFloor,
|
||||
|
||||
@@ -101,7 +101,7 @@ const HELD_SNAP_MAX_DRIFT_M = 250;
|
||||
* alone, and the asymmetry is not tidiness:
|
||||
*
|
||||
* - A cell reading LOW against a real sample is the EXPECTED error here, not a
|
||||
* contradiction. Cells are a one-shot first-write latch over ~111 m, the
|
||||
* contradiction. Cells hold a few point readings over ~111 m, the
|
||||
* borrowed-neighbour path leans to the LOWEST on purpose (`neighborFloorM`),
|
||||
* and `displayFloorHeightM` only ever RAISES — the whole floor chain is
|
||||
* built to under-read rather than to invent. Releasing on that sign would
|
||||
|
||||
@@ -11,6 +11,7 @@ export function createMeshFloorSampler({
|
||||
coarseFloorCoord,
|
||||
cachedMeshFloor,
|
||||
reportValidatedMeshFloorCell,
|
||||
meshFloorPriorReady,
|
||||
setMeshFloorPreferred,
|
||||
meshFloorPreferred,
|
||||
cachedGroundFloor,
|
||||
@@ -23,13 +24,16 @@ export function createMeshFloorSampler({
|
||||
// which sits above the Re:Earth bare-earth DEM (measured ~17 m at the Austin
|
||||
// airport apron) — so DEM-floored sprites/trails still buried themselves in
|
||||
// the mesh while 3D models (which groundSnap against the mesh) looked right.
|
||||
// This module samples the mesh height ONE-SHOT per coarse cell and reports it
|
||||
// to groundFloor.js, whose cachedGroundFloor() then answers mesh-first.
|
||||
// This module samples the mesh height at the callers' points, a few probes
|
||||
// per coarse cell, and reports them to groundFloor.js, which keeps each
|
||||
// reading that passes its gate at its own point; cachedGroundFloor() then
|
||||
// answers mesh-first, from the reading nearest the asked point.
|
||||
//
|
||||
// Rationing (the CCTV loading-jitter lesson): scene.sampleHeight forces
|
||||
// tile-load prioritization at the probe point, so sampling is (a) gated to
|
||||
// cells NEAR the viewer — where tiles are streamed and where an error is
|
||||
// visible at all, (b) capped per call, (c) one-shot per cell for the session,
|
||||
// visible at all, (b) capped per call, (c) at most MAX_PROBES_PER_CELL
|
||||
// readings per cell for the session, each at a point apart from the others,
|
||||
// (d) skipped entirely outside the google-3d regime. A returned-undefined
|
||||
// probe (tiles not streamed yet) is NOT latched — it retries on a later call.
|
||||
//
|
||||
@@ -49,6 +53,16 @@ export function createMeshFloorSampler({
|
||||
* streamed LOD under a high camera is coarse everywhere, so every probe
|
||||
* would latch junk. */
|
||||
const MAX_CAMERA_HEIGHT_M = 25_000;
|
||||
/** @constant {number} Readings per cell: a cell is probed at most this
|
||||
* many times, each at a point apart from the others, and groundFloor keeps
|
||||
* each reading at its point (a read answers from the nearest one). */
|
||||
const MAX_PROBES_PER_CELL = 8;
|
||||
/** @constant {number} How far apart (degrees, about 17 m) a new probe must
|
||||
* be from the cell's earlier ones to be worth taking. */
|
||||
const PROBE_APART_DEG = 0.00015;
|
||||
/** @type {Map<string, Array<{lat: number, lon: number}>>} Points probed
|
||||
* per cell that returned a reading. */
|
||||
const _probedPoints = new Map();
|
||||
/** @type {Cesium.Cartographic} Scratch for probe coordinates. */
|
||||
const _scratchProbe = new Cesium.Cartographic();
|
||||
|
||||
@@ -82,7 +96,7 @@ export function createMeshFloorSampler({
|
||||
|
||||
/**
|
||||
* Samples the rendered mesh height for the coarse cells containing `points`,
|
||||
* one-shot per cell, budget-capped, viewer-proximity-gated. Call ONCE per
|
||||
* a few probes per cell, budget-capped, viewer-proximity-gated. Call ONCE per
|
||||
* layer poll (never per contact, never per frame). Synchronous — Cesium's
|
||||
* sampleHeight is a CPU-side query against loaded tiles.
|
||||
*
|
||||
@@ -119,8 +133,21 @@ export function createMeshFloorSampler({
|
||||
const cell = coarseFloorCoord(p.lat, p.lon);
|
||||
const key = `${cell.lat},${cell.lon}`;
|
||||
if (attempted.has(key)) continue;
|
||||
// A cell with a reading is probed again only at a point well apart
|
||||
// from the ones already probed, a few times at most, so the cell's
|
||||
// readings follow where its vehicles actually are.
|
||||
const probes = _probedPoints.get(key);
|
||||
if (
|
||||
cachedMeshFloor(cell.lat, cell.lon) != null &&
|
||||
(probes?.length >= MAX_PROBES_PER_CELL ||
|
||||
probes?.some(
|
||||
(q) =>
|
||||
Math.abs(q.lat - p.lat) < PROBE_APART_DEG &&
|
||||
Math.abs(q.lon - p.lon) < PROBE_APART_DEG,
|
||||
))
|
||||
)
|
||||
continue;
|
||||
attempted.add(key);
|
||||
if (cachedMeshFloor(cell.lat, cell.lon) != null) continue; // one-shot latch
|
||||
if (
|
||||
Number.isFinite(viewerLat) &&
|
||||
Number.isFinite(viewerLon) &&
|
||||
@@ -131,9 +158,11 @@ export function createMeshFloorSampler({
|
||||
}
|
||||
let height;
|
||||
try {
|
||||
// Probe where the caller's point is, not at the cell's centre: a
|
||||
// centre that falls on a roof would lift everything in the cell.
|
||||
const carto = Cesium.Cartographic.fromDegrees(
|
||||
cell.lon,
|
||||
cell.lat,
|
||||
p.lon,
|
||||
p.lat,
|
||||
0,
|
||||
_scratchProbe,
|
||||
);
|
||||
@@ -171,7 +200,23 @@ export function createMeshFloorSampler({
|
||||
// feasible and deliberately deferred — see the P2 in
|
||||
// the project roadmap, which bundles it with the fly_route probe so
|
||||
// one post-launch pass settles rendered-mesh sampling everywhere.
|
||||
reportValidatedMeshFloorCell(cell.lat, cell.lon, height);
|
||||
const accepted = reportValidatedMeshFloorCell(p.lat, p.lon, height);
|
||||
// A spot counts as probed once the gate has judged it against a real
|
||||
// prior: accepted, or refused with the cell's DEM warm. A spot refused
|
||||
// only because the DEM was still cold is tried again later. Each spot is
|
||||
// recorded once, so a cell retried while it has no reading does not fill
|
||||
// its probe allowance with the same spot.
|
||||
if (!accepted && !meshFloorPriorReady(cell.lat, cell.lon)) continue;
|
||||
if (!probes) _probedPoints.set(key, [{ lat: p.lat, lon: p.lon }]);
|
||||
else if (
|
||||
probes.length < MAX_PROBES_PER_CELL &&
|
||||
!probes.some(
|
||||
(q) =>
|
||||
Math.abs(q.lat - p.lat) < PROBE_APART_DEG &&
|
||||
Math.abs(q.lon - p.lon) < PROBE_APART_DEG,
|
||||
)
|
||||
)
|
||||
probes.push({ lat: p.lat, lon: p.lon });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user