mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
- One provider is lit at a time: lighting a chip darkens the others, and a chip click records every registered provider's switch in one update so the share link keeps the swap (also off Google 3D, where Street View is not listed). The layer starts with the first default-on provider (Mapillary, free); params that light several keep the one already on, so reapplying them never flips (litByParams). The now-unreachable "several lit" chip paths are gone. - Leaving Google 3D during a Street View lookup ends it without an error or a stuck loading state (openImage's early return resets it; the lookup loop rechecks availability). - A source swap during a lookup no longer reports "no imagery". - Street View lit off Google 3D says the panel needs the Google 3D map. - Comments trimmed to the why; keySetupCore.test.mjs back to upstream's formatting plus the one new test (was a 551-line reformat). Co-Authored-By: Claude Opus 5.5 <[email protected]>
185 lines
5.4 KiB
JavaScript
185 lines
5.4 KiB
JavaScript
import { FOLLOW_MAP_STACK_ID } from '../../policy.js';
|
|
import { createMapsLoader } from './mapsLoader.js';
|
|
import { createGoogleViewer } from './viewer.js';
|
|
import {
|
|
COLORS,
|
|
GOOGLE_CREDIT_HTML,
|
|
GOOGLE_KEY_ID,
|
|
GOOGLE_LABEL,
|
|
GOOGLE_NAME,
|
|
GOOGLE_PROVIDER_ID,
|
|
GROUND_CLICK_HINT,
|
|
KEY_REJECTED_MESSAGE,
|
|
NEAREST_RADIUS_M,
|
|
NEAREST_TIMEOUT_MS,
|
|
NO_ANSWER_MESSAGE,
|
|
PICK_PREFIX,
|
|
} from './policy.js';
|
|
|
|
/** `promise`, unless it times out, `signal` aborts or Google refuses the key. */
|
|
function settleWithin(promise, { ms, signal, loader }) {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(
|
|
() => finish(reject, new Error(NO_ANSWER_MESSAGE)),
|
|
ms,
|
|
);
|
|
const onAbort = () => finish(reject, signal.reason);
|
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
const stopAuthWatch = loader.onAuthFailure(() =>
|
|
finish(reject, new Error(KEY_REJECTED_MESSAGE)),
|
|
);
|
|
promise.then(
|
|
(value) => finish(resolve, value),
|
|
(error) => finish(reject, error),
|
|
);
|
|
function finish(settle, value) {
|
|
clearTimeout(timer);
|
|
signal?.removeEventListener('abort', onAbort);
|
|
stopAuthWatch();
|
|
settle(value);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Google Street View provider: Google's panorama viewer and nearest-panorama
|
|
* lookups. It draws no coverage; a click on a street opens the panorama.
|
|
* @returns {import('../../registry.js').StreetLevelProvider}
|
|
*/
|
|
export function createGoogleProvider({
|
|
getApiKey,
|
|
loader = createMapsLoader({ getApiKey }),
|
|
}) {
|
|
if (typeof getApiKey !== 'function')
|
|
throw new TypeError('Google Street View needs getApiKey');
|
|
|
|
return Object.freeze({
|
|
id: GOOGLE_PROVIDER_ID,
|
|
name: GOOGLE_NAME,
|
|
label: GOOGLE_LABEL,
|
|
requiresKeyId: GOOGLE_KEY_ID,
|
|
pickPrefix: PICK_PREFIX.root,
|
|
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.
|
|
defaultOn: false,
|
|
// All 360°, and no coverage for a date filter to thin.
|
|
filters: [],
|
|
marker: Object.freeze({ kind: 'view-cone', color: COLORS.coverage }),
|
|
|
|
create(context) {
|
|
let keyRequired = !getApiKey();
|
|
let active = false;
|
|
let service = null;
|
|
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(() => {
|
|
// Switched off: its chip says so.
|
|
if (context.isActive?.())
|
|
context.actions.reportError(KEY_REJECTED_MESSAGE);
|
|
context.notify();
|
|
});
|
|
|
|
return {
|
|
async status() {
|
|
keyRequired = !getApiKey();
|
|
context.notify();
|
|
return { configured: !keyRequired };
|
|
},
|
|
|
|
init() {},
|
|
|
|
activate() {
|
|
active = true;
|
|
},
|
|
|
|
deactivate() {
|
|
active = false;
|
|
},
|
|
|
|
destroy() {
|
|
active = false;
|
|
stopAuthWatch();
|
|
},
|
|
|
|
refreshCoverage() {},
|
|
|
|
/** No filter applies to Street View (see `filters`). */
|
|
setFilter() {},
|
|
|
|
coverageStats() {
|
|
const keyRejected = loader.authFailed();
|
|
return {
|
|
count: 0,
|
|
zoom: null,
|
|
kind: null,
|
|
loading: false,
|
|
hint:
|
|
!active || keyRequired || keyRejected ? '' : GROUND_CLICK_HINT,
|
|
error: keyRejected ? KEY_REJECTED_MESSAGE : null,
|
|
keyRequired,
|
|
keyRejected,
|
|
};
|
|
},
|
|
|
|
handlePick: () => false,
|
|
|
|
/**
|
|
* 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();
|
|
// 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;
|
|
const lookup = (async () => {
|
|
const lib = await loader.importLibrary('streetView');
|
|
await getService();
|
|
if (settled) return null;
|
|
const { GOOGLE, OUTDOOR } = lib.StreetViewSource;
|
|
try {
|
|
const { data } = await service.getPanorama({
|
|
location: { lat, lng: lon },
|
|
radius: NEAREST_RADIUS_M,
|
|
// Both: Google's own imagery, outdoors (sources intersect).
|
|
sources: [GOOGLE, OUTDOOR],
|
|
preference: lib.StreetViewPreference.NEAREST,
|
|
});
|
|
return data;
|
|
} catch (error) {
|
|
if (error?.code === 'ZERO_RESULTS') return null;
|
|
throw error;
|
|
}
|
|
})();
|
|
const data = await settleWithin(lookup, {
|
|
ms: NEAREST_TIMEOUT_MS,
|
|
signal,
|
|
loader,
|
|
}).finally(() => {
|
|
settled = true;
|
|
});
|
|
const panoId = data?.location?.pano;
|
|
if (!panoId) return null;
|
|
viewer.remember(panoId, data);
|
|
return panoId;
|
|
},
|
|
|
|
viewer,
|
|
};
|
|
},
|
|
});
|
|
}
|