fix(street-level): read the older {zoom, kind} zoomFor shape (#1288)

Restore legacy provider zoomFor compatibility and document supported return values.

Contribution by @asturksever.
This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-24 23:33:10 -07:00
1 parent 8dec9eaa10
commit 5b8e6fbc38
4 files changed
+67 -7

No files matched your search

+4
View File
@@ -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).
+4
View File
@@ -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
+23 -3
View File
@@ -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<object>}>} 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.
+36 -4
View File
@@ -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();