mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Main gained three imagery providers (Google Street View, KartaView,
Panoramax), shared line-coverage, cone and photo-viewer modules, one source
lit at a time and the translated interface. Their shared modules still
carried what this branch removes, so the merge ports them onto it:
- lineCoverage.js is this branch's coverage.js made provider-neutral
(draped lines near the ground only, the selection drawn over them, key
and rate-limit blocking); sequenceCones.js is sequences.js the same way.
Mapillary, KartaView and Panoramax all build on them; Mapillary moves
back to providers/mapillary/ beside the others.
- No ground casting, mesh sampling, surface modes, FOLLOW, legend, prewarm,
orbit overview points (Panoramax's density dots included), stale-tile
swaps or water-filled point budget come back.
- Kept from main because the new providers need them: requiresMapStack and
attachMapStackController (Street View on Google 3D only), ground clicks,
openNearest with an optional provider nearestImage, openImage {frame},
reportError, the view-cone marker (now an SVG glyph), filters /
coverageLabel / sinceBasis, OPEN STREET VIEW, keyAlternatives and i18n.
- photoViewer.js treats the viewer as expanded when its wrap is full screen
or the iPhone popover, as EXPAND now is.
- The fixture browser gate keeps its 8 steps and adds one each for
KartaView and Panoramax; catalogs, docs, boundaries and format scope
follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
182 lines
5.4 KiB
JavaScript
182 lines
5.4 KiB
JavaScript
import { GOOGLE_3D_MAP_STACK_ID } from '../../policy.js';
|
|
import { createMapsLoader } from './mapsLoader.js';
|
|
import { getLocale } from '../../../../i18n/index.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, getLanguage: getLocale }),
|
|
}) {
|
|
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: GOOGLE_3D_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();
|
|
},
|
|
|
|
/** No filter applies to Street View (see `filters`). */
|
|
setFilter() {},
|
|
|
|
coverageStats() {
|
|
const keyRejected = loader.authFailed();
|
|
return {
|
|
count: 0,
|
|
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,
|
|
};
|
|
},
|
|
});
|
|
}
|