mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge origin/main: Google Street View, KartaView and Panoramax on the slim Street Level
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]>
This commit is contained in:
commit
6390a3346f
809 files changed
+81825
-6035
No files matched your search
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createMapillarySource, MapillarySourceError } from './source.js';
|
||||
|
||||
function fakeFetch(handler) {
|
||||
const calls = [];
|
||||
const fetchImpl = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
return handler(String(url), init);
|
||||
};
|
||||
return { calls, fetchImpl };
|
||||
}
|
||||
|
||||
const jsonResponse = (payload, status = 200) =>
|
||||
new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
test('tiles go through the local proxy and never carry the token', async () => {
|
||||
const { calls, fetchImpl } = fakeFetch(
|
||||
() => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
||||
);
|
||||
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
||||
const bytes = await source.getTile('coverage', 14, 2662, 6285);
|
||||
assert.deepEqual([...bytes], [1, 2, 3]);
|
||||
assert.equal(calls[0].url, '/api/mapillary/tiles/coverage/14/2662/6285');
|
||||
assert.doesNotMatch(calls[0].url, /MLY/);
|
||||
});
|
||||
|
||||
test('an empty tile (204) is an empty byte array, not an error', async () => {
|
||||
const { fetchImpl } = fakeFetch(() => new Response(null, { status: 204 }));
|
||||
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
||||
assert.equal((await source.getTile('coverage', 14, 1, 1)).length, 0);
|
||||
});
|
||||
|
||||
test('a missing key on the proxy surfaces as keyRequired', async () => {
|
||||
const { fetchImpl } = fakeFetch(() =>
|
||||
jsonResponse({ error: 'no_key', keyRequired: true }, 503),
|
||||
);
|
||||
const source = createMapillarySource({ token: '', fetchImpl });
|
||||
assert.equal(source.hasToken(), false);
|
||||
await assert.rejects(
|
||||
() => source.getTile('coverage', 14, 1, 1),
|
||||
(error) =>
|
||||
error instanceof MapillarySourceError &&
|
||||
error.status === 503 &&
|
||||
error.keyRequired === true,
|
||||
);
|
||||
});
|
||||
|
||||
test('graph lookups call graph.mapillary.com with the client token and requested fields', async () => {
|
||||
const { calls, fetchImpl } = fakeFetch(() =>
|
||||
jsonResponse({ data: [{ id: '1', geometry: { coordinates: [1, 2] } }] }),
|
||||
);
|
||||
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
||||
const images = await source.getSequenceImages('seq-1');
|
||||
assert.equal(images.length, 1);
|
||||
const url = new URL(calls[0].url);
|
||||
assert.equal(url.host, 'graph.mapillary.com');
|
||||
assert.equal(url.pathname, '/images');
|
||||
assert.equal(url.searchParams.get('access_token'), 'MLY|1|abc');
|
||||
assert.equal(url.searchParams.get('sequence_ids'), 'seq-1');
|
||||
assert.match(url.searchParams.get('fields'), /geometry/);
|
||||
});
|
||||
|
||||
test('graph calls without a token fail fast as keyRequired without a request', async () => {
|
||||
const { calls, fetchImpl } = fakeFetch(() => jsonResponse({}));
|
||||
const source = createMapillarySource({ token: '', fetchImpl });
|
||||
await assert.rejects(
|
||||
() => source.getSequenceImages('123'),
|
||||
(error) =>
|
||||
error instanceof MapillarySourceError && error.keyRequired === true,
|
||||
);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test('graph API errors carry the upstream message and status', async () => {
|
||||
const { fetchImpl } = fakeFetch(() =>
|
||||
jsonResponse({ error: { message: 'Invalid OAuth 2.0 Access Token' } }, 400),
|
||||
);
|
||||
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
||||
await assert.rejects(
|
||||
() => source.getSequenceImages('999'),
|
||||
(error) =>
|
||||
error instanceof MapillarySourceError &&
|
||||
error.status === 400 &&
|
||||
/Invalid OAuth/.test(error.message),
|
||||
);
|
||||
});
|
||||
|
||||
test('a rejected key and a rate limit are told apart from other tile errors', async () => {
|
||||
const rejectedSource = createMapillarySource({
|
||||
token: 'MLY|1|abc',
|
||||
fetchImpl: fakeFetch(() =>
|
||||
jsonResponse(
|
||||
{ error: 'Mapillary rejected the access token', keyRejected: true },
|
||||
403,
|
||||
),
|
||||
).fetchImpl,
|
||||
});
|
||||
await assert.rejects(
|
||||
() => rejectedSource.getTile('coverage', 14, 1, 1),
|
||||
(error) =>
|
||||
error.keyRejected === true &&
|
||||
error.keyRequired === false &&
|
||||
error.retryAfterSec === null,
|
||||
);
|
||||
const limitedSource = createMapillarySource({
|
||||
token: 'MLY|1|abc',
|
||||
fetchImpl: fakeFetch(
|
||||
() =>
|
||||
new Response(
|
||||
JSON.stringify({ error: 'rate limited', retryAfter: 45 }),
|
||||
{
|
||||
status: 429,
|
||||
headers: { 'retry-after': '30' },
|
||||
},
|
||||
),
|
||||
).fetchImpl,
|
||||
});
|
||||
await assert.rejects(
|
||||
() => limitedSource.getTile('coverage', 14, 1, 1),
|
||||
(error) => error.retryAfterSec === 30 && error.keyRejected === false,
|
||||
'the Retry-After header wins over the body',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user