diff --git a/CHANGELOG.md b/CHANGELOG.md index d658b4212..43a2620b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Explicitly removing either source leaves its layer unavailable; standalone defaults preserve the existing providers. +- Street Level with no providers uses the shared unavailable state and refuses + activation before initialization, while keeping its layer identity visible. + - Application source configuration accepts replacements and explicit removal. Missing catalog feeds leave their layers unavailable without blocking startup or making fallback requests; malformed supplied feeds remain configuration errors. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6f4e3e546..27a5815e9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -7,7 +7,8 @@ visibility lifecycle before initialization. Invalid supplied methods still fail construction. `createStandaloneApplication` and `createStandaloneCatalog` accept `sources` overrides; unspecified entries retain standalone defaults, and null entries explicitly remove a source. Street Level -continues to accept an explicit provider list. Directions (`getRoute`) and +continues to accept an explicit provider list. An empty Street Level provider +list uses the same unavailable lifecycle and disabled layer toggle. Directions (`getRoute`) and Recent Imagery (`searchHls`, `getThumbnail`, `getTileTemplate`) use the same source configuration boundary. Standalone selects their default adapters; layers retain route normalization, imagery selection and rendering. Recent Imagery adapters @@ -21,7 +22,10 @@ An explicit list replaces the default Mapillary provider; an empty list removes all imagery providers. Without that option, Mapillary is registered only when `sources.mapillary` is supplied. An absent source leaves Street Level registered with a truthful no-provider state, preserving layer controls and share tokens. -Malformed supplied sources and provider definitions still fail validation. +Malformed selected sources and provider definitions still fail validation. +An explicit provider list bypasses the unused Mapillary source, including its +validation. Layer share identities are preserved; custom provider switches require +a corresponding serialization contract. ## God's Eye View in conversations — October 2, 2026 diff --git a/scripts/qa-street-level.mjs b/scripts/qa-street-level.mjs index 59b0584da..f8d50f85f 100644 --- a/scripts/qa-street-level.mjs +++ b/scripts/qa-street-level.mjs @@ -622,11 +622,17 @@ async function main() { await target.click( '.panel-collapse-btn[data-collapse-target="street-level-panel"]', ); - await settle( - target, - () => - getComputedStyle(document.getElementById('sl-body')).display === - 'flex', + assert.ok( + await settle( + target, + () => + !document + .getElementById('street-level-panel') + .classList.contains('collapsed') && + getComputedStyle(document.getElementById('sl-body')).display === + 'flex', + ), + 'Street Level panel did not expand before the timeout', ); }; await expandStrip(); diff --git a/src/app/layers/streetLevel.js b/src/app/layers/streetLevel.js index 41e8b2a8b..18a3e7a3b 100644 --- a/src/app/layers/streetLevel.js +++ b/src/app/layers/streetLevel.js @@ -5,7 +5,11 @@ import * as picking from '../../data/pickRegistry.js'; import * as input from '../../data/inputOwnership.js'; import * as render from '../../renderGovernor.js'; -/** Inject providers, or use Mapillary only when its source is supplied. */ +/** + * Inject providers, or use Mapillary only when its source is supplied. + * An explicit provider list replaces the default; unused sources.mapillary is + * neither constructed nor validated, including when the list is empty. + */ export function createApplicationStreetLevel({ surface, sources = {}, diff --git a/src/app/sourceComposition.test.mjs b/src/app/sourceComposition.test.mjs index 9d6823e40..beafa5c51 100644 --- a/src/app/sourceComposition.test.mjs +++ b/src/app/sourceComposition.test.mjs @@ -82,3 +82,20 @@ test('standalone defaults allow exact replacements and explicit removal, and rej /Unknown source/, ); }); + +test('empty Street Level uses the shared unavailable lifecycle without initialization', async (t) => { + const actual = catalog({}, t); + const manager = new LayerLifecycle({}); + manager.register(actual.get('street-level')); + assert.equal(await manager.setEnabled('street-level', true), false); + assert.equal(manager.layers.get('street-level').initialized, false); + const row = manager.getAll()[0]; + assert.equal(row.stats.status, 'unavailable'); + assert.equal(row.stats.error, 'No street-level imagery providers configured'); + assert.equal(await manager.setEnabled('street-level', false), true); + const configured = catalog(createStandaloneLayerSources(), t); + assert.equal( + configured.get('street-level').getSourceAvailability().available, + true, + ); +}); diff --git a/src/layers/streetLevel/index.js b/src/layers/streetLevel/index.js index dd4478c58..60a31691e 100644 --- a/src/layers/streetLevel/index.js +++ b/src/layers/streetLevel/index.js @@ -333,6 +333,16 @@ export function createStreetLevelLayer({ requiresKeyId: requiresKeyIdFor(definitionsFrozen), /** Registered providers, in chip order. */ providerIds: definitionsFrozen.map((def) => def.id), + getSourceAvailability() { + const available = definitionsFrozen.length > 0; + return { + available, + source: STREET_LEVEL_LAYER_ID, + reason: available + ? null + : 'No street-level imagery providers configured', + }; + }, init(viewer) { if (state.initialized)