fix(street-level): point a key-gated layer at Street View; button beside the hint

Review of #966: with a Google Maps key but no Mapillary token, switching
Street Level on read KEY REQUIRED / "Needs MAPILLARY_CLIENT_TOKEN" with
nothing about Street View, which stayed off (billing), so the layer looked
broken.

- While the switched-on providers lack their keys, the layer reports the
  switched-off ones that would draw with a key already set
  (keyAlternatives: Street View on Google 3D with a Google key). The
  panel's KEY REQUIRED tooltip, its coverage line and the layer row's
  tooltip then read "Needs MAPILLARY_CLIENT_TOKEN — add it in Provider
  Settings, or light STREET VIEW to use your Google Maps key"
  (keySetupWithAlternatives). Off Google 3D the plain KEY REQUIRED stays.
  Street View stays off by default.
- OPEN STREET VIEW moves beside the coverage hint: at the default panel
  width the PROVIDERS row has no room for it, so it wrapped onto a line of
  its own.
- The panel gate's corner drag names what lay under the pointer when it
  fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-09 00:05:58 +01:00
1 parent f5c4f3fda3
commit e823e15025
15 files changed
+669 -138

No files matched your search

+16
View File
@@ -141,3 +141,19 @@ test('the manager publishes the declared key id on the row', async () => {
);
assert.equal(layerKeyRequirementTooltip(ungated), '');
});
test('a key-gated layer with another source ready says how to use it', () => {
const text = layerKeyRequirementTooltip({
requiresKeyId: 'mapillary',
stats: {
keyRequired: true,
keyAlternatives: [
{ id: 'google', label: 'STREET VIEW', requiresKeyId: 'google-maps' },
],
},
});
assert.equal(
text,
'Needs MAPILLARY_CLIENT_TOKEN — add it in Provider Settings, or light STREET VIEW to use your Google Maps key',
);
});
+9 -2
View File
@@ -3,7 +3,10 @@ import { syncRowList } from './rowList.js';
import { layerFeedState } from '../data/feedState.js';
export { layerFeedState } from '../data/feedState.js';
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
import { keySetupRequirement } from '../keySetupCore.mjs';
import {
keySetupRequirement,
keySetupWithAlternatives,
} from '../keySetupCore.mjs';
import { createWeatherPanel } from './weatherPanel.js';
const FEED_STATE_LABELS = Object.freeze({
nominal: 'ON',
@@ -100,7 +103,11 @@ function panelLabel(layer) {
export function layerKeyRequirementTooltip(layer = {}) {
if (layer?.stats?.keyRequired !== true) return '';
const requiresKeyId = String(layer.requiresKeyId || '').trim();
return requiresKeyId ? keySetupRequirement(requiresKeyId) : '';
// A layer with several sources also names the ones it could use instead.
return keySetupWithAlternatives(
requiresKeyId ? keySetupRequirement(requiresKeyId) : '',
layer.stats.keyAlternatives || [],
);
}
/** Layer row presentation over supplied state and actions; no layer imports. */
+3 -2
View File
@@ -305,7 +305,6 @@ function panelDom() {
const settings = add(main, 'div', { classes: ['sl-settings'] });
for (const id of ['sl-provider-chips', 'sl-error', 'sl-error-text'])
add(settings, 'div', { id });
add(settings, 'button', { id: 'sl-nearest-btn' });
// A missing key gates the filters only; the chips stay outside the gate.
const controls = add(settings, 'fieldset', { id: 'sl-controls' });
for (const pano of ['all', 'pano', 'flat'])
@@ -313,7 +312,9 @@ function panelDom() {
add(controls, 'input', { id: 'sl-since' });
add(controls, 'output', { id: 'sl-since-label' });
add(settings, 'ul', { id: 'sl-legend' });
add(settings, 'div', { id: 'sl-coverage-meta' });
const metaRow = add(settings, 'div', { classes: ['sl-meta-row'] });
add(metaRow, 'div', { id: 'sl-coverage-meta' });
add(metaRow, 'button', { id: 'sl-nearest-btn' });
// The globe's canvas is focusable too (tabindex=0).
const globe = add(
add(document.body, 'div', { id: 'cesiumContainer' }),
+28 -2
View File
@@ -1,4 +1,7 @@
import { keySetupRequirement } from '../keySetupCore.mjs';
import {
keySetupRequirement,
keySetupWithAlternatives,
} from '../keySetupCore.mjs';
// Street Level UI state to panel strings and flags. Pure, so it is testable.
@@ -54,13 +57,33 @@ function formatDate(ms, precision = 'day') {
}
}
/**
* What the key-gated layer needs: each switched-on provider's key, and the
* switched-off ones that would draw with a key already set.
*/
function keyGuidance(state) {
const needs = (state.providers || [])
.filter((p) => p.on && p.keyRequired && p.requiresKeyId)
.map((p) => keySetupRequirement(p.requiresKeyId))
.filter(Boolean);
return keySetupWithAlternatives(
[...new Set(needs)].join('; '),
state.keyAlternatives || [],
);
}
function presentStatus(state) {
const pressed = state.enabled === true;
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 };
return {
text: 'KEY REQUIRED',
tone: 'warn',
pressed,
title: keyGuidance(state) || title,
};
if (state.coverage.loading)
return { text: 'LOADING', tone: 'busy', pressed, title };
return pressed
@@ -162,6 +185,9 @@ function presentNearest(state) {
function presentMeta(state) {
if (!state.enabled) return 'Switch a provider on to draw its coverage.';
// Key-gated with another source ready: say how to use it.
if (state.keyRequired && state.keyAlternatives?.length)
return keyGuidance(state);
if (state.sequence.selectedId)
return state.sequence.loading
? 'Loading this sequence…'
+39
View File
@@ -483,3 +483,42 @@ test('OPEN STREET VIEW shows while Street View is on, and waits for street zoom'
false,
);
});
test('KEY REQUIRED points at Street View when it would draw with the Google key', () => {
const view = presentStreetLevelPanel(
snapshot({
enabled: true,
keyRequired: true,
providers: [
provider({ keyRequired: true, configured: false }),
provider({
id: 'google',
name: 'Google Street View',
label: 'STREET VIEW',
on: false,
configured: true,
requiresKeyId: 'google-maps',
}),
],
keyAlternatives: [
{ id: 'google', label: 'STREET VIEW', requiresKeyId: 'google-maps' },
],
}),
);
const guidance =
'Needs MAPILLARY_CLIENT_TOKEN — add it in Provider Settings, or light STREET VIEW to use your Google Maps key';
assert.equal(view.status.text, 'KEY REQUIRED');
assert.equal(view.status.title, guidance);
assert.equal(view.meta, guidance);
// Off Google 3D Street View is not listed: plain KEY REQUIRED.
const plain = presentStreetLevelPanel(
snapshot({
enabled: true,
keyRequired: true,
providers: [provider({ keyRequired: true, configured: false })],
keyAlternatives: [],
}),
);
assert.equal(plain.status.text, 'KEY REQUIRED');
assert.notEqual(plain.meta, guidance);
});
+13 -1
View File
@@ -183,8 +183,20 @@ button.sl-status:focus-visible {
border-color: rgba(255, 179, 0, 0.55);
}
/* The coverage hint and OPEN STREET VIEW share a line; the hint wraps. */
.sl-meta-row {
display: flex;
align-items: center;
gap: 8px;
}
.sl-meta-row .sl-meta {
flex: 1 1 auto;
min-width: 0;
}
.sl-nearest-btn {
margin-left: auto;
flex: 0 0 auto;
}
.sl-nearest-btn[hidden] {
+5 -2
View File
@@ -113,7 +113,6 @@
<div class="sl-providers" role="group" aria-label="Imagery providers">
<span class="cctv-summary-label">PROVIDERS</span>
<div id="sl-provider-chips" class="sl-chips"></div>
<button id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" hidden>OPEN STREET VIEW</button>
</div>
<div id="sl-error" class="sl-error" role="alert" hidden><span aria-hidden="true">⚠</span> <span id="sl-error-text"></span></div>
<!-- A missing key gates the filters only: the chips above stay
@@ -134,7 +133,11 @@
</div>
</fieldset>
<ul id="sl-legend" class="sl-legend" aria-label="Coverage colours"></ul>
<div id="sl-coverage-meta" class="sl-meta"></div>
<!-- The hint says how to open imagery; the button does it without a pointer. -->
<div class="sl-meta-row">
<div id="sl-coverage-meta" class="sl-meta"></div>
<button id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" hidden>OPEN STREET VIEW</button>
</div>
</div>
</div>
</div>