From 5b8e6fbc38efec15e520a4f6f24403078efd6ecf Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Sun, 25 Oct 2026 06:33:10 +0000 Subject: [PATCH] fix(street-level): read the older {zoom, kind} zoomFor shape (#1288) Restore legacy provider zoomFor compatibility and document supported return values. Contribution by @asturksever. --- changelog.d/1288-zoomfor-legacy-shape.md | 4 ++ docs/CURRENT-STATE.md | 4 ++ src/layers/streetLevel/lineCoverage.js | 26 +++++++++++-- src/layers/streetLevel/lineCoverage.test.mjs | 40 ++++++++++++++++++-- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 changelog.d/1288-zoomfor-legacy-shape.md diff --git a/changelog.d/1288-zoomfor-legacy-shape.md b/changelog.d/1288-zoomfor-legacy-shape.md new file mode 100644 index 000000000..e23b524e8 --- /dev/null +++ b/changelog.d/1288-zoomfor-legacy-shape.md @@ -0,0 +1,4 @@ +- A Street Level provider whose `zoomFor` still answers `{zoom, kind}`, the + shape before the core went slim, draws its lines again instead of a silent + blank; an overview level or any answer that is not a whole zoom shows the + zoom-in hint (asturksever, #1288). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6f0f0bc85..e4e0063a7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3753,6 +3753,10 @@ disabled. With it, coverage draws as z11–14 sequence lines below 60 km; the pr tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent requests for a tile. +Line coverage accepts a provider’s numeric `zoomFor` result or its legacy +`{zoom, kind}` shape. Overview levels and invalid zooms show the zoom-in hint +instead of silently requesting no tiles. + Coverage is z11–14 sequence lines below 60 km; above that nothing is drawn and the panel says to zoom in. Tiles come from the visible ground within range of the camera, nearest first, at most 9 (the halves of a view across diff --git a/src/layers/streetLevel/lineCoverage.js b/src/layers/streetLevel/lineCoverage.js index 5182f2309..bab1af46d 100644 --- a/src/layers/streetLevel/lineCoverage.js +++ b/src/layers/streetLevel/lineCoverage.js @@ -105,6 +105,25 @@ function pointsOf(sequence) { * @property {boolean} [keepOrder] Cap a tile's lines in the order decoded (default: newest first). */ +/** + * The tile zoom a provider's `zoomFor` answered, or null. Providers written + * before the core went slim return `{zoom, kind}`: its zoom is read, and an + * `'overview'` level draws nothing, since nothing is drawn from orbit. Any + * other answer that is not a whole zoom draws nothing too, rather than + * asking for no tiles without a hint. + * @param {unknown} level + * @returns {number|null} + */ +export function coverageZoom(level) { + const zoom = + level && typeof level === 'object' + ? level.kind === 'overview' + ? null + : level.zoom + : level; + return Number.isInteger(zoom) && zoom >= 0 ? zoom : null; +} + /** * Camera-driven sequence lines from any XYZ line-tile source, near the * ground only, draped on terrain and 3D tiles alike. Nothing is drawn from @@ -115,8 +134,9 @@ function pointsOf(sequence) { * @param {(tile: {x: number, y: number, z: number}, init: {signal: AbortSignal}) => Promise<{sequences: Array}>} options.fetchTile * A decoded tile; sequences are `{id, parts, capturedAt, isPano}`. A * rejection may carry `keyRequired`, `keyRejected` or `retryAfterSec`. - * @param {(heightM: number|null) => number|null} options.zoomFor - * Tile zoom for a camera height above ground; null draws nothing. + * @param {(heightM: number|null) => number|{zoom: number, kind?: string}|null} options.zoomFor + * Tile zoom for a camera height above ground; null draws nothing. The + * older `{zoom, kind}` shape is still read (see `coverageZoom`). * @param {{coverage: string, selected: string}} options.colors * @param {string} options.pickPrefix Pick-id prefix of the lines. * @param {string} options.renderKey Render-governor reason. @@ -380,7 +400,7 @@ export function createLineCoverage({ const height = Number.isFinite(cameraHeight) ? cameraHeight - (ground ?? 0) : null; - const zoom = zoomFor(height); + const zoom = coverageZoom(zoomFor(height)); if (zoom == null) return { hint: messages.tooHigh || ZOOM_IN_HINT }; // Rays meet the ground where it really is (1,600 m up in Denver) and stop // short of the horizon, which would stretch the box to the world. diff --git a/src/layers/streetLevel/lineCoverage.test.mjs b/src/layers/streetLevel/lineCoverage.test.mjs index 049d2f746..60bae7eb3 100644 --- a/src/layers/streetLevel/lineCoverage.test.mjs +++ b/src/layers/streetLevel/lineCoverage.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import * as Cesium from 'cesium'; import { + coverageZoom, createLineCoverage, freshCoverage, ZOOM_IN_HINT, @@ -28,7 +29,7 @@ const { maxSequences: COVERAGE_MAX_SEQUENCES, maxTiles: COVERAGE_MAX_TILES } = COVERAGE_TUNING; /** Line coverage as the Mapillary provider builds it, over `source`. */ -function createCoverage({ state, source }) { +function createCoverage({ state, source, zoomFor = coverageZoomForHeight }) { return createLineCoverage({ state, fetchTile: async (tile, { signal }) => @@ -36,7 +37,7 @@ function createCoverage({ state, source }) { await source.getTile('coverage', tile.z, tile.x, tile.y, { signal }), tile, ), - zoomFor: coverageZoomForHeight, + zoomFor, colors: COLORS, pickPrefix: PICK_PREFIX.sequence, renderKey: 'mapillary-coverage', @@ -123,7 +124,7 @@ function deferredSource() { }; } -function setup() { +function setup({ zoomFor } = {}) { const centre = tileCentre(-121.4944, 38.5816); const viewer = fakeViewer({ lon: centre.lon, lat: centre.lat, height: 900 }); const source = deferredSource(); @@ -146,7 +147,7 @@ function setup() { }, sequence: { selectedId: null }, }; - const coverage = createCoverage({ state, source }); + const coverage = createCoverage({ state, source, zoomFor }); const bytes = encodeCoverageTile(centre.tile, { sequences: [ { @@ -239,6 +240,37 @@ test('panning off a tile that is still loading stops counting it as loading', as coverage.clear(); }); +test('a zoomFor in the older {zoom, kind} shape still draws its lines', async () => { + // Providers written before the core went slim (#1139's Vegbilder) answer + // an object: it must not ask for no tiles and say nothing. + const { viewer, source, coverage, bytes } = setup({ + zoomFor: (height) => { + const zoom = coverageZoomForHeight(height); + return zoom == null ? null : { zoom, kind: 'sequence' }; + }, + }); + coverage.refresh(); + assert.equal(source.calls.length, 1, 'the tile is asked for'); + assert.equal(source.calls[0].z, 14); + source.calls[0].resolve(bytes); + await settle(); + assert.equal(viewer.scene.groundPrimitives.items.size, 1, 'its line drawn'); + assert.equal(coverage.stats().hint, ''); + coverage.clear(); +}); + +test('a zoom level is read from either shape; an overview or a bad answer draws nothing', () => { + assert.equal(coverageZoom(14), 14); + assert.equal(coverageZoom({ zoom: 15, kind: 'sequence' }), 15); + assert.equal(coverageZoom({ zoom: 12 }), 12); + assert.equal(coverageZoom({ zoom: 5, kind: 'overview' }), null, 'orbit'); + assert.equal(coverageZoom(null), null); + assert.equal(coverageZoom(undefined), null); + assert.equal(coverageZoom({}), null); + assert.equal(coverageZoom(14.5), null); + assert.equal(coverageZoom('14'), null); +}); + test('from orbit nothing is requested or drawn, and the hint says to zoom in', async () => { const { viewer, source, state, coverage, bytes } = setup(); coverage.refresh();