mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The layer now owns its Mapillary parts directly: the provider registry,
pick router, multi-provider UI summary, params codec helper and provider
context are gone. createStreetLevelLayer({ source }) builds coverage,
sequences, the nearest-image lookup and the MapillaryJS viewer itself; a
single providerOn switch keeps the share link's `mapillary` option (off
draws nothing), and getParams/setParams keep { mapillary, pano, sinceDays }.
No z0-5 overview points: above 60 km nothing is drawn and the panel says to
zoom in; the proxy now serves only coverage z11-14. Tiles come from the
visible ground within range of the camera, ranked from the ground under it
(no line-of-sight ranking or near-range padding). MapillaryJS loads on the
first photo; viewer prewarm is removed. Key status, a rejected key and a
rate limit share one `blocked` state on coverage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
253 lines
7.6 KiB
JavaScript
253 lines
7.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
presentStreetLevelPanel,
|
|
SINCE_STOPS,
|
|
sinceStopIndex,
|
|
} from './streetLevelPresentation.js';
|
|
function snapshot(overrides = {}) {
|
|
const base = {
|
|
enabled: false,
|
|
providerOn: true,
|
|
keyRequired: false,
|
|
keyRejected: false,
|
|
filter: { pano: 'all', sinceDays: 0 },
|
|
coverage: { loading: false, count: 0, hint: '', error: null },
|
|
legend: [
|
|
{ key: 'mapillary', label: 'Mapillary', color: '#05cb63' },
|
|
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
|
|
],
|
|
sequence: { selectedId: null, images: 0, loading: false },
|
|
street: {
|
|
open: false,
|
|
loading: false,
|
|
error: null,
|
|
imageId: null,
|
|
position: null,
|
|
bearing: null,
|
|
isPano: false,
|
|
capturedAt: null,
|
|
sequenceId: null,
|
|
creator: null,
|
|
externalUrl: null,
|
|
renderMode: 'letterbox',
|
|
},
|
|
};
|
|
return deepMerge(base, overrides);
|
|
}
|
|
|
|
function deepMerge(target, source) {
|
|
const out = { ...target };
|
|
for (const [key, value] of Object.entries(source)) {
|
|
out[key] =
|
|
value &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
target[key] &&
|
|
typeof target[key] === 'object'
|
|
? deepMerge(target[key], value)
|
|
: value;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
test('a key-gated layer disables every control and flags KEY REQUIRED', () => {
|
|
const view = presentStreetLevelPanel(snapshot({ keyRequired: true }));
|
|
assert.equal(view.controlsDisabled, true);
|
|
assert.equal(view.status.text, 'KEY REQUIRED');
|
|
assert.equal(view.status.tone, 'warn');
|
|
});
|
|
|
|
test('KEY REQUIRED says how to add the key instead of a raw error code', () => {
|
|
const view = presentStreetLevelPanel(
|
|
snapshot({
|
|
enabled: true,
|
|
keyRequired: true,
|
|
coverage: { error: 'no_key' },
|
|
}),
|
|
);
|
|
assert.equal(view.status.text, 'KEY REQUIRED');
|
|
assert.match(view.error, /^Mapillary: Needs MAPILLARY_CLIENT_TOKEN/);
|
|
});
|
|
|
|
test('a key Mapillary rejected reads KEY REJECTED and names the fix', () => {
|
|
const error =
|
|
'Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings';
|
|
const view = presentStreetLevelPanel(
|
|
snapshot({
|
|
enabled: true,
|
|
keyRequired: true,
|
|
keyRejected: true,
|
|
coverage: { error },
|
|
}),
|
|
);
|
|
assert.equal(view.status.text, 'KEY REJECTED');
|
|
assert.equal(view.status.tone, 'warn');
|
|
assert.equal(view.controlsDisabled, true);
|
|
assert.equal(view.error, error);
|
|
});
|
|
|
|
test('a key problem outranks loading on the pill: KEY REJECTED, then KEY REQUIRED (M65)', () => {
|
|
// A keyless layer can still report loading (its status check, a retry):
|
|
// the pill must say what is wrong, not that something is on its way.
|
|
const loading = { enabled: true, coverage: { loading: true } };
|
|
assert.equal(
|
|
presentStreetLevelPanel(snapshot({ ...loading, keyRequired: true })).status
|
|
.text,
|
|
'KEY REQUIRED',
|
|
);
|
|
assert.equal(
|
|
presentStreetLevelPanel(
|
|
snapshot({ ...loading, keyRequired: true, keyRejected: true }),
|
|
).status.text,
|
|
'KEY REJECTED',
|
|
);
|
|
});
|
|
|
|
test('the header pill is the on/off switch and reads OFF, LOADING or ON', () => {
|
|
assert.deepEqual(presentStreetLevelPanel(snapshot()).status, {
|
|
text: 'OFF',
|
|
tone: '',
|
|
pressed: false,
|
|
title: 'Turn Street Level on',
|
|
});
|
|
assert.deepEqual(
|
|
presentStreetLevelPanel(
|
|
snapshot({ enabled: true, coverage: { loading: true } }),
|
|
).status,
|
|
{
|
|
text: 'LOADING',
|
|
tone: 'busy',
|
|
pressed: true,
|
|
title: 'Turn Street Level off',
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
presentStreetLevelPanel(snapshot({ enabled: true })).status,
|
|
{ text: 'ON', tone: 'on', pressed: true, title: 'Turn Street Level off' },
|
|
);
|
|
assert.equal(
|
|
presentStreetLevelPanel(snapshot({ enabled: true, keyRequired: true }))
|
|
.status.pressed,
|
|
true,
|
|
'a keyless layer that is on can still be switched off',
|
|
);
|
|
});
|
|
|
|
test('errors from the viewer or the coverage web surface in one alert', () => {
|
|
assert.equal(presentStreetLevelPanel(snapshot()).error, null);
|
|
assert.equal(
|
|
presentStreetLevelPanel(
|
|
snapshot({ street: { error: 'Image could not be opened' } }),
|
|
).error,
|
|
'Image could not be opened',
|
|
);
|
|
assert.equal(
|
|
presentStreetLevelPanel(snapshot({ coverage: { error: 'Tile HTTP 502' } }))
|
|
.error,
|
|
'Tile HTTP 502',
|
|
);
|
|
});
|
|
|
|
test('the SINCE slider runs from any date on the left to the last month on the right', () => {
|
|
assert.deepEqual(
|
|
SINCE_STOPS.map((stop) => stop.days),
|
|
[0, 3652, 1826, 1095, 730, 365, 182, 91, 30],
|
|
);
|
|
assert.equal(sinceStopIndex(0), 0);
|
|
assert.equal(sinceStopIndex(365), 5);
|
|
assert.equal(
|
|
sinceStopIndex(400),
|
|
5,
|
|
'an off-stop link lands on the nearest stop',
|
|
);
|
|
assert.equal(sinceStopIndex(-4), 0);
|
|
});
|
|
|
|
test('the SINCE readout names the window and the cut-off date it means today', () => {
|
|
const now = Date.UTC(2026, 8, 25);
|
|
const any = presentStreetLevelPanel(snapshot(), { now });
|
|
assert.deepEqual(any.since, { index: 0, days: 0, label: 'ANY DATE' });
|
|
const year = presentStreetLevelPanel(
|
|
snapshot({ filter: { pano: 'pano', sinceDays: 365 } }),
|
|
{ now },
|
|
);
|
|
assert.deepEqual(year.filter, { pano: 'pano', sinceDays: 365 });
|
|
assert.deepEqual(year.since, {
|
|
index: 5,
|
|
days: 365,
|
|
label: 'LAST YEAR · SINCE 2025-09-25',
|
|
});
|
|
const custom = presentStreetLevelPanel(
|
|
snapshot({ filter: { pano: 'all', sinceDays: 400 } }),
|
|
{ now },
|
|
);
|
|
assert.equal(custom.since.index, 5);
|
|
assert.equal(custom.since.label, 'LAST 400 DAYS · SINCE 2025-08-21');
|
|
});
|
|
|
|
test('the meta line never mixes the visible-sequence count with the selected sequence', () => {
|
|
assert.equal(
|
|
presentStreetLevelPanel(snapshot()).meta,
|
|
'Switch Street Level on to draw its coverage.',
|
|
);
|
|
const browsing = presentStreetLevelPanel(
|
|
snapshot({ enabled: true, coverage: { count: 812 } }),
|
|
);
|
|
assert.match(browsing.meta, /^812 sequences in view · click a line/);
|
|
const selected = presentStreetLevelPanel(
|
|
snapshot({
|
|
enabled: true,
|
|
coverage: { count: 812 },
|
|
sequence: { selectedId: 'abc', images: 33 },
|
|
}),
|
|
);
|
|
assert.equal(selected.meta, '33 images in this sequence · Esc clears');
|
|
assert.doesNotMatch(selected.meta, /sequences in view/);
|
|
const hinted = presentStreetLevelPanel(
|
|
snapshot({
|
|
enabled: true,
|
|
coverage: { hint: 'Point the camera at the globe' },
|
|
}),
|
|
);
|
|
assert.equal(hinted.meta, 'Point the camera at the globe');
|
|
});
|
|
|
|
test('viewer caption reads "Image by" left, date right, and links to Mapillary', () => {
|
|
const view = presentStreetLevelPanel(
|
|
snapshot({
|
|
enabled: true,
|
|
street: {
|
|
open: true,
|
|
imageId: '1814275685699406',
|
|
creator: 'mapfool',
|
|
capturedAt: Date.UTC(2023, 9, 8),
|
|
bearing: 93.4,
|
|
isPano: true,
|
|
externalUrl:
|
|
'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo',
|
|
},
|
|
}),
|
|
);
|
|
assert.equal(view.viewer.captionLeft, 'Image by mapfool');
|
|
assert.equal(view.viewer.captionRight, '360° · 93° · 2023-10-08');
|
|
assert.equal(
|
|
view.viewer.link,
|
|
'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo',
|
|
);
|
|
assert.equal(view.viewer.linkLabel, 'MAPILLARY ↗');
|
|
assert.equal(view.viewer.open, true);
|
|
});
|
|
|
|
test('an image without a creator name leaves the left caption empty', () => {
|
|
const view = presentStreetLevelPanel(
|
|
snapshot({
|
|
street: { open: true, imageId: '1', capturedAt: Date.UTC(2024, 0, 2) },
|
|
}),
|
|
);
|
|
assert.equal(view.viewer.captionLeft, '');
|
|
assert.equal(view.viewer.captionRight, '2024-01-02');
|
|
assert.equal(view.viewer.link, null);
|
|
assert.equal(view.viewer.linkLabel, '');
|
|
});
|