fix(street-level): IC8 review of the Street View provider

Google's Maps Platform terms forbid Street View imagery beside non-Google
maps, so the provider declares requiresMapStack: 'photoreal': off Google
3D its chip is dashed with the reason, lookups and opens skip it, and
leaving Google 3D closes its panorama.

- Ground clicks: only a click on the map itself (scenePick.isSurfacePick:
  nothing picked or Google 3D tile content) at street zoom opens Street
  View, and the camera stays put; higher up the hint says to zoom in.
  scenePick.pickGroundPosition replaces the copy in Directions.
- Viewer: poses go out at once and again when the date and address
  arrive, so an open never hangs on that lookup; one lookup per panorama,
  seeded by the nearest lookup; a close settles a pending open; failed
  lookups are asked again; poses follow position changes, not pano_changed.
- Loader: the gm_authFailure handler is installed once, so a retried load
  reports a refusal once.
- Viewer host: a prewarm that lands after another provider took the host
  is released; FIT/FILL availability is set on every open.
- Provider: a refusal while switched off reports no error over another
  photo; SINCE compares against the end of Google's month; messages name
  the referrer and network causes; FLAT says Street View is 360° only.
- The layer row names the key of the providers that are on; the MCP panel
  CSP admits Street View's tiles, control images and photos.
- Tests for each, against fakes that now follow Google's event order; the
  browser gate restores its state, waits instead of sleeping, aims its
  panel clicks and corner drags, and asserts the camera stays put.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-08 02:59:51 +01:00
1 parent 22b0009138
commit 7bfcf177ac
35 files changed
+1108 -388

No files matched your search

@@ -1,4 +1,5 @@
import { passesImageryFilter } from '../../filter.js';
import { FOLLOW_MAP_STACK_ID } from '../../policy.js';
import { createMapsLoader } from './mapsLoader.js';
import { createGoogleViewer } from './viewer.js';
import {
@@ -8,13 +9,14 @@ 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,
imageDateMs,
imageMonthEndMs,
} from './policy.js';
/**
@@ -69,6 +71,8 @@ export function createGoogleProvider({
colors: COLORS,
credit: Object.freeze({ html: GOOGLE_CREDIT_HTML }),
groundClick: true,
// Google's terms forbid Street View beside non-Google maps.
requiresMapStack: FOLLOW_MAP_STACK_ID,
// Every panorama is billed to the Maps key: the user lights the chip.
defaultOn: false,
@@ -76,12 +80,21 @@ export function createGoogleProvider({
let keyRequired = !getApiKey();
let active = false;
let service = null;
/** The one StreetViewService the lookups and the viewer share. */
async function getService() {
const lib = await loader.importLibrary('streetView');
service ||= new lib.StreetViewService();
return service;
}
const viewer = createGoogleViewer({
loader,
getService,
render: context.services?.render,
});
const stopAuthWatch = loader.onAuthFailure(() => {
context.actions.reportError(KEY_REJECTED_MESSAGE);
// Switched off since: its chip says so; no error over another photo.
if (context.isActive?.())
context.actions.reportError(KEY_REJECTED_MESSAGE);
context.notify();
});
@@ -120,7 +133,11 @@ export function createGoogleProvider({
kind: null,
loading: false,
hint:
active && !keyRequired && !keyRejected ? GROUND_CLICK_HINT : '',
!active || keyRequired || keyRejected
? ''
: context.getFilter().pano === 'flat'
? FLAT_FILTER_HINT
: GROUND_CLICK_HINT,
error: keyRejected ? KEY_REJECTED_MESSAGE : null,
keyRequired,
keyRejected,
@@ -140,13 +157,14 @@ export function createGoogleProvider({
if (filter.pano === 'flat') return null;
const lookup = (async () => {
const lib = await loader.importLibrary('streetView');
service ||= new lib.StreetViewService();
await getService();
const { GOOGLE, OUTDOOR } = lib.StreetViewSource;
try {
const { data } = await service.getPanorama({
location: { lat, lng: lon },
radius: NEAREST_RADIUS_M,
sources: GOOGLE ? [GOOGLE, OUTDOOR] : [OUTDOOR],
// Both: Google's own imagery, outdoors (sources intersect).
sources: [GOOGLE, OUTDOOR],
preference: lib.StreetViewPreference.NEAREST,
});
return data;
@@ -163,10 +181,12 @@ export function createGoogleProvider({
const panoId = data?.location?.pano;
if (!panoId) return null;
const passes = passesImageryFilter(
{ isPano: true, capturedAt: imageDateMs(data.imageDate) ?? 0 },
{ isPano: true, capturedAt: imageMonthEndMs(data.imageDate) ?? 0 },
filter,
);
return passes ? panoId : null;
if (!passes) return null;
viewer.remember(panoId, data);
return panoId;
},
viewer,