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 : []; }, }; }