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").