mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(street-level): drop hover, legend and byte-budget cache; flatten the provider directory
- No pointer cursor over coverage lines (no other layer has one); a click test replaces the hover tests. - No legend: one source, one colour. - The on-globe credit lives in index.js; credits.js is gone. - Billboard glyphs are SVG data URLs with the same geometry, so tests need no canvas stub. - The tile cache keeps the 120 most recently used tiles, same 24 h TTL, instead of a byte budget; listTileLayers was test-only. - providers/mapillary/ is flattened into the layer and its policy merged into one policy.js; the source's method list is a literal in the catalog. - One fakeCesiumViewer for the layer tests. - The fixture gate keeps the steps the unit tests do not cover: coverage and credit, a rejected key, line -> cone -> photo, close, a keyless page. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
34d43c7e3f
commit
e214129e45
41 files changed
+335
-909
No files matched your search
@@ -1,127 +0,0 @@
|
||||
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 : [];
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user