Compare commits

..
Author SHA1 Message Date
Mazeyar Moeini FeizabadiandClaude Fable 5.2 f04308e45d docs(changelog): add the #1706 fragment
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 16:05:55 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 9582f2f28b perf(alpr): read anchors from the terrain globe and sample outside the paint
The overlay took up to six scene.sampleHeight depth renders inside one
paint, and kept the answers on entities, so every refresh and every
re-enable sampled the same cameras again. On a terrain globe the anchor now
comes from globe.getHeight; answers are kept per camera id until the map
source changes; and what sampling remains runs after the paint, one per
frame.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 16:03:18 +01:00
14 changed files with 318 additions and 714 deletions

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
- Cut the stutter while zooming and panning with Street Traffic on in dense
cities. Coarse roads are no longer clipped against every detail tile in one
task, roads already parsed in a load are not parsed again, and ground floors
are read once per cell (mazy1998, #1705).
+4
View File
@@ -0,0 +1,4 @@
- ALPR camera badges no longer take depth samples while the overlay paints, or
again each time the layer is switched back on. On a terrain globe their
height comes from the loaded terrain and is remembered per camera; some
badges move a few metres onto the drawn surface (mazy1998, #1706).
+8 -4
View File
@@ -1513,10 +1513,14 @@ tile streamed in; floors resolve before new badges appear (bounded wait), and a
badge whose cell has not resolved clamps until it does, then moves in place.
Overlay anchors start on the same floor and write marker properties only on
change. Once the camera is still and tiles have settled, each overlay anchor is
replaced once by the rendered surface under it (two depth samples per paint,
accepted only within -30/+40 m of the floor cell), so badges do not float or
sink on slopes; wedge corners follow their own floor cells instead of staying
level with the apex. Unknown bearings never acquire a wedge. The selected
replaced once by the rendered surface under it (accepted only within -30/+40 m
of the floor cell), so badges do not float or sink on slopes; wedge corners
follow their own floor cells instead of staying level with the apex. The
surface is read outside the overlay paint, one camera per frame: a terrain
globe answers from its loaded mesh (`globe.getHeight`) and only photoreal
tiles are depth-sampled (`scene.sampleHeight`). Answers are kept per camera
until the map source changes, so a refresh or turning the layer off and on
samples nothing again. Unknown bearings never acquire a wedge. The selected
card's `DIRECTION` wraps after rounding to whole degrees, so a mapped bearing
of 359.5° or more reads `0°` rather than `360°`. The row reads
`N nearby · M on screen`; when every loaded camera is outside the view it adds
+139 -57
View File
@@ -4,10 +4,10 @@ import {
MARKER_ICON_SIZE,
SELECTED_MARKER_ICON_SIZE,
MARKER_FLOOR_LIFT_M,
ANCHOR_SAMPLES_PER_PAINT,
REMEMBERED_ANCHORS,
SURFACE_BELOW_FLOOR_M,
SURFACE_ABOVE_FLOOR_M,
SURFACE_SAMPLES_PER_PAINT,
SURFACE_SAMPLES_PER_FRAME,
} from './policy.js';
import {
MARKER_IMAGE,
@@ -19,11 +19,17 @@ import {
validAlprGroundHeight,
} from './visuals.js';
const nextAnimationFrame = (step) =>
typeof requestAnimationFrame === 'function'
? requestAnimationFrame(step)
: setTimeout(step, 16);
/** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */
export function createAlprOverlay({
state,
services,
onSurfaceChange = () => {},
requestFrame = nextAnimationFrame,
}) {
let lane,
removeMapListener,
@@ -33,7 +39,12 @@ export function createAlprOverlay({
hits = [],
images = [],
destroyed = false,
lastCamera = null;
lastCamera = null,
sampling = false;
// Rendered-surface answers by camera id. Entities are rebuilt by a refresh
// and by turning the layer off and on; keeping the answers on them meant
// every rebuild sampled the same cameras again.
const surfaceAnswers = new Map();
const requestPaint = () => {
if (!destroyed && state.enabled) lane?.requestPaint();
};
@@ -92,38 +103,72 @@ export function createAlprOverlay({
entity.gevAlprDisplayPosition = null;
nativeVisible(entity, true);
}
surfaceAnswers.clear();
state.lastAnchorSampleAt = 0;
retryUntil = Date.now() + 15000;
requestPaint();
}
function anchorFor(record, entity, budget) {
if (entity.gevAlprCanvasPosition) return entity.gevAlprCanvasPosition;
function rememberSurface(record, height, checked) {
surfaceAnswers.delete(record.id);
surfaceAnswers.set(record.id, {
latitude: record.latitude,
longitude: record.longitude,
height,
checked,
});
if (surfaceAnswers.size > REMEMBERED_ANCHORS)
surfaceAnswers.delete(surfaceAnswers.keys().next().value);
}
/**
* Rendered surface height under a camera. A terrain globe answers from its
* loaded mesh; only photoreal tiles need `sampleHeight`, which renders depth
* and compiles its shaders on first use (a freeze of up to 1.9 s at load).
*/
function surfaceHeight(record, entity) {
const scene = state.viewer.scene;
const location = Cesium.Cartographic.fromDegrees(
record.longitude,
record.latitude,
);
// A floor-placed marker anchors where its native badge sits; only a
// marker still waiting for its floor samples the rendered surface, and
// each paint samples a few at most (each sample is a depth render).
if (scene.globe?.show) return scene.globe.getHeight?.(location);
if (!scene.sampleHeightSupported) return undefined;
try {
return scene.sampleHeight(location, [entity]);
} catch {
/* streaming tiles */
return undefined;
}
}
/** The anchor a paint can have without sampling, or null while it waits for one. */
function anchorFor(record, entity) {
if (entity.gevAlprCanvasPosition) return entity.gevAlprCanvasPosition;
const scene = state.viewer.scene;
const answer = surfaceAnswers.get(record.id);
if (
answer?.latitude === record.latitude &&
answer.longitude === record.longitude
) {
setAnchor(record, entity, answer.height);
if (answer.checked)
entity.gevAlprSurfaceCheckedFor = entity.gevAlprCanvasPosition;
return entity.gevAlprCanvasPosition;
}
// A floor-placed marker anchors where its native badge sits. A marker
// still waiting for its floor reads the terrain globe, or on photoreal
// tiles waits for sampleAnchors().
let height;
const floor = services.groundFloor.cachedGroundFloor(
record.latitude,
record.longitude,
);
if (validAlprGroundHeight(floor)) height = floor + MARKER_FLOOR_LIFT_M;
else if (scene.sampleHeightSupported) {
if (budget.samples <= 0) return null;
budget.samples -= 1;
try {
height = scene.sampleHeight(location, [entity]);
} catch {
/* streaming tiles */
}
}
if (!validAlprGroundHeight(height) && scene.globe.show)
height = scene.globe.getHeight?.(location);
else if (scene.globe.show)
height = scene.globe.getHeight?.(
Cesium.Cartographic.fromDegrees(record.longitude, record.latitude),
);
if (!validAlprGroundHeight(height)) return null;
setAnchor(record, entity, height);
return entity.gevAlprCanvasPosition;
@@ -158,14 +203,19 @@ export function createAlprOverlay({
}
/**
* Replace floor anchors with the rendered surface under each overlay camera.
* Floors come from ~111 m cells, so on a slope a badge could float or sink
* by tens of metres. Runs only while the camera is still and tiles have
* settled, a few depth samples per paint, and each anchor at most once;
* samples far outside the floor prior (a roof or a streaming tile) are
* rejected. Returns whether anchors remain to check.
* Sample the rendered surface for the overlay cameras, outside the paint.
*
* A camera with no floor yet gets its anchor from a sample. An anchored one
* is checked against the surface once: floors come from ~111 m cells, so on
* a slope a badge could float or sink by tens of metres. Checks run only
* while the camera is still and tiles have settled, and samples far outside
* the floor prior (a roof or a streaming tile) are rejected. Each frame
* takes SURFACE_SAMPLES_PER_FRAME samples, so no frame carries the whole
* cohort; sampling inside paint() put up to six depth renders in one frame.
*/
function refineAnchors(budget) {
function sampleAnchors() {
sampling = false;
if (destroyed || !state.enabled) return;
const camera = state.viewer.camera;
const scene = state.viewer.scene;
const still =
@@ -188,27 +238,41 @@ export function createAlprOverlay({
position: Cesium.Cartesian3.clone(camera.positionWC),
direction: Cesium.Cartesian3.clone(camera.directionWC),
};
if (!scene.sampleHeightSupported) return false;
let pending = false;
const canSample = scene.globe?.show || scene.sampleHeightSupported;
let samples = SURFACE_SAMPLES_PER_FRAME,
pending = false,
progressed = false,
moved = false;
// Cameras with no anchor come first: they are not on the overlay yet.
for (const record of records) {
const entity = state.dataSource.entities.getById(record.id);
if (!entity || entity.gevAlprCanvasPosition) continue;
// The floor may have resolved since the paint that found none.
if (anchorFor(record, entity)) moved = true;
else if (!canSample || samples <= 0) pending = true;
else {
samples -= 1;
const sample = surfaceHeight(record, entity);
if (validAlprGroundHeight(sample)) {
setAnchor(record, entity, sample);
rememberSurface(record, sample, false);
moved = progressed = true;
} else pending = true;
}
}
for (const record of records) {
const entity = state.dataSource.entities.getById(record.id);
const anchor = entity?.gevAlprCanvasPosition;
if (!anchor || entity.gevAlprSurfaceCheckedFor === anchor) continue;
if (!still || !surfaceSettled(scene) || budget.samples <= 0) {
if (!anchor || !canSample || entity.gevAlprSurfaceCheckedFor === anchor)
continue;
if (!still || !surfaceSettled(scene) || samples <= 0) {
pending = true;
continue;
}
budget.samples -= 1;
let sample;
try {
sample = scene.sampleHeight(
Cesium.Cartographic.fromDegrees(record.longitude, record.latitude),
[entity],
);
} catch {
/* streaming tiles */
}
const current = Cesium.Cartographic.fromCartesian(anchor).height;
samples -= 1;
progressed = true;
const sample = surfaceHeight(record, entity);
let height = Cesium.Cartographic.fromCartesian(anchor).height;
const floor = services.groundFloor.cachedGroundFloor(
record.latitude,
record.longitude,
@@ -218,21 +282,43 @@ export function createAlprOverlay({
(!validAlprGroundHeight(floor) ||
(sample >= floor - SURFACE_BELOW_FLOOR_M &&
sample <= floor + SURFACE_ABOVE_FLOOR_M));
if (accepted && Math.abs(sample + MARKER_FLOOR_LIFT_M - current) > 1)
setAnchor(record, entity, sample + MARKER_FLOOR_LIFT_M);
if (accepted && Math.abs(sample + MARKER_FLOOR_LIFT_M - height) > 1) {
height = sample + MARKER_FLOOR_LIFT_M;
setAnchor(record, entity, height);
moved = true;
}
entity.gevAlprSurfaceCheckedFor = entity.gevAlprCanvasPosition;
// A surface that did not answer is asked again after a rebuild.
if (validAlprGroundHeight(sample)) rememberSurface(record, height, true);
if (entity.id === state.selectedId)
entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition;
}
return pending;
if (moved) requestPaint();
if (!pending || Date.now() >= retryUntil) return;
// Take the next ration on the next frame; when nothing could be sampled
// (camera moving, tiles streaming, no floor yet) look again in 250 ms.
if (progressed) scheduleSampling();
else if (!retryTimer)
retryTimer = setTimeout(() => {
retryTimer = null;
scheduleSampling();
}, 250);
}
function scheduleSampling() {
if (sampling || destroyed) return;
sampling = true;
requestFrame(sampleAnchors);
}
function paint({ ctx, width, height, keyhole, occluder }) {
hits = [];
if (!state.enabled || destroyed) return;
let unresolved = false;
let waiting = false;
const painted = [];
const budget = { samples: ANCHOR_SAMPLES_PER_PAINT };
const canSample =
state.viewer.scene.globe?.show ||
state.viewer.scene.sampleHeightSupported;
for (const record of records) {
const entity = state.dataSource.entities.getById(record.id);
if (!entity) continue;
@@ -246,12 +332,14 @@ export function createAlprOverlay({
nativeVisible(entity, true);
continue;
}
const anchor = anchorFor(record, entity, budget);
const anchor = anchorFor(record, entity);
if (!anchor) {
nativeVisible(entity, true);
unresolved = true;
waiting = true;
continue;
}
if (canSample && entity.gevAlprSurfaceCheckedFor !== anchor)
waiting = true;
canvasOwned(entity, true);
if (occluder && !occluder.isPointVisible(anchor)) {
setShown(entity, false);
@@ -335,15 +423,9 @@ export function createAlprOverlay({
ctx.restore();
hits.push({ id: record.id, x: origin.x, y: origin.y, radius: size / 2 });
}
const refining = refineAnchors({
samples: Math.min(budget.samples, SURFACE_SAMPLES_PER_PAINT),
});
if ((unresolved || refining) && !retryTimer && Date.now() < retryUntil) {
retryTimer = setTimeout(() => {
retryTimer = null;
requestPaint();
}, 250);
}
// Anchors still to find or to check are sampled after this paint, a few
// per frame, never inside it.
if (waiting && !retryTimer && Date.now() < retryUntil) scheduleSampling();
}
return {
+135 -19
View File
@@ -45,8 +45,10 @@ function harness(t, count = 2, { floor = null } = {}) {
active,
removed = 0,
paints = 0;
const frames = [];
const overlay = createAlprOverlay({
state,
requestFrame: (step) => frames.push(step),
services: {
groundFloor: { cachedGroundFloor: () => floor },
overlays: {
@@ -106,6 +108,12 @@ function harness(t, count = 2, { floor = null } = {}) {
entities,
draw,
paint: (occluder) => paint({ ctx, width: 800, height: 600, occluder }),
/** Run the sampling step queued for the next animation frame, if any. */
frame() {
const step = frames.shift();
step?.();
return Boolean(step);
},
mapChanged() {
mapListener({ detail: { status: 'ready' } });
},
@@ -243,41 +251,149 @@ test('overlay glyphs and native badges shrink alike with distance', () => {
assert.ok(Math.abs(markerScale(20_250) - 0.8) < 1e-9);
});
test('a still view replaces floor anchors with the rendered surface, within the floor window', (t) => {
// Floors come from ~111 m cells; on a slope a badge floated above the mesh.
const h = harness(t, 2, { floor: 150 });
/** A photoreal scene: no globe, the rendered surface read by depth samples. */
function photoreal(h, heightAt = () => 172) {
const scene = h.state.viewer.scene;
const camera = h.state.viewer.camera;
camera.directionWC = Cesium.Cartesian3.UNIT_X;
h.state.viewer.camera.directionWC = Cesium.Cartesian3.UNIT_X;
scene.globe.show = false;
scene.sampleHeightSupported = true;
scene.primitives = { length: 0, get: () => null };
let samples = 0;
const sampled = [];
scene.sampleHeight = (carto) => {
samples++;
return Cesium.Math.toDegrees(carto.latitude) > 30.00005 ? 400 : 172;
sampled.push(carto);
return heightAt(Cesium.Math.toDegrees(carto.latitude));
};
return sampled;
}
const anchorHeight = (h, id) =>
Cesium.Cartographic.fromCartesian(
h.entities.getById(id).gevAlprCanvasPosition,
).height;
/** Run sampling frames until none is queued, waiting out the 250 ms look-again. */
function settle(t, h) {
for (let i = 0; i < 400; i++) if (!h.frame()) t.mock.timers.tick(250);
}
test('a still view replaces floor anchors with the rendered surface, within the floor window', (t) => {
// Floors come from ~111 m cells; on a slope a badge floated above the mesh.
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const h = harness(t, 2, { floor: 150 });
const sampled = photoreal(h, (latitude) => (latitude > 30.00005 ? 400 : 172));
h.overlay.sync(h.records);
h.paint();
const height = (id) =>
Cesium.Cartographic.fromCartesian(
h.entities.getById(id).gevAlprCanvasPosition,
).height;
assert.ok(
Math.abs(anchorHeight(h, 'camera:0') - 151.5) < 0.01,
'floor anchor first',
);
assert.equal(h.frame(), true, 'the paint queued a sampling frame');
assert.equal(
samples,
sampled.length,
0,
'no depth samples while the camera may still be moving',
);
assert.ok(Math.abs(height('camera:0') - 151.5) < 0.01, 'floor anchor first');
h.paint();
settle(t, h);
assert.ok(
Math.abs(height('camera:0') - 173.5) < 0.01,
Math.abs(anchorHeight(h, 'camera:0') - 173.5) < 0.01,
'rendered surface once still',
);
assert.ok(
Math.abs(height('camera:1') - 151.5) < 0.01,
Math.abs(anchorHeight(h, 'camera:1') - 151.5) < 0.01,
'a rooftop-height sample is rejected',
);
const taken = samples;
assert.equal(sampled.length, 2);
h.paint();
assert.equal(samples, taken, 'each anchor is checked once');
settle(t, h);
assert.equal(sampled.length, 2, 'each anchor is checked once');
});
test('a paint never samples the surface; frames take one sample each', (t) => {
// Sampling inside paint() put up to six depth renders into one frame, and
// the first of them compiles shaders: a freeze of 0.8 to 1.9 s at load.
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const h = harness(t, 8);
const sampled = photoreal(h);
h.overlay.sync(h.records);
h.paint();
h.paint();
assert.equal(sampled.length, 0, 'no sample inside a paint');
assert.equal(h.draw.length, 0, 'unanchored cameras keep their native badge');
for (let taken = 1; taken <= 8; taken++) {
assert.equal(h.frame(), true);
assert.equal(sampled.length, taken, 'one sample per frame');
}
for (const record of h.records)
assert.ok(Math.abs(anchorHeight(h, record.id) - 172) < 0.01);
h.paint();
assert.ok(h.draw.length > 0, 'anchored cameras paint on the overlay');
});
test('a terrain globe is read from its mesh, never depth-sampled', (t) => {
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const h = harness(t, 3, { floor: 150 });
const sampled = photoreal(h);
const scene = h.state.viewer.scene;
scene.globe.show = true;
scene.globe.tilesLoaded = true;
let reads = 0;
scene.globe.getHeight = () => {
reads++;
return 172;
};
h.overlay.sync(h.records);
h.paint();
settle(t, h);
assert.equal(sampled.length, 0);
assert.equal(reads, 3);
// The same anchor the depth sample gave: the surface plus the marker lift.
for (const record of h.records)
assert.ok(Math.abs(anchorHeight(h, record.id) - 173.5) < 0.01);
});
test('surface answers outlive rebuilt markers and a re-enable, until the map changes', (t) => {
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const h = harness(t, 4, { floor: 150 });
const sampled = photoreal(h);
h.overlay.sync(h.records);
h.paint();
settle(t, h);
assert.equal(sampled.length, 4);
const before = h.records.map((record) => anchorHeight(h, record.id));
// A refresh or a toggle removes the entities and builds new ones.
const rebuild = () => {
h.overlay.clear();
h.entities.removeAll();
for (const record of h.records)
h.entities.add({
id: record.id,
billboard: { image: 'fallback', show: true },
polyline: { show: true },
polygon: { show: true },
});
h.overlay.sync(h.records);
h.paint();
settle(t, h);
};
rebuild();
h.state.enabled = false;
h.overlay.clear();
h.state.enabled = true;
rebuild();
assert.equal(sampled.length, 4, 'no camera is sampled again');
assert.deepEqual(
h.records.map((record) => anchorHeight(h, record.id)),
before,
);
// A camera that moved is a different question.
h.records[0] = { ...h.records[0], latitude: h.records[0].latitude + 0.001 };
rebuild();
assert.equal(sampled.length, 5);
h.mapChanged();
h.paint();
settle(t, h);
assert.equal(sampled.length, 9, 'a new map surface is sampled afresh');
});
+5 -4
View File
@@ -66,14 +66,15 @@ export const MAX_CANVAS_FRUSTUMS = 64;
/** Metres a floor-placed marker sits above its resolved ground cell. */
export const MARKER_FLOOR_LIFT_M = 1.5;
/** Rendered-surface samples one overlay paint may take (each is a depth render). */
export const ANCHOR_SAMPLES_PER_PAINT = 4;
/** Rendered-surface samples one frame may take (on photoreal tiles each is a
* depth render of several milliseconds, so they are spread over frames). */
export const SURFACE_SAMPLES_PER_FRAME = 1;
/** Surface answers kept by camera id, so a refresh or re-enable asks nothing. */
export const REMEMBERED_ANCHORS = 2048;
/** Accepted rendered-surface window around a floor cell (a cut or an embankment
* sits within it; a rooftop or an unstreamed tile usually does not). */
export const SURFACE_BELOW_FLOOR_M = 30;
/** Rendered-surface refinements per paint while the camera is still. */
export const SURFACE_SAMPLES_PER_PAINT = 2;
export const SURFACE_ABOVE_FLOOR_M = 40;
/** OSM attribution may collapse after five seconds; full credit stays in Data attribution. */
+1 -3
View File
@@ -1,7 +1,6 @@
import { roadIdentity } from './retention.js';
import * as Cesium from 'cesium';
import {
floorCellReader,
prepareRoadSurfaces,
roadHeightStations,
trafficSurfaceReady,
@@ -302,7 +301,6 @@ export function createFlow({ state: layerState, services, parts, source }) {
}
}
if (depthHeight !== undefined) layerState._viewHeightEstimate = depthHeight;
const floorAt = floorCellReader(services.ground);
for (const road of roads) {
road.key ||= roadIdentity(road);
const prepared = !refining && preparedRoads.get(road.key);
@@ -321,7 +319,7 @@ export function createFlow({ state: layerState, services, parts, source }) {
);
for (let i = 0; i < road.coords.length; i++) {
const [lon, lat] = road.coords[i];
const floor = floorAt(lat, lon);
const floor = services.ground?.cachedGroundFloor?.(lat, lon);
const height =
Number.isFinite(floor) && Math.abs(floor) <= 9000
? Math.max(floor, layerState._viewHeightEstimate)
+3 -3
View File
@@ -39,10 +39,10 @@ export function createTrafficLayer({ services, source }) {
parts.controls = createControls(context);
parts.lifecycle = createLifecycle(context);
state._parseRoads = TRAFFIC_TIMING_ENABLED
? (data, trace, parsedBySource) =>
? (data, trace) =>
trace
? parts.timing.parseRoadsTimed(data, trace, parsedBySource)
: parts.model.parseRoads(data, trace, parsedBySource)
? parts.timing.parseRoadsTimed(data, trace)
: parts.model.parseRoads(data)
: parts.model.parseRoads;
state._loadRoadsForBounds = TRAFFIC_TIMING_ENABLED
+1 -7
View File
@@ -348,16 +348,11 @@ export function createIngestion({
layerState._retainedPreview = false;
layerState._detailRoadsReady = false;
let paintRevision = 0;
// The snapshot that ends a pass replaces the streamed roads with the
// composed set, which is mostly the tile records already parsed and
// drawn. Remembering those per pass means only the records new to that
// snapshot (the clipped coarse roads) are parsed, not the whole set again.
let parsedBySource = new WeakMap();
const onTile = (data, passSignal, preview) => {
if (generation !== layerState._loadGeneration || passSignal.aborted)
return;
const start = performance.now();
const parsed = layerState._parseRoads(data, trace, parsedBySource);
const parsed = layerState._parseRoads(data, trace);
phaseTiming('parse', start, { roads: parsed.length, incremental: true });
if (data.replace) streamed = parsed;
else streamed.push(...parsed);
@@ -536,7 +531,6 @@ export function createIngestion({
if (altitude > FAST_FETCH_ALTITUDE) return;
streamed = [];
parsedBySource = new WeakMap();
majorPass = false;
// Detailed pass: fetch the full road graph (tertiary, residential, etc.)
console.log(`[Data:Traffic] Full fetch local roads [${cacheKey}]`);
+4 -18
View File
@@ -1,5 +1,5 @@
import * as Cesium from 'cesium';
import { floorCellReader, roadSurfaceChunks } from './surface.js';
import { roadSurfaceChunks } from './surface.js';
import { t, translateKnown } from '../../i18n/index.js';
import {
flowDensityMult,
@@ -13,35 +13,22 @@ import {
} from './policy.js';
export function createModel({ state: layerState, services, parts, source }) {
/**
* Build scene waypoints from source records; thinning and terrain remain rendering policy.
*
* `parsedBySource` (source record → its parsed roads) is filled as records
* are parsed, and a record already in it is answered from it.
*/
function parseRoads(roadData, _trace, parsedBySource) {
/** Build scene waypoints from source records; thinning and terrain remain rendering policy. */
function parseRoads(roadData) {
if (!roadData || !roadData.roads) {
return [];
}
const roads = [];
const floorAt = floorCellReader(services.ground);
for (const road of roadData.roads) {
if (!road.coordinates || road.coordinates.length < 2) continue;
// A replacing snapshot repeats records this load already parsed.
const known = parsedBySource?.get(road);
if (known) {
for (const parsed of known) roads.push(parsed);
continue;
}
const first = roads.length;
for (const coords of roadSurfaceChunks(road.coordinates)) {
const type = road.type;
const oneway = road.oneway;
// Pre-compute Cartesian3 waypoints (lon, lat, height) for fast lerp animation
const waypoints = coords.map(([lng, lat]) => {
const floor = floorAt(lat, lng);
const floor = services.ground?.cachedGroundFloor?.(lat, lng);
const h =
(Number.isFinite(floor) && Math.abs(floor) <= 9000
? floor
@@ -73,7 +60,6 @@ export function createModel({ state: layerState, services, parts, source }) {
flow: road.flow || null,
});
}
parsedBySource?.set(road, roads.slice(first));
}
return roads;
-444
View File
@@ -1,444 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createTrafficSource, outsideDetailTiles } from './source.js';
import { createIngestion } from './ingestion.js';
import { createModel } from './model.js';
import { floorCellReader, roadSurfaceChunks } from './surface.js';
import { DOT_HEIGHT_OFFSET } from './policy.js';
import { clipTileLine } from '../../sources/openFreeMap.js';
import { tileToBBox } from '../../data/tomtomTiles.js';
// The clip as it was before the extent shortcut: every road against the four
// regions around every tile. The shortcut must return exactly this.
function clipEveryRoadAgainstEveryTile(roads, tiles) {
return roads.flatMap((road) => {
let lines = [road.coordinates];
for (const tile of tiles) {
const box = tileToBBox(tile.z, tile.x, tile.y);
const regions = [
{ west: -180, east: box.west, south: -85, north: 85 },
{ west: box.east, east: 180, south: -85, north: 85 },
{ west: box.west, east: box.east, south: -85, north: box.south },
{ west: box.west, east: box.east, south: box.north, north: 85 },
];
lines = lines.flatMap((line) =>
regions.flatMap((region) => clipTileLine(line, region)),
);
}
return lines.map((coordinates) => ({ ...road, coordinates }));
});
}
function seeded(seed) {
let state = seed >>> 0;
return () => {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
return state / 2 ** 32;
};
}
function tileBlock(x, y, size = 4) {
const tiles = [];
for (let dx = 0; dx < size; dx++)
for (let dy = 0; dy < size; dy++)
tiles.push({ z: 14, x: x + dx, y: y + dy });
return tiles;
}
/** Roads in and around a tile block: wandering, straight, tiny, and on tile edges. */
function roadsAround(tiles, random, count = 400) {
const boxes = tiles.map((tile) => tileToBBox(tile.z, tile.x, tile.y));
const west = Math.min(...boxes.map((box) => box.west)),
east = Math.max(...boxes.map((box) => box.east)),
south = Math.min(...boxes.map((box) => box.south)),
north = Math.max(...boxes.map((box) => box.north));
const width = east - west,
height = north - south;
const roads = [];
for (let i = 0; i < count; i++) {
const step = i % 7 === 0 ? 0.00002 : i % 3 === 0 ? 0.01 : 0.002;
let lon = west - width / 2 + random() * width * 2,
lat = south - height / 2 + random() * height * 2;
const coordinates = [[lon, lat]];
const points = 1 + Math.floor(random() * 12);
for (let j = 0; j < points; j++) {
lon += (random() - 0.5) * step;
lat += (random() - 0.5) * step;
coordinates.push([lon, lat]);
}
roads.push({ type: 'primary', oneway: i % 2, id: i, coordinates });
}
const box = boxes[5];
const midLat = (box.south + box.north) / 2,
midLon = (box.west + box.east) / 2;
roads.push(
// Along a tile edge, ending on a corner, and touching an edge from outside.
{
id: 'edge',
coordinates: [
[box.west, box.south - 0.01],
[box.west, box.north + 0.01],
],
},
{
id: 'corner',
coordinates: [
[box.west - 0.01, box.south - 0.01],
[box.west, box.south],
[box.east, box.north],
],
},
{
id: 'touch',
coordinates: [
[box.west - 0.01, midLat],
[box.west, midLat],
],
},
// Across the whole block, and a repeated vertex.
{
id: 'across',
coordinates: [
[west - 0.01, midLat],
[east + 0.01, midLat + 0.0003],
],
},
{
id: 'repeat',
coordinates: [
[midLon, midLat],
[midLon, midLat],
[midLon + 0.001, midLat],
],
},
// Records the clip drops or leaves alone.
{ id: 'single', coordinates: [[west - 0.02, midLat]] },
{ id: 'empty', coordinates: [] },
{
id: 'negative-zero',
coordinates: [
[-0, south - 0.02],
[0.001, south - 0.02],
],
},
{
id: 'polar',
coordinates: [
[west - 0.02, 86],
[west - 0.02, 86.01],
],
},
);
return roads;
}
for (const [place, x, y] of [
['Manhattan', 4823, 6158],
['London, across longitude 0', 8190, 5446],
['longitude 0 and latitude 0', 8190, 8190],
['the southern and eastern hemispheres', 15070, 9826],
])
test(`coarse roads clipped around detail tiles match the every-tile clip: ${place}`, () => {
const tiles = tileBlock(x, y);
const roads = roadsAround(tiles, seeded(x * 31 + y));
for (const loaded of [tiles, tiles.slice(0, 5), tiles.slice(3, 4), []]) {
const expected = clipEveryRoadAgainstEveryTile(roads, loaded);
assert.ok(loaded.length === 0 || expected.length !== roads.length);
// deepStrictEqual separates -0 from 0 and compares every vertex exactly.
assert.deepStrictEqual(outsideDetailTiles(roads, loaded), expected);
}
});
test('only a road that reaches a detail tile is clipped against it', () => {
const tiles = tileBlock(4823, 6158);
const block = tiles.map((tile) => tileToBBox(tile.z, tile.x, tile.y));
const west = Math.min(...block.map((box) => box.west)),
north = Math.max(...block.map((box) => box.north));
// 300 roads beside the block and between its tile edges, none touching one.
const clear = [];
for (let i = 0; i < 300; i++) {
const box = block[i % block.length];
const lon = i % 2 ? west - 0.02 - i * 1e-5 : box.west + 0.004;
const lat = i % 2 ? box.south + 0.004 : north + 0.01 + i * 1e-5;
clear.push({
coordinates: [
[lon, lat],
[lon + 0.002, lat + 0.0005],
[lon + 0.003, lat + 0.001],
],
});
}
const first = block[0];
const crossing = {
coordinates: [
[first.west + 0.004, north + 0.01],
[first.west + 0.004, (first.south + first.north) / 2],
],
};
let clips = 0;
const counted = (line, region) => {
clips++;
return clipTileLine(line, region);
};
const roads = outsideDetailTiles([...clear, crossing], tiles, counted);
assert.deepStrictEqual(
roads,
clipEveryRoadAgainstEveryTile([...clear, crossing], tiles),
);
assert.equal(roads.length, 301);
// Four regions for each tile the crossing road reaches, never the 19,264
// clips of every road against every tile.
assert.ok(clips >= 4, `expected the crossing road to be clipped: ${clips}`);
assert.ok(clips <= 4 * tiles.length, `clipped ${clips} times`);
});
/** A DEM-regime ground service: one floor per toFixed(3) cell, counting reads. */
function cellGround({ mesh = false, warm = () => true } = {}) {
const ground = {
reads: 0,
meshFloorPreferred: () => mesh,
cachedGroundFloor(lat, lon) {
ground.reads++;
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
// Number() as the shared cache does it: "-0.000" and "0.000" are one cell.
const cell = `${Number(lat.toFixed(3))},${Number(lon.toFixed(3))}`;
if (!warm(cell)) return null;
let hash = 0;
for (const character of cell)
hash = (Math.imul(hash, 31) + character.charCodeAt(0)) | 0;
// The mesh regime answers per point, not per cell.
return (hash % 4000) / 10 + (mesh ? lat - Math.floor(lat * 1000) / 1000 : 0);
},
};
return ground;
}
test('floor reads are shared per cell and equal the per-coordinate reads', () => {
const random = seeded(7);
const points = [];
for (let i = 0; i < 4000; i++)
points.push([40.75 + random() * 0.01, -73.99 + random() * 0.01]);
// Cell edges, where toFixed(3) and Math.round can disagree, in each sign.
for (const lat of [40.7505, -40.7505, 0.0005, -0.0005, 1.0005, 8.5745])
for (const lon of [-73.9905, 73.9905, 0.0005, -0.0005, 1.0005, 2.3515])
points.push([lat, lon], [lat + 1e-9, lon - 1e-9], [lat - 1e-9, lon]);
points.push([Number.NaN, 0], [0, Infinity], [0, 0], [-0, -0]);
const direct = cellGround();
const expected = points.map(([lat, lon]) =>
direct.cachedGroundFloor(lat, lon),
);
const ground = cellGround();
const floorAt = floorCellReader(ground);
assert.deepStrictEqual(
points.map(([lat, lon]) => floorAt(lat, lon)),
expected,
);
// 4000 points in a 0.01 degree square share about 121 cells.
assert.ok(ground.reads < 600, `read the shared cache ${ground.reads} times`);
// A cold cell is remembered as cold for the pass, not asked again.
const cold = cellGround({ warm: () => false });
const coldAt = floorCellReader(cold);
assert.equal(coldAt(40.7512, -73.9812), null);
assert.equal(coldAt(40.7513, -73.9813), null);
assert.equal(cold.reads, 1);
});
test('mesh floors and unknown ground services are read per coordinate', () => {
const mesh = cellGround({ mesh: true });
const meshAt = floorCellReader(mesh);
const a = meshAt(40.7512, -73.9812),
b = meshAt(40.75121, -73.9812);
assert.equal(mesh.reads, 2);
assert.notEqual(a, b);
// No regime to ask: nothing says a floor is the same across a cell.
let reads = 0;
const bareAt = floorCellReader({ cachedGroundFloor: () => ++reads });
bareAt(40.7512, -73.9812);
bareAt(40.7512, -73.9812);
assert.equal(reads, 2);
assert.equal(floorCellReader(undefined)(1, 2), undefined);
assert.equal(floorCellReader({})(1, 2), undefined);
});
function road(lon, lat, points = 40) {
const coordinates = [];
for (let i = 0; i < points; i++)
coordinates.push([lon + i * 0.00004, lat + i * 0.00003]);
return { type: 'residential', oneway: 1, coordinates };
}
test('parsing reads each floor cell once and builds the same waypoints', () => {
const ground = cellGround({ warm: (cell) => !cell.endsWith('3') });
const state = { _viewHeightEstimate: 12 };
const model = createModel({
state,
services: { ground },
parts: {},
source: {},
});
const records = [road(-73.99, 40.75), road(-73.9899, 40.7501)];
const parsed = model.parseRoads({ roads: records });
const coordinates = records.reduce(
(count, record) => count + record.coordinates.length,
0,
);
assert.ok(
ground.reads < coordinates / 3,
`${ground.reads} floor reads for ${coordinates} coordinates`,
);
const direct = cellGround({ warm: (cell) => !cell.endsWith('3') });
const expected = records.flatMap((record) =>
roadSurfaceChunks(record.coordinates).map((coords) =>
coords.map(([lon, lat]) => {
const floor = direct.cachedGroundFloor(lat, lon);
return Cesium.Cartesian3.fromDegrees(
lon,
lat,
(floor ?? state._viewHeightEstimate) + DOT_HEIGHT_OFFSET,
);
}),
),
);
assert.ok(direct.reads >= coordinates);
assert.deepStrictEqual(
parsed.map((entry) => entry.waypoints),
expected,
);
});
test('a record parsed earlier in the pass is not parsed again', () => {
const ground = cellGround();
const model = createModel({
state: {},
services: { ground },
parts: {},
source: {},
});
const first = { ...road(-73.99, 40.75), oneway: 0 },
second = road(-73.98, 40.76),
short = { type: 'primary', coordinates: [[-73.97, 40.77]] };
const parsedBySource = new WeakMap();
const tile = model.parseRoads({ roads: [first] }, null, parsedBySource);
assert.equal(tile.length, 2, 'a two-way record parses to both directions');
const reads = ground.reads;
const replaced = model.parseRoads(
{ roads: [second, short, first], replace: true },
null,
parsedBySource,
);
assert.equal(replaced.length, 3);
// The same parsed roads, in the snapshot's order: dots stay attached.
assert.equal(replaced[1], tile[0]);
assert.equal(replaced[2], tile[1]);
const fresh = cellGround();
createModel({
state: {},
services: { ground: fresh },
parts: {},
source: {},
}).parseRoads({ roads: [second] });
assert.equal(ground.reads - reads, fresh.reads, 'only the new record read');
// Without the map every call parses, as before.
const again = model.parseRoads({ roads: [first] });
assert.notEqual(again[0], tile[0]);
assert.deepStrictEqual(again, tile);
});
test('the snapshot that ends a pass parses only the records it adds', async () => {
const detail = { z: 14, x: 3743, y: 6745 };
const core = tileToBBox(detail.z, detail.x, detail.y);
const local = {
type: 'residential',
oneway: 1,
coordinates: [
[core.west + 0.002, core.south + 0.002],
[core.west + 0.004, core.south + 0.004],
],
};
const distant = {
type: 'primary',
oneway: 1,
coordinates: [
[core.east + 0.01, core.south],
[core.east + 0.02, core.south],
],
};
const bounds = {
...core,
east: core.east + 0.03,
coverage: {
detail: [detail],
coarse: [{ z: 12, x: 935, y: 1686 }],
key: 'test',
},
};
const source = createTrafficSource({
mapTiles: {
async fetchBounds(box, { zoom, onTile }) {
const tile = { roads: zoom === 14 ? [local] : [distant] };
onTile?.(tile);
return {
tiles: [tile],
loadedTiles: zoom === 14 ? [detail] : bounds.coverage.coarse,
partial: false,
};
},
},
});
const parsedRecords = [];
const painted = [];
const state = {
_enabled: true,
_loadGeneration: 0,
_tileCache: new Map(),
_roadMode: null,
_liveMode: false,
// The model's contract: fill the map, answer known records from it.
_parseRoads: (data, trace, parsedBySource) =>
data.roads.flatMap((record) => {
const known = parsedBySource?.get(record);
if (known) return known;
parsedRecords.push(record);
const parsed = [{ record }];
parsedBySource?.set(record, parsed);
return parsed;
}),
};
const ingestion = createIngestion({
state,
source,
services: {},
parts: {
viewport: {
clampBounds: (b) => b,
getBoundsCenter: () => ({ lat: 30.26, lon: -97.74 }),
},
flow: {
warmFlow: () => Promise.resolve([]),
applyFlowThenRender: async (roads, box, generation, altitude, label) => {
if (label === 'Loaded full') painted.push(roads);
return true;
},
},
},
});
await ingestion.loadRoadsForBounds(bounds, 350);
assert.equal(
parsedRecords.filter((record) => record === local).length,
1,
'the detail tile record is parsed when its tile streams, not again',
);
// The full graph is still the detail record plus the coarse road outside it.
assert.equal(painted.at(-1).length, 2);
assert.equal(painted.at(-1)[0].record, local);
assert.deepStrictEqual(
painted.at(-1)[1].record.coordinates,
distant.coordinates,
);
});
+15 -100
View File
@@ -33,108 +33,23 @@ export function trafficDetailBounds(box) {
return detail;
}
/** Longitude/latitude extent of one line; NaN coordinates leave it unusable. */
function lineExtent(coordinates) {
let west = Infinity,
east = -Infinity,
south = Infinity,
north = -Infinity;
for (const [lon, lat] of coordinates) {
if (!(lon >= west)) west = lon;
if (!(lon <= east)) east = lon;
if (!(lat >= south)) south = lat;
if (!(lat <= north)) north = lat;
}
return { coordinates, west, east, south, north, settled: false };
}
/**
* What `clipTileLine` returns for a line lying wholly inside the clip region:
* the same line with each vertex recomputed from the one before it. Returns
* null for a line under the 12 m floor `clipTileLine` drops.
*/
function unclippedLine(coordinates) {
if (coordinates.length < 2) return null;
const out = [];
let metres = 0;
for (let i = 1; i < coordinates.length; i++) {
const a = coordinates[i - 1],
b = coordinates[i];
const dx = b[0] - a[0],
dy = b[1] - a[1];
if (i === 1) out.push([a[0] + 0 * dx, a[1] + 0 * dy]);
out.push([a[0] + dx, a[1] + dy]);
metres +=
Math.hypot(
(out[i][0] - out[i - 1][0]) * Math.cos((out[i][1] * Math.PI) / 180),
out[i][1] - out[i - 1][1],
) * 111320;
}
return metres >= 12 ? out : null;
}
const sameLine = (a, b) =>
a.every(
(point, i) => Object.is(point[0], b[i][0]) && Object.is(point[1], b[i][1]),
);
/**
* Keep coarse roads only outside the detailed tile cores.
*
* Each tile splits a line into the four regions around its core. Nearly every
* coarse road lies wholly inside one of those regions for nearly every tile,
* where the clip hands the line back unchanged, so that case is answered from
* the line's extent and only a line that reaches a tile's core or edge is
* clipped. Clipping every road against every tile froze the page for hundreds
* of milliseconds per road load at street level.
*
* `clip` is injectable for tests.
*/
export function outsideDetailTiles(roads, tiles, clip = clipTileLine) {
const boxes = tiles.map((tile) => tileToBBox(tile.z, tile.x, tile.y));
/** Keep coarse roads only outside the detailed tile cores. */
export function outsideDetailTiles(roads, tiles) {
return roads.flatMap((road) => {
let lines = [lineExtent(road.coordinates)];
for (const box of boxes) {
const next = [];
for (const line of lines) {
// Strictly inside one region, and inside the bounds every region
// shares: the other three regions return nothing.
const clear =
line.west > -180 &&
line.east < 180 &&
line.south > -85 &&
line.north < 85 &&
(line.east < box.west ||
line.west > box.east ||
(line.west > box.west &&
line.east < box.east &&
(line.north < box.south || line.south > box.north)));
if (!clear) {
const regions = [
{ west: -180, east: box.west, south: -85, north: 85 },
{ west: box.east, east: 180, south: -85, north: 85 },
{ west: box.west, east: box.east, south: -85, north: box.south },
{ west: box.west, east: box.east, south: box.north, north: 85 },
];
for (const region of regions)
for (const coordinates of clip(line.coordinates, region))
next.push(lineExtent(coordinates));
} else if (line.settled) {
next.push(line);
} else {
// The first unclipped pass can still move a vertex by a rounding
// step (a road crossing longitude or latitude 0); once a pass
// changes nothing, no later one will.
const coordinates = unclippedLine(line.coordinates);
if (!coordinates) continue;
const kept = lineExtent(coordinates);
kept.settled = sameLine(coordinates, line.coordinates);
next.push(kept);
}
}
lines = next;
let lines = [road.coordinates];
for (const tile of tiles) {
const box = tileToBBox(tile.z, tile.x, tile.y);
const regions = [
{ west: -180, east: box.west, south: -85, north: 85 },
{ west: box.east, east: 180, south: -85, north: 85 },
{ west: box.west, east: box.east, south: -85, north: box.south },
{ west: box.west, east: box.east, south: box.north, north: 85 },
];
lines = lines.flatMap((line) =>
regions.flatMap((region) => clipTileLine(line, region)),
);
}
return lines.map(({ coordinates }) => ({ ...road, coordinates }));
return lines.map((coordinates) => ({ ...road, coordinates }));
});
}
-39
View File
@@ -69,45 +69,6 @@ export function trafficSurfaceReady(scene) {
return currentRenderedFrame(scene)?.ready === true;
}
/**
* Ground-floor reads for one synchronous pass over road coordinates, asking
* the shared cache once per floor cell instead of once per coordinate.
*
* A road tile puts thousands of coordinates into a few hundred ~111 m cells,
* and each shared read formats and looks up two string keys, which added up
* to seconds per street-level load. Mesh floors answer per point (the nearest
* reading in the cell), so they are read through unchanged; only the DEM
* regime is one value per cell. Build a reader per pass: cells warm up
* between passes.
*/
export function floorCellReader(ground) {
const read = ground?.cachedGroundFloor;
if (!read) return () => undefined;
if (ground.meshFloorPreferred?.() !== false)
return (lat, lon) => read(lat, lon);
const cells = new Map();
return (lat, lon) => {
const y = lat * 1000,
x = lon * 1000;
const row = Math.round(y),
column = Math.round(x);
// The shared cache rounds with toFixed(3). At a cell edge that and
// Math.round can pick different cells, so an edge point asks directly.
if (!(
Math.abs(Math.abs(y - row) - 0.5) > 1e-6 &&
Math.abs(Math.abs(x - column) - 0.5) > 1e-6
))
return read(lat, lon);
const key = row * 1e6 + column;
let floor = cells.get(key);
if (floor === undefined) {
floor = read(lat, lon) ?? null;
cells.set(key, floor);
}
return floor;
};
}
/** Preserve bends and insert height samples at most 150 m apart, splitting long roads. */
export function roadSurfaceChunks(coordinates) {
const chunks = [];
+3 -12
View File
@@ -1,5 +1,5 @@
import * as Cesium from 'cesium';
import { floorCellReader, roadSurfaceChunks } from './surface.js';
import { roadSurfaceChunks } from './surface.js';
import { TRAFFIC_TIMING_ENABLED, DOT_HEIGHT_OFFSET } from './policy.js';
export function createTiming({ state: layerState, services, parts, source }) {
@@ -190,7 +190,7 @@ export function createTiming({ state: layerState, services, parts, source }) {
* waypoint-materialization time independently.
*/
function parseRoadsTimed(roadData, trace, parsedBySource) {
function parseRoadsTimed(roadData, trace) {
/* TRACE_ONLY_BEGIN */
const _trafficTimingState = trafficTimingPass(
trace,
@@ -231,19 +231,11 @@ export function createTiming({ state: layerState, services, parts, source }) {
}
const roads = [];
const floorAt = floorCellReader(services.ground);
/* TRACE_ONLY_BEGIN */
let _trafficTimingWaypointMaterializationMs = 0;
/* TRACE_ONLY_END */
for (const road of roadData.roads) {
if (!road.coordinates || road.coordinates.length < 2) continue;
// A replacing snapshot repeats records this load already parsed.
const known = parsedBySource?.get(road);
if (known) {
for (const parsed of known) roads.push(parsed);
continue;
}
const first = roads.length;
for (const coords of roadSurfaceChunks(road.coordinates)) {
const type = road.type;
@@ -253,7 +245,7 @@ export function createTiming({ state: layerState, services, parts, source }) {
const _trafficTimingMaterializeStart = performance.now();
/* TRACE_ONLY_END */
const waypoints = coords.map(([lng, lat]) => {
const floor = floorAt(lat, lng);
const floor = services.ground?.cachedGroundFloor?.(lat, lng);
const h =
(Number.isFinite(floor) && Math.abs(floor) <= 9000
? floor
@@ -287,7 +279,6 @@ export function createTiming({ state: layerState, services, parts, source }) {
flow: road.flow || null,
});
}
parsedBySource?.set(road, roads.slice(first));
}
/* TRACE_ONLY_BEGIN */
const _trafficTimingMetrics = {