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]>
124 lines
4.3 KiB
JavaScript
124 lines
4.3 KiB
JavaScript
import { keySetupRequirement } from '../keySetupCore.mjs';
|
|
|
|
// Street Level UI state to panel strings and flags. Pure, so it is testable.
|
|
|
|
const DAY_MS = 86_400_000;
|
|
|
|
/** SINCE slider stops, in relative days so a share link keeps its meaning. */
|
|
export const SINCE_STOPS = Object.freeze([
|
|
Object.freeze({ days: 0, label: 'ANY DATE' }),
|
|
Object.freeze({ days: 3652, label: 'LAST 10 YEARS' }),
|
|
Object.freeze({ days: 1826, label: 'LAST 5 YEARS' }),
|
|
Object.freeze({ days: 1095, label: 'LAST 3 YEARS' }),
|
|
Object.freeze({ days: 730, label: 'LAST 2 YEARS' }),
|
|
Object.freeze({ days: 365, label: 'LAST YEAR' }),
|
|
Object.freeze({ days: 182, label: 'LAST 6 MONTHS' }),
|
|
Object.freeze({ days: 91, label: 'LAST 3 MONTHS' }),
|
|
Object.freeze({ days: 30, label: 'LAST MONTH' }),
|
|
]);
|
|
|
|
/** The exact stop for a day count, else the nearest one. */
|
|
export function sinceStopIndex(days) {
|
|
const value = Number(days) || 0;
|
|
if (value <= 0) return 0;
|
|
let best = 1;
|
|
for (let i = 1; i < SINCE_STOPS.length; i++)
|
|
if (
|
|
Math.abs(SINCE_STOPS[i].days - value) <
|
|
Math.abs(SINCE_STOPS[best].days - value)
|
|
)
|
|
best = i;
|
|
return best;
|
|
}
|
|
|
|
function presentSince(days, now) {
|
|
const value = Number(days) || 0;
|
|
const index = sinceStopIndex(value);
|
|
if (value <= 0) return { index, days: 0, label: 'ANY DATE' };
|
|
const stop = SINCE_STOPS[index];
|
|
const window = stop.days === value ? stop.label : `LAST ${value} DAYS`;
|
|
return {
|
|
index,
|
|
days: value,
|
|
label: `${window} · SINCE ${formatDate(now - value * DAY_MS)}`,
|
|
};
|
|
}
|
|
|
|
function formatDate(ms) {
|
|
if (!Number.isFinite(ms)) return '';
|
|
try {
|
|
return new Date(ms).toISOString().slice(0, 10);
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function presentStatus(state) {
|
|
const pressed = state.enabled === true;
|
|
const title = pressed ? 'Turn Street Level off' : 'Turn Street Level on';
|
|
if (state.keyRejected)
|
|
return { text: 'KEY REJECTED', tone: 'warn', pressed, title };
|
|
if (state.keyRequired)
|
|
return { text: 'KEY REQUIRED', tone: 'warn', pressed, title };
|
|
if (state.coverage.loading)
|
|
return { text: 'LOADING', tone: 'busy', pressed, title };
|
|
return pressed
|
|
? { text: 'ON', tone: 'on', pressed, title }
|
|
: { text: 'OFF', tone: '', pressed, title };
|
|
}
|
|
|
|
/** How to add the missing Mapillary key. */
|
|
function keyHint() {
|
|
const requirement = keySetupRequirement('mapillary');
|
|
return requirement ? `Mapillary: ${requirement}` : null;
|
|
}
|
|
|
|
function presentViewer(state) {
|
|
const { street } = state;
|
|
const right = [];
|
|
if (street.isPano) right.push('360°');
|
|
if (Number.isFinite(street.bearing))
|
|
right.push(`${Math.round(street.bearing)}°`);
|
|
if (street.capturedAt) right.push(formatDate(street.capturedAt));
|
|
return {
|
|
open: street.open === true,
|
|
loading: street.loading === true && !street.imageId,
|
|
renderMode: street.renderMode === 'fill' ? 'fill' : 'letterbox',
|
|
captionLeft: street.creator ? `Image by ${street.creator}` : '',
|
|
captionRight: right.join(' · '),
|
|
link: street.externalUrl || null,
|
|
linkLabel: street.externalUrl ? 'MAPILLARY ↗' : '',
|
|
};
|
|
}
|
|
|
|
function presentMeta(state) {
|
|
if (!state.enabled) return 'Switch Street Level on to draw its coverage.';
|
|
if (state.sequence.selectedId)
|
|
return state.sequence.loading
|
|
? 'Loading this sequence…'
|
|
: `${state.sequence.images.toLocaleString()} images in this sequence · Esc clears`;
|
|
if (state.coverage.count > 0)
|
|
return `${state.coverage.count.toLocaleString()} sequences in view · click a line for its photos`;
|
|
return state.coverage.hint || '';
|
|
}
|
|
|
|
/** @param {{now?: number}} [options] Clock for the SINCE readout. */
|
|
export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
|
|
const filter = state.filter || { pano: 'all', sinceDays: 0 };
|
|
// A missing key says how to add it; a rejected key's own error names the fix.
|
|
const keyMissing = state.keyRequired === true && state.keyRejected !== true;
|
|
return {
|
|
enabled: state.enabled === true,
|
|
status: presentStatus(state),
|
|
controlsDisabled: state.keyRequired === true,
|
|
error: keyMissing
|
|
? keyHint()
|
|
: state.street.error || state.coverage.error || null,
|
|
filter: { pano: filter.pano, sinceDays: Number(filter.sinceDays) || 0 },
|
|
since: presentSince(filter.sinceDays, now),
|
|
legend: state.legend || [],
|
|
viewer: presentViewer(state),
|
|
meta: presentMeta(state),
|
|
};
|
|
}
|