Files
gods-eye-view/src/ui/streetLevelPresentation.js
T
German KhokhlovandClaude Opus 5.5 37a96072fa refactor(street-level): one provider, no orbit overview, no prewarm, simpler tile choice
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]>
2026-10-07 22:19:24 +04:00

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),
};
}