mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
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:
1 parent
22b0009138
commit
7bfcf177ac
35 files changed
+1108
-388
No files matched your search
@@ -1,14 +1,13 @@
|
||||
import {
|
||||
GOOGLE_PROVIDER_ID,
|
||||
KEY_REJECTED_MESSAGE,
|
||||
NO_ANSWER_MESSAGE,
|
||||
OPEN_TIMEOUT_MS,
|
||||
creatorFrom,
|
||||
imageDateMs,
|
||||
streetViewUrl,
|
||||
} from './policy.js';
|
||||
|
||||
/** An open that sees no panorama by then has failed. */
|
||||
const OPEN_TIMEOUT_MS = 20_000;
|
||||
|
||||
/** Google's own controls, minus those the Street Level panel already has. */
|
||||
const PANORAMA_OPTIONS = Object.freeze({
|
||||
visible: false,
|
||||
@@ -31,21 +30,26 @@ const PANORAMA_OPTIONS = Object.freeze({
|
||||
* Viewer adapter over Google's StreetViewPanorama. It renders in its own
|
||||
* element inside the core's host, and emits a provider-neutral pose for every
|
||||
* panorama and view change, including steps along Google's own arrows.
|
||||
* @param {{loader: ReturnType<import('./mapsLoader.js').createMapsLoader>, render?: object}} options
|
||||
* @returns {import('../../registry.js').ViewerAdapter}
|
||||
* `getService` hands out the StreetViewService the provider shares.
|
||||
* @param {{loader: ReturnType<import('./mapsLoader.js').createMapsLoader>, getService?: () => Promise<object>, render?: object}} options
|
||||
* @returns {import('../../registry.js').ViewerAdapter & {remember: Function}}
|
||||
*/
|
||||
export function createGoogleViewer({ loader, render } = {}) {
|
||||
export function createGoogleViewer({ loader, getService, render } = {}) {
|
||||
let library = null;
|
||||
let panorama = null;
|
||||
let element = null;
|
||||
let host = null;
|
||||
let service = null;
|
||||
let pendingOpen = null;
|
||||
/** Settles the open waiting for its panorama, when a close overtakes it. */
|
||||
let cancelShown = null;
|
||||
/** Bumped by `unmount`, so a construction it overtook builds nothing. */
|
||||
let generation = 0;
|
||||
let subscriptions = [];
|
||||
/** Panorama id → its {capturedAt, creator} lookup. */
|
||||
const metadata = new Map();
|
||||
/** Panorama id → its {capturedAt, creator, title}. */
|
||||
const described = new Map();
|
||||
/** Panorama ids whose lookup is in flight. */
|
||||
const lookingUp = new Set();
|
||||
const listeners = new Set();
|
||||
|
||||
function requestRender() {
|
||||
@@ -67,41 +71,44 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function metaFrom(data) {
|
||||
return {
|
||||
capturedAt: imageDateMs(data?.imageDate),
|
||||
creator: creatorFrom(data?.copyright),
|
||||
title: data?.location?.description || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Date and photographer of a panorama, looked up once: pano_changed and
|
||||
* position_changed arrive together, so the lookup itself is shared.
|
||||
* Look up a panorama's date, photographer and address once; the pose
|
||||
* already out is sent again with them. A failed lookup is asked again on
|
||||
* the next event.
|
||||
*/
|
||||
function describe(panoId) {
|
||||
if (!metadata.has(panoId)) metadata.set(panoId, lookUp(panoId));
|
||||
return metadata.get(panoId);
|
||||
}
|
||||
|
||||
async function lookUp(panoId) {
|
||||
async function describe(panoId) {
|
||||
if (described.has(panoId) || lookingUp.has(panoId)) return;
|
||||
lookingUp.add(panoId);
|
||||
try {
|
||||
service ||= new library.StreetViewService();
|
||||
service ||= getService
|
||||
? await getService()
|
||||
: new (await ensureLibrary()).StreetViewService();
|
||||
const { data } = await service.getPanorama({ pano: panoId });
|
||||
return {
|
||||
capturedAt: imageDateMs(data?.imageDate),
|
||||
creator: creatorFrom(data?.copyright),
|
||||
title: data?.location?.description || null,
|
||||
};
|
||||
described.set(panoId, metaFrom(data));
|
||||
} catch {
|
||||
// The pose still goes out, without a date.
|
||||
return { capturedAt: null, creator: null, title: null };
|
||||
return;
|
||||
} finally {
|
||||
lookingUp.delete(panoId);
|
||||
}
|
||||
publishPose();
|
||||
}
|
||||
|
||||
async function publishPose() {
|
||||
const shown = panorama;
|
||||
const panoId = shown?.getPano?.();
|
||||
const position = shown?.getPosition?.();
|
||||
if (!panoId || !position || pendingOpen === null) return;
|
||||
const meta = await describe(panoId);
|
||||
// Closed, unmounted or moved on while the date was looked up.
|
||||
if (pendingOpen === null || shown !== panorama) return;
|
||||
if (panorama.getPano() !== panoId) return;
|
||||
/** Emit the pose on screen now, dated once its lookup has answered. */
|
||||
function publishPose() {
|
||||
const panoId = panorama?.getPano?.();
|
||||
const latLng = panorama?.getPosition?.();
|
||||
if (!panoId || !latLng || pendingOpen === null) return;
|
||||
const meta = described.get(panoId);
|
||||
if (!meta) describe(panoId);
|
||||
const pov = panorama.getPov() || {};
|
||||
const latLng = panorama.getPosition() || position;
|
||||
emit({
|
||||
providerId: GOOGLE_PROVIDER_ID,
|
||||
imageId: panoId,
|
||||
@@ -110,10 +117,10 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
tilt: Number.isFinite(pov.pitch) ? pov.pitch : 0,
|
||||
altitude: null,
|
||||
isPano: true,
|
||||
capturedAt: meta.capturedAt,
|
||||
capturedAt: meta?.capturedAt ?? null,
|
||||
capturedAtPrecision: 'month',
|
||||
creator: meta.creator,
|
||||
title: meta.title,
|
||||
creator: meta?.creator ?? null,
|
||||
title: meta?.title ?? null,
|
||||
sequenceId: null,
|
||||
externalUrl: streetViewUrl(panoId, {
|
||||
heading: pov.heading || 0,
|
||||
@@ -136,8 +143,10 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
target.append(element);
|
||||
host = target;
|
||||
panorama = new StreetViewPanorama(element, PANORAMA_OPTIONS);
|
||||
subscriptions = ['pano_changed', 'position_changed', 'pov_changed'].map(
|
||||
(name) => panorama.addListener(name, () => publishPose()),
|
||||
// Not pano_changed: it can fire before the new panorama's data, with the
|
||||
// old position. A step along an arrow moves the position.
|
||||
subscriptions = ['position_changed', 'pov_changed'].map((name) =>
|
||||
panorama.addListener(name, () => publishPose()),
|
||||
);
|
||||
return panorama;
|
||||
}
|
||||
@@ -157,41 +166,42 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once `panoId` shows; rejects when Google has no imagery for it,
|
||||
* or refuses the key (which it reports apart from the panorama's status).
|
||||
* Resolves once `panoId` shows: an OK status, or (when the status was
|
||||
* already OK and does not change) the new position. Rejects when Google has
|
||||
* no imagery for it, refuses the key (reported apart from the status), or
|
||||
* does not answer. Google may show a newer id for the same place.
|
||||
*/
|
||||
function shown(instance, panoId) {
|
||||
if (instance.getPano() === panoId && instance.getStatus() === 'OK')
|
||||
return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
done();
|
||||
reject(new Error('Street View did not answer'));
|
||||
}, OPEN_TIMEOUT_MS);
|
||||
const stopAuthWatch = loader.onAuthFailure(() => {
|
||||
done();
|
||||
reject(new Error(KEY_REJECTED_MESSAGE));
|
||||
});
|
||||
const subscription = instance.addListener('status_changed', () => {
|
||||
if (pendingOpen !== panoId) {
|
||||
done();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
// Google may answer with a newer id for the same place: any OK counts.
|
||||
const timer = setTimeout(
|
||||
() => finish(reject, new Error(NO_ANSWER_MESSAGE)),
|
||||
OPEN_TIMEOUT_MS,
|
||||
);
|
||||
const stopAuthWatch = loader.onAuthFailure(() =>
|
||||
finish(reject, new Error(KEY_REJECTED_MESSAGE)),
|
||||
);
|
||||
cancelShown = () => finish(resolve);
|
||||
const check = () => {
|
||||
if (pendingOpen !== panoId) return finish(resolve);
|
||||
const status = instance.getStatus();
|
||||
if (status === 'OK') {
|
||||
done();
|
||||
resolve();
|
||||
} else if (status && status !== 'OK') {
|
||||
done();
|
||||
reject(new Error('Street View has no imagery for this place'));
|
||||
}
|
||||
});
|
||||
function done() {
|
||||
if (status === 'OK') finish(resolve);
|
||||
else if (status)
|
||||
finish(
|
||||
reject,
|
||||
new Error('Street View has no imagery for this place'),
|
||||
);
|
||||
};
|
||||
const subscriptions = ['status_changed', 'position_changed'].map((name) =>
|
||||
instance.addListener(name, check),
|
||||
);
|
||||
function finish(settle, value) {
|
||||
cancelShown = null;
|
||||
clearTimeout(timer);
|
||||
subscription.remove();
|
||||
for (const subscription of subscriptions) subscription.remove();
|
||||
stopAuthWatch();
|
||||
settle(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -215,11 +225,12 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
instance.setVisible(true);
|
||||
await ready;
|
||||
if (pendingOpen !== panoId) return;
|
||||
await publishPose();
|
||||
publishPose();
|
||||
},
|
||||
|
||||
close() {
|
||||
pendingOpen = null;
|
||||
cancelShown?.();
|
||||
try {
|
||||
panorama?.setVisible(false);
|
||||
} catch {
|
||||
@@ -229,6 +240,7 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
|
||||
unmount() {
|
||||
pendingOpen = null;
|
||||
cancelShown?.();
|
||||
generation++;
|
||||
destroyPanorama();
|
||||
},
|
||||
@@ -255,5 +267,10 @@ export function createGoogleViewer({ loader, render } = {}) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
|
||||
/** Keep a lookup's answer, so the panorama it found needs no second one. */
|
||||
remember(panoId, data) {
|
||||
if (panoId && data) described.set(String(panoId), metaFrom(data));
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user