mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The flights ground-floor pipeline subtracted longitudes without wrapping. approxDistanceKm, and its copy in the mesh-floor sampler, measured two points 0.2° apart either side of 180° (21 km at Fiji) as about 38,000 km, so low and grounded aircraft just across the line from the viewer were never floored or mesh-sampled. corridorFloorCells walked a segment across 180° the long way round, filling its cell budget with ground behind the contact and never requesting the cells it was about to cross, and stickyFloorCell dropped its hysteresis for a contact on the seam. Each now takes the short way round, and corridor points are normalised back into [-180, 180]. SEARCH NEARBY SITES centred its Google Places search on the mean of west and east, which for a view across 180° (west > east) is longitude 0, and sized the radius from the view's height alone. It now measures the view's width through 180° and searches its real centre.
655 lines
30 KiB
JavaScript
655 lines
30 KiB
JavaScript
// src/data/groundFloor.test.mjs
|
||
// Field-test round (2026-07-06): locks the coarse ground-floor helpers used to
|
||
// clamp military render altitudes and trail waypoints so they never render
|
||
// below the local ellipsoidal surface (RS46 heli-in-hillside + WAKE01
|
||
// trail-underground findings).
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
coarseFloorCoord, floorAltitudeM, GROUND_FLOOR_LIFT_M, displayFloorHeightM,
|
||
corridorFloorCells, CORRIDOR_MAX_CELLS, stickyFloorCell, allocateCorridorCells,
|
||
CORRIDOR_WALK_STEP_DEG,
|
||
meshFloorSampleWithinPrior,
|
||
reportMeshFloorCell, cachedMeshFloor, cachedGroundFloor,
|
||
setMeshFloorPreferred, meshFloorPreferred, _clearMeshFloorCellsForTest,
|
||
neighborFloorM,
|
||
} from './groundFloor.js';
|
||
import {
|
||
corridorPathLatLon, projectGroundArcLatLon,
|
||
CORRIDOR_SAMPLE_SPACING_M, CORRIDOR_MAX_LENGTH_M,
|
||
} from './motionModel.js';
|
||
|
||
test('coarseFloorCoord rounds to a 3-decimal (~111 m) grid cell', () => {
|
||
const c = coarseFloorCoord(35.049876, -106.591432);
|
||
assert.equal(c.lat, 35.05);
|
||
assert.equal(c.lon, -106.591);
|
||
});
|
||
|
||
test('coarseFloorCoord: nearby points collapse to the same cell (cache reuse)', () => {
|
||
const a = coarseFloorCoord(35.0501, -106.5914);
|
||
const b = coarseFloorCoord(35.0504, -106.5909);
|
||
assert.deepEqual(a, b);
|
||
});
|
||
|
||
test('floorAltitudeM lifts a below-ground altitude to ground + lift', () => {
|
||
// RS46 case: baro+N ≈ 334 m but the ridge is ~400 m ellipsoidal.
|
||
const got = floorAltitudeM(334, 400);
|
||
assert.equal(got, 400 + GROUND_FLOOR_LIFT_M);
|
||
});
|
||
|
||
test('floorAltitudeM leaves an above-ground altitude untouched', () => {
|
||
assert.equal(floorAltitudeM(1200, 400), 1200);
|
||
});
|
||
|
||
test('floorAltitudeM with unknown ground returns the altitude unchanged', () => {
|
||
assert.equal(floorAltitudeM(334, null), 334);
|
||
assert.equal(floorAltitudeM(334, NaN), 334);
|
||
});
|
||
|
||
test('floorAltitudeM with null altitude and known ground returns ground + lift (ground-waypoint case)', () => {
|
||
// WAKE01 case: a readsb trace point of "ground" has NO altitude — it must
|
||
// sit ON the local surface (~1590 m at Kirtland), never the old 50 m sentinel.
|
||
const got = floorAltitudeM(null, 1590);
|
||
assert.equal(got, 1590 + GROUND_FLOOR_LIFT_M);
|
||
});
|
||
|
||
test('floorAltitudeM with null altitude and unknown ground returns null (caller fallback applies)', () => {
|
||
assert.equal(floorAltitudeM(null, null), null);
|
||
});
|
||
|
||
test('floorAltitudeM honors a custom lift', () => {
|
||
assert.equal(floorAltitudeM(10, 100, 5), 105);
|
||
});
|
||
|
||
test('floorAltitudeM treats ground of 0 (sea-level coast) as a real floor', () => {
|
||
assert.equal(floorAltitudeM(-20, 0), 0 + GROUND_FLOOR_LIFT_M);
|
||
});
|
||
|
||
// Mesh-floor cells (round 4): the rendered-surface layer over the DEM prior.
|
||
test('mesh cell: report + read round-trips within the same coarse cell', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.1971, -97.6663, 138.4);
|
||
// ~30 m away, same 111 m cell
|
||
assert.equal(cachedMeshFloor(30.19725, -97.66645), 138.4);
|
||
});
|
||
|
||
test('mesh cell: first accepted sample wins (one-shot latch)', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.197, -97.666, 138.4);
|
||
reportMeshFloorCell(30.197, -97.666, 150.0);
|
||
assert.equal(cachedMeshFloor(30.197, -97.666), 138.4);
|
||
});
|
||
|
||
test('mesh cell: reads are null on globe stacks (DEM is the rendered surface there)', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.197, -97.666, 138.4);
|
||
setMeshFloorPreferred(false);
|
||
assert.equal(cachedMeshFloor(30.197, -97.666), null);
|
||
// cells survive the regime round-trip (latched for the session)
|
||
setMeshFloorPreferred(true);
|
||
assert.equal(cachedMeshFloor(30.197, -97.666), 138.4);
|
||
});
|
||
|
||
test('cachedGroundFloor prefers the mesh cell over the DEM cell (AUS 17 m case)', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
// no DEM warm needed for this assertion: mesh present -> mesh wins outright
|
||
reportMeshFloorCell(30.197, -97.666, 138.1);
|
||
assert.equal(cachedGroundFloor(30.197, -97.666), 138.1);
|
||
});
|
||
|
||
test('meshFloorPreferred reflects the setter (sampler stack-listener contract)', () => {
|
||
setMeshFloorPreferred(false);
|
||
assert.equal(meshFloorPreferred(), false);
|
||
setMeshFloorPreferred(true);
|
||
assert.equal(meshFloorPreferred(), true);
|
||
});
|
||
|
||
test('all mesh-floor writers share the asymmetric real-DEM acceptance window', () => {
|
||
assert.equal(meshFloorSampleWithinPrior(85, 100), true);
|
||
assert.equal(meshFloorSampleWithinPrior(180, 100), true);
|
||
assert.equal(meshFloorSampleWithinPrior(84.99, 100), false);
|
||
assert.equal(meshFloorSampleWithinPrior(180.01, 100), false);
|
||
assert.equal(meshFloorSampleWithinPrior(120, null), false);
|
||
});
|
||
|
||
// --- Display-time floor (2026-08-19, buried taxiing contacts at KAUS) -------
|
||
// A grounded contact's render height is fixed at POLL time from the floor of
|
||
// the fix's cell, but what renders is the DEAD-RECKONED position, which drifts
|
||
// across cells for the whole 30 s segment (and far further while a stale
|
||
// ground contact coasts). On a graded apron the sprite ends up under the mesh
|
||
// it drifted over. displayFloorHeightM is the decision at the point of display.
|
||
|
||
test('displayFloorHeightM lifts a display height that drifted under the floor', () => {
|
||
// KAUS taxiway: fix cell floor 124.7 m, drifted onto a 140.2 m cell.
|
||
assert.equal(displayFloorHeightM(124.7, 140.2), 140.2 + GROUND_FLOOR_LIFT_M);
|
||
});
|
||
|
||
test('displayFloorHeightM returns null (no rebuild) when already above the floor', () => {
|
||
assert.equal(displayFloorHeightM(140.2, 124.7), null);
|
||
});
|
||
|
||
test('displayFloorHeightM returns null exactly at the lifted floor (no jitter)', () => {
|
||
assert.equal(displayFloorHeightM(124.7 + GROUND_FLOOR_LIFT_M, 124.7), null);
|
||
});
|
||
|
||
test('displayFloorHeightM returns null when the floor cell is not warm', () => {
|
||
assert.equal(displayFloorHeightM(124.7, null), null);
|
||
assert.equal(displayFloorHeightM(124.7, NaN), null);
|
||
assert.equal(displayFloorHeightM(124.7, undefined), null);
|
||
});
|
||
|
||
test('displayFloorHeightM returns null for a non-finite display height', () => {
|
||
assert.equal(displayFloorHeightM(NaN, 124.7), null);
|
||
assert.equal(displayFloorHeightM(null, 124.7), null);
|
||
});
|
||
|
||
test('displayFloorHeightM honours a caller-supplied lift', () => {
|
||
assert.equal(displayFloorHeightM(100, 140, 5), 145);
|
||
});
|
||
|
||
// --- Display corridor ------------------------------------------------------
|
||
// A grounded contact's DISPLAY walks from where it currently renders toward
|
||
// the newest fix over the following poll(s), so those are the cells it is
|
||
// about to need a floor for. Warming only the cell it is standing on warms the
|
||
// cell it is LEAVING — a contact taxiing at 10 m/s crosses a 111 m cell every
|
||
// ~11 s while the warm batch runs once per 30 s poll, so it stays permanently
|
||
// ahead of its own floor data (measured at KAUS: sprites buried 2–4 m with
|
||
// `cellFloor=COLD` for the whole observation).
|
||
|
||
test('corridorFloorCells covers both ends of the display→fix walk', () => {
|
||
const cells = corridorFloorCells([{ lat: 30.200, lon: -97.660 }, { lat: 30.203, lon: -97.660 }]);
|
||
assert.deepEqual(cells[0], { lat: 30.2, lon: -97.66 });
|
||
assert.deepEqual(cells[cells.length - 1], { lat: 30.203, lon: -97.66 });
|
||
});
|
||
|
||
test('corridorFloorCells has no gaps along a multi-cell walk', () => {
|
||
// ~330 m north = 3 coarse cells.
|
||
const lats = corridorFloorCells([{ lat: 30.200, lon: -97.660 }, { lat: 30.203, lon: -97.660 }]).map((c) => c.lat);
|
||
assert.deepEqual(lats, [30.2, 30.201, 30.202, 30.203]);
|
||
});
|
||
|
||
test('corridorFloorCells dedupes when both ends share one cell', () => {
|
||
const cells = corridorFloorCells([{ lat: 30.2001, lon: -97.6601 }, { lat: 30.2004, lon: -97.6604 }]);
|
||
assert.deepEqual(cells, [{ lat: 30.2, lon: -97.66 }]);
|
||
});
|
||
|
||
test('corridorFloorCells is bounded on a long walk (never unbounded)', () => {
|
||
const cells = corridorFloorCells([{ lat: 30.2, lon: -97.66 }, { lat: 31.2, lon: -96.66 }]);
|
||
assert.ok(cells.length <= CORRIDOR_MAX_CELLS, `bounded, got ${cells.length}`);
|
||
});
|
||
|
||
test('corridorFloorCells returns the start cell alone for a bad endpoint', () => {
|
||
assert.deepEqual(corridorFloorCells([{ lat: 30.2, lon: -97.66 }, { lat: NaN, lon: -97.66 }]), [{ lat: 30.2, lon: -97.66 }]);
|
||
assert.deepEqual(corridorFloorCells([{ lat: 30.2, lon: -97.66 }, { lat: null, lon: null }]), [{ lat: 30.2, lon: -97.66 }]);
|
||
});
|
||
|
||
test('corridorFloorCells returns nothing for a bad start', () => {
|
||
assert.deepEqual(corridorFloorCells([{ lat: NaN, lon: -97.66 }, { lat: 30.2, lon: -97.66 }]), []);
|
||
});
|
||
|
||
// --- F5: contiguous prefix + guaranteed endpoint ---------------------------
|
||
// The first cut capped the interpolation at 20 half-cell steps, so past ~1.1 km
|
||
// the walk sampled SPARSELY: cells between the samples were skipped and the
|
||
// destination could be dropped entirely. A corridor with holes warms ground the
|
||
// contact never crosses while leaving ground it does cross cold.
|
||
|
||
test('corridorFloorCells: 3 km corridor keeps a gap-free prefix', () => {
|
||
// ~3.3 km north.
|
||
const cells = corridorFloorCells([{ lat: 30.200, lon: -97.660 }, { lat: 30.230, lon: -97.660 }]);
|
||
assert.ok(cells.length <= CORRIDOR_MAX_CELLS, `bounded, got ${cells.length}`);
|
||
// Every prefix cell is exactly one cell on from the previous — no holes.
|
||
const prefix = cells.slice(0, -1);
|
||
for (let i = 1; i < prefix.length; i++) {
|
||
const step = Math.round((prefix[i].lat - prefix[i - 1].lat) * 1000);
|
||
assert.equal(step, 1, `hole between ${JSON.stringify(prefix[i - 1])} and ${JSON.stringify(prefix[i])}`);
|
||
}
|
||
});
|
||
|
||
test('corridorFloorCells: the destination cell always survives truncation', () => {
|
||
const cells = corridorFloorCells([{ lat: 30.200, lon: -97.660 }, { lat: 30.230, lon: -97.660 }]);
|
||
assert.deepEqual(cells[cells.length - 1], { lat: 30.23, lon: -97.66 },
|
||
'the far end is where the contact is headed — it must be warmed, not dropped');
|
||
});
|
||
|
||
test('corridorFloorCells: a diagonal 3 km corridor is still gap-free', () => {
|
||
const cells = corridorFloorCells([{ lat: 30.200, lon: -97.660 }, { lat: 30.220, lon: -97.640 }]);
|
||
const prefix = cells.slice(0, -1);
|
||
for (let i = 1; i < prefix.length; i++) {
|
||
const dLat = Math.abs(Math.round((prefix[i].lat - prefix[i - 1].lat) * 1000));
|
||
const dLon = Math.abs(Math.round((prefix[i].lon - prefix[i - 1].lon) * 1000));
|
||
assert.ok(dLat <= 1 && dLon <= 1 && (dLat + dLon) > 0,
|
||
`non-adjacent step ${JSON.stringify(prefix[i - 1])} -> ${JSON.stringify(prefix[i])}`);
|
||
}
|
||
});
|
||
|
||
// --- F6: cell-boundary hysteresis ------------------------------------------
|
||
// A dead-reckoned position jittering across a 0.001° edge would otherwise pick
|
||
// a different cell — and so a different floor — at 12 Hz, popping the sprite
|
||
// between two heights.
|
||
|
||
test('stickyFloorCell picks the containing cell with no previous cell', () => {
|
||
assert.deepEqual(stickyFloorCell(30.2004, -97.6604, null), { lat: 30.2, lon: -97.66 });
|
||
});
|
||
|
||
test('stickyFloorCell holds the previous cell for a jitter-sized excursion', () => {
|
||
const prev = { lat: 30.2, lon: -97.66 };
|
||
// 30.2006 belongs to cell 30.201, but it is only ~11 m past the boundary.
|
||
assert.deepEqual(stickyFloorCell(30.2006, -97.66, prev), prev);
|
||
assert.deepEqual(stickyFloorCell(30.1994, -97.66, prev), prev);
|
||
});
|
||
|
||
test('stickyFloorCell releases once the contact is properly into the next cell', () => {
|
||
const prev = { lat: 30.2, lon: -97.66 };
|
||
assert.deepEqual(stickyFloorCell(30.2011, -97.66, prev), { lat: 30.201, lon: -97.66 });
|
||
});
|
||
|
||
test('stickyFloorCell holds across a diagonal corner excursion', () => {
|
||
const prev = { lat: 30.2, lon: -97.66 };
|
||
assert.deepEqual(stickyFloorCell(30.2006, -97.6606, prev), prev);
|
||
});
|
||
|
||
test('stickyFloorCell does not stick to a far-away stale cell', () => {
|
||
const prev = { lat: 30.1, lon: -97.66 };
|
||
assert.deepEqual(stickyFloorCell(30.2004, -97.6604, prev), { lat: 30.2, lon: -97.66 });
|
||
});
|
||
|
||
// --- Antimeridian ------------------------------------------------------------
|
||
// Raw longitude arithmetic walks a segment across 180° the long way round and
|
||
// reads a contact on the seam as half a world from its previous cell.
|
||
|
||
test('corridorFloorCells walks across the antimeridian, not the long way round', () => {
|
||
for (const [from, to, across, behind] of [
|
||
[179.999, -179.998, -179.999, 179.998],
|
||
[-179.999, 179.998, 179.999, -179.998],
|
||
]) {
|
||
const cells = corridorFloorCells([{ lat: -16.69, lon: from }, { lat: -16.69, lon: to }]);
|
||
const lons = cells.map((c) => c.lon);
|
||
assert.equal(lons[0], from);
|
||
assert.equal(lons.at(-1), to);
|
||
assert.ok(lons.includes(across), `crosses through ${across}: ${lons}`);
|
||
assert.ok(!lons.includes(behind), `never walks back through ${behind}: ${lons}`);
|
||
assert.ok(lons.every((lon) => Math.abs(lon) <= 180), `normalised: ${lons}`);
|
||
}
|
||
});
|
||
|
||
test('stickyFloorCell holds across the antimeridian', () => {
|
||
for (const [prevLon, lon] of [[180, -179.9998], [-180, 179.9998]]) {
|
||
const prev = { lat: -16.69, lon: prevLon };
|
||
assert.deepEqual(stickyFloorCell(-16.69, lon, prev), prev);
|
||
}
|
||
});
|
||
|
||
// --- F2: corridor budget fairness ------------------------------------------
|
||
// The first cut spent the budget in Map insertion order and decremented it
|
||
// BEFORE deduping, so parked contacts (whose corridor is the fix cell the poll
|
||
// already collected) burned slots and the contacts actually outrunning their
|
||
// floor data got nothing.
|
||
|
||
/** Warmth oracle for the budget-policy tests: nothing is warm, so every cell
|
||
* is charged and the allocation order under test is the only variable. */
|
||
const NONE_WARM = () => false;
|
||
|
||
const cellsFrom = (lat, n) => Array.from({ length: n }, (_, i) => ({ lat: +(lat + i * 0.001).toFixed(3), lon: -97.66 }));
|
||
|
||
test('allocateCorridorCells: already-collected cells cost nothing', () => {
|
||
const seen = new Set(['30.2,-97.66']);
|
||
const got = allocateCorridorCells(
|
||
[{ cells: [{ lat: 30.2, lon: -97.66 }], cold: 0, speedMps: 0 }], seen, 4, 4, 0, NONE_WARM,
|
||
);
|
||
assert.deepEqual(got, [], 'a parked contact on a collected cell allocates nothing');
|
||
});
|
||
|
||
test('allocateCorridorCells: a parked contact never crowds out a mover', () => {
|
||
const seen = new Set(['30.2,-97.66']);
|
||
const parked = { cells: [{ lat: 30.2, lon: -97.66 }], cold: 0, speedMps: 0 };
|
||
const mover = { cells: cellsFrom(30.3, 3), cold: 3, speedMps: 12 };
|
||
// Parked FIRST in insertion order — the old bug's shape.
|
||
const got = allocateCorridorCells([parked, mover], seen, 3, 4, 0, NONE_WARM);
|
||
assert.equal(got.length, 3);
|
||
assert.deepEqual(got.map((c) => c.lat), [30.3, 30.301, 30.302]);
|
||
});
|
||
|
||
test('allocateCorridorCells: 85 grounded contacts — the needy are served first', () => {
|
||
// 64 parked contacts whose cells the poll already collected, then 21 movers
|
||
// with cold ground ahead. Insertion-order budgeting starved every mover.
|
||
const seen = new Set();
|
||
const candidates = [];
|
||
for (let i = 0; i < 64; i++) {
|
||
const cell = { lat: +(31 + i * 0.001).toFixed(3), lon: -97.66 };
|
||
seen.add(`${cell.lat},${cell.lon}`);
|
||
candidates.push({ cells: [cell], cold: 0, speedMps: 0 });
|
||
}
|
||
const movers = [];
|
||
for (let i = 0; i < 21; i++) {
|
||
const cells = cellsFrom(32 + i, 4);
|
||
movers.push(cells);
|
||
candidates.push({ cells, cold: 4, speedMps: 10 });
|
||
}
|
||
const got = allocateCorridorCells(candidates, seen, 64, 4, 0, NONE_WARM);
|
||
const gotKeys = new Set(got.map((c) => `${c.lat},${c.lon}`));
|
||
// Demand (21 x 4 = 84) exceeds the 64-cell budget, so not every mover can be
|
||
// filled — but none may be left with NOTHING, and no slot may go to a parked
|
||
// contact whose cell the poll already had.
|
||
for (const cells of movers) {
|
||
assert.ok(gotKeys.has(`${cells[0].lat},${cells[0].lon}`),
|
||
`mover at ${cells[0].lat} was starved outright`);
|
||
}
|
||
assert.equal(got.length, 64, 'budget is spent, and spent entirely on movers');
|
||
for (const cell of got) {
|
||
assert.ok(cell.lat >= 32, `slot wasted on an already-collected parked cell (${cell.lat})`);
|
||
}
|
||
});
|
||
|
||
test('allocateCorridorCells: fair share caps a long corridor in the first pass', () => {
|
||
const seen = new Set();
|
||
const hog = { cells: cellsFrom(30, 12), cold: 12, speedMps: 30 };
|
||
const small = { cells: cellsFrom(40, 2), cold: 2, speedMps: 5 };
|
||
const got = allocateCorridorCells([hog, small], seen, 6, 4, 0, NONE_WARM);
|
||
const lats = got.map((c) => c.lat);
|
||
assert.ok(lats.includes(40) && lats.includes(40.001),
|
||
'the small corridor is served before the hog takes seconds');
|
||
});
|
||
|
||
test('allocateCorridorCells: leftover budget goes to the neediest in pass two', () => {
|
||
const seen = new Set();
|
||
const hog = { cells: cellsFrom(30, 8), cold: 8, speedMps: 30 };
|
||
const small = { cells: cellsFrom(40, 1), cold: 1, speedMps: 5 };
|
||
const got = allocateCorridorCells([hog, small], seen, 8, 4, 0, NONE_WARM);
|
||
assert.equal(got.length, 8);
|
||
assert.ok(got.filter((c) => c.lat >= 30 && c.lat < 31).length === 7, 'hog gets the remainder');
|
||
});
|
||
|
||
test('allocateCorridorCells: mutates `seen` so the caller stays deduped', () => {
|
||
const seen = new Set();
|
||
allocateCorridorCells([{ cells: cellsFrom(30, 2), cold: 2, speedMps: 1 }], seen, 4, 4, 0, NONE_WARM);
|
||
assert.ok(seen.has('30,-97.66') && seen.has('30.001,-97.66'));
|
||
});
|
||
|
||
test('allocateCorridorCells: no budget, no allocation', () => {
|
||
assert.deepEqual(allocateCorridorCells([{ cells: cellsFrom(30, 2), cold: 2, speedMps: 1 }], new Set(), 0, 4, 0, NONE_WARM), []);
|
||
assert.deepEqual(allocateCorridorCells([], new Set(), 10, 4, 0, NONE_WARM), []);
|
||
});
|
||
|
||
// --- F1: the corridor covers the ARC, not the chord -------------------------
|
||
|
||
test('corridorFloorCells walks a turning path\'s arc, not the chord across it', () => {
|
||
const path = corridorPathLatLon({
|
||
extrapolating: true,
|
||
displayLat: 30.200, displayLon: -97.660, courseDeg: 0, speedMps: 12, turnRateDps: 3,
|
||
fixLat: 30.199, fixLon: -97.660, lookaheadSec: 30,
|
||
});
|
||
const arcCells = corridorFloorCells(path);
|
||
const chordCells = corridorFloorCells([path[0], path[path.length - 1]]);
|
||
const chordKeys = new Set(chordCells.map((c) => `${c.lat},${c.lon}`));
|
||
const offChord = arcCells.filter((c) => !chordKeys.has(`${c.lat},${c.lon}`));
|
||
assert.ok(offChord.length > 0,
|
||
'the turn leaves the chord — those cells are the ground a straight corridor left cold');
|
||
});
|
||
|
||
test('corridorFloorCells keeps a multi-leg path gap-free', () => {
|
||
const path = corridorPathLatLon({
|
||
extrapolating: true,
|
||
displayLat: 30.200, displayLon: -97.660, courseDeg: 0, speedMps: 12, turnRateDps: 2,
|
||
fixLat: 30.199, fixLon: -97.660, lookaheadSec: 40,
|
||
});
|
||
const cells = corridorFloorCells(path);
|
||
const prefix = cells.slice(0, -1);
|
||
for (let i = 1; i < prefix.length; i++) {
|
||
const dLat = Math.abs(Math.round((prefix[i].lat - prefix[i - 1].lat) * 1000));
|
||
const dLon = Math.abs(Math.round((prefix[i].lon - prefix[i - 1].lon) * 1000));
|
||
assert.ok(dLat <= 1 && dLon <= 1 && (dLat + dLon) > 0,
|
||
`non-adjacent step ${JSON.stringify(prefix[i - 1])} -> ${JSON.stringify(prefix[i])}`);
|
||
}
|
||
});
|
||
|
||
// --- F2-shifted: a tie larger than the budget must not starve its tail ------
|
||
// Need-ranking alone left the stable ranked tail with nothing, poll after poll:
|
||
// starvation moved rather than being removed. The epoch rotates equal-ranked
|
||
// runs so the tail cycles in.
|
||
|
||
test('allocateCorridorCells: the epoch rotates a tie, so a different prefix leads each poll', () => {
|
||
const cellsFor = (i) => [{ lat: +(50 + i).toFixed(3), lon: -97.66 }];
|
||
const candidates = Array.from({ length: 5 }, (_, i) => ({ cells: cellsFor(i), cold: 1, speedMps: 10 }));
|
||
const first = (epoch) => allocateCorridorCells(candidates, new Set(), 1, 4, epoch, NONE_WARM)[0].lat;
|
||
const leaders = new Set([first(0), first(1), first(2)]);
|
||
assert.equal(leaders.size, 3, 'the same contact led every poll — the tie never cycles');
|
||
});
|
||
|
||
test('allocateCorridorCells: rotation never demotes a needier contact below a less needy one', () => {
|
||
const needy = { cells: [{ lat: 60, lon: -97.66 }], cold: 4, speedMps: 1 };
|
||
const idle = { cells: [{ lat: 61, lon: -97.66 }], cold: 1, speedMps: 99 };
|
||
for (let epoch = 0; epoch < 6; epoch++) {
|
||
const got = allocateCorridorCells([idle, needy], new Set(), 1, 4, epoch, NONE_WARM);
|
||
assert.equal(got[0].lat, 60, `epoch ${epoch} served the less needy contact first`);
|
||
}
|
||
});
|
||
|
||
// --- F1: the arc walk must be CELL-COMPLETE ---------------------------------
|
||
// Six FIXED samples were fine at taxi speed and coarse at speed, and a short
|
||
// leg rounds to a single walk step — so the collector degenerated to testing
|
||
// the sample points themselves and skipped cells the arc genuinely crosses.
|
||
// Concrete miss: 5 m/s, 0.4 deg/s, 60 s from (30.200, -97.660) passes through
|
||
// 30.202,-97.659 for ~4.4 s (~22 m of ground) and never sampled it.
|
||
|
||
const TURNING_COAST = {
|
||
extrapolating: true,
|
||
displayLat: 30.200, displayLon: -97.660,
|
||
courseDeg: 0, speedMps: 5, turnRateDps: 0.4,
|
||
fixLat: 30.199, fixLon: -97.660,
|
||
lookaheadSec: 60,
|
||
};
|
||
|
||
test('corridorFloorCells covers the cell a slow sustained turn actually occupies', () => {
|
||
const cells = corridorFloorCells(corridorPathLatLon(TURNING_COAST));
|
||
const keys = new Set(cells.map((c) => `${c.lat},${c.lon}`));
|
||
assert.ok(keys.has('30.202,-97.659'),
|
||
`the arc sits in 30.202,-97.659 for ~22 m; collected ${JSON.stringify(cells)}`);
|
||
});
|
||
|
||
test('corridorFloorCells: every cell the sampled arc passes through is collected', () => {
|
||
// Independent oracle: re-walk the arc at 2 m granularity and demand that any
|
||
// cell it occupies for a meaningful stretch is in the corridor.
|
||
const cells = corridorFloorCells(corridorPathLatLon(TURNING_COAST));
|
||
const keys = new Set(cells.map((c) => `${c.lat},${c.lon}`));
|
||
const dwell = new Map();
|
||
const stepSec = 60 / 500;
|
||
for (let i = 0; i <= 500; i++) {
|
||
const p = projectGroundArcLatLon(
|
||
TURNING_COAST.displayLat, TURNING_COAST.displayLon, TURNING_COAST.courseDeg,
|
||
TURNING_COAST.speedMps, TURNING_COAST.turnRateDps, (60 * i) / 500,
|
||
);
|
||
const c = coarseFloorCoord(p.lat, p.lon);
|
||
const key = `${c.lat},${c.lon}`;
|
||
dwell.set(key, (dwell.get(key) || 0) + stepSec * TURNING_COAST.speedMps);
|
||
}
|
||
// The walk guarantees cells occupied for at least a step's worth of ground.
|
||
const guaranteedM = CORRIDOR_WALK_STEP_DEG * 111320;
|
||
const missed = [...dwell.entries()]
|
||
.filter(([key, metres]) => metres >= guaranteedM * 1.5 && !keys.has(key))
|
||
.map(([key]) => key);
|
||
assert.deepEqual(missed, [], `cells the arc occupies but the corridor missed: ${missed.join(' ')}`);
|
||
});
|
||
|
||
test('corridorPathLatLon spacing stays cell-sized as speed rises', () => {
|
||
const fast = corridorPathLatLon({ ...TURNING_COAST, speedMps: 25, turnRateDps: 0 });
|
||
for (let i = 1; i < fast.length; i++) {
|
||
const dM = Math.hypot(
|
||
(fast[i].lat - fast[i - 1].lat) * 111320,
|
||
(fast[i].lon - fast[i - 1].lon) * 111320 * Math.cos(30.2 * Math.PI / 180),
|
||
);
|
||
assert.ok(dM <= CORRIDOR_SAMPLE_SPACING_M + 1, `sample gap ${dM.toFixed(1)} m`);
|
||
}
|
||
});
|
||
|
||
test('corridorPathLatLon truncates a very long arc instead of thinning it', () => {
|
||
const path = corridorPathLatLon({ ...TURNING_COAST, speedMps: 120, lookaheadSec: 60 });
|
||
const end = path[path.length - 1];
|
||
const lenM = Math.hypot(
|
||
(end.lat - TURNING_COAST.displayLat) * 111320,
|
||
(end.lon - TURNING_COAST.displayLon) * 111320 * Math.cos(30.2 * Math.PI / 180),
|
||
);
|
||
assert.ok(lenM <= CORRIDOR_MAX_LENGTH_M + 50, `projected ${lenM.toFixed(0)} m`);
|
||
});
|
||
|
||
// --- F2: fairness under PRODUCTION `seen` semantics -------------------------
|
||
// Production's `seen` holds only the CURRENT poll's batch, so a cell warmed on
|
||
// an earlier poll looked new and was charged again — a contact's near cells
|
||
// could eat its whole share every poll while the ground ahead stayed cold, and
|
||
// the ranked tail waited far longer than the policy implies.
|
||
|
||
test('allocateCorridorCells: warm cells are emitted but never charged', () => {
|
||
const warm = new Set(['30,-97.66']);
|
||
const cells = [{ lat: 30, lon: -97.66 }, { lat: 30.001, lon: -97.66 }];
|
||
const got = allocateCorridorCells(
|
||
[{ cells, cold: 1, speedMps: 5 }], new Set(), 1, 4, 0,
|
||
(c) => warm.has(`${c.lat},${c.lon}`),
|
||
);
|
||
assert.equal(got.length, 2, 'the warm cell still reaches the mesh sampler');
|
||
assert.deepEqual(got.map((c) => c.lat), [30, 30.001]);
|
||
});
|
||
|
||
test('allocateCorridorCells: a warm near-cell cannot eat the budget every poll', () => {
|
||
const warm = new Set(['30,-97.66']);
|
||
const hogWarmPrefix = { cells: [{ lat: 30, lon: -97.66 }, { lat: 30.001, lon: -97.66 }], cold: 1, speedMps: 5 };
|
||
const other = { cells: [{ lat: 45, lon: -97.66 }], cold: 1, speedMps: 5 };
|
||
const got = allocateCorridorCells(
|
||
[hogWarmPrefix, other], new Set(), 2, 1, 0,
|
||
(c) => warm.has(`${c.lat},${c.lon}`),
|
||
);
|
||
const lats = got.map((c) => c.lat);
|
||
assert.ok(lats.includes(30.001), 'the cold cell BEHIND the warm one is still reached');
|
||
assert.ok(lats.includes(45), 'and the other contact is still served');
|
||
});
|
||
|
||
test('allocateCorridorCells: 80 movers are served within the bound the policy implies', () => {
|
||
const CELLS_EACH = 4;
|
||
const BUDGET = 64;
|
||
const COUNT = 80;
|
||
// Round one charges at most one cell per candidate, so >= min(COUNT, BUDGET)
|
||
// candidates are served per poll and an unserved candidate outranks every
|
||
// served one next poll. Bound falls straight out of that.
|
||
const boundPolls = Math.ceil(COUNT / BUDGET);
|
||
const movers = [];
|
||
for (let i = 0; i < COUNT; i++) {
|
||
movers.push(Array.from(
|
||
{ length: CELLS_EACH },
|
||
(_, k) => ({ lat: +(40 + i + k * 0.001).toFixed(3), lon: -97.66 }),
|
||
));
|
||
}
|
||
const warmed = new Set();
|
||
const servedPoll = new Map();
|
||
for (let poll = 0; poll < boundPolls; poll++) {
|
||
const candidates = movers.map((cells) => ({
|
||
cells,
|
||
cold: cells.filter((c) => !warmed.has(`${c.lat},${c.lon}`)).length,
|
||
speedMps: 10,
|
||
}));
|
||
// PRODUCTION SEMANTICS: `seen` starts empty every poll — it is this poll's
|
||
// batch, not a memory of what has already warmed.
|
||
const got = allocateCorridorCells(
|
||
candidates, new Set(), BUDGET, 4, poll,
|
||
(c) => warmed.has(`${c.lat},${c.lon}`),
|
||
);
|
||
for (const c of got) warmed.add(`${c.lat},${c.lon}`);
|
||
movers.forEach((cells, i) => {
|
||
if (servedPoll.has(i)) return;
|
||
if (cells.some((c) => warmed.has(`${c.lat},${c.lon}`))) servedPoll.set(i, poll);
|
||
});
|
||
}
|
||
const starved = movers.map((_, i) => i).filter((i) => !servedPoll.has(i));
|
||
assert.deepEqual(starved, [],
|
||
`unserved after the ceil(${COUNT}/${BUDGET}) = ${boundPolls}-poll bound: ${starved.join(',')}`);
|
||
});
|
||
|
||
test('allocateCorridorCells: every mover reaches its FULL corridor in bounded polls', () => {
|
||
const CELLS_EACH = 4;
|
||
const BUDGET = 64;
|
||
const COUNT = 80;
|
||
// Total cold demand / budget, rounded up — the work simply cannot finish
|
||
// sooner, and the policy must not take longer.
|
||
const boundPolls = Math.ceil((COUNT * CELLS_EACH) / BUDGET);
|
||
const movers = [];
|
||
for (let i = 0; i < COUNT; i++) {
|
||
movers.push(Array.from(
|
||
{ length: CELLS_EACH },
|
||
(_, k) => ({ lat: +(40 + i + k * 0.001).toFixed(3), lon: -97.66 }),
|
||
));
|
||
}
|
||
const warmed = new Set();
|
||
for (let poll = 0; poll < boundPolls; poll++) {
|
||
const candidates = movers.map((cells) => ({
|
||
cells,
|
||
cold: cells.filter((c) => !warmed.has(`${c.lat},${c.lon}`)).length,
|
||
speedMps: 10,
|
||
}));
|
||
const got = allocateCorridorCells(
|
||
candidates, new Set(), BUDGET, 4, poll,
|
||
(c) => warmed.has(`${c.lat},${c.lon}`),
|
||
);
|
||
for (const c of got) warmed.add(`${c.lat},${c.lon}`);
|
||
}
|
||
const incomplete = movers
|
||
.map((cells, i) => [i, cells.filter((c) => !warmed.has(`${c.lat},${c.lon}`)).length])
|
||
.filter(([, cold]) => cold > 0);
|
||
assert.deepEqual(incomplete, [],
|
||
`corridors still cold after the ceil(${COUNT * CELLS_EACH}/${BUDGET}) = ${boundPolls}-poll bound`);
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// neighborFloorM (2026-08-21 terrain-outage round). The Re:Earth proxy timed
|
||
// out four times in a row and a grounded contact rendered at the geoid, under
|
||
// the mesh it was parked on. An adjacent resolved cell is the measured
|
||
// stand-in the flights display-floor hold borrows when its own cell is cold.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
test('neighborFloorM leans to the LOWEST resolved neighbour', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.201, -97.66, 141);
|
||
reportMeshFloorCell(30.199, -97.66, 152);
|
||
reportMeshFloorCell(30.2, -97.659, 145);
|
||
// Flat-ish ground: the neighbours are within metres and the choice barely
|
||
// matters. It matters at a structure edge, which the next case covers.
|
||
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), 141);
|
||
});
|
||
|
||
test('neighborFloorM takes the apron, not the roof, at a structure edge', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.201, -97.66, 120); // the apron
|
||
reportMeshFloorCell(30.199, -97.66, 205); // the terminal roof next door
|
||
// A parked contact is on the apron; it is never on the roof. Leaning high
|
||
// here is what put planes in midair at gates during the owner playtest.
|
||
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), 120);
|
||
});
|
||
|
||
test('neighborFloorM refuses a LONE neighbour — one reading cannot be checked', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.199, -97.66, 205); // could be a roof; nothing to compare it to
|
||
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), null,
|
||
'refusing leaves the contact unclamped, which is inert — borrowing floats it permanently');
|
||
// A second reading makes the pair comparable, and the low one is the ground.
|
||
reportMeshFloorCell(30.201, -97.66, 120);
|
||
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), 120);
|
||
});
|
||
|
||
test('neighborFloorM is null when nothing around the cell has resolved', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), null);
|
||
assert.equal(neighborFloorM(null), null);
|
||
assert.equal(neighborFloorM({ lat: Number.NaN, lon: -97.66 }), null);
|
||
});
|
||
|
||
test('neighborFloorM never counts the cell itself — it exists because that one is cold', () => {
|
||
_clearMeshFloorCellsForTest();
|
||
setMeshFloorPreferred(true);
|
||
reportMeshFloorCell(30.2, -97.66, 500);
|
||
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), null);
|
||
});
|