diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab3b03cc..1b71b6b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is unchanged; extra cameras share it round-robin with the other packs. +- Street Level counts unique tiles before ranking so overlapping dateline halves + in polar views do not waste slots in the 16-tile overview budget. + - Directions no longer shows a truncation notice for a route with exactly 200 turns, and a roundabout at the 200th turn keeps its exit distance. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 054aff963..7f9c94ff4 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3132,6 +3132,10 @@ sequence lines below 60 km; the proxy strips the unused `image` layer from z14 tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent requests for a tile. +Mapillary coverage ranks unique tile addresses before applying the 16-tile +overview budget. Overlapping dateline halves in a polar view therefore do not +waste overview slots on the same tile; loading already skips duplicate keys. + On Google 3D (`photoreal`) at street zoom (in below 1,400 m above ground, out above 1,800 m) the surface mode switches from `draped` to `terrain`: draped lines land on roofs and tree tops, so `groundCast.js` instead places lines, diff --git a/src/layers/streetLevel/street-level-unique-tiles.test.mjs b/src/layers/streetLevel/street-level-unique-tiles.test.mjs new file mode 100644 index 000000000..52e7b6aea --- /dev/null +++ b/src/layers/streetLevel/street-level-unique-tiles.test.mjs @@ -0,0 +1,31 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { tilesForBbox } from './tileMath.js'; + +test('reachable polar dateline coverage ranks 24 distinct tiles before overview truncation', () => { + // coverage uses the actual view bbox above zoom 1; polar views can wrap. + const bbox = [-10, 60, -20, 85]; + const result = tilesForBbox(bbox, 3); + const keys = result.tiles.map((tile) => `${tile.x}/${tile.y}/${tile.z}`); + assert.equal(keys.length, 24); + assert.equal(new Set(keys).size, 24); + assert.equal(result.total, 24); + assert.equal(result.truncated, false); + const overview = tilesForBbox(bbox, 3, { limit: 16 }); + assert.equal(overview.total, 24); + assert.equal(overview.tiles.length, 16); + assert.equal( + new Set(overview.tiles.map((tile) => `${tile.x}/${tile.y}/${tile.z}`)).size, + 16, + ); + assert.equal(overview.truncated, true); +}); + +test('a limit that fits every distinct tile is not spent on repeats', () => { + // Before de-duplication this box yielded 27 entries for 24 tiles, so a + // 24-slot overview held repeats and still reported itself truncated. + const result = tilesForBbox([-10, 60, -20, 85], 3, { limit: 24 }); + const keys = result.tiles.map((tile) => `${tile.x}/${tile.y}/${tile.z}`); + assert.equal(new Set(keys).size, 24); + assert.equal(result.truncated, false); +}); diff --git a/src/layers/streetLevel/tileMath.js b/src/layers/streetLevel/tileMath.js index fe3c3ec18..350188200 100644 --- a/src/layers/streetLevel/tileMath.js +++ b/src/layers/streetLevel/tileMath.js @@ -146,8 +146,16 @@ export function tilesForBbox(bbox, z, { limit = Infinity, focus = null } = {}) { const dx = Math.abs(tile.x - centre.x); return Math.min(dx, n - dx) ** 2 + (tile.y - centre.y) ** 2; }; - // Each tile's distance is computed once; ties keep their row order. - const ranked = tiles + // The date-line halves share a column when the gap between east and west is + // narrower than a tile; count that column once so a capped overview does not + // spend its slots on repeats. Each tile's distance is computed once; ties keep + // their row order. + const unique = [ + ...new Map( + tiles.map((tile) => [`${tile.x}/${tile.y}/${tile.z}`, tile]), + ).values(), + ]; + const ranked = unique .map((tile) => ({ tile, d: distance2(tile) })) .sort((a, b) => a.d - b.d) .map(({ tile }) => tile);