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

@@ -1,4 +1,3 @@
import { passesImageryFilter } from '../../filter.js';
import { FOLLOW_MAP_STACK_ID } from '../../policy.js';
import { createMapsLoader } from './mapsLoader.js';
import { createGoogleViewer } from './viewer.js';
@@ -9,14 +8,12 @@ import {
GOOGLE_LABEL,
GOOGLE_NAME,
GOOGLE_PROVIDER_ID,
FLAT_FILTER_HINT,
GROUND_CLICK_HINT,
KEY_REJECTED_MESSAGE,
NEAREST_RADIUS_M,
NEAREST_TIMEOUT_MS,
NO_ANSWER_MESSAGE,
PICK_PREFIX,
imageMonthEndMs,
} from './policy.js';
/**
@@ -75,6 +72,11 @@ export function createGoogleProvider({
requiresMapStack: FOLLOW_MAP_STACK_ID,
// Every panorama is billed to the Maps key: the user lights the chip.
defaultOn: false,
// Every panorama is 360°, and with no coverage drawn there is nothing
// for a date filter to thin: the user picks the place by clicking it.
filters: [],
// Google Maps' own look for where a panorama stands and looks.
marker: Object.freeze({ kind: 'view-cone', color: COLORS.coverage }),
create(context) {
let keyRequired = !getApiKey();
@@ -122,7 +124,7 @@ export function createGoogleProvider({
refreshCoverage() {},
/** Nothing drawn to redraw: lookups read the filter when they run. */
/** No filter applies to Street View (see `filters`). */
setFilter() {},
coverageStats() {
@@ -133,11 +135,7 @@ export function createGoogleProvider({
kind: null,
loading: false,
hint:
!active || keyRequired || keyRejected
? ''
: context.getFilter().pano === 'flat'
? FLAT_FILTER_HINT
: GROUND_CLICK_HINT,
!active || keyRequired || keyRejected ? '' : GROUND_CLICK_HINT,
error: keyRejected ? KEY_REJECTED_MESSAGE : null,
keyRequired,
keyRejected,
@@ -147,14 +145,12 @@ export function createGoogleProvider({
handlePick: () => false,
/**
* Nearest Google-collected outdoor panorama that passes the imagery
* filter, or null. Every Street View panorama is 360°.
* Nearest Google-collected outdoor panorama, or null. The imagery
* filters do not apply (see `filters`).
*/
async nearestImage({ lat, lon }, { signal } = {}) {
if (loader.authFailed()) throw new Error(KEY_REJECTED_MESSAGE);
signal?.throwIfAborted();
const filter = context.getFilter();
if (filter.pano === 'flat') return null;
// Set once the caller has its answer (aborted, timed out, refused):
// a lookup still waiting on Google's script then asks nothing.
let settled = false;
@@ -186,11 +182,6 @@ export function createGoogleProvider({
});
const panoId = data?.location?.pano;
if (!panoId) return null;
const passes = passesImageryFilter(
{ isPano: true, capturedAt: imageMonthEndMs(data.imageDate) ?? 0 },
filter,
);
if (!passes) return null;
viewer.remember(panoId, data);
return panoId;
},