fix(infrastructure): declutter stems and correct close-up sizing

This commit is contained in:
Bilawal Sidhu authored and Sameh Khamis committed 2026-09-11 08:59:32 -07:00
1 parent c7ef01827d
commit 6dd00d1e76
7 files changed
+1505 -4

No files matched your search

+4
View File
@@ -13,6 +13,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Datacenter and dam marker stems use bounded, zoom-dependent active sets with
stable selection during camera motion. Close-up stems scale to the actual
camera distance; source totals and submarine cables remain unchanged.
- Keyboard focus rings now survive active/selected button styles across the
interface. Visual Styles, Location cities and points of interest, search,
Context/mission actions, Cockpit utilities, and sliders retain a distinct
+11
View File
@@ -1,5 +1,16 @@
# God's Eye View Current State
## Infrastructure marker visibility
Datacenters and dams retain their full datasets while limiting active marker
stems per layer: 80 at camera heights of 3,000 km or above, 200 from 200 km,
and 420 below 200 km. Selection favors existing label priority and nearby
features, retains stable choices across small camera movements, and refreshes
during continuous motion. Disable/re-enable resets selection cleanly.
Close-up stem sizing uses camera-to-feature distance without a 5 km minimum.
The submarine-cable renderer is unchanged. These limits bound active stem work;
they do not reduce the materialized entity count or establish an FPS gain.
## Keyboard interaction and focus
- Enter on the map-source disclosure opens immediately. A short Space press
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env node
/**
* qa-infra-lod — browser checks for the local-infrastructure LOD declutter.
*
* Datacenters and dams use a bounded active-stem set. Cables are loaded for
* a representative combined view but use their own unchanged renderer.
*
* Two jobs:
* 1. GATE — with datacenters + dams enabled:
* a. at a full-earth camera, each layer's getLodDiagnostics() reports
* computed===true, active<=budgetLimit, budgetLimit===INFRA_LOD_ACTIVE_MIN,
* and active<total (the declutter is actually engaged);
* b. flying to a city lifts budgetLimit to INFRA_LOD_ACTIVE_MAX and the
* active count grows (or pins to the in-view count);
* c. the selection does not churn between camera moves.
* 2. MEASURE — actual postRender frame intervals over a ~10 s driven orbit at
* global zoom; --control disables infra. This is not a before/after test.
* Report the renderer; software-GL timings are relative-only evidence.
*
* Visual proof saved to qa-shots/ (gitignored).
*
* Usage: node scripts/qa-infra-lod.mjs [--url http://localhost:5180] [--control] [--headful]
* Requires a running dev server with the bundled local_data present. Headless;
* rAF throttling disabled so the frame clock is honest. Exits non-zero on any
* FAIL. Commits nothing.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
INFRA_LOD_ACTIVE_MIN,
INFRA_LOD_ACTIVE_MAX,
} from '../src/data/localGeojsonLod.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const SHOTS_DIR = process.env.QA_SHOTS_DIR || path.join(REPO_ROOT, 'qa-shots');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:5180');
const CONTROL = argv.includes('--control');
const HEADFUL = argv.includes('--headful');
const INFRA_LAYER_IDS = ['local-datacenters', 'local-dams'];
const CABLE_LAYER_ID = 'telegeography-submarine-cables';
const results = [];
function check(name, pass, detail) {
results.push({ name, pass });
const tag = pass ? 'PASS' : 'FAIL';
console.log(` [${tag}] ${name}${detail !== undefined ? ` — ${JSON.stringify(detail)}` : ''}`);
}
function report(name, detail) {
console.log(` [MEAS] ${name} — ${JSON.stringify(detail)}`);
}
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
protocolTimeout: 300_000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--window-size=1440,900',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
],
});
try {
const page = await browser.newPage();
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(String(error.message).replace(/https?:\/\/\S+/g, '[URL]')));
await page.setViewport({ width: 1440, height: 860 });
const testUrl = new URL(APP_URL);
testUrl.searchParams.set('welcome', '0');
await page.goto(testUrl.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__godsEyeView?.styleManager
&& document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 90_000 });
check('first-run chooser does not cover the test surface', await page.evaluate(() => (
!document.querySelector('#first-run-launcher:not([hidden])')
)));
report('renderer', await page.evaluate(() => {
const gl = window.__godsEyeView.viewer.scene.context._gl;
const debug = gl.getExtension('WEBGL_debug_renderer_info');
return debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
}));
// Park at a full-earth view and disable every layer so infra is measured
// in isolation.
await page.evaluate(async () => {
const gev = window.__godsEyeView;
const v = gev.viewer;
v.camera.cancelFlight();
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
longitude: 0, latitude: 15 * Math.PI / 180, height: 24_000_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
for (const [id, entry] of gev.dataManager.layers) {
if (entry.enabled) {
try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* measured via counts */ }
}
}
});
await new Promise((r) => setTimeout(r, 4_000));
// Enable the infrastructure layers (skipped in --control so the orbit cost
// measures the empty scene for attribution).
if (!CONTROL) {
const loaded = await page.evaluate(async (infraIds, cableId) => {
const gev = window.__godsEyeView;
const ids = [...infraIds, cableId];
for (const id of ids) {
try { await gev.dataManager.setEnabled(id, true, { origin: 'user' }); } catch { /* reported below */ }
}
const deadline = performance.now() + 60_000;
const stat = (id) => gev.dataManager.layers.get(id)?.module?.getStats?.() || {};
while (performance.now() < deadline) {
// Every id, cables included. The cable layer's enable() only starts
// `void load()`, and the manager's immediate update() returns early
// because `_loading` is already true — so waiting on datacenters and
// dams alone releases the measurement before ~2,600 cable references
// exist, and the advertised three-layer frame cost underreports.
const done = ids.every((id) => {
const s = stat(id);
return (s.count || 0) > 0 || s.error;
});
if (done) break;
gev.viewer.scene.requestRender?.();
await new Promise((r) => setTimeout(r, 200));
}
const out = {};
for (const id of ids) out[id] = stat(id);
return out;
}, INFRA_LAYER_IDS, CABLE_LAYER_ID);
report('layer load stats', loaded);
for (const id of [...INFRA_LAYER_IDS, CABLE_LAYER_ID]) {
check(`${id} loaded its bundled dataset`, (loaded[id]?.count || 0) > 0, loaded[id]);
}
// Nudge a render pass so the first post-enable LOD walk runs.
await page.evaluate(() => window.__godsEyeView?.viewer?.scene?.requestRender?.());
await new Promise((r) => setTimeout(r, 2_000));
}
// ── GATE a: full-earth budget engaged ─────────────────────────────────
const readLod = (label) => page.evaluate((infraIds) => {
const gev = window.__godsEyeView;
const out = {};
for (const id of infraIds) {
out[id] = gev.dataManager.layers.get(id)?.module?.getLodDiagnostics?.() || null;
}
return out;
}, INFRA_LAYER_IDS).then((lod) => { report(label, lod); return lod; });
if (!CONTROL) {
const globalLod = await readLod('getLodDiagnostics @ full-earth');
for (const id of INFRA_LAYER_IDS) {
const d = globalLod[id];
check(`${id}: LOD selection has run`, !!d && d.computed === true, d);
check(`${id}: active stems within the band budget`, !!d && d.active <= d.budgetLimit, d);
check(`${id}: full-earth band budget is INFRA_LOD_ACTIVE_MIN`,
!!d && d.budgetLimit === INFRA_LOD_ACTIVE_MIN, { got: d?.budgetLimit, want: INFRA_LOD_ACTIVE_MIN });
// Every bound above is satisfied by an EMPTY active set: `computed` is
// true, `0 <= budgetLimit`, and `0 < total`. A regressed candidate build
// or occlusion test would therefore pass the gate while both layers
// render nothing, so assert the selection is actually populated.
check(`${id}: active set is non-empty`, !!d && d.active > 0, d);
// "declutter engaged" only asserts when the dataset actually exceeds the
// cap — a tiny dataset legitimately shows every feature. This fixture is
// a full-earth view of a global dataset an order of magnitude past the
// global band, so the only correct outcome is a cap-filled set.
if (d && d.total > d.budgetLimit) {
check(`${id}: declutter is engaged (active < total)`, d.active < d.total, d);
check(`${id}: full-earth active set fills the band budget`,
d.active === d.budgetLimit, { active: d.active, budgetLimit: d.budgetLimit });
}
}
// ── GATE b: zoom in widens the budget ───────────────────────────────
await page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const ell = v.scene.globe.ellipsoid;
v.camera.cancelFlight();
v.camera.setView({
destination: ell.cartographicToCartesian({
longitude: -97.74 * Math.PI / 180, latitude: 30.27 * Math.PI / 180, height: 55_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 3, roll: 0 },
});
// setView does not fire moveEnd; raise the LOD recompute trigger.
v.camera.moveEnd.raiseEvent?.();
v.scene.requestRender?.();
});
await new Promise((r) => setTimeout(r, 3_000));
const regionalLod = await readLod('getLodDiagnostics @ city');
for (const id of INFRA_LAYER_IDS) {
const g = globalLod[id];
const r = regionalLod[id];
check(`${id}: city band lifts the budget to INFRA_LOD_ACTIVE_MAX`,
!!r && r.budgetLimit === INFRA_LOD_ACTIVE_MAX, { got: r?.budgetLimit, want: INFRA_LOD_ACTIVE_MAX });
// `>=` alone accepts 0 >= 0, so the non-shrink check needs a floor too.
check(`${id}: active set did not shrink when zooming in`,
!!r && !!g && r.active > 0 && r.active >= Math.min(g.active, r.total),
{ global: g?.active, regional: r?.active });
}
// ── GATE c: no churn between camera moves ───────────────────────────
const churn = await page.evaluate(async (infraIds) => {
const gev = window.__godsEyeView;
const snap = () => infraIds.map((id) => {
const visibleIds = [];
for (let i = 0; i < gev.viewer.dataSources.length; i++) {
for (const entity of gev.viewer.dataSources.get(i).entities.values) {
if (entity.__localLayerId === id && entity.show) visibleIds.push(String(entity.id));
}
}
return visibleIds.sort();
});
const before = snap();
for (let i = 0; i < 5; i++) {
gev.viewer.scene.requestRender?.();
await new Promise((r) => setTimeout(r, 120));
}
return { before, after: snap() };
}, INFRA_LAYER_IDS);
check('the same visible IDs remain stable without a camera move',
churn.before.every((ids) => ids.length > 0)
&& JSON.stringify(churn.before) === JSON.stringify(churn.after),
{ beforeCounts: churn.before.map((ids) => ids.length), afterCounts: churn.after.map((ids) => ids.length) });
// Back to full-earth for the frame-cost measurement.
await page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
longitude: 0, latitude: 15 * Math.PI / 180, height: 24_000_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
v.camera.moveEnd.raiseEvent?.();
v.scene.requestRender?.();
});
await new Promise((r) => setTimeout(r, 3_000));
}
// Count actual rendered frames: scene.render() can return without drawing.
const frameCost = await page.evaluate(() => new Promise((resolve) => {
const v = window.__godsEyeView.viewer;
const scene = v.scene;
const durations = [];
let frames = 0;
let previous;
const remove = scene.postRender.addEventListener(() => {
const now = performance.now();
if (previous !== undefined) durations.push(now - previous);
previous = now;
frames++;
});
const t0 = performance.now();
const tick = () => {
v.camera.rotateRight(0.0004);
scene.requestRender();
if (performance.now() - t0 < 10_000) requestAnimationFrame(tick);
else {
remove();
const elapsedMs = performance.now() - t0;
durations.sort((a, b) => a - b);
const n = durations.length;
const sum = durations.reduce((s, d) => s + d, 0);
resolve({
frames,
elapsedMs: +elapsedMs.toFixed(1),
meanMs: +(sum / Math.max(1, n)).toFixed(2),
p50Ms: +(durations[Math.floor(n * 0.5)] || 0).toFixed(2),
p95Ms: +(durations[Math.floor(n * 0.95)] || 0).toFixed(2),
maxMs: +(durations[n - 1] || 0).toFixed(2),
effectiveFps: +(frames * 1000 / elapsedMs).toFixed(1),
});
}
};
requestAnimationFrame(tick);
}));
report(CONTROL
? 'rendered-frame intervals, control (infra OFF), 10s orbit'
: 'rendered-frame intervals, infra ON, 10s orbit', frameCost);
// ── parked-idle honesty: the LOD walk must not force continuous render ─
await new Promise((r) => setTimeout(r, 4_000));
const idle = await page.evaluate(() => new Promise((resolve) => {
const scene = window.__godsEyeView.viewer.scene;
let renders = 0;
const remove = scene.postRender.addEventListener(() => { renders += 1; });
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
}));
report(CONTROL
? 'parked idle, control (postRender fires / 5s)'
: 'parked idle with infra ON (postRender fires / 5s)', idle);
if (!CONTROL) {
check('parked idle stays near zero (≤6 / 5s)', idle.renders <= 6, idle);
}
check('no uncaught application errors', pageErrors.length === 0, pageErrors);
try {
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const shot = path.join(SHOTS_DIR, CONTROL ? 'infra-lod-control.png' : 'infra-lod.png');
await page.screenshot({ path: shot });
console.log(` [SHOT] ${shot}`);
} catch (e) {
console.log(` [SHOT] skipped — ${e?.message || e}`);
}
} finally {
await browser.close();
}
const passed = results.filter((r) => r.pass).length;
console.log(`\nqa-infra-lod: ${passed}/${results.length} passed${CONTROL ? ' (control mode — gates skipped)' : ''}`);
console.log(`RESULT: ${passed} passed, ${results.length - passed} failed, 0 skipped`);
process.exit(passed === results.length ? 0 : 1);
+141 -4
View File
@@ -10,6 +10,11 @@ import {
setOverlayEntries,
setOverlaySourceVisible,
} from '../overlays/worldOverlay.js';
import {
selectInfraLod,
applyInfraEvictionGrace,
shouldRecomputeInfraLod,
} from './localGeojsonLod.js';
const DEFAULT_LABEL_MAX = 900;
const DEFAULT_LABEL_GRID_PX = 132;
@@ -305,6 +310,37 @@ export function createLocalGeoJsonLayer({
let _stemGeometryDirty = true;
let _lastVisibilityUpdate = 0;
let _destroyed = false;
/**
* Globe-LOD active set: the record ids allowed to carry a live stem right
* now. This bounds geometry refreshes and ground-sample work to the
* camera-height budget in localGeojsonLod.js. Recomputed only
* when the camera moves (`_stemGeometryDirty`, raised by moveEnd and by the
* motion fallback below). Empty means "not computed yet" — the first
* post-enable walk fills it.
* @type {Set<string>}
*/
let _activeLodIds = new Set();
/** Eviction-grace bookkeeping for `_activeLodIds` (id -> {misses, since}). */
let _lodGraceState = new Map();
/** Budget cap the most recent camera-height band produced (0 until computed). */
let _lastLodBudgetLimit = 0;
/**
* False until the first post-enable preRender walk has run the LOD
* selection. While false the walk treats every record as active (old
* behavior); once true, an EMPTY `_activeLodIds` legitimately means "nothing
* should carry a stem right now" (e.g. every feature occluded), not "not
* computed yet".
*/
let _lodComputed = false;
/**
* Camera position the last LOD selection ran from — the reference the motion
* fallback measures travel against. Preallocated: the walk clones into it,
* so a moving camera allocates nothing per pass. Only meaningful while
* `_lodComputed` is true.
*/
const _lastLodCameraPos = new Cesium.Cartesian3();
/** Last time the motion-fallback probe window opened (or a selection ran). */
let _lastLodProbeMs = Number.NEGATIVE_INFINITY;
let _groundRetryTimer = null;
/** Consecutive self-armed retries since the last grounding/camera motion. */
let _groundRetryArms = 0;
@@ -355,6 +391,11 @@ export function createLocalGeoJsonLayer({
const disableLayer = (viewer) => {
_enabled = false;
clearGroundRetryRender();
_activeLodIds = new Set();
_lodGraceState = new Map();
_lastLodBudgetLimit = 0;
_lodComputed = false;
_lastLodProbeMs = Number.NEGATIVE_INFINITY;
if (_dataSource) _dataSource.show = false;
_overlayPublisher.hide();
clearSelectedEntityContextForLayer(id);
@@ -397,6 +438,21 @@ export function createLocalGeoJsonLayer({
return { count: _count, lastUpdate: _lastUpdate, error: _error };
},
/**
* Globe-LOD state for QA harnesses (scripts/qa-infra-lod.mjs) and tests.
* `active` is how many records currently carry a live stem; `total` is the
* full materialized set. `active < total` means the declutter is engaged.
* `budgetLimit` is the cap the last camera-height band produced; `computed`
* is false until the first post-enable walk has run the selection.
* @returns {{total:number, active:number, budgetLimit:number, computed:boolean}}
*/
getLodDiagnostics: () => ({
total: _stemRecords.length,
active: _activeLodIds.size,
budgetLimit: _lastLodBudgetLimit,
computed: _lodComputed,
}),
enable: async (viewer) => {
if (_destroyed) return;
_enabled = true;
@@ -404,6 +460,11 @@ export function createLocalGeoJsonLayer({
_lastVisibilityUpdate = Number.NEGATIVE_INFINITY;
_groundRetryArms = 0; // fresh give-up budget per enable-cycle
_lastGroundSampleCapability = null;
_activeLodIds = new Set(); // fresh globe-LOD selection per enable-cycle
_lodGraceState = new Map();
_lastLodBudgetLimit = 0;
_lodComputed = false;
_lastLodProbeMs = Number.NEGATIVE_INFINITY;
_overlayPublisher.show();
// 1. Initialize data source
@@ -632,7 +693,29 @@ export function createLocalGeoJsonLayer({
const cameraPos = viewer.camera.positionWC;
if (!cameraPos) return;
// --- Globe-LOD motion fallback. ---
// `_stemGeometryDirty` is otherwise raised only by moveEnd, which a
// tracked-entity follow, Cockpit, route flight, or continuous orbit
// never emits. Left alone the selection stays pinned to the region
// the camera left: the walk below hides those records one by one as
// they pass behind the globe and admits nothing newly visible, so
// the layer bleeds down to sparse-or-empty until motion stops.
// Bounded: rate-limited to a probe window, and only when the camera
// has actually travelled since the last selection.
if (!_stemGeometryDirty && _lodComputed && _stemRecords.length > 0) {
const probe = shouldRecomputeInfraLod({
nowMs: now,
lastProbeMs: _lastLodProbeMs,
movedSqM: Cesium.Cartesian3.distanceSquared(cameraPos, _lastLodCameraPos),
cameraHeightM: viewer.camera.positionCartographic?.height,
});
_lastLodProbeMs = probe.lastProbeMs;
// Re-selecting demands fresh stem geometry: a record admitted
// mid-motion has never had its tip sized for this camera.
if (probe.recompute) _stemGeometryDirty = true;
}
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84, cameraPos);
const visibleOverlayRecords = [];
const refreshStemGeometry = _stemGeometryDirty;
@@ -649,8 +732,62 @@ export function createLocalGeoJsonLayer({
_lastGroundSampleCapability = canSampleGround;
let groundRetryPending = false;
let groundSampleProgress = false;
// --- Globe-LOD: recompute the active-stem set on camera moves. ---
// `_stemGeometryDirty` is set by moveEnd, the first enable, and the
// motion fallback above — exactly when the camera-height budget and
// per-record distances can have changed. Without this, all local
// infrastructure features run stem trig + Cesium property writes on
// every move; with it, only the ~80 (global) to ~420 (regional)
// budgeted records do.
// The occluder test is the same cheap dot product used below.
if (refreshStemGeometry && _stemRecords.length > 0) {
const cameraHeightM = viewer.camera.positionCartographic?.height;
const candidates = new Array(_stemRecords.length);
for (let i = 0; i < _stemRecords.length; i++) {
const record = _stemRecords[i];
candidates[i] = {
id: record.id,
priority: record.priority,
distanceM: Cesium.Cartesian3.distance(cameraPos, record.base),
inView: occluder.isPointVisible(record.base),
};
}
const selection = selectInfraLod(candidates, {
cameraHeightM,
incumbentIds: _activeLodIds,
});
const grace = applyInfraEvictionGrace({
selectedIds: selection.activeIds,
builtIds: [..._activeLodIds],
graceState: _lodGraceState,
nowMs: now,
activeLimit: selection.budget.activeLimit,
});
_activeLodIds = new Set(grace.keepIds);
_lodGraceState = grace.graceState;
_lastLodBudgetLimit = selection.budget.activeLimit;
_lodComputed = true;
// Adopt this pass as the motion-fallback reference. Travel is
// measured from the last SELECTION, never the previous walk, so
// jitter around a parked camera never accumulates into a
// recompute while genuine slow travel eventually does.
Cesium.Cartesian3.clone(cameraPos, _lastLodCameraPos);
_lastLodProbeMs = now;
}
for (let i = 0; i < _stemRecords.length; i++) {
const record = _stemRecords[i];
const isActive = !_lodComputed || _activeLodIds.has(record.id);
const isVisible = isActive && occluder.isPointVisible(record.base);
if (record.entity.show !== isVisible) record.entity.show = isVisible;
// Records outside the globe-LOD active set carry no visible stem,
// so skip every per-frame cost for them — geometry trig, the
// ground-sample GPU readback, overlay-label candidacy. They rejoin
// on the next camera move if the budget has room.
if (!isActive) continue;
const wasGroundSampled = record.groundSampled;
if (refreshStemGeometry) {
updateLocalStemGeometry(viewer, record, now);
@@ -678,8 +815,6 @@ export function createLocalGeoJsonLayer({
< GROUND_SAMPLE_MAX_DISTANCE_M) {
groundRetryPending = true;
}
const isVisible = occluder.isPointVisible(record.base);
if (record.entity.show !== isVisible) record.entity.show = isVisible;
if (isVisible && record.entry) visibleOverlayRecords.push(record);
}
_stemGeometryDirty = false;
@@ -787,7 +922,9 @@ function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
? knownDistance
: Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M) sampleLocalGroundHeight(viewer, record, now);
const effectiveDistance = Math.max(distance, 5000);
// Keep the intended screen-size scaling in close-up views too. A 5 km
// minimum made a marker hundreds of metres tall beside a nearby building.
const effectiveDistance = Math.max(distance, 1);
const canvasHeight = viewer.scene.canvas.clientHeight || 1080;
const fov = viewer.camera.frustum.fov || (Math.PI / 3);
const targetPx = 65;
+273
View File
@@ -14,6 +14,7 @@ import {
selectLocalInfrastructureOverlayCohort,
} from './localGeojson.js';
import { layerFeedState } from './manager.js';
import { INFRA_LOD_ACTIVE_MIN, INFRA_LOD_ACTIVE_MAX } from './localGeojsonLod.js';
import {
installRenderGovernor,
getRenderGovernorDiagnostics,
@@ -445,6 +446,21 @@ test('a real enabled local layer has no native label graphics at runtime', async
env.cleanup();
});
for (const [heightM, minStemM, maxStemM] of [[500, 45, 90], [10000, 1100, 1400]]) {
test(`local infrastructure keeps stems proportional at ${heightM} m`, async (t) => {
const env = await createRealLocalLayerHarness();
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
const entity = env.dataSources[0].entities.values[0];
const carto = entity.__localBaseCarto;
env.viewer.camera.positionWC = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, heightM);
env.preRender.raise(env.viewer.scene, Cesium.JulianDate.now());
const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
const stemM = Cesium.Cartesian3.distance(positions[0], positions[1]);
assert.ok(stemM >= minStemM && stemM <= maxStemM,
`a ${heightM} m close-up must not inherit a globe-sized stem: ${stemM.toFixed(1)} m`);
});
}
test('local infrastructure creates no native labels or per-frame geometry callbacks', () => {
const source = readFileSync(new URL('./localGeojson.js', import.meta.url), 'utf8');
assert.doesNotMatch(source, /new Cesium\.LabelGraphics/);
@@ -963,3 +979,260 @@ test('after the cap a camera-motion frame still samples, and re-opens the budget
'a grounded record asks for no further frames',
);
});
// ── Globe-LOD: bound the active-stem set by camera height ────────────────────
//
// createLocalGeoJsonLayer used to give every feature a live stem and walk all
// of them (geometry trig + Cesium property writes) on every camera move. With
// three bundled-infra layers that is ~5,700 records on a full-earth view — the
// frame-rate cliff that got the INFRASTRUCTURE first-run tile cut. The walk
// now asks src/data/localGeojsonLod.js which records may carry a stem, sized to
// the camera-height budget, and skips all per-frame cost for the rest.
/**
* Harness with N in-view point features and a controllable camera height.
* Even-indexed features are named (high label priority), odd ones anonymous.
*/
async function createMultiFeatureLodHarness({ featureCount = 150, cameraHeightM = 9_000_000 } = {}) {
const originalFetch = globalThis.fetch;
const originalWindow = globalThis.window;
const preRender = new MockLayerEvent();
const moveEnd = new MockLayerEvent();
const dataSources = [];
const centerLon = -97.7;
const centerLat = 30.2;
// Polygons, not Points: Cesium's GeoJsonDataSource builds a PinBuilder
// billboard for a Point, which needs a DOM canvas the node test env lacks.
// The layer takes the polygon's bounding-sphere centre as the stem anchor.
const lines = Array.from({ length: featureCount }, (_, i) => {
const lon = centerLon + (i % 12) * 0.02;
const lat = centerLat + Math.floor(i / 12) * 0.02;
return JSON.stringify({
type: 'Feature',
id: `dc-${i}`,
properties: i % 2 === 0
? { name: `Datacenter ${i}`, tags: { name: `Datacenter ${i}`, operator: 'Example Cloud' } }
: { tags: {} },
geometry: {
type: 'Polygon',
coordinates: [[
[lon, lat],
[lon + 0.004, lat],
[lon + 0.004, lat + 0.004],
[lon, lat],
]],
},
});
});
globalThis.fetch = async () => ({ ok: true, status: 200, text: async () => lines.join('\n') });
globalThis.window = { dispatchEvent() {} };
const setPos = (m) => {
const p = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, m);
viewer.camera.positionWC = p;
viewer.camera.positionCartographic = Cesium.Cartographic.fromCartesian(p);
};
const viewer = {
selectedEntity: undefined,
dataSources: {
add(ds) { dataSources.push(ds); return ds; },
remove(ds) {
const i = dataSources.indexOf(ds);
if (i >= 0) dataSources.splice(i, 1);
return i >= 0;
},
},
camera: {
positionWC: null,
positionCartographic: null,
frustum: { fov: Math.PI / 3 },
moveEnd,
flyTo() {},
},
scene: {
canvas: { clientWidth: 1440, clientHeight: 900 },
preRender,
sampleHeightSupported: false,
screenSpaceCameraController: { enableInputs: true },
pick() { return null; },
requestRender() {},
},
};
setPos(cameraHeightM);
const layer = createLocalGeoJsonLayer({
id: 'local-datacenters',
url: '/lod-fixture.geojsonl',
name: 'LOD Datacenters',
color: '#00ffff',
overlayHost: { setVisible() {}, setEntries() {}, clearSource() {} },
projectToWindow: () => ({ x: 700, y: 450 }),
screenSpaceEventHandlerFactory: () => ({ setInputAction() {}, destroy() {} }),
});
try {
await layer.enable(viewer);
} finally {
globalThis.fetch = originalFetch;
}
return {
layer,
viewer,
dataSources,
preRender,
moveEnd,
setCameraHeight: setPos,
shownCount() {
return dataSources[0].entities.values.filter((entity) => entity.show === true).length;
},
cleanup() {
if (originalWindow === undefined) delete globalThis.window;
else globalThis.window = originalWindow;
},
};
}
test('globe-LOD caps live stems at the camera-height budget and widens as you zoom in', async (t) => {
const env = await createMultiFeatureLodHarness({ featureCount: 150, cameraHeightM: 9_000_000 });
const clock = installFakeClock(t);
t.after(() => {
env.layer.destroy(env.viewer);
env.cleanup();
});
// Full-earth framing: 150 in-view features, but the global band allows 80.
env.preRender.raise();
assert.equal(env.dataSources[0].entities.values.length, 150, 'all features are materialized');
assert.equal(env.shownCount(), INFRA_LOD_ACTIVE_MIN, 'only the global-band budget carries a stem');
// Named features (label priority ~1240) outrank anonymous nodes (~60), so
// every one of the 75 named features keeps a stem; the 5 remaining budget
// slots go to unnamed ones.
const shownIds = new Set(
env.dataSources[0].entities.values.filter((entity) => entity.show === true).map((entity) => entity.id),
);
const namedIds = Array.from({ length: 150 }, (_, i) => i).filter((i) => i % 2 === 0).map((i) => `dc-${i}`);
assert.ok(namedIds.every((id) => shownIds.has(id)), 'every named feature wins a stem before any unnamed one');
// Zoom to continental framing: the budget opens to MID (200), so all 150
// in-view features now get a stem.
env.setCameraHeight(1_000_000);
env.moveEnd.raise();
clock.advance(500); // clear the 450 ms visibility gate
env.preRender.raise();
assert.equal(env.shownCount(), 150, 'a closer camera lifts the cap above the in-view count');
});
test('globe-LOD selection is stable between camera moves (no per-frame churn)', async (t) => {
const env = await createMultiFeatureLodHarness({ featureCount: 150, cameraHeightM: 9_000_000 });
const clock = installFakeClock(t);
t.after(() => {
env.layer.destroy(env.viewer);
env.cleanup();
});
env.preRender.raise();
const firstSet = env.dataSources[0].entities.values
.filter((entity) => entity.show === true)
.map((entity) => entity.id)
.sort();
assert.equal(firstSet.length, INFRA_LOD_ACTIVE_MIN);
// A second walk with no intervening moveEnd must not re-select.
clock.advance(500);
env.preRender.raise();
const secondSet = env.dataSources[0].entities.values
.filter((entity) => entity.show === true)
.map((entity) => entity.id)
.sort();
assert.deepEqual(secondSet, firstSet, 'the active set only changes on camera moves');
});
test('getLodDiagnostics reports the active/total split and the band budget', async (t) => {
const env = await createMultiFeatureLodHarness({ featureCount: 150, cameraHeightM: 9_000_000 });
const clock = installFakeClock(t);
t.after(() => {
env.layer.destroy(env.viewer);
env.cleanup();
});
assert.deepEqual(env.layer.getLodDiagnostics(), {
total: 150, active: 0, budgetLimit: 0, computed: false,
}, 'before the first walk: materialized but not yet selected');
env.preRender.raise();
const global = env.layer.getLodDiagnostics();
assert.equal(global.total, 150);
assert.equal(global.active, INFRA_LOD_ACTIVE_MIN);
assert.equal(global.budgetLimit, INFRA_LOD_ACTIVE_MIN);
assert.equal(global.computed, true);
assert.ok(global.active < global.total, 'active < total means the declutter is engaged');
env.setCameraHeight(60_000);
env.moveEnd.raise();
clock.advance(500);
env.preRender.raise();
const regional = env.layer.getLodDiagnostics();
assert.equal(regional.active, 150, 'regional band lifts the cap above the in-view count');
assert.ok(regional.budgetLimit >= 150, `regional budget widened (${regional.budgetLimit})`);
});
test('globe-LOD releases every stem when the layer is disabled', async (t) => {
const env = await createMultiFeatureLodHarness({ featureCount: 40, cameraHeightM: 9_000_000 });
t.after(() => {
env.layer.destroy(env.viewer);
env.cleanup();
});
env.preRender.raise();
assert.equal(env.shownCount(), 40, 'under-budget: all 40 show');
env.layer.disable(env.viewer);
// The data source is hidden wholesale on disable; re-enabling starts from a
// fresh (empty) LOD selection rather than inheriting the old active set.
await env.layer.enable(env.viewer);
env.preRender.raise();
assert.equal(env.shownCount(), 40, 're-enable rebuilds the selection cleanly');
});
test('globe-LOD re-selects during continuous motion, without ever seeing a moveEnd', async (t) => {
// A tracked-entity follow, Cockpit view, route flight, or continuous orbit
// moves the camera indefinitely without emitting moveEnd. Before the motion
// fallback the active set stayed pinned to the region the camera left, and
// the walk hid those records as they passed behind the globe while admitting
// nothing newly visible — the layer bled down to sparse-or-empty until the
// motion stopped.
const env = await createMultiFeatureLodHarness({ featureCount: 150, cameraHeightM: 9_000_000 });
const clock = installFakeClock(t);
t.after(() => {
env.layer.destroy(env.viewer);
env.cleanup();
});
env.preRender.raise();
assert.equal(env.layer.getLodDiagnostics().budgetLimit, INFRA_LOD_ACTIVE_MIN);
assert.equal(env.shownCount(), INFRA_LOD_ACTIVE_MIN, 'global band caps the first selection');
// The camera travels; NO moveEnd is raised, here or anywhere below.
env.setCameraHeight(60_000);
// Inside the probe window the selection deliberately stays put: the fallback
// is rate-limited so continuous motion cannot recompute on every walk.
clock.advance(500); // > the 450 ms visibility gate, < the 1 s probe window
env.preRender.raise();
assert.equal(env.layer.getLodDiagnostics().budgetLimit, INFRA_LOD_ACTIVE_MIN,
'the probe window rate-limits the recompute');
// Past the window, the travelled camera re-selects on its own.
clock.advance(600);
env.preRender.raise();
const moved = env.layer.getLodDiagnostics();
assert.equal(moved.budgetLimit, INFRA_LOD_ACTIVE_MAX, 'the new camera height re-banded the budget');
assert.equal(moved.active, 150, 'every in-view record is admitted again');
assert.equal(env.shownCount(), 150, 'and actually carries a stem');
// A parked camera past the same window costs nothing: travel is measured
// from the last selection, so there is none to report.
const parked = env.layer.getLodDiagnostics();
clock.advance(2_000);
env.preRender.raise();
assert.deepEqual(env.layer.getLodDiagnostics(), parked, 'a parked camera never re-selects');
});
+369
View File
@@ -0,0 +1,369 @@
// src/data/localGeojsonLod.js
/**
* @module localGeojsonLod
*
* Pure selection policy for the bundled local-infrastructure layers
* (`local-datacenters`, `local-dams`, and any future `createLocalGeoJsonLayer`
* dataset). Cesium-specific projection, occlusion, and stem geometry stay in
* `localGeojson.js`; this module turns the resulting in-view records into a
* bounded "active" set — the only records that layer should spend per-frame
* stem-geometry and ground-sample work on.
*
* The screen-space label overlay is already decluttered
* (LOCAL_OVERLAY_COHORT_LIMIT); this also bounds world-space stems and their
* geometry/ground-sampling work. It does not reduce the number of materialized
* entities. Submarine cables use their own renderer and are not changed here.
* Performance must be measured against the current renderer, not inferred
* from the number of hidden stems.
*
* Sense is INVERTED relative to `cctvLod.js`: a CCTV metro view earns MORE
* cards, but a zoomed-out infrastructure view must show FEWER stems — that is
* exactly the view where every feature piles onto the screen at once. Zoom in
* and the in-view count falls naturally, so the budget opens back up.
*
* The engine keeps three concerns, all pure and individually testable:
* - BUDGET: camera height → how many stems may be active.
* - RANK: importance (the existing label-priority score) blended with a
* bounded proximity penalty, plus an incumbency bonus so the active set
* does not batch-swap on small camera moves.
* - EVICTION GRACE: a hysteresis planner (ported from cctvLod) so a stem at
* the budget edge does not blink in and out while the camera orbits.
*/
/* ------------------------------------------------------------------ *
* BUDGET
* ------------------------------------------------------------------ */
/** Active-stem budget at full-earth / global framing — the hot case. */
export const INFRA_LOD_ACTIVE_MIN = 80;
/** Active-stem budget at continental framing. */
export const INFRA_LOD_ACTIVE_MID = 200;
/** Active-stem budget at regional framing and closer (effectively "all in view"). */
export const INFRA_LOD_ACTIVE_MAX = 420;
/** At or above this camera height (m) the view is global: clamp hardest. */
export const INFRA_LOD_GLOBAL_HEIGHT_M = 3_000_000;
/** At or above this camera height (m) the view is continental. */
export const INFRA_LOD_REGIONAL_HEIGHT_M = 200_000;
/**
* Bounded active-stem budget for the current camera height. Non-finite input
* resolves to the GLOBAL band (fewest stems) — the safe default for a value we
* could not read is the cheapest one, not the most expensive.
*
* @param {number} cameraHeightM
* @returns {{activeLimit:number}}
*/
export function infraLodBudget(cameraHeightM) {
const height = Number.isFinite(cameraHeightM)
? Math.max(0, cameraHeightM)
: INFRA_LOD_GLOBAL_HEIGHT_M;
if (height >= INFRA_LOD_GLOBAL_HEIGHT_M) return { activeLimit: INFRA_LOD_ACTIVE_MIN };
if (height >= INFRA_LOD_REGIONAL_HEIGHT_M) return { activeLimit: INFRA_LOD_ACTIVE_MID };
return { activeLimit: INFRA_LOD_ACTIVE_MAX };
}
/* ------------------------------------------------------------------ *
* RANK
* ------------------------------------------------------------------ */
/**
* Priority points added for a record that currently holds an active stem, so a
* non-incumbent displaces it only when meaningfully better — not on sub-pixel
* camera jitter. Deliberately smaller than the label-priority "has a name"
* gap (≥700 in labelPriorityFromProperties), so incumbency reorders WITHIN a
* tier but never keeps an unnamed stem alive over a fresh named feature.
*/
export const INFRA_LOD_INCUMBENT_BONUS = 250;
/**
* Distance at or beyond which the proximity penalty is fully applied (m).
* Matches LOCAL_OVERLAY_MAX_DISTANCE_M's order of magnitude in localGeojson.js.
*/
export const INFRA_LOD_FAR_M = 12_000_000;
/**
* Maximum priority points subtracted for distance. Capped well below the
* label-priority name gap on purpose: proximity breaks ties among
* similarly-important features (show the near named dam before the far named
* dam) but can never promote an unnamed node over a named one.
*/
export const INFRA_LOD_MAX_DISTANCE_PENALTY = 200;
/**
* Keep-score for one record. Higher wins. Pure; no clamping of the result
* (callers only compare it), but every input is normalized so the output is
* always a finite number.
*
* @param {number} priority Existing label-priority score (localGeojson.js).
* @param {number} distanceM Camera→feature distance in metres.
* @param {boolean} isIncumbent Record currently holds an active stem.
* @param {object} [options]
* @param {number} [options.incumbentBonus]
* @param {number} [options.farM]
* @param {number} [options.maxDistancePenalty]
* @returns {number}
*/
export function infraRankScore(priority, distanceM, isIncumbent, {
incumbentBonus = INFRA_LOD_INCUMBENT_BONUS,
farM = INFRA_LOD_FAR_M,
maxDistancePenalty = INFRA_LOD_MAX_DISTANCE_PENALTY,
} = {}) {
const p = Number.isFinite(priority) ? priority : 0;
const far = Number.isFinite(farM) && farM > 0 ? farM : INFRA_LOD_FAR_M;
const d = Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : far;
const penaltyCap = Number.isFinite(maxDistancePenalty) && maxDistancePenalty >= 0
? maxDistancePenalty
: INFRA_LOD_MAX_DISTANCE_PENALTY;
const bonus = isIncumbent && Number.isFinite(incumbentBonus) ? incumbentBonus : 0;
return p + bonus - penaltyCap * Math.min(1, d / far);
}
/** Infinity-safe distance for a total sort order. */
function sortableDistance(distanceM) {
return Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : Number.MAX_VALUE;
}
/**
* Select the active-stem set from already projected in-view candidates.
*
* A candidate is `{ id, priority, distanceM, inView }`. Only `inView === true`
* candidates are eligible — the caller runs the (cheap) ellipsoidal-occluder
* test for every record and hands the result here; this module then decides
* which of those get the (expensive) stem geometry + ground-sample work.
*
* Ranking: `infraRankScore` descending, then nearer first, then id — a total
* order, so an under-budget cut is deterministic. Duplicate ids collapse to
* their best-scoring representative before the cut.
*
* @param {Array<{id:string,priority?:number,distanceM?:number,inView?:boolean}>} candidates
* @param {object} [options]
* @param {number} [options.cameraHeightM]
* @param {Iterable<string>|Set<string>} [options.incumbentIds] Ids that currently hold an active stem.
* @returns {{activeIds:string[], budget:{activeLimit:number}}}
*/
export function selectInfraLod(candidates, { cameraHeightM, incumbentIds } = {}) {
const budget = infraLodBudget(cameraHeightM);
const incumbents = incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
const byId = new Map();
for (const candidate of Array.isArray(candidates) ? candidates : []) {
if (!candidate || typeof candidate.id !== 'string' || !candidate.id) continue;
if (candidate.inView !== true) continue;
const distanceM = Number.isFinite(candidate.distanceM) && candidate.distanceM >= 0
? candidate.distanceM
: Number.POSITIVE_INFINITY;
const normalized = {
id: candidate.id,
distanceM,
score: infraRankScore(candidate.priority, candidate.distanceM, incumbents.has(candidate.id)),
};
const current = byId.get(normalized.id);
if (!current
|| normalized.score > current.score
|| (normalized.score === current.score && normalized.distanceM < current.distanceM)) {
byId.set(normalized.id, normalized);
}
}
const ranked = [...byId.values()].sort((a, b) => (
b.score - a.score
|| sortableDistance(a.distanceM) - sortableDistance(b.distanceM)
|| a.id.localeCompare(b.id)
));
return {
activeIds: ranked.slice(0, budget.activeLimit).map((entry) => entry.id),
budget,
};
}
/* ------------------------------------------------------------------ *
* EVICTION GRACE
* ------------------------------------------------------------------ */
export const INFRA_LOD_GRACE_PASSES = 2;
export const INFRA_LOD_GRACE_MS = 4_000;
/**
* Eviction-grace hysteresis for the active-stem set. Ported from
* cctvLod.applyEvictionGrace — same algorithm, infra-tuned constants.
*
* Raw per-pass selection has no memory: a stem sitting at the budget edge
* churns in and out on every small camera move. This planner keeps an
* already-built stem alive for a short grace window after it falls out of the
* selection — dropped only once it STAYS unselected for `gracePasses`
* consecutive passes or `graceMs` of wall time, whichever comes first. A newly
* selected record enters immediately; the hard `activeLimit` cap is never
* exceeded (grace-period stems are evicted first under cap pressure,
* oldest-in-grace first).
*
* Pure: `graceState` is never mutated; the returned `graceState` replaces it.
*
* @param {object} [input]
* @param {string[]} [input.selectedIds] This pass's selection (already capped).
* @param {string[]} [input.builtIds] Ids that currently have a live stem.
* @param {Map<string,{misses:number,since:number}>} [input.graceState]
* @param {number} [input.nowMs]
* @param {number} [input.activeLimit] Hard cap on total kept stems.
* @param {number} [input.gracePasses]
* @param {number} [input.graceMs]
* @returns {{keepIds:string[], evictIds:string[], graceState:Map<string,{misses:number,since:number}>}}
*/
export function applyInfraEvictionGrace({
selectedIds = [],
builtIds = [],
graceState = new Map(),
nowMs = 0,
activeLimit = INFRA_LOD_ACTIVE_MAX,
gracePasses = INFRA_LOD_GRACE_PASSES,
graceMs = INFRA_LOD_GRACE_MS,
} = {}) {
const selectedSet = new Set(
(Array.isArray(selectedIds) ? selectedIds : []).filter((id) => typeof id === 'string' && id),
);
const keepIds = [...selectedSet];
const evictIds = [];
const nextGrace = new Map();
const graced = [];
for (const id of Array.isArray(builtIds) ? builtIds : []) {
if (typeof id !== 'string' || !id || selectedSet.has(id)) continue;
const prior = graceState instanceof Map ? graceState.get(id) : undefined;
const misses = (prior?.misses || 0) + 1;
const since = Number.isFinite(prior?.since) ? prior.since : nowMs;
if (misses > gracePasses || nowMs - since >= graceMs) {
evictIds.push(id);
} else {
graced.push({ id, misses, since });
}
}
// Under cap pressure, grace-period stems go first: oldest-in-grace first
// (longest chance to return), then more misses, then id for determinism.
graced.sort((a, b) => a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id));
const cap = Number.isFinite(activeLimit) ? Math.max(0, Math.floor(activeLimit)) : INFRA_LOD_ACTIVE_MAX;
const capacity = Math.max(0, cap - keepIds.length);
const overflow = Math.max(0, graced.length - capacity);
for (let i = 0; i < graced.length; i++) {
if (i < overflow) {
evictIds.push(graced[i].id);
continue;
}
keepIds.push(graced[i].id);
nextGrace.set(graced[i].id, { misses: graced[i].misses, since: graced[i].since });
}
return { keepIds, evictIds, graceState: nextGrace };
}
/* ------------------------------------------------------------------ *
* MOTION FALLBACK
* ------------------------------------------------------------------ */
/**
* Probe window for the motion fallback. `localGeojson.js` marks its selection
* dirty on `moveEnd`, which covers drags, one-shot `setView`, and voice
* `flyTo` — but a tracked-entity follow camera, a Cockpit view, a route
* flight, or a continuous orbit never emits one. Without a fallback the
* active set stays pinned to the region the camera left: the walk hides those
* records one by one as they rotate behind the globe and admits nothing newly
* visible, so the layer bleeds down to sparse-or-empty until motion stops.
*
* The fallback therefore samples camera travel at most this often. Shorter
* than the cable layer's 2 s window because the walk that consumes it already
* runs on a 450 ms cadence and the work it re-triggers is bounded by the
* budget above; longer than that cadence so continuous motion recomputes on a
* fixed clock rather than on every walk.
*/
export const INFRA_LOD_MOTION_PROBE_INTERVAL_MS = 1_000;
/**
* Camera travel that counts as material motion, as a fraction of camera
* height. Scale-relative rather than absolute because the selection is a
* function of what the globe shows: 500 m of travel re-frames a city view
* completely and is invisible from 3,000 km up.
*/
export const INFRA_LOD_MOTION_EPSILON_RATIO = 0.02;
/**
* Floor for that epsilon (m), so a camera near the surface still needs real
* travel to spend a recompute. Matches the cable layer's flat
* CABLE_SWEEP_MOTION_EPSILON_M.
*/
export const INFRA_LOD_MOTION_EPSILON_MIN_M = 250;
/**
* Camera travel that counts as material motion at this camera height.
*
* An unreadable height resolves to the FLOOR — the most eager epsilon. That is
* the opposite polarity to `infraLodBudget`, deliberately: there, a height we
* could not read must not buy the most expensive band; here, a missed
* recompute empties the layer while a surplus one costs a single bounded pass.
*
* @param {number} cameraHeightM
* @param {object} [options]
* @param {number} [options.ratio]
* @param {number} [options.minM]
* @returns {number}
*/
export function infraLodMotionEpsilonM(cameraHeightM, {
ratio = INFRA_LOD_MOTION_EPSILON_RATIO,
minM = INFRA_LOD_MOTION_EPSILON_MIN_M,
} = {}) {
const floor = Number.isFinite(minM) && minM >= 0 ? minM : INFRA_LOD_MOTION_EPSILON_MIN_M;
const scale = Number.isFinite(ratio) && ratio >= 0 ? ratio : INFRA_LOD_MOTION_EPSILON_RATIO;
const height = Number.isFinite(cameraHeightM) && cameraHeightM > 0 ? cameraHeightM : 0;
return Math.max(floor, height * scale);
}
/**
* Should the layer re-run its LOD selection on this pass? Pure: the caller
* owns `lastProbeMs` and the camera position the last selection ran from, and
* writes the returned `lastProbeMs` back.
*
* Two conditions, both required:
* - the probe window has elapsed — the rate limit, so continuous motion
* cannot recompute on every walk;
* - the camera has travelled past the height-scaled epsilon SINCE THE LAST
* SELECTION, not since the previous walk. A parked camera therefore costs
* zero recomputes and sub-epsilon jitter never accumulates into one, while
* genuine slow travel eventually does.
*
* The window re-arms whenever it opens, whether or not it recomputes, so a
* creeping camera probes on a fixed cadence instead of on every walk.
*
* Travel arrives SQUARED so the caller can hand over
* `Cesium.Cartesian3.distanceSquared` and spend no sqrt on a per-walk probe,
* the same trade `createCableReferenceSweepGate` makes.
*
* @param {object} [input]
* @param {number} [input.nowMs]
* @param {number} [input.lastProbeMs] When the window last opened, or the last selection ran.
* @param {number} [input.movedSqM] Squared camera travel (m²) since the last selection.
* @param {number} [input.cameraHeightM] Current height — a near camera earns a tighter epsilon.
* @param {number} [input.probeIntervalMs]
* @param {number} [input.motionEpsilonM] Explicit epsilon (m); overrides the height scale.
* @returns {{recompute:boolean, lastProbeMs:number}}
*/
export function shouldRecomputeInfraLod({
nowMs = 0,
lastProbeMs = Number.NEGATIVE_INFINITY,
movedSqM = 0,
cameraHeightM,
probeIntervalMs = INFRA_LOD_MOTION_PROBE_INTERVAL_MS,
motionEpsilonM,
} = {}) {
const now = Number.isFinite(nowMs) ? nowMs : 0;
const last = Number.isFinite(lastProbeMs) ? lastProbeMs : Number.NEGATIVE_INFINITY;
const interval = Number.isFinite(probeIntervalMs) && probeIntervalMs >= 0
? probeIntervalMs
: INFRA_LOD_MOTION_PROBE_INTERVAL_MS;
if (now - last < interval) return { recompute: false, lastProbeMs: last };
const movedSq = Number.isFinite(movedSqM) && movedSqM > 0 ? movedSqM : 0;
const epsilon = Number.isFinite(motionEpsilonM) && motionEpsilonM >= 0
? motionEpsilonM
: infraLodMotionEpsilonM(cameraHeightM);
return { recompute: movedSq > epsilon * epsilon, lastProbeMs: now };
}
+379
View File
@@ -0,0 +1,379 @@
// src/data/localGeojsonLod.test.mjs
// Pure LOD-engine tests for the bundled local-infrastructure layers:
// camera-height budget bands (inverted sense vs CCTV), importance+proximity
// ranking with an incumbency bonus, in-view filtering, dedupe, and the
// ported eviction-grace planner.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
INFRA_LOD_ACTIVE_MIN,
INFRA_LOD_ACTIVE_MID,
INFRA_LOD_ACTIVE_MAX,
INFRA_LOD_GLOBAL_HEIGHT_M,
INFRA_LOD_REGIONAL_HEIGHT_M,
INFRA_LOD_INCUMBENT_BONUS,
INFRA_LOD_FAR_M,
INFRA_LOD_MAX_DISTANCE_PENALTY,
INFRA_LOD_GRACE_PASSES,
INFRA_LOD_GRACE_MS,
infraLodBudget,
infraRankScore,
selectInfraLod,
applyInfraEvictionGrace,
INFRA_LOD_MOTION_PROBE_INTERVAL_MS,
INFRA_LOD_MOTION_EPSILON_RATIO,
INFRA_LOD_MOTION_EPSILON_MIN_M,
infraLodMotionEpsilonM,
shouldRecomputeInfraLod,
} from './localGeojsonLod.js';
/**
* `count` in-view candidates. index 0 is the most important (highest priority)
* and the nearest; importance and distance both worsen with index.
*/
function candidates(count, options = {}) {
return Array.from({ length: count }, (_, index) => ({
id: `f-${String(index).padStart(3, '0')}`,
priority: 1000 - index,
distanceM: index * 1000,
inView: options.hiddenAt !== index,
}));
}
/* ----------------------------- budget ----------------------------- */
test('infraLodBudget clamps hardest at global height and opens up as you zoom in', () => {
assert.equal(INFRA_LOD_ACTIVE_MIN, 80);
assert.equal(INFRA_LOD_ACTIVE_MID, 200);
assert.equal(INFRA_LOD_ACTIVE_MAX, 420);
assert.deepEqual(infraLodBudget(9_000_000), { activeLimit: INFRA_LOD_ACTIVE_MIN });
assert.deepEqual(infraLodBudget(1_000_000), { activeLimit: INFRA_LOD_ACTIVE_MID });
assert.deepEqual(infraLodBudget(50_000), { activeLimit: INFRA_LOD_ACTIVE_MAX });
});
test('infraLodBudget band boundaries are inclusive at the lower edge', () => {
assert.deepEqual(infraLodBudget(INFRA_LOD_GLOBAL_HEIGHT_M), { activeLimit: INFRA_LOD_ACTIVE_MIN });
assert.deepEqual(infraLodBudget(INFRA_LOD_GLOBAL_HEIGHT_M - 1), { activeLimit: INFRA_LOD_ACTIVE_MID });
assert.deepEqual(infraLodBudget(INFRA_LOD_REGIONAL_HEIGHT_M), { activeLimit: INFRA_LOD_ACTIVE_MID });
assert.deepEqual(infraLodBudget(INFRA_LOD_REGIONAL_HEIGHT_M - 1), { activeLimit: INFRA_LOD_ACTIVE_MAX });
});
test('infraLodBudget treats non-finite / negative height as the global (cheapest) band', () => {
assert.deepEqual(infraLodBudget(undefined), { activeLimit: INFRA_LOD_ACTIVE_MIN });
assert.deepEqual(infraLodBudget(NaN), { activeLimit: INFRA_LOD_ACTIVE_MIN });
assert.deepEqual(infraLodBudget(Number.POSITIVE_INFINITY), { activeLimit: INFRA_LOD_ACTIVE_MIN });
assert.deepEqual(infraLodBudget(-1), { activeLimit: INFRA_LOD_ACTIVE_MAX }); // max(0,-1) -> 0 -> closest band
});
/* ------------------------------ rank ----------------------------- */
test('infraRankScore is priority minus a bounded distance penalty', () => {
assert.equal(infraRankScore(1000, 0, false), 1000);
assert.equal(infraRankScore(1000, INFRA_LOD_FAR_M, false), 1000 - INFRA_LOD_MAX_DISTANCE_PENALTY);
// half-way to FAR -> half the penalty
assert.equal(infraRankScore(1000, INFRA_LOD_FAR_M / 2, false), 1000 - INFRA_LOD_MAX_DISTANCE_PENALTY / 2);
// beyond FAR the penalty is capped, not extrapolated
assert.equal(infraRankScore(1000, INFRA_LOD_FAR_M * 10, false), 1000 - INFRA_LOD_MAX_DISTANCE_PENALTY);
});
test('infraRankScore incumbency bonus reorders within a tier but never beats a name gap', () => {
// Two similarly-important features: incumbency flips the order.
const freshNear = infraRankScore(900, 0, false);
const incumbentFar = infraRankScore(900, INFRA_LOD_FAR_M, true);
assert.ok(incumbentFar > freshNear);
// An unnamed incumbent (priority ~60) must NOT outrank a fresh named feature
// (priority ~1000): the incumbency bonus is smaller than that gap.
const unnamedIncumbent = infraRankScore(60, 0, true);
const namedFresh = infraRankScore(1000, INFRA_LOD_FAR_M, false);
assert.ok(namedFresh > unnamedIncumbent);
assert.ok(INFRA_LOD_INCUMBENT_BONUS < 700);
});
test('infraRankScore normalizes garbage inputs to a finite number', () => {
assert.equal(infraRankScore(NaN, NaN, false), 0 - INFRA_LOD_MAX_DISTANCE_PENALTY); // priority 0, distance -> FAR
assert.equal(Number.isFinite(infraRankScore('x', -5, true)), true);
assert.equal(infraRankScore(500, -5, false), 500 - INFRA_LOD_MAX_DISTANCE_PENALTY); // negative dist -> FAR
});
/* --------------------------- selectInfraLod ---------------------- */
test('selectInfraLod returns everything in view when under budget', () => {
const { activeIds, budget } = selectInfraLod(candidates(10), { cameraHeightM: 9_000_000 });
assert.equal(budget.activeLimit, INFRA_LOD_ACTIVE_MIN);
assert.equal(activeIds.length, 10);
assert.deepEqual(activeIds.slice(0, 3), ['f-000', 'f-001', 'f-002']);
});
test('selectInfraLod caps at the global budget and keeps the most important', () => {
const { activeIds } = selectInfraLod(candidates(500), { cameraHeightM: 9_000_000 });
assert.equal(activeIds.length, INFRA_LOD_ACTIVE_MIN);
assert.equal(activeIds[0], 'f-000');
assert.equal(activeIds.at(-1), `f-${String(INFRA_LOD_ACTIVE_MIN - 1).padStart(3, '0')}`);
});
test('selectInfraLod cap follows the zoom band', () => {
assert.equal(selectInfraLod(candidates(600), { cameraHeightM: 9_000_000 }).activeIds.length, INFRA_LOD_ACTIVE_MIN);
assert.equal(selectInfraLod(candidates(600), { cameraHeightM: 1_000_000 }).activeIds.length, INFRA_LOD_ACTIVE_MID);
assert.equal(selectInfraLod(candidates(600), { cameraHeightM: 50_000 }).activeIds.length, INFRA_LOD_ACTIVE_MAX);
});
test('selectInfraLod excludes out-of-view records', () => {
const input = candidates(20, { hiddenAt: 0 });
const { activeIds } = selectInfraLod(input, { cameraHeightM: 50_000 });
assert.equal(activeIds.includes('f-000'), false);
assert.equal(activeIds.length, 19);
});
test('selectInfraLod orders by score: a near unnamed node never displaces a far named one', () => {
const input = [
{ id: 'named-far', priority: 1000, distanceM: INFRA_LOD_FAR_M, inView: true },
{ id: 'unnamed-near', priority: 60, distanceM: 0, inView: true },
];
const { activeIds } = selectInfraLod(input, { cameraHeightM: 9_000_000 });
assert.deepEqual(activeIds, ['named-far', 'unnamed-near']);
});
test('selectInfraLod proximity breaks ties among equally important features', () => {
const input = [
{ id: 'far', priority: 500, distanceM: 5_000_000, inView: true },
{ id: 'near', priority: 500, distanceM: 1_000, inView: true },
];
const { activeIds } = selectInfraLod(input, { cameraHeightM: 9_000_000 });
assert.deepEqual(activeIds, ['near', 'far']);
});
test('selectInfraLod incumbency keeps a budget-edge stem that would otherwise be cut', () => {
const base = candidates(INFRA_LOD_ACTIVE_MIN + 5, {});
// The record that sits just past the cap:
const edgeId = `f-${String(INFRA_LOD_ACTIVE_MIN).padStart(3, '0')}`;
const withoutIncumbency = selectInfraLod(base, { cameraHeightM: 9_000_000 });
assert.equal(withoutIncumbency.activeIds.includes(edgeId), false);
const withIncumbency = selectInfraLod(base, {
cameraHeightM: 9_000_000,
incumbentIds: new Set([edgeId]),
});
assert.equal(withIncumbency.activeIds.includes(edgeId), true);
assert.equal(withIncumbency.activeIds.length, INFRA_LOD_ACTIVE_MIN);
});
test('selectInfraLod accepts an array or a Set of incumbent ids', () => {
const base = candidates(INFRA_LOD_ACTIVE_MIN + 3, {});
const edgeId = `f-${String(INFRA_LOD_ACTIVE_MIN).padStart(3, '0')}`;
const viaArray = selectInfraLod(base, { cameraHeightM: 9_000_000, incumbentIds: [edgeId] });
assert.equal(viaArray.activeIds.includes(edgeId), true);
});
test('selectInfraLod collapses duplicate ids to their best-scoring representative', () => {
const input = [
{ id: 'dup', priority: 100, distanceM: INFRA_LOD_FAR_M, inView: true },
{ id: 'dup', priority: 900, distanceM: 0, inView: true },
{ id: 'other', priority: 500, distanceM: 0, inView: true },
];
const { activeIds } = selectInfraLod(input, { cameraHeightM: 9_000_000 });
assert.deepEqual(activeIds, ['dup', 'other']);
assert.equal(activeIds.filter((id) => id === 'dup').length, 1);
});
test('selectInfraLod is defensive about junk input', () => {
assert.deepEqual(selectInfraLod(null, {}).activeIds, []);
assert.deepEqual(selectInfraLod(undefined).activeIds, []);
assert.deepEqual(
selectInfraLod([null, {}, { id: '' }, { id: 'x', inView: false }], { cameraHeightM: 1000 }).activeIds,
[],
);
});
test('selectInfraLod produces a deterministic order for score+distance ties', () => {
const input = [
{ id: 'b', priority: 500, distanceM: 1000, inView: true },
{ id: 'a', priority: 500, distanceM: 1000, inView: true },
{ id: 'c', priority: 500, distanceM: 1000, inView: true },
];
const { activeIds } = selectInfraLod(input, { cameraHeightM: 50_000 });
assert.deepEqual(activeIds, ['a', 'b', 'c']);
});
/* ---------------------- applyInfraEvictionGrace ------------------ */
test('applyInfraEvictionGrace keeps every selected id and starts no grace for them', () => {
const out = applyInfraEvictionGrace({
selectedIds: ['a', 'b'],
builtIds: ['a', 'b'],
nowMs: 1000,
});
assert.deepEqual(out.keepIds.sort(), ['a', 'b']);
assert.deepEqual(out.evictIds, []);
assert.equal(out.graceState.size, 0);
});
test('applyInfraEvictionGrace holds a dropped stem through the grace window then evicts it', () => {
let grace = new Map();
// pass 1: 'x' built but not selected -> graced (miss 1)
let out = applyInfraEvictionGrace({ selectedIds: ['a'], builtIds: ['a', 'x'], graceState: grace, nowMs: 0 });
assert.deepEqual(out.keepIds.sort(), ['a', 'x']);
assert.deepEqual(out.evictIds, []);
grace = out.graceState;
// pass 2: still missing -> miss 2, still within gracePasses (2)
out = applyInfraEvictionGrace({ selectedIds: ['a'], builtIds: ['a', 'x'], graceState: grace, nowMs: 10 });
assert.equal(out.keepIds.includes('x'), true);
grace = out.graceState;
// pass 3: miss 3 > gracePasses -> evicted
out = applyInfraEvictionGrace({ selectedIds: ['a'], builtIds: ['a', 'x'], graceState: grace, nowMs: 20 });
assert.deepEqual(out.evictIds, ['x']);
assert.equal(out.keepIds.includes('x'), false);
assert.equal(out.graceState.has('x'), false);
});
test('applyInfraEvictionGrace evicts on wall-clock even before the pass count', () => {
const grace = new Map([['x', { misses: 1, since: 0 }]]);
const out = applyInfraEvictionGrace({
selectedIds: [],
builtIds: ['x'],
graceState: grace,
nowMs: INFRA_LOD_GRACE_MS,
gracePasses: 99,
});
assert.deepEqual(out.evictIds, ['x']);
});
test('applyInfraEvictionGrace never exceeds the cap: oldest-in-grace evicted first', () => {
const grace = new Map([
['old', { misses: 1, since: 0 }],
['mid', { misses: 1, since: 5 }],
['new', { misses: 1, since: 9 }],
]);
const out = applyInfraEvictionGrace({
selectedIds: ['s1', 's2'],
builtIds: ['s1', 's2', 'old', 'mid', 'new'],
graceState: grace,
nowMs: 10,
activeLimit: 3, // room for only ONE graced stem on top of s1,s2
});
assert.deepEqual(out.keepIds.slice(0, 2).sort(), ['s1', 's2']);
assert.equal(out.keepIds.includes('new'), true);
assert.deepEqual(out.evictIds.sort(), ['mid', 'old']);
});
test('applyInfraEvictionGrace does not mutate the graceState it was given', () => {
const grace = new Map([['x', { misses: 1, since: 0 }]]);
const snapshot = JSON.stringify([...grace]);
applyInfraEvictionGrace({ selectedIds: [], builtIds: ['x'], graceState: grace, nowMs: 5 });
assert.equal(JSON.stringify([...grace]), snapshot);
});
test('applyInfraEvictionGrace tolerates missing / malformed input', () => {
const out = applyInfraEvictionGrace();
assert.deepEqual(out.keepIds, []);
assert.deepEqual(out.evictIds, []);
assert.equal(out.graceState instanceof Map, true);
const out2 = applyInfraEvictionGrace({ selectedIds: ['a', '', null], builtIds: [null, 'a', 3] });
assert.deepEqual(out2.keepIds, ['a']);
});
test('grace constants are the documented defaults', () => {
assert.equal(INFRA_LOD_GRACE_PASSES, 2);
assert.equal(INFRA_LOD_GRACE_MS, 4_000);
});
/* -------------------------- motion fallback ------------------------- */
test('infraLodMotionEpsilonM scales with camera height above a fixed floor', () => {
assert.equal(INFRA_LOD_MOTION_EPSILON_RATIO, 0.02);
assert.equal(INFRA_LOD_MOTION_EPSILON_MIN_M, 250);
// Well above the floor: pure ratio.
assert.equal(infraLodMotionEpsilonM(3_000_000), 60_000);
assert.equal(infraLodMotionEpsilonM(1_000_000), 20_000);
// Near the surface the floor wins, so a low camera still needs real travel.
assert.equal(infraLodMotionEpsilonM(5_000), INFRA_LOD_MOTION_EPSILON_MIN_M);
assert.equal(infraLodMotionEpsilonM(0), INFRA_LOD_MOTION_EPSILON_MIN_M);
});
test('infraLodMotionEpsilonM falls back to the floor (most eager) on unreadable height', () => {
// Opposite polarity to infraLodBudget on purpose: a missed recompute empties
// the layer, a surplus one costs a single bounded pass.
assert.equal(infraLodMotionEpsilonM(undefined), INFRA_LOD_MOTION_EPSILON_MIN_M);
assert.equal(infraLodMotionEpsilonM(NaN), INFRA_LOD_MOTION_EPSILON_MIN_M);
assert.equal(infraLodMotionEpsilonM(Number.POSITIVE_INFINITY), INFRA_LOD_MOTION_EPSILON_MIN_M);
assert.equal(infraLodMotionEpsilonM(-9_000), INFRA_LOD_MOTION_EPSILON_MIN_M);
});
test('shouldRecomputeInfraLod recomputes once the camera has moved far enough', () => {
const out = shouldRecomputeInfraLod({
nowMs: 10_000,
lastProbeMs: 0,
movedSqM: 500_000 ** 2, // 500 km >> the 60 km epsilon at 3,000 km up
cameraHeightM: 3_000_000,
});
assert.equal(out.recompute, true);
assert.equal(out.lastProbeMs, 10_000, 'the window re-arms at this pass');
});
test('shouldRecomputeInfraLod holds the window shut inside the probe interval', () => {
const out = shouldRecomputeInfraLod({
nowMs: 500,
lastProbeMs: 0,
movedSqM: 9_000_000 ** 2, // enormous motion, but the rate limit comes first
cameraHeightM: 3_000_000,
probeIntervalMs: INFRA_LOD_MOTION_PROBE_INTERVAL_MS,
});
assert.equal(out.recompute, false);
assert.equal(out.lastProbeMs, 0, 'a shut window does not re-arm');
});
test('shouldRecomputeInfraLod spends nothing on a parked camera', () => {
const out = shouldRecomputeInfraLod({
nowMs: 60_000,
lastProbeMs: 0,
movedSqM: 0,
cameraHeightM: 3_000_000,
});
assert.equal(out.recompute, false);
assert.equal(out.lastProbeMs, 60_000, 'the window still re-arms, so the probe stays on a fixed cadence');
});
test('shouldRecomputeInfraLod ignores sub-epsilon jitter but accumulates real travel', () => {
const height = 1_000_000; // epsilon 20,000 m
const jitter = shouldRecomputeInfraLod({
nowMs: 5_000, lastProbeMs: 0, movedSqM: 19_999 ** 2, cameraHeightM: height,
});
assert.equal(jitter.recompute, false);
// Travel is measured from the last SELECTION, so a creeping camera keeps
// closing on the epsilon across windows instead of resetting every pass.
const crept = shouldRecomputeInfraLod({
nowMs: 10_000, lastProbeMs: 5_000, movedSqM: 20_001 ** 2, cameraHeightM: height,
});
assert.equal(crept.recompute, true);
});
test('shouldRecomputeInfraLod opens on first use and tolerates missing / malformed input', () => {
// No prior probe: the window is open immediately.
const first = shouldRecomputeInfraLod({ nowMs: 1_000, movedSqM: 5_000 ** 2, cameraHeightM: 50_000 });
assert.equal(first.recompute, true);
assert.equal(first.lastProbeMs, 1_000);
const empty = shouldRecomputeInfraLod();
assert.equal(empty.recompute, false, 'no motion reported means no recompute');
assert.equal(empty.lastProbeMs, 0);
const garbage = shouldRecomputeInfraLod({
nowMs: NaN, lastProbeMs: NaN, movedSqM: NaN, cameraHeightM: NaN, probeIntervalMs: NaN,
});
assert.equal(garbage.recompute, false);
// An explicit epsilon overrides the height scale.
const explicit = shouldRecomputeInfraLod({
nowMs: 2_000, lastProbeMs: 0, movedSqM: 300 ** 2, cameraHeightM: 9_000_000, motionEpsilonM: 100,
});
assert.equal(explicit.recompute, true);
});
test('motion-fallback constants are the documented defaults', () => {
assert.equal(INFRA_LOD_MOTION_PROBE_INTERVAL_MS, 1_000);
assert.equal(INFRA_LOD_MOTION_EPSILON_RATIO, 0.02);
assert.equal(INFRA_LOD_MOTION_EPSILON_MIN_M, 250);
});