mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24: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]>
128 lines
3.6 KiB
JavaScript
128 lines
3.6 KiB
JavaScript
import {
|
|
MAPILLARY_GRAPH_HOST,
|
|
SEQUENCE_IMAGE_FIELDS,
|
|
SEQUENCE_IMAGES_LIMIT,
|
|
} from './policy.js';
|
|
|
|
/** HTTP error flagged `keyRequired`, `keyRejected` or `retryAfterSec` (429). */
|
|
export class MapillarySourceError extends Error {
|
|
constructor(message, { status = 0, payload = null, retryAfterSec } = {}) {
|
|
super(message);
|
|
this.name = 'MapillarySourceError';
|
|
this.status = status;
|
|
this.payload = payload;
|
|
this.keyRequired = payload?.keyRequired ?? payload?.error === 'no_key';
|
|
this.keyRejected = payload?.keyRejected === true;
|
|
const wait = Number(retryAfterSec ?? payload?.retryAfter);
|
|
this.retryAfterSec =
|
|
status === 429 ? (Number.isFinite(wait) && wait > 0 ? wait : 60) : null;
|
|
}
|
|
}
|
|
|
|
async function readJsonOrThrow(response, label) {
|
|
let payload = null;
|
|
try {
|
|
payload = await response.json();
|
|
} catch {
|
|
/* status is authoritative */
|
|
}
|
|
if (!response.ok)
|
|
throw new MapillarySourceError(
|
|
payload?.error?.message ||
|
|
payload?.error ||
|
|
`${label} HTTP ${response.status}`,
|
|
{ status: response.status, payload },
|
|
);
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* Mapillary network access: tiles via the local proxy, which adds the token
|
|
* and caches; Graph API lookups direct with the client token, as MapillaryJS.
|
|
* @param {{token?: string, fetchImpl?: Function, endpoints?: object}} options
|
|
*/
|
|
export function createMapillarySource({
|
|
token = '',
|
|
fetchImpl = (...args) => globalThis.fetch(...args),
|
|
endpoints = {},
|
|
} = {}) {
|
|
const urls = {
|
|
status: '/api/mapillary/status',
|
|
tiles: '/api/mapillary/tiles',
|
|
graph: MAPILLARY_GRAPH_HOST,
|
|
...endpoints,
|
|
};
|
|
|
|
async function graph(path, params = {}, { signal } = {}) {
|
|
if (!token)
|
|
throw new MapillarySourceError('Mapillary token not configured', {
|
|
status: 503,
|
|
payload: { keyRequired: true },
|
|
});
|
|
const search = new URLSearchParams({ access_token: token, ...params });
|
|
const response = await fetchImpl(`${urls.graph}/${path}?${search}`, {
|
|
signal,
|
|
});
|
|
return readJsonOrThrow(response, 'Mapillary graph');
|
|
}
|
|
|
|
return {
|
|
get token() {
|
|
return token;
|
|
},
|
|
hasToken: () => Boolean(token),
|
|
|
|
async getStatus({ signal } = {}) {
|
|
const response = await fetchImpl(urls.status, {
|
|
signal,
|
|
cache: 'no-store',
|
|
});
|
|
return readJsonOrThrow(response, 'Mapillary status');
|
|
},
|
|
|
|
/** Raw protobuf bytes for one tile; an empty array means no data. */
|
|
async getTile(layer, z, x, y, { signal } = {}) {
|
|
const response = await fetchImpl(
|
|
`${urls.tiles}/${layer}/${z}/${x}/${y}`,
|
|
{
|
|
signal,
|
|
},
|
|
);
|
|
if (response.status === 204) return new Uint8Array(0);
|
|
if (!response.ok) {
|
|
let payload = null;
|
|
try {
|
|
payload = await response.json();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new MapillarySourceError(
|
|
payload?.error || `Tile HTTP ${response.status}`,
|
|
{
|
|
status: response.status,
|
|
payload,
|
|
retryAfterSec: response.headers?.get?.('retry-after') ?? undefined,
|
|
},
|
|
);
|
|
}
|
|
return new Uint8Array(await response.arrayBuffer());
|
|
},
|
|
|
|
async getSequenceImages(
|
|
sequenceId,
|
|
{ signal, limit = SEQUENCE_IMAGES_LIMIT } = {},
|
|
) {
|
|
const payload = await graph(
|
|
'images',
|
|
{
|
|
sequence_ids: String(sequenceId),
|
|
fields: SEQUENCE_IMAGE_FIELDS,
|
|
limit: String(limit),
|
|
},
|
|
{ signal },
|
|
);
|
|
return Array.isArray(payload?.data) ? payload.data : [];
|
|
},
|
|
};
|
|
}
|