From d59b4d05bb3ae7f0159a7ce522ba6f83b7985cca Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 8 Oct 2026 17:04:05 +0400 Subject: [PATCH] refactor(street-level): trim dead paths, duplicated labels and test-only API - visibleBbox: no view-rectangle fallback or grid option; its one caller always ranges the rays - tilesForBbox returns the tiles; normalizeBbox takes arrays only - viewerHost: no try/catch around adapter calls that catch for themselves - coverage: counts come from the drawn sequences; no sequenceCount export - one keyStatusLabel for the pill and the layer list - no constructor source check, double-init guard or selectSequence / clearSequence on the layer Co-Authored-By: Claude Opus 5.5 --- scripts/package-boundaries.json | 1 + src/layers/streetLevel/index.js | 24 ++--------- src/layers/streetLevel/index.test.mjs | 20 +++++---- src/layers/streetLevel/policy.js | 6 +++ .../providers/mapillary/coverage.js | 6 +-- .../providers/mapillary/coverage.test.mjs | 13 +++--- src/layers/streetLevel/state.js | 1 - src/layers/streetLevel/tileMath.js | 24 ++++------- src/layers/streetLevel/tileMath.test.mjs | 42 +++++++------------ src/layers/streetLevel/view.js | 30 ++++++------- src/layers/streetLevel/view.test.mjs | 2 +- src/layers/streetLevel/viewerHost.js | 12 +----- src/testSupport/streetLevelFakes.mjs | 1 - src/ui/streetLevelPresentation.js | 7 ++-- 14 files changed, 73 insertions(+), 116 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 283bb0127..8202f5584 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -660,6 +660,7 @@ "src/ui/shellFacade.js", "src/ui/streetLevelControls.js", "src/ui/streetLevelPresentation.js", + "src/layers/streetLevel/policy.js", "src/ui/localSdrControls.js", "src/layers/localAdsb/feedNames.js", "src/ui/localSdrPresentation.js", diff --git a/src/layers/streetLevel/index.js b/src/layers/streetLevel/index.js index 67bbfb52d..9f99883a9 100644 --- a/src/layers/streetLevel/index.js +++ b/src/layers/streetLevel/index.js @@ -5,14 +5,13 @@ import { createCredit } from './credits.js'; import { createSelection } from './selection.js'; import { createViewerHost } from './viewerHost.js'; import { normalizeFilter, sameFilter } from './filter.js'; -import { STREET_LEVEL_LAYER_ID } from './policy.js'; +import { keyStatusLabel, STREET_LEVEL_LAYER_ID } from './policy.js'; import { createCoverage } from './providers/mapillary/coverage.js'; import { createSequences } from './providers/mapillary/sequences.js'; import { createMapillaryViewer } from './providers/mapillary/viewer.js'; import { MAPILLARY_CREDIT_HTML, MAPILLARY_KEY_ID, - MAPILLARY_SOURCE_METHODS, } from './providers/mapillary/policy.js'; export { STREET_LEVEL_LAYER_ID } from './policy.js'; @@ -39,12 +38,6 @@ export function createStreetLevelLayer({ services = {}, photoViewer = null, }) { - if ( - !MAPILLARY_SOURCE_METHODS.every( - (method) => typeof source?.[method] === 'function', - ) - ) - throw new TypeError('A Mapillary source is required'); const state = createState({ services }); const parts = {}; const context = { state, parts }; @@ -169,10 +162,7 @@ export function createStreetLevelLayer({ requiresKeyId: MAPILLARY_KEY_ID, init(viewer) { - if (state.initialized) - throw new Error('Street Level layer is already initialized'); state.viewer = viewer; - state.initialized = true; parts.marker.ensure(viewer); parts.marker.setVisible(false); parts.sequences.ensureCollections(viewer); @@ -209,14 +199,13 @@ export function createStreetLevelLayer({ parts.framing.attachNavigation(null); state.listeners.clear(); state.viewer = null; - state.initialized = false; }, getStats() { const coverage = coverageStats(); - const keyLabel = coverage.keyRejected ? 'KEY REJECTED' : 'KEY REQUIRED'; + const keyLabel = keyStatusLabel(coverage); let loadingLabel = ''; - if (coverage.keyRequired) loadingLabel = keyLabel; + if (keyLabel) loadingLabel = keyLabel; else if (coverage.loading) loadingLabel = 'loading coverage...'; else if (coverage.hint && state.enabled) loadingLabel = coverage.hint; return { @@ -224,10 +213,7 @@ export function createStreetLevelLayer({ loading: coverage.loading, keyRequired: coverage.keyRequired, // A rejected key's message names the fix; a missing one says so. - error: - coverage.keyRequired && !coverage.keyRejected - ? keyLabel - : coverage.error, + error: keyLabel && !coverage.keyRejected ? keyLabel : coverage.error, loadingLabel, }; }, @@ -283,8 +269,6 @@ export function createStreetLevelLayer({ }, setViewerRenderMode: (mode) => parts.viewerHost.setRenderMode(mode), resizeViewer: () => parts.viewerHost.resize(), - selectSequence: (sequenceId) => parts.sequences.select(sequenceId), - clearSequence: () => parts.sequences.clearSelection(), }; return layer; } diff --git a/src/layers/streetLevel/index.test.mjs b/src/layers/streetLevel/index.test.mjs index 26ce49345..78fd9a5a4 100644 --- a/src/layers/streetLevel/index.test.mjs +++ b/src/layers/streetLevel/index.test.mjs @@ -205,23 +205,26 @@ test('switching Mapillary off clears coverage and closes the image it shows', as assert.equal(layer.getUIState().providerOn, false); }); -test('a sequence error goes with the sequence and leaves the viewer’s own', async (t) => { +test('a sequence that fails to load reports it apart from the photo, and the error goes with it', async (t) => { const source = fakeMapillarySource({ getSequenceImages: async () => { throw new Error('Sequence images unavailable'); }, }); - const { layer } = await startLayer(t, { source }); - await layer.selectSequence('seq-1'); - layer.attachViewerHost(null); - await layer.openImage('img1'); + // The photo's pose names its sequence, which opening selects. + const photoViewer = fakePhotoViewer({ + pose: { position: { lon: -121.49, lat: 38.58 }, sequenceId: 'seq-1' }, + }); + const { layer } = await startLayer(t, { source, photoViewer }); + layer.attachViewerHost({}); + assert.equal(await layer.openImage('img1'), true); + await settle(); let ui = layer.getUIState(); assert.equal(ui.sequence.error, 'Sequence images unavailable'); - assert.match(ui.street.error, /Open the Street Level panel/); - layer.clearSequence(); + assert.equal(ui.street.error, null, 'the photo itself is fine'); + layer.closeViewer(); ui = layer.getUIState(); assert.equal(ui.sequence.error, null, 'withdrawn with the sequence'); - assert.match(ui.street.error, /Open the Street Level panel/, 'kept'); }); test('closing the photo, switching the layer off or destroying it stops the framing flight', async (t) => { @@ -245,7 +248,6 @@ test('closing the photo, switching the layer off or destroying it stops the fram }); test('setParams takes "any date" (0 days) and the switch over the current values (share-link defaults)', async (t) => { - assert.throws(() => createStreetLevelLayer({ source: {} }), /source/); const { layer } = await startLayer(t); assert.deepEqual(layer.getParams(), { mapillary: true, diff --git a/src/layers/streetLevel/policy.js b/src/layers/streetLevel/policy.js index 4575e6621..0269ddcad 100644 --- a/src/layers/streetLevel/policy.js +++ b/src/layers/streetLevel/policy.js @@ -18,3 +18,9 @@ export const MAX_SINCE_DAYS = 36_500; /** Filter the layer starts with: all imagery, any date. */ export const FILTER_DEFAULT = Object.freeze({ pano: 'all', sinceDays: 0 }); + +/** The key gate's label for the pill and the layer list, or null when the key is fine. */ +export function keyStatusLabel({ keyRequired, keyRejected }) { + if (keyRejected) return 'KEY REJECTED'; + return keyRequired ? 'KEY REQUIRED' : null; +} diff --git a/src/layers/streetLevel/providers/mapillary/coverage.js b/src/layers/streetLevel/providers/mapillary/coverage.js index 6ada951d9..89a31812d 100644 --- a/src/layers/streetLevel/providers/mapillary/coverage.js +++ b/src/layers/streetLevel/providers/mapillary/coverage.js @@ -162,7 +162,6 @@ export function createCoverage({ state, source }) { const sequences = drawnSequences(entry); entry.sequences = new Map(sequences.map((s) => [s.id, s])); entry.primitives = buildSequencePrimitives(sequences); - entry.count = sequences.length; for (const primitive of entry.primitives) scene.groundPrimitives.add(primitive); } @@ -212,7 +211,7 @@ export function createCoverage({ state, source }) { ) return; const decoded = decodeCoverageTile(bytes, tile); - const entry = { primitives: [], count: 0, sequences: new Map() }; + const entry = { primitives: [], sequences: new Map() }; // Newest first; the per-tile cap applies to what passes the filter. entry.sequenceList = decoded.sequences.sort( (a, b) => (b.capturedAt || 0) - (a.capturedAt || 0), @@ -311,7 +310,7 @@ export function createCoverage({ state, source }) { if (!bbox) return { hint: NO_GROUND_HINT }; // Ranked from between the camera's ground point and the screen centre's: // a tilted view's box centre can sit kilometres past both. - const { tiles } = tilesForBbox(bbox, zoom, { + const tiles = tilesForBbox(bbox, zoom, { limit: COVERAGE_MAX_TILES, from: viewFocus(viewer, ranged), }); @@ -430,7 +429,6 @@ export function createCoverage({ state, source }) { setKeyStatus, rebuild, highlight, - sequenceCount, stats, }; } diff --git a/src/layers/streetLevel/providers/mapillary/coverage.test.mjs b/src/layers/streetLevel/providers/mapillary/coverage.test.mjs index 0f62f4621..14587059a 100644 --- a/src/layers/streetLevel/providers/mapillary/coverage.test.mjs +++ b/src/layers/streetLevel/providers/mapillary/coverage.test.mjs @@ -66,7 +66,6 @@ function fakeViewer(view) { view.lat + 0.002 - (point.y / 100) * 0.004, ); }, - computeViewRectangle: () => undefined, }, }; } @@ -318,14 +317,18 @@ test('the per-tile cap applies after the imagery filter, so a dense tile keeps i await settle(); const [entry] = state.coverage.tiles.values(); const cap = Math.floor(COVERAGE_MAX_SEQUENCES / COVERAGE_MAX_TILES); - assert.equal(entry.count, Math.min(300, cap), 'every 360° line is drawn'); - assert.equal(coverage.sequenceCount(), Math.min(300, cap)); + assert.equal( + entry.sequences.size, + Math.min(300, cap), + 'every 360° line is drawn', + ); + assert.equal(coverage.stats().count, Math.min(300, cap)); // Picking and the highlight look sequences up among the drawn lines. assert.ok(entry.sequences.has('pano-0'), 'drawn lines can be looked up'); assert.equal(entry.sequences.has('flat-699'), false); state.filter = { pano: 'all', sinceDays: 0 }; coverage.rebuild(); - assert.equal(entry.count, cap, 'all imagery is capped as before'); + assert.equal(entry.sequences.size, cap, 'all imagery is capped as before'); assert.ok(entry.sequences.has('flat-699'), 'the newest flat line is drawn'); assert.equal(entry.sequences.has('pano-299'), false, 'over the cap'); coverage.clear(); @@ -359,7 +362,7 @@ test('a sequence that crosses a tile edge is counted once', async () => { ); } await settle(); - assert.equal(coverage.sequenceCount(), 3, 'across, plus one per tile'); + assert.equal(coverage.stats().count, 3, 'across, plus one per tile'); coverage.clear(); }); diff --git a/src/layers/streetLevel/state.js b/src/layers/streetLevel/state.js index 84c24fd11..bc45e6358 100644 --- a/src/layers/streetLevel/state.js +++ b/src/layers/streetLevel/state.js @@ -52,7 +52,6 @@ export function createState({ services }) { services, viewer: null, enabled: false, - initialized: false, listeners: new Set(), notify: null, /** The share link's `mapillary` switch: off draws nothing. */ diff --git a/src/layers/streetLevel/tileMath.js b/src/layers/streetLevel/tileMath.js index 926df9d9e..7e317f63d 100644 --- a/src/layers/streetLevel/tileMath.js +++ b/src/layers/streetLevel/tileMath.js @@ -58,15 +58,10 @@ export function tileBounds(x, y, z) { }; } -/** A [west, south, east, north] array or object as finite, ordered bounds; null if unusable. */ +/** A [west, south, east, north] array as finite, ordered bounds; null if unusable. */ export function normalizeBbox(input) { - const values = Array.isArray(input) - ? input - : input && typeof input === 'object' - ? [input.west, input.south, input.east, input.north] - : null; - if (!values || values.length !== 4) return null; - const [w, s, e, n] = values.map(Number); + if (!Array.isArray(input) || input.length !== 4) return null; + const [w, s, e, n] = input.map(Number); if (![w, s, e, n].every(Number.isFinite)) return null; const west = Math.max(-180, Math.min(w, e)); const east = Math.min(180, Math.max(w, e)); @@ -81,7 +76,7 @@ export function normalizeBbox(input) { * crosses the date line), nearest to `from` first (default: the box centre), * capped at `limit`. Distances are measured the short way round ±180°. * @param {{limit?: number, from?: {lon: number, lat: number}|null}} [options] - * @returns {{tiles: Array<{x:number,y:number,z:number}>, truncated: boolean, total: number}} + * @returns {Array<{x:number,y:number,z:number}>} */ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) { const [west, south, east, north] = Array.isArray(bbox) @@ -95,7 +90,7 @@ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) { [-180, south, east, north], ] : [bbox]; - const tiles = halves + const grid = halves .map(normalizeBbox) .filter(Boolean) .flatMap((box) => tileGrid(box, z)); @@ -107,7 +102,7 @@ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) { const cx = tileXAt(wrapLon(centre.lon), z); const cy = tileYAt(centre.lat, z); // Each tile's distance is computed once; ties keep their row order. - const ranked = tiles + const ranked = grid .map((tile) => { const dx = Math.abs(tile.x + 0.5 - cx); const d = Math.min(dx, n - dx) ** 2 + (tile.y + 0.5 - cy) ** 2; @@ -115,12 +110,7 @@ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) { }) .sort((a, b) => a.d - b.d) .map(({ tile }) => tile); - const truncated = ranked.length > limit; - return { - tiles: truncated ? ranked.slice(0, limit) : ranked, - truncated, - total: ranked.length, - }; + return ranked.slice(0, limit); } /** The tiles at zoom z in a normalized box, row by row. */ diff --git a/src/layers/streetLevel/tileMath.test.mjs b/src/layers/streetLevel/tileMath.test.mjs index 344864f93..5ed5f73d2 100644 --- a/src/layers/streetLevel/tileMath.test.mjs +++ b/src/layers/streetLevel/tileMath.test.mjs @@ -37,24 +37,21 @@ test('normalizeBbox orders and clamps coordinates', () => { north: 38.69, }); assert.equal(normalizeBbox([0, 0, 0, 1]), null); - assert.equal(normalizeBbox('nope'), null); + assert.equal(normalizeBbox([1, 2, 3]), null); assert.equal(normalizeBbox([1, 2, Number.NaN, 3]), null); }); test('tilesForBbox orders from the centre outwards and honours the cap', () => { - const result = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14, { + const tiles = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14, { limit: 5, }); - assert.equal(result.total, 160); - assert.equal(result.truncated, true); - assert.equal(result.tiles.length, 5); + assert.equal(tiles.length, 5); const centreX = lonToTileX(-121.46, 14); const centreY = latToTileY(38.565, 14); - assert.ok(Math.abs(result.tiles[0].x - centreX) <= 1); - assert.ok(Math.abs(result.tiles[0].y - centreY) <= 1); + assert.ok(Math.abs(tiles[0].x - centreX) <= 1); + assert.ok(Math.abs(tiles[0].y - centreY) <= 1); const full = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14); - assert.equal(full.truncated, false); - assert.equal(full.tiles.length, 160); + assert.equal(full.length, 160); }); test('coverageZoomForHeight steps from coarse to z14 as the camera descends', () => { @@ -71,13 +68,13 @@ test('longitude 180 is the last column, so a world view keeps the eastern hemisp assert.equal(lonToTileX(179.99, 1), 1); assert.equal(lonToTileX(-180, 1), 0); const columns = new Set( - tilesForBbox([-180, -85, 180, 85], 1).tiles.map((tile) => tile.x), + tilesForBbox([-180, -85, 180, 85], 1).map((tile) => tile.x), ); assert.deepEqual([...columns].sort(), [0, 1]); }); test('a box across the date line takes tiles on both sides of it, not the far side of the globe', () => { - const { tiles } = tilesForBbox([179, -1, -179, 1], 8); + const tiles = tilesForBbox([179, -1, -179, 1], 8); const columns = new Set(tiles.map((tile) => tile.x)); assert.ok(columns.has(255), 'west of the line'); assert.ok(columns.has(0), 'east of the line'); @@ -86,7 +83,7 @@ test('a box across the date line takes tiles on both sides of it, not the far si 'nothing from the middle of the map', ); // The limit keeps the tiles nearest the line. - const limited = tilesForBbox([170, -1, -170, 1], 8, { limit: 4 }).tiles; + const limited = tilesForBbox([170, -1, -170, 1], 8, { limit: 4 }); assert.ok(limited.every((tile) => tile.x <= 8 || tile.x >= 247)); }); @@ -97,12 +94,12 @@ test('a box across the date line ranks from the line, or from the camera across const key = (t) => `${t.x}/${t.y}`; const [north, south] = [latToTileY(0.1, z), latToTileY(0, z)]; const plain = tilesForBbox(bbox, z); - assert.equal(plain.total, 10 * (south - north + 1), '5 columns either side'); + assert.equal(plain.length, 10 * (south - north + 1), '5 columns either side'); // The box centre is the date line itself, not the middle of the map: the // columns touching it come first, then the ones beside them. - for (const tile of plain.tiles.slice(0, 4)) + for (const tile of plain.slice(0, 4)) assert.ok([n - 1, 0].includes(tile.x), `${key(tile)} at the line`); - for (const tile of plain.tiles.slice(4, 12)) + for (const tile of plain.slice(4, 12)) assert.ok( [n - 2, n - 1, 0, 1].includes(tile.x), `${key(tile)} near the line`, @@ -112,15 +109,9 @@ test('a box across the date line ranks from the line, or from the camera across const { south: rowSouth, north: rowNorth } = tileBounds(0, row, z); const lat = (rowSouth + rowNorth) / 2; const west = tilesForBbox(bbox, z, { from: { lon: 179.999, lat } }); - assert.deepEqual(west.tiles.slice(0, 2).map(key), [ - `${n - 1}/${row}`, - `0/${row}`, - ]); + assert.deepEqual(west.slice(0, 2).map(key), [`${n - 1}/${row}`, `0/${row}`]); const east = tilesForBbox(bbox, z, { from: { lon: -179.999, lat } }); - assert.deepEqual(east.tiles.slice(0, 2).map(key), [ - `0/${row}`, - `${n - 1}/${row}`, - ]); + assert.deepEqual(east.slice(0, 2).map(key), [`0/${row}`, `${n - 1}/${row}`]); }); test('ranking from the camera keeps the ground under it, not the box centre', () => { @@ -131,11 +122,10 @@ test('ranking from the camera keeps the ground under it, not the box centre', () const z = 14; const key = (t) => `${t.x}/${t.y}`; const under = `${lonToTileX(from.lon, z)}/${latToTileY(from.lat, z)}`; - const plain = tilesForBbox(bbox, z, { limit: 9 }).tiles.map(key); + const plain = tilesForBbox(bbox, z, { limit: 9 }).map(key); assert.equal(plain.includes(under), false, 'the box centre drops it'); const ranked = tilesForBbox(bbox, z, { limit: 9, from }); - assert.equal(key(ranked.tiles[0]), under, 'the tile under the camera first'); - assert.equal(ranked.total > 9, true); + assert.equal(key(ranked[0]), under, 'the tile under the camera first'); }); test('wrapLon wraps longitudes and their differences into [-180, 180]', () => { diff --git a/src/layers/streetLevel/view.js b/src/layers/streetLevel/view.js index ead085c35..b58388c51 100644 --- a/src/layers/streetLevel/view.js +++ b/src/layers/streetLevel/view.js @@ -77,17 +77,19 @@ function groundHit(camera, point, ellipsoid, maxRange) { return Cesium.Cartographic.fromCartesian(cartesian, ellipsoid) || null; } +/** Screen rays per side, less one, that `visibleBbox` casts. */ +const BBOX_GRID = 5; + /** - * Visible [west, south, east, north] degrees, or null. Only screen rays that - * hit count, so a horizon cannot inflate the box to the world. + * Visible [west, south, east, north] degrees, or null when fewer than four + * rays hit. Only hits count, so a horizon cannot inflate the box to the world. * `groundHeight` raises the ellipsoid to the ground (rays to the bare * ellipsoid under Denver land kilometres too far out) and `maxRange` (m) - * drops near-horizon hits; with it, too few hits give null rather than - * Cesium's view rectangle, which can span the horizon. + * drops near-horizon hits. */ export function visibleBbox( viewer, - { grid = 5, groundHeight = null, maxRange = null } = {}, + { groundHeight = null, maxRange = null } = {}, ) { const scene = viewer?.scene; const camera = viewer?.camera; @@ -101,10 +103,10 @@ export function visibleBbox( groundHeight, ); const point = new Cesium.Cartesian2(); - for (let i = 0; i <= grid; i++) { - for (let j = 0; j <= grid; j++) { - point.x = (width * i) / grid; - point.y = (height * j) / grid; + for (let i = 0; i <= BBOX_GRID; i++) { + for (let j = 0; j <= BBOX_GRID; j++) { + point.x = (width * i) / BBOX_GRID; + point.y = (height * j) / BBOX_GRID; const carto = groundHit(camera, point, ellipsoid, maxRange); if (carto) hits.push(carto); } @@ -142,15 +144,7 @@ export function visibleBbox( } if (west !== east && north - south > 0) return [west, south, east, north]; } - if (Number.isFinite(maxRange)) return null; - const rectangle = camera.computeViewRectangle?.(scene.globe?.ellipsoid); - if (!rectangle) return null; - return [ - Cesium.Math.toDegrees(rectangle.west), - Cesium.Math.toDegrees(rectangle.south), - Cesium.Math.toDegrees(rectangle.east), - Cesium.Math.toDegrees(rectangle.north), - ]; + return null; } /** The point on the ground under the camera, as {lon, lat} degrees. */ diff --git a/src/layers/streetLevel/view.test.mjs b/src/layers/streetLevel/view.test.mjs index d0da93f36..8f5a4dd99 100644 --- a/src/layers/streetLevel/view.test.mjs +++ b/src/layers/streetLevel/view.test.mjs @@ -155,7 +155,7 @@ test('a tilted view from 3 km loads the tile at the centre of the screen', () => const ahead = lat + 3000 / Math.tan((20 * Math.PI) / 180) / 110_540; const centreTile = `${lonToTileX(lon, 13)}/${latToTileY(ahead, 13)}`; const chosen = (from) => - tilesForBbox(bbox, 13, { limit: 9, from }).tiles.map( + tilesForBbox(bbox, 13, { limit: 9, from }).map( (tile) => `${tile.x}/${tile.y}`, ); assert.ok( diff --git a/src/layers/streetLevel/viewerHost.js b/src/layers/streetLevel/viewerHost.js index ad05fec47..b1c618c9b 100644 --- a/src/layers/streetLevel/viewerHost.js +++ b/src/layers/streetLevel/viewerHost.js @@ -121,20 +121,12 @@ export function createViewerHost({ state, parts, adapter }) { } function resize() { - try { - if (unsubscribe) adapter.resize(); - } catch { - /* no-op */ - } + if (unsubscribe) adapter.resize(); } function setRenderMode(mode) { state.street.renderMode = mode === 'fill' ? 'fill' : 'letterbox'; - try { - if (unsubscribe) adapter.setRenderMode?.(state.street.renderMode); - } catch { - /* adapter not ready */ - } + if (unsubscribe) adapter.setRenderMode?.(state.street.renderMode); notify(); } diff --git a/src/testSupport/streetLevelFakes.mjs b/src/testSupport/streetLevelFakes.mjs index 76a8125bd..de9b739e4 100644 --- a/src/testSupport/streetLevelFakes.mjs +++ b/src/testSupport/streetLevelFakes.mjs @@ -103,6 +103,5 @@ export function rayCamera({ const hit = Cesium.IntersectionTests.rayEllipsoid(ray, ellipsoid); return hit ? Cesium.Ray.getPoint(ray, hit.start) : undefined; }, - computeViewRectangle: () => undefined, }; } diff --git a/src/ui/streetLevelPresentation.js b/src/ui/streetLevelPresentation.js index cbe645417..63640d1f4 100644 --- a/src/ui/streetLevelPresentation.js +++ b/src/ui/streetLevelPresentation.js @@ -1,4 +1,5 @@ import { keySetupRequirement } from '../keySetupCore.mjs'; +import { keyStatusLabel } from '../layers/streetLevel/policy.js'; // Street Level UI state to panel strings and flags. Pure, so it is testable. @@ -59,10 +60,8 @@ const drawing = (state) => state.enabled === true && state.providerOn !== false; function presentStatus(state) { const pressed = drawing(state); const title = pressed ? 'Turn Street Level off' : 'Turn Street Level on'; - if (state.keyRejected) - return { text: 'KEY REJECTED', tone: 'warn', pressed, title }; - if (state.keyRequired) - return { text: 'KEY REQUIRED', tone: 'warn', pressed, title }; + const key = keyStatusLabel(state); + if (key) return { text: key, tone: 'warn', pressed, title }; if (state.coverage.loading) return { text: 'LOADING', tone: 'busy', pressed, title }; return pressed