From 346a08a42e682509cf0acc08eb4f238e5407d827 Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Fri, 13 Nov 2026 16:03:19 +0100 Subject: [PATCH] perf(traffic): stop clipping and re-parsing the whole road set in one task After the detail tiles arrived, every coarse road was clipped against every detail tile for four regions, the composed set was parsed again although each tile had been parsed as it streamed, and every coordinate asked the ground floor service for its cell. A road wholly inside one region is now answered from its extent, records already parsed in a pass are reused, and the floor is read once per cell within a parse (when floors are per cell). Co-Authored-By: Claude Fable 5.2 --- src/layers/traffic/flow.js | 4 +- src/layers/traffic/index.js | 6 +- src/layers/traffic/ingestion.js | 8 +- src/layers/traffic/model.js | 22 +- src/layers/traffic/roadLoadCost.test.mjs | 444 +++++++++++++++++++++++ src/layers/traffic/source.js | 115 +++++- src/layers/traffic/surface.js | 39 ++ src/layers/traffic/timing.js | 15 +- 8 files changed, 626 insertions(+), 27 deletions(-) create mode 100644 src/layers/traffic/roadLoadCost.test.mjs diff --git a/src/layers/traffic/flow.js b/src/layers/traffic/flow.js index caf8aede2..ff5c7c189 100644 --- a/src/layers/traffic/flow.js +++ b/src/layers/traffic/flow.js @@ -1,6 +1,7 @@ import { roadIdentity } from './retention.js'; import * as Cesium from 'cesium'; import { + floorCellReader, prepareRoadSurfaces, roadHeightStations, trafficSurfaceReady, @@ -301,6 +302,7 @@ 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); @@ -319,7 +321,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 = services.ground?.cachedGroundFloor?.(lat, lon); + const floor = floorAt(lat, lon); const height = Number.isFinite(floor) && Math.abs(floor) <= 9000 ? Math.max(floor, layerState._viewHeightEstimate) diff --git a/src/layers/traffic/index.js b/src/layers/traffic/index.js index 4a5839f7d..eb0f8f544 100644 --- a/src/layers/traffic/index.js +++ b/src/layers/traffic/index.js @@ -39,10 +39,10 @@ export function createTrafficLayer({ services, source }) { parts.controls = createControls(context); parts.lifecycle = createLifecycle(context); state._parseRoads = TRAFFIC_TIMING_ENABLED - ? (data, trace) => + ? (data, trace, parsedBySource) => trace - ? parts.timing.parseRoadsTimed(data, trace) - : parts.model.parseRoads(data) + ? parts.timing.parseRoadsTimed(data, trace, parsedBySource) + : parts.model.parseRoads(data, trace, parsedBySource) : parts.model.parseRoads; state._loadRoadsForBounds = TRAFFIC_TIMING_ENABLED diff --git a/src/layers/traffic/ingestion.js b/src/layers/traffic/ingestion.js index a5e09f543..0065f5638 100644 --- a/src/layers/traffic/ingestion.js +++ b/src/layers/traffic/ingestion.js @@ -348,11 +348,16 @@ 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); + const parsed = layerState._parseRoads(data, trace, parsedBySource); phaseTiming('parse', start, { roads: parsed.length, incremental: true }); if (data.replace) streamed = parsed; else streamed.push(...parsed); @@ -531,6 +536,7 @@ 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}]`); diff --git a/src/layers/traffic/model.js b/src/layers/traffic/model.js index ff1c89ddb..10b2a4bbb 100644 --- a/src/layers/traffic/model.js +++ b/src/layers/traffic/model.js @@ -1,5 +1,5 @@ import * as Cesium from 'cesium'; -import { roadSurfaceChunks } from './surface.js'; +import { floorCellReader, roadSurfaceChunks } from './surface.js'; import { t, translateKnown } from '../../i18n/index.js'; import { flowDensityMult, @@ -13,22 +13,35 @@ import { } from './policy.js'; export function createModel({ state: layerState, services, parts, source }) { - /** Build scene waypoints from source records; thinning and terrain remain rendering policy. */ - function parseRoads(roadData) { + /** + * 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) { 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 = services.ground?.cachedGroundFloor?.(lat, lng); + const floor = floorAt(lat, lng); const h = (Number.isFinite(floor) && Math.abs(floor) <= 9000 ? floor @@ -60,6 +73,7 @@ export function createModel({ state: layerState, services, parts, source }) { flow: road.flow || null, }); } + parsedBySource?.set(road, roads.slice(first)); } return roads; diff --git a/src/layers/traffic/roadLoadCost.test.mjs b/src/layers/traffic/roadLoadCost.test.mjs new file mode 100644 index 000000000..c248624c4 --- /dev/null +++ b/src/layers/traffic/roadLoadCost.test.mjs @@ -0,0 +1,444 @@ +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, + ); +}); diff --git a/src/layers/traffic/source.js b/src/layers/traffic/source.js index 7327a3f98..135db0f38 100644 --- a/src/layers/traffic/source.js +++ b/src/layers/traffic/source.js @@ -33,23 +33,108 @@ export function trafficDetailBounds(box) { return detail; } -/** Keep coarse roads only outside the detailed tile cores. */ -export function outsideDetailTiles(roads, tiles) { +/** 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)); 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)), - ); + 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; } - return lines.map((coordinates) => ({ ...road, coordinates })); + return lines.map(({ coordinates }) => ({ ...road, coordinates })); }); } diff --git a/src/layers/traffic/surface.js b/src/layers/traffic/surface.js index 3344faa64..37a528901 100644 --- a/src/layers/traffic/surface.js +++ b/src/layers/traffic/surface.js @@ -69,6 +69,45 @@ 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 = []; diff --git a/src/layers/traffic/timing.js b/src/layers/traffic/timing.js index 0be95a330..bd1a3abb2 100644 --- a/src/layers/traffic/timing.js +++ b/src/layers/traffic/timing.js @@ -1,5 +1,5 @@ import * as Cesium from 'cesium'; -import { roadSurfaceChunks } from './surface.js'; +import { floorCellReader, 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) { + function parseRoadsTimed(roadData, trace, parsedBySource) { /* TRACE_ONLY_BEGIN */ const _trafficTimingState = trafficTimingPass( trace, @@ -231,11 +231,19 @@ 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; @@ -245,7 +253,7 @@ export function createTiming({ state: layerState, services, parts, source }) { const _trafficTimingMaterializeStart = performance.now(); /* TRACE_ONLY_END */ const waypoints = coords.map(([lng, lat]) => { - const floor = services.ground?.cachedGroundFloor?.(lat, lng); + const floor = floorAt(lat, lng); const h = (Number.isFinite(floor) && Math.abs(floor) <= 9000 ? floor @@ -279,6 +287,7 @@ export function createTiming({ state: layerState, services, parts, source }) { flow: road.flow || null, }); } + parsedBySource?.set(road, roads.slice(first)); } /* TRACE_ONLY_BEGIN */ const _trafficTimingMetrics = {