feat(street-level): Street View polish — name, filters, legend, marker

- The chip reads GOOGLE STREET VIEW (and its caption link likewise).
- Providers declare the imagery filters they honour (`filters`, default
  pano + since). Street View declares none: every panorama is 360° and it
  draws no coverage to thin, so its lookups ignore the filters and the
  panel hides IMAGERY and SINCE while it is the only lit provider.
- The legend lists only providers that draw coverage lines, with
  Selected only beside one: Street View alone shows no legend.
- Providers may pick their on-globe marker (`marker`); Street View's is a
  Google-blue field-of-view cone from a white-ringed dot, in place of
  GEV's amber pin, and switches back for a Mapillary photo.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-13 14:05:57 +01:00
1 parent eb07b9d5ad
commit 64d939f3d2
23 files changed
+305 -86

No files matched your search

+7
View File
@@ -69,6 +69,8 @@ export class StreetLevelControls {
nearestBtn: byId('sl-nearest-btn'),
error: byId('sl-error'),
errorText: byId('sl-error-text'),
panoRow: byId('sl-pano-row'),
sinceRow: byId('sl-since-row'),
sinceRange: byId('sl-since'),
sinceLabel: byId('sl-since-label'),
legend: byId('sl-legend'),
@@ -532,6 +534,11 @@ export class StreetLevelControls {
_renderFilters(view) {
const el = this._elements;
const shows = view.filterControls || { pano: true, since: true };
setProp(el.panoRow, 'hidden', !shows.pano);
setProp(el.sinceRow, 'hidden', !shows.since);
setProp(el.controls, 'hidden', !shows.pano && !shows.since);
setProp(el.legend, 'hidden', view.legend.length === 0);
this._renderRadios(
el.panoButtons,
(button) => button.dataset.slPano === view.filter.pano,
+41 -3
View File
@@ -307,10 +307,12 @@ function panelDom() {
add(settings, 'div', { id });
// A missing key gates the filters only; the chips stay outside the gate.
const controls = add(settings, 'fieldset', { id: 'sl-controls' });
const panoRow = add(controls, 'div', { id: 'sl-pano-row' });
for (const pano of ['all', 'pano', 'flat'])
add(controls, 'button', { dataset: { slPano: pano } });
add(controls, 'input', { id: 'sl-since' });
add(controls, 'output', { id: 'sl-since-label' });
add(panoRow, 'button', { dataset: { slPano: pano } });
const sinceRow = add(controls, 'div', { id: 'sl-since-row' });
add(sinceRow, 'input', { id: 'sl-since' });
add(sinceRow, 'output', { id: 'sl-since-label' });
add(settings, 'ul', { id: 'sl-legend' });
const metaRow = add(settings, 'div', { classes: ['sl-meta-row'] });
add(metaRow, 'div', { id: 'sl-coverage-meta' });
@@ -1207,3 +1209,39 @@ test('a refused key on one provider leaves the other chip switchable, and the re
]);
assert.equal(calls.toasts.length, 0);
}));
test('Street View alone hides the imagery filters and the legend; Mapillary beside it brings them back', () =>
withDom(async (dom) => {
const state = uiState();
const { layer } = stubPanel(dom, state);
const shown = () => ({
pano: !dom.root.querySelector('#sl-pano-row').hidden,
since: !dom.root.querySelector('#sl-since-row').hidden,
controls: !dom.root.querySelector('#sl-controls').hidden,
legend: !dom.root.querySelector('#sl-legend').hidden,
});
assert.deepEqual(shown(), {
pano: true,
since: true,
controls: true,
legend: true,
});
layer.publish({
...state,
filterControls: { pano: false, since: false },
legend: [],
});
assert.deepEqual(shown(), {
pano: false,
since: false,
controls: false,
legend: false,
});
layer.publish(state);
assert.deepEqual(shown(), {
pano: true,
since: true,
controls: true,
legend: true,
});
}));
+5
View File
@@ -222,6 +222,11 @@ export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
? null
: state.street.error || state.coverage.error || null,
filter: { pano: filter.pano, sinceDays: Number(filter.sinceDays) || 0 },
// Only the filters a lit provider honours (none for Street View alone).
filterControls: {
pano: state.filterControls?.pano !== false,
since: state.filterControls?.since !== false,
},
since: presentSince(filter.sinceDays, now),
legend: state.legend || [],
viewer: presentViewer(state),
+5 -1
View File
@@ -229,7 +229,11 @@ button.sl-status:focus-visible {
border-radius: 999px;
}
.sl-seg[hidden] {
.sl-seg[hidden],
.sl-controls[hidden],
.sl-filters[hidden],
.sl-since[hidden],
.sl-legend[hidden] {
display: none;
}
+2 -2
View File
@@ -118,7 +118,7 @@
<!-- A missing key gates the filters only: the chips above stay
focusable so their tooltip can say how to add the key. -->
<fieldset id="sl-controls" class="sl-controls">
<div class="sl-filters" role="group" aria-label="Imagery filters">
<div id="sl-pano-row" class="sl-filters" role="group" aria-label="Imagery filters">
<span class="cctv-summary-label">IMAGERY</span>
<div class="sl-seg" role="radiogroup" aria-label="Panorama filter">
<button type="button" class="sl-seg-btn is-active" data-sl-pano="all" role="radio" aria-checked="true">ALL</button>
@@ -126,7 +126,7 @@
<button type="button" class="sl-seg-btn" data-sl-pano="flat" role="radio" aria-checked="false" tabindex="-1">FLAT</button>
</div>
</div>
<div class="sl-since">
<div id="sl-since-row" class="sl-since">
<label class="cctv-summary-label" for="sl-since">SINCE</label>
<input id="sl-since" class="sl-range" type="range" min="0" max="8" step="1" value="0" aria-valuetext="ANY DATE" aria-describedby="sl-since-label" />
<output id="sl-since-label" class="sl-since-label" for="sl-since">ANY DATE</output>