From 579acf50810f3d554898335970de723ffde04047 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Fri, 6 Nov 2026 01:03:41 +0000 Subject: [PATCH 1/4] Street Level: without a Mapillary key, light a keyless source instead of opening on KEY REQUIRED --- src/layers/streetLevel/index.js | 23 ++++++- src/layers/streetLevel/index.test.mjs | 61 +++++++++++++++++++ .../streetLevel/providers/mapillary/index.js | 3 + src/layers/streetLevel/registry.js | 2 + 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/layers/streetLevel/index.js b/src/layers/streetLevel/index.js index 1f7b0f7f0..8d332c227 100644 --- a/src/layers/streetLevel/index.js +++ b/src/layers/streetLevel/index.js @@ -126,8 +126,17 @@ export function createStreetLevelLayer({ }); } - // One source at a time: the first provider that defaults on. - const startsOn = defs.find((def) => def.defaultOn !== false); + /** `def`, or the first keyless provider not refused when `def` has no key. */ + function keylessInstead(def, refused = () => false) { + if (def.keyReady?.() !== false) return def; + return defs.find((d) => !d.requiresKeyId && !refused(d.id)) || def; + } + + // One source at a time: the first provider that defaults on. Without its + // key, the first keyless source instead, so a fresh keyless install opens + // on coverage rather than KEY REQUIRED. + const preferred = defs.find((def) => def.defaultOn !== false); + const startsOn = preferred && keylessInstead(preferred); for (const def of defs) { const entry = { def, @@ -585,7 +594,15 @@ export function createStreetLevelLayer({ providerIds: state.providers.keys(), filter: state.filter, }); - const lit = litByParams(decoded.providers); + // Links and stored state omit Mapillary when it is on (the codec + // default), so a keyless install would restore a source it cannot draw. + const asked = litByParams(decoded.providers); + const lit = + asked && + keylessInstead( + state.providers.get(asked).def, + (id) => decoded.providers.get(id) === false, + ).id; const before = litIds(); for (const [id, entry] of state.providers) if (id !== lit) switchProvider(entry, false); diff --git a/src/layers/streetLevel/index.test.mjs b/src/layers/streetLevel/index.test.mjs index 6c79a52ca..93fc96d23 100644 --- a/src/layers/streetLevel/index.test.mjs +++ b/src/layers/streetLevel/index.test.mjs @@ -1127,3 +1127,64 @@ test('the panel says why a lit Street View draws nothing off Google 3D', async ( const view = presentStreetLevelPanel(layer.getUIState()); assert.match(view.meta, /Google Street View needs the Google 3D map/); }); + +test('without a Mapillary key the first keyless source is lit instead', async (t) => { + const kartaViewSource = { + getTile: async () => ({ lines: [] }), + nearestPhotos: async () => [], + getPhoto: async () => { + throw new Error('KartaView has no such photo'); + }, + getSequencePhotos: async () => ({ photos: [], hasMore: false }), + probeImage: async () => true, + }; + const lit = (hasToken) => { + const layer = createStreetLevelLayer({ + providers: [ + createMapillaryProvider({ + source: fakeMapillarySource({ hasToken: () => hasToken }), + photoViewer: fakePhotoViewer(), + }), + createKartaViewProvider({ source: kartaViewSource }), + ], + }); + t.after(() => layer.destroy()); + return layer + .getUIState() + .providers.filter((provider) => provider.on) + .map((provider) => provider.id); + }; + assert.deepEqual(lit(false), ['kartaview']); + assert.deepEqual(lit(true), ['mapillary']); +}); + +test('restoring the default "Mapillary on" without its key keeps a keyless source', (t) => { + const layer = createStreetLevelLayer({ + providers: [ + createMapillaryProvider({ + source: fakeMapillarySource({ hasToken: () => false }), + photoViewer: fakePhotoViewer(), + }), + createKartaViewProvider({ + source: { + getTile: async () => ({ lines: [] }), + nearestPhotos: async () => [], + getPhoto: async () => null, + getSequencePhotos: async () => ({ photos: [], hasMore: false }), + probeImage: async () => true, + }, + }), + ], + }); + t.after(() => layer.destroy()); + const litIds = () => + layer + .getUIState() + .providers.filter((provider) => provider.on) + .map((provider) => provider.id); + layer.setParams({ mapillary: true, providerSwitches: '' }); + assert.deepEqual(litIds(), ['kartaview']); + // A link that switches KartaView off keeps its word. + layer.setParams({ mapillary: true, providerSwitches: 'kartaview-0' }); + assert.deepEqual(litIds(), ['mapillary']); +}); diff --git a/src/layers/streetLevel/providers/mapillary/index.js b/src/layers/streetLevel/providers/mapillary/index.js index 928922195..c2e9e7850 100644 --- a/src/layers/streetLevel/providers/mapillary/index.js +++ b/src/layers/streetLevel/providers/mapillary/index.js @@ -74,6 +74,9 @@ export function createMapillaryProvider({ source, photoViewer = null }) { name: MAPILLARY_NAME, label: MAPILLARY_LABEL, requiresKeyId: MAPILLARY_KEY_ID, + /** A browser token or a token-adding Graph proxy; else KEY REQUIRED. */ + keyReady: () => + source.hasToken?.() !== false || Boolean(source.graphEndpoint), pickPrefix: PICK_PREFIX.root, colors: COLORS, credit: Object.freeze({ html: MAPILLARY_CREDIT_HTML }), diff --git a/src/layers/streetLevel/registry.js b/src/layers/streetLevel/registry.js index 5e51929a6..4ba1763ab 100644 --- a/src/layers/streetLevel/registry.js +++ b/src/layers/streetLevel/registry.js @@ -20,6 +20,8 @@ export function filtersOf(provider) { * @property {boolean} [groundClick] No coverage: a ground click at street zoom opens its nearest image. * @property {string} [requiresMapStack] Usable only on this map stack. * @property {boolean} [defaultOn] Default true; the first that defaults on is lit. + * @property {() => boolean} [keyReady] False when its key is missing now; the + * first keyless provider is lit instead of a default that cannot draw. * @property {{kind: 'view-cone', color: string}} [marker] Default: GEV's pin. * @property {Array<'pano'|'since'>} [filters] Filters it honours (default both). * @property {{noun: string, action: string}} [coverageLabel] How the panel counts its lines and says what a click does (default: sequences, "click a line for its photos"). From dda5ac6c000503b5f317d7b1df4132e071dd2833 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Fri, 6 Nov 2026 11:59:31 +0000 Subject: [PATCH 2/4] Changelog: 1601 fragment --- changelog.d/1601-street-level-keyless-default.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/1601-street-level-keyless-default.md diff --git a/changelog.d/1601-street-level-keyless-default.md b/changelog.d/1601-street-level-keyless-default.md new file mode 100644 index 000000000..04d96b1bd --- /dev/null +++ b/changelog.d/1601-street-level-keyless-default.md @@ -0,0 +1,4 @@ +- Without a Mapillary token, Street Level starts on the first keyless source + (KartaView) instead of opening on Mapillary's `KEY REQUIRED` with nothing on + the globe. A share link that turns a source off is respected + (asturksever, #1601). From 8a7402d80c4f1e14d48918e38a75e9306263e089 Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Fri, 6 Nov 2026 18:14:16 +0100 Subject: [PATCH 3/4] fix(street-level): keep explicit provider choices on filter-only updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes on top of #1601: - Only redirect a provider the params switch on to a keyless source, so a filter-only setParams (360°, SINCE) no longer moves an explicitly chosen keyless Mapillary to KartaView. - Keyless QA gate: assert the new KartaView default and that the Mapillary chip explains its key instead of switching (it used to expect KEY REQUIRED). - Tests: Graph-proxy keyReady, filter-only updates; shared test helper. - README and CURRENT-STATE describe the keyless default. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +- docs/CURRENT-STATE.md | 8 +- scripts/qa-street-level.mjs | 30 +++++- src/layers/streetLevel/index.js | 18 ++-- src/layers/streetLevel/index.test.mjs | 91 +++++++++---------- .../streetLevel/providers/mapillary/index.js | 2 +- 6 files changed, 89 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index c9eb6e52f..32981b337 100644 --- a/README.md +++ b/README.md @@ -485,9 +485,9 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED > Street-level photos from any provider in one panel. This build ships **Mapillary** (a free client token), **Google Street View** (your Google Maps key), **KartaView** and **Panoramax** (no key). -**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it the panel reads **KEY REQUIRED**. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless. +**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it Street Level starts on **KartaView** instead, and the Mapillary chip says which key to add. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless. -**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo. +**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default when its token is set; otherwise KartaView is. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo. Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3540c3096..7bfb74470 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4429,8 +4429,10 @@ provider draws in one colour (`PROVIDER_COLORS` in sequence is GEV cyan. Share options: `m` (Mapillary), `r` (the other providers' switches by id, e.g. `google-1` for Street View), `p` (`a`/`p`/`f` panoramas), `s` (since, days). Mapillary, Google Street View, KartaView and Panoramax are registered. Without -`MAPILLARY_CLIENT_TOKEN` the panel reads KEY REQUIRED and the controls are -disabled. With it, coverage draws as z11–14 sequence lines below 60 km; the proxy strips the unused `image` layer from z14 +`MAPILLARY_CLIENT_TOKEN` (and no Graph proxy) Street Level starts on the first +keyless provider (KartaView), including a restored "Mapillary on" unless the link +switches the keyless providers off; filter-only updates never change the source. +With it, coverage draws as z11–14 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. @@ -4444,7 +4446,7 @@ the camera, nearest first, at most 9 (the halves of a view across the date line count a shared tile once). Lines are draped `GroundPolylinePrimitive`s (`ClassificationType.BOTH`), so they follow terrain and Google 3D tiles alike; cones and the photo marker are ground-clamped -billboards with a horizon cull. Without `MAPILLARY_CLIENT_TOKEN` the panel +billboards with a horizon cull. When Mapillary is lit without `MAPILLARY_CLIENT_TOKEN` the panel reads KEY REQUIRED, says which key to add and disables the filters; a rejected key reads KEY REJECTED and stops tile requests until the layer goes off; a 429 pauses them for its Retry-After. diff --git a/scripts/qa-street-level.mjs b/scripts/qa-street-level.mjs index 0dd841acf..4b5a0d958 100644 --- a/scripts/qa-street-level.mjs +++ b/scripts/qa-street-level.mjs @@ -296,6 +296,22 @@ async function expandPanel(page) { ); } +/** A keyless bundle opens on the first keyless source with live controls. */ +async function assertKeylessDefault(page) { + await page.evaluate(() => + window.__godsEyeView.dataManager.setEnabled('street-level', true, { + origin: 'user', + }), + ); + await uiUntil(page, litAlone, 'kartaview'); + const shown = await page.evaluate(() => ({ + status: document.getElementById('sl-status').textContent, + disabled: document.getElementById('sl-controls').disabled, + })); + assert.notEqual(shown.status, 'KEY REQUIRED'); + assert.equal(shown.disabled, false, 'controls stay live'); +} + async function assertKeyRequired(page) { await page.evaluate(() => window.__godsEyeView.dataManager.setEnabled('street-level', true, { @@ -529,8 +545,16 @@ async function main() { await expandPanel(page); if (!configured) { - await step('keyless server: KEY REQUIRED gates the controls', () => - assertKeyRequired(page), + await step('keyless server: opens on KartaView, not KEY REQUIRED', () => + assertKeylessDefault(page), + ); + await step( + 'keyless server: the MAPILLARY chip explains its key instead of switching', + async () => { + await press(page, '#sl-provider-chips [data-chip-id="mapillary"]'); + await new Promise((resolve) => setTimeout(resolve, 500)); + assert.ok(litAlone(await getUI(page), 'kartaview')); + }, ); skip('no Mapillary key', 'the keyed steps'); } else { @@ -751,6 +775,8 @@ async function main() { await park(page); await fixtureStep( + // The fixture bundle carries a token, so this covers a keyless + // server behind a keyed bundle; the keyless default is checked above. 'keyless server (second page): KEY REQUIRED gates the controls', async () => { const keyless = await open( diff --git a/src/layers/streetLevel/index.js b/src/layers/streetLevel/index.js index 8d332c227..9c22b95a5 100644 --- a/src/layers/streetLevel/index.js +++ b/src/layers/streetLevel/index.js @@ -126,7 +126,7 @@ export function createStreetLevelLayer({ }); } - /** `def`, or the first keyless provider not refused when `def` has no key. */ + /** `def` if its key is ready, else the first keyless provider not refused. */ function keylessInstead(def, refused = () => false) { if (def.keyReady?.() !== false) return def; return defs.find((d) => !d.requiresKeyId && !refused(d.id)) || def; @@ -594,15 +594,17 @@ export function createStreetLevelLayer({ providerIds: state.providers.keys(), filter: state.filter, }); - // Links and stored state omit Mapillary when it is on (the codec - // default), so a keyless install would restore a source it cannot draw. + // A restored "Mapillary on" (the codec default) would light a source a + // keyless install cannot draw. Only redirect a provider the params switch + // on: a filter-only update keeps whatever the user chose. const asked = litByParams(decoded.providers); const lit = - asked && - keylessInstead( - state.providers.get(asked).def, - (id) => decoded.providers.get(id) === false, - ).id; + asked && decoded.providers.get(asked) === true + ? keylessInstead( + state.providers.get(asked).def, + (id) => decoded.providers.get(id) === false, + ).id + : asked; const before = litIds(); for (const [id, entry] of state.providers) if (id !== lit) switchProvider(entry, false); diff --git a/src/layers/streetLevel/index.test.mjs b/src/layers/streetLevel/index.test.mjs index 93fc96d23..0a26df1f1 100644 --- a/src/layers/streetLevel/index.test.mjs +++ b/src/layers/streetLevel/index.test.mjs @@ -1128,63 +1128,58 @@ test('the panel says why a lit Street View draws nothing off Google 3D', async ( assert.match(view.meta, /Google Street View needs the Google 3D map/); }); -test('without a Mapillary key the first keyless source is lit instead', async (t) => { - const kartaViewSource = { - getTile: async () => ({ lines: [] }), - nearestPhotos: async () => [], - getPhoto: async () => { - throw new Error('KartaView has no such photo'); - }, - getSequencePhotos: async () => ({ photos: [], hasMore: false }), - probeImage: async () => true, - }; - const lit = (hasToken) => { - const layer = createStreetLevelLayer({ - providers: [ - createMapillaryProvider({ - source: fakeMapillarySource({ hasToken: () => hasToken }), - photoViewer: fakePhotoViewer(), - }), - createKartaViewProvider({ source: kartaViewSource }), - ], - }); - t.after(() => layer.destroy()); - return layer - .getUIState() - .providers.filter((provider) => provider.on) - .map((provider) => provider.id); - }; - assert.deepEqual(lit(false), ['kartaview']); - assert.deepEqual(lit(true), ['mapillary']); -}); - -test('restoring the default "Mapillary on" without its key keeps a keyless source', (t) => { +/** Mapillary (keyless unless `mapillary` says otherwise) plus KartaView. */ +function keylessLayer(t, mapillary = { hasToken: () => false }) { const layer = createStreetLevelLayer({ providers: [ createMapillaryProvider({ - source: fakeMapillarySource({ hasToken: () => false }), + source: fakeMapillarySource(mapillary), photoViewer: fakePhotoViewer(), }), - createKartaViewProvider({ - source: { - getTile: async () => ({ lines: [] }), - nearestPhotos: async () => [], - getPhoto: async () => null, - getSequencePhotos: async () => ({ photos: [], hasMore: false }), - probeImage: async () => true, - }, - }), + createKartaViewProvider(), ], }); t.after(() => layer.destroy()); - const litIds = () => - layer - .getUIState() - .providers.filter((provider) => provider.on) - .map((provider) => provider.id); + return layer; +} +const litProviders = (layer) => + layer + .getUIState() + .providers.filter((provider) => provider.on) + .map((provider) => provider.id); + +test('without a Mapillary key the first keyless source is lit instead', (t) => { + assert.deepEqual(litProviders(keylessLayer(t)), ['kartaview']); + assert.deepEqual(litProviders(keylessLayer(t, {})), ['mapillary']); + // A token-adding Graph proxy counts as a key. + const proxied = keylessLayer(t, { + hasToken: () => false, + graphEndpoint: '/api/mapillary/graph', + }); + assert.deepEqual(litProviders(proxied), ['mapillary']); +}); + +test('restoring the default "Mapillary on" without its key keeps a keyless source', (t) => { + const layer = keylessLayer(t); layer.setParams({ mapillary: true, providerSwitches: '' }); - assert.deepEqual(litIds(), ['kartaview']); + assert.deepEqual(litProviders(layer), ['kartaview']); // A link that switches KartaView off keeps its word. layer.setParams({ mapillary: true, providerSwitches: 'kartaview-0' }); - assert.deepEqual(litIds(), ['mapillary']); + assert.deepEqual(litProviders(layer), ['mapillary']); +}); + +test('filter-only updates keep the lit source, even a chosen keyless Mapillary', (t) => { + const layer = keylessLayer(t); + layer.setParams({ pano: 'pano' }); + assert.deepEqual(litProviders(layer), ['kartaview']); + // What the MAPILLARY chip sends, and what stored state restores. + layer.setParams({ mapillary: true, providerSwitches: 'kartaview-0' }); + for (const params of [{ pano: 'pano' }, { sinceDays: 365 }, {}]) { + layer.setParams(params); + assert.deepEqual( + litProviders(layer), + ['mapillary'], + JSON.stringify(params), + ); + } }); diff --git a/src/layers/streetLevel/providers/mapillary/index.js b/src/layers/streetLevel/providers/mapillary/index.js index c2e9e7850..b930405f1 100644 --- a/src/layers/streetLevel/providers/mapillary/index.js +++ b/src/layers/streetLevel/providers/mapillary/index.js @@ -74,7 +74,7 @@ export function createMapillaryProvider({ source, photoViewer = null }) { name: MAPILLARY_NAME, label: MAPILLARY_LABEL, requiresKeyId: MAPILLARY_KEY_ID, - /** A browser token or a token-adding Graph proxy; else KEY REQUIRED. */ + /** A browser token, or a Graph proxy that adds one server-side. */ keyReady: () => source.hasToken?.() !== false || Boolean(source.graphEndpoint), pickPrefix: PICK_PREFIX.root, From f7b9a2e5af07fc285af4f1273346c307fd605a1c Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Fri, 6 Nov 2026 18:20:47 +0100 Subject: [PATCH 4/4] test(street-level): keyless QA branch handles a keyed bundle behind a keyless server Co-Authored-By: Claude Opus 5.5 --- scripts/qa-street-level.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/qa-street-level.mjs b/scripts/qa-street-level.mjs index 4b5a0d958..982c4e581 100644 --- a/scripts/qa-street-level.mjs +++ b/scripts/qa-street-level.mjs @@ -544,7 +544,13 @@ async function main() { ); await expandPanel(page); - if (!configured) { + if (!configured && litAlone(await getUI(page), 'mapillary')) { + // A keyed bundle behind a server without the token keeps Mapillary. + await step('keyless server: KEY REQUIRED gates the controls', () => + assertKeyRequired(page), + ); + skip('no Mapillary key', 'the keyed steps'); + } else if (!configured) { await step('keyless server: opens on KartaView, not KEY REQUIRED', () => assertKeylessDefault(page), );