mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat(street-level): provider contract with Mapillary as the first provider
Street Level is now a provider-neutral layer modelled on the iD editor's photo overlay. The core owns what every imagery provider shares: the enable state and a switch per provider, the imagery filter (360°/flat and captured-since in relative days), one click handler routed by pick prefix, one viewer host that mounts whichever provider's viewer adapter owns the open image, the position marker, camera follow, one on-globe credit per active provider, and share-link parameters (one bit per provider plus the filter). Providers implement the contract in registry.js: coverage, sequences, nearest-image lookup and a viewer adapter emitting a common pose. Mapillary is the only registered provider; Google Street View, KartaView and Panoramax slot in beside it. Panel: a PROVIDERS chip row (one chip per registered provider, keyless chips read as errors), relative-day SINCE options, a provider-neutral caption whose link label follows the provider, and no vendor mark in the header. The panel inner is a rail scroller like CCTV's. Tests cover the registry, params codec, filter, pick router, UI-state composition, the provider definition and coverage-tile decoding; the QA harness switches the provider off and on and checks the filter. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
2dbc684fb1
commit
28e6a1faa7
41 files changed
+2460
-744
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { syncChipGroup } from './chipGroup.js';
|
||||
import { presentStreetLevelPanel } from './streetLevelPresentation.js';
|
||||
|
||||
const RENDER_MODE_KEY = 'gev:street-level:render-mode';
|
||||
@@ -7,8 +8,8 @@ const FOCUSABLE =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
/**
|
||||
* Own the Street Level panel: the imagery filters, the legend and the
|
||||
* embedded street-level viewer. The panel itself is
|
||||
* Own the Street Level panel: the provider chips, the imagery filters, the
|
||||
* legend and the embedded street-level viewer. The panel itself is
|
||||
* ordinary GEV chrome (collapse button, rail layout, persistence) driven by
|
||||
* the application shell; this class only fills the body and asks the shell
|
||||
* to open the panel when something worth seeing arrives.
|
||||
@@ -38,6 +39,7 @@ export class StreetLevelControls {
|
||||
controls: byId('sl-controls'),
|
||||
enableBtn: byId('sl-enable-btn'),
|
||||
lookBtn: byId('sl-look-btn'),
|
||||
providerChips: byId('sl-provider-chips'),
|
||||
error: byId('sl-error'),
|
||||
errorText: byId('sl-error-text'),
|
||||
sinceSelect: byId('sl-since'),
|
||||
@@ -72,19 +74,27 @@ export class StreetLevelControls {
|
||||
if (!(await this._ensureEnabled())) return;
|
||||
this.layer.openNearest?.();
|
||||
});
|
||||
// One delegated listener; chips are re-synced in place on every render.
|
||||
this.listen(el.providerChips, 'click', async (event) => {
|
||||
const button = event.target?.closest?.('.data-toggle-chip');
|
||||
if (!button || button.disabled) return;
|
||||
const chip = this._view?.providers.find(
|
||||
(entry) => entry.id === button.dataset.chipId,
|
||||
);
|
||||
if (!chip) return;
|
||||
const next = !chip.active;
|
||||
if (next && !(await this._ensureEnabled())) return;
|
||||
this.layer.setProviderEnabled?.(chip.id, next);
|
||||
});
|
||||
for (const button of this.root.querySelectorAll('[data-sl-pano]')) {
|
||||
this.listen(button, 'click', () =>
|
||||
this.layer.setCoverageFilter?.({ pano: button.dataset.slPano }),
|
||||
);
|
||||
}
|
||||
this.listen(el.sinceSelect, 'change', () => {
|
||||
const value = String(el.sinceSelect.value || '0');
|
||||
let sinceMs = null;
|
||||
if (value.startsWith('year:'))
|
||||
sinceMs = Date.UTC(Number(value.slice(5)), 0, 1);
|
||||
else if (Number(value) > 0)
|
||||
sinceMs = Date.now() - Number(value) * 86_400_000;
|
||||
this.layer.setCoverageFilter?.({ sinceMs });
|
||||
this.layer.setCoverageFilter?.({
|
||||
sinceDays: Number(el.sinceSelect.value) || 0,
|
||||
});
|
||||
});
|
||||
this.listen(el.followBtn, 'click', () => {
|
||||
this.layer.setFollow?.(!(this._state?.street?.follow === true));
|
||||
@@ -254,6 +264,7 @@ export class StreetLevelControls {
|
||||
this._view = view;
|
||||
this._renderHeader(view);
|
||||
this._renderGate(view);
|
||||
this._renderProviders(view);
|
||||
this._renderError(view);
|
||||
this._renderFilters(view);
|
||||
this._renderViewer(view, state);
|
||||
@@ -282,6 +293,10 @@ export class StreetLevelControls {
|
||||
if (el.controls) el.controls.disabled = view.controlsDisabled;
|
||||
}
|
||||
|
||||
_renderProviders(view) {
|
||||
syncChipGroup(this._elements.providerChips, view.providers);
|
||||
}
|
||||
|
||||
_renderError(view) {
|
||||
const el = this._elements;
|
||||
if (!el.error) return;
|
||||
@@ -296,6 +311,14 @@ export class StreetLevelControls {
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-checked', String(active));
|
||||
}
|
||||
if (el.sinceSelect) {
|
||||
const value = String(view.filter.sinceDays);
|
||||
if (
|
||||
el.sinceSelect.value !== value &&
|
||||
[...el.sinceSelect.options].some((option) => option.value === value)
|
||||
)
|
||||
el.sinceSelect.value = value;
|
||||
}
|
||||
if (el.legend && el.legend.childElementCount !== view.legend.length) {
|
||||
el.legend.replaceChildren(
|
||||
...view.legend.map((entry) => {
|
||||
@@ -333,6 +356,7 @@ export class StreetLevelControls {
|
||||
if (el.imageLink) {
|
||||
el.imageLink.hidden = !viewer.link;
|
||||
if (viewer.link) el.imageLink.href = viewer.link;
|
||||
if (viewer.linkLabel) el.imageLink.textContent = viewer.linkLabel;
|
||||
}
|
||||
if (state.street.open) this.layer.resizeViewer?.();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { keySetupRequirement } from '../keySetupCore.mjs';
|
||||
|
||||
/**
|
||||
* Turn the Street Level layer's UI state into the strings and flags the panel
|
||||
* renders. Pure: no DOM, no layer calls, so every wording decision is testable.
|
||||
*/
|
||||
|
||||
/** Relative "captured since" windows the panel offers, in days. */
|
||||
export const SINCE_OPTIONS = Object.freeze([
|
||||
Object.freeze({ days: 0, label: 'any date' }),
|
||||
Object.freeze({ days: 365, label: 'last year' }),
|
||||
Object.freeze({ days: 730, label: '2 years' }),
|
||||
Object.freeze({ days: 1826, label: '5 years' }),
|
||||
Object.freeze({ days: 3652, label: '10 years' }),
|
||||
]);
|
||||
|
||||
function formatDate(ms) {
|
||||
if (!Number.isFinite(ms)) return '';
|
||||
try {
|
||||
@@ -12,19 +23,38 @@ function formatDate(ms) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Deep link to an image on mapillary.com, as the web app shares them. */
|
||||
export function mapillaryImageUrl(imageId) {
|
||||
const id = String(imageId || '').trim();
|
||||
if (!id) return 'https://www.mapillary.com/app/';
|
||||
return `https://www.mapillary.com/app/?pKey=${encodeURIComponent(id)}&focus=photo`;
|
||||
}
|
||||
|
||||
function presentStatus(state) {
|
||||
if (state.keyRequired) return { text: 'KEY REQUIRED', tone: 'warn' };
|
||||
if (state.coverage.loading) return { text: 'LOADING', tone: 'busy' };
|
||||
return state.enabled ? { text: 'ON', tone: 'on' } : { text: 'OFF', tone: '' };
|
||||
}
|
||||
|
||||
/** One chip per registered provider; a keyless provider reads as an error chip. */
|
||||
function presentProviders(state) {
|
||||
return (state.providers || []).map((provider) => {
|
||||
const keyRequired = provider.keyRequired === true;
|
||||
let title = `${provider.name} imagery ${provider.on ? 'on' : 'off'}`;
|
||||
if (keyRequired && provider.requiresKeyId)
|
||||
title = `${provider.name}: ${keySetupRequirement(provider.requiresKeyId)}`;
|
||||
else if (provider.error) title = `${provider.name}: ${provider.error}`;
|
||||
return {
|
||||
id: provider.id,
|
||||
label: provider.label,
|
||||
title,
|
||||
active: provider.on === true,
|
||||
disabled: false,
|
||||
state: keyRequired
|
||||
? 'error'
|
||||
: provider.loading
|
||||
? 'loading'
|
||||
: provider.on
|
||||
? 'active'
|
||||
: 'idle',
|
||||
busy: provider.loading === true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function presentViewer(state) {
|
||||
const { street } = state;
|
||||
const right = [];
|
||||
@@ -38,7 +68,8 @@ function presentViewer(state) {
|
||||
renderMode: street.renderMode === 'fill' ? 'fill' : 'letterbox',
|
||||
captionLeft: street.creator ? `Image by ${street.creator}` : '',
|
||||
captionRight: right.join(' · '),
|
||||
link: street.imageId ? mapillaryImageUrl(street.imageId) : null,
|
||||
link: street.externalUrl || null,
|
||||
linkLabel: street.providerLabel ? `${street.providerLabel} ↗` : '',
|
||||
follow: {
|
||||
pressed: street.follow === true,
|
||||
disabled: street.open !== true,
|
||||
@@ -52,8 +83,8 @@ function presentMeta(state) {
|
||||
return state.sequence.loading
|
||||
? 'Loading this sequence…'
|
||||
: `${state.sequence.images.toLocaleString()} images in this sequence · Esc clears`;
|
||||
if (state.coverage.zoom && state.coverage.sequences > 0)
|
||||
return `${state.coverage.sequences.toLocaleString()} sequences in view · click a line for its photos`;
|
||||
if (state.coverage.count > 0)
|
||||
return `${state.coverage.count.toLocaleString()} sequences in view · click a line for its photos`;
|
||||
return state.coverage.hint || '';
|
||||
}
|
||||
|
||||
@@ -64,6 +95,7 @@ function presentMeta(state) {
|
||||
export function presentStreetLevelPanel(state) {
|
||||
const enabled = state.enabled === true;
|
||||
const keyRequired = state.keyRequired === true;
|
||||
const filter = state.filter || { pano: 'all', sinceDays: 0 };
|
||||
return {
|
||||
enabled,
|
||||
keyRequired,
|
||||
@@ -73,9 +105,10 @@ export function presentStreetLevelPanel(state) {
|
||||
text: enabled ? 'STREET LEVEL ON' : 'STREET LEVEL OFF',
|
||||
pressed: enabled,
|
||||
},
|
||||
providers: presentProviders(state),
|
||||
error: state.street.error || state.coverage.error || null,
|
||||
filter: state.coverage.filter || { pano: 'all', sinceMs: null },
|
||||
legend: state.coverage.legend || [],
|
||||
filter: { pano: filter.pano, sinceDays: Number(filter.sinceDays) || 0 },
|
||||
legend: state.legend || [],
|
||||
viewer: presentViewer(state),
|
||||
meta: presentMeta(state),
|
||||
/** The panel opens itself when an image opens (a native panel stays put otherwise). */
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
mapillaryImageUrl,
|
||||
presentStreetLevelPanel,
|
||||
SINCE_OPTIONS,
|
||||
} from './streetLevelPresentation.js';
|
||||
|
||||
const provider = (overrides = {}) => ({
|
||||
id: 'mapillary',
|
||||
name: 'Mapillary',
|
||||
label: 'MAPILLARY',
|
||||
on: true,
|
||||
configured: true,
|
||||
keyRequired: false,
|
||||
requiresKeyId: 'mapillary',
|
||||
loading: false,
|
||||
count: 0,
|
||||
hint: '',
|
||||
error: null,
|
||||
legend: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
function snapshot(overrides = {}) {
|
||||
const base = {
|
||||
enabled: false,
|
||||
keyRequired: false,
|
||||
coverage: {
|
||||
zoom: null,
|
||||
kind: null,
|
||||
filter: { pano: 'all', sinceMs: null },
|
||||
legend: [
|
||||
{ key: 'recent', label: 'Recent (≤2 yr)', color: '#05cb63' },
|
||||
{ key: 'older', label: 'Older', color: '#2e7d5b' },
|
||||
{ key: 'pano', label: '360°', color: '#ff4fd8' },
|
||||
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
|
||||
],
|
||||
loading: false,
|
||||
sequences: 0,
|
||||
hint: '',
|
||||
error: null,
|
||||
},
|
||||
sequence: { selectedId: null, images: 0, loading: false },
|
||||
filter: { pano: 'all', sinceDays: 0 },
|
||||
providers: [provider()],
|
||||
coverage: { loading: false, count: 0, hint: '', error: null },
|
||||
legend: [
|
||||
{ key: 'mapillary:recent', label: 'Recent (≤2 yr)', color: '#05cb63' },
|
||||
{ key: 'mapillary:older', label: 'Older', color: '#2e7d5b' },
|
||||
{ key: 'mapillary:pano', label: '360°', color: '#ff4fd8' },
|
||||
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
|
||||
],
|
||||
sequence: { providerId: null, selectedId: null, images: 0, loading: false },
|
||||
street: {
|
||||
open: false,
|
||||
follow: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
providerId: null,
|
||||
providerName: null,
|
||||
providerLabel: null,
|
||||
imageId: null,
|
||||
position: null,
|
||||
bearing: null,
|
||||
@@ -37,6 +50,7 @@ function snapshot(overrides = {}) {
|
||||
capturedAt: null,
|
||||
sequenceId: null,
|
||||
creator: null,
|
||||
externalUrl: null,
|
||||
renderMode: 'letterbox',
|
||||
},
|
||||
};
|
||||
@@ -58,7 +72,7 @@ function deepMerge(target, source) {
|
||||
return out;
|
||||
}
|
||||
|
||||
test('a missing Mapillary token gates every control and flags KEY REQUIRED', () => {
|
||||
test('a key-gated layer disables every control and flags KEY REQUIRED', () => {
|
||||
const view = presentStreetLevelPanel(snapshot({ keyRequired: true }));
|
||||
assert.equal(view.controlsDisabled, true);
|
||||
assert.deepEqual(view.status, { text: 'KEY REQUIRED', tone: 'warn' });
|
||||
@@ -77,13 +91,64 @@ test('status reads LOADING while coverage streams, then ON or OFF', () => {
|
||||
);
|
||||
assert.deepEqual(
|
||||
presentStreetLevelPanel(snapshot({ enabled: true })).status,
|
||||
{
|
||||
text: 'ON',
|
||||
tone: 'on',
|
||||
},
|
||||
{ text: 'ON', tone: 'on' },
|
||||
);
|
||||
});
|
||||
|
||||
test('one chip per provider: on, off, loading, and keyless as an error chip', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({
|
||||
providers: [
|
||||
provider(),
|
||||
provider({
|
||||
id: 'panoramax',
|
||||
name: 'Panoramax',
|
||||
label: 'PANORAMAX',
|
||||
on: false,
|
||||
requiresKeyId: null,
|
||||
}),
|
||||
provider({
|
||||
id: 'kartaview',
|
||||
name: 'KartaView',
|
||||
label: 'KARTAVIEW',
|
||||
loading: true,
|
||||
requiresKeyId: null,
|
||||
}),
|
||||
provider({
|
||||
id: 'google-street-view',
|
||||
name: 'Google Street View',
|
||||
label: 'STREET VIEW',
|
||||
keyRequired: true,
|
||||
requiresKeyId: 'google-maps',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(
|
||||
view.providers.map((chip) => [chip.id, chip.active, chip.state, chip.busy]),
|
||||
[
|
||||
['mapillary', true, 'active', false],
|
||||
['panoramax', false, 'idle', false],
|
||||
['kartaview', true, 'loading', true],
|
||||
['google-street-view', true, 'error', false],
|
||||
],
|
||||
);
|
||||
assert.equal(view.providers[0].label, 'MAPILLARY');
|
||||
assert.equal(view.providers[1].title, 'Panoramax imagery off');
|
||||
assert.match(
|
||||
view.providers[3].title,
|
||||
/^Google Street View: Needs GOOGLE_MAPS_API_KEY/,
|
||||
);
|
||||
assert.ok(view.providers.every((chip) => chip.disabled === false));
|
||||
});
|
||||
|
||||
test('a provider error is explained on its chip', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({ providers: [provider({ error: 'Tile HTTP 502' })] }),
|
||||
);
|
||||
assert.equal(view.providers[0].title, 'Mapillary: Tile HTTP 502');
|
||||
});
|
||||
|
||||
test('errors from the viewer or the coverage web surface in one alert', () => {
|
||||
assert.equal(presentStreetLevelPanel(snapshot()).error, null);
|
||||
assert.equal(
|
||||
@@ -99,24 +164,35 @@ test('errors from the viewer or the coverage web surface in one alert', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('the filter passes through as the select and segment values', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({ filter: { pano: 'pano', sinceDays: 730 } }),
|
||||
);
|
||||
assert.deepEqual(view.filter, { pano: 'pano', sinceDays: 730 });
|
||||
assert.deepEqual(
|
||||
SINCE_OPTIONS.map((option) => option.days),
|
||||
[0, 365, 730, 1826, 3652],
|
||||
);
|
||||
});
|
||||
|
||||
test('legend passes through in the layer’s order', () => {
|
||||
const view = presentStreetLevelPanel(snapshot());
|
||||
assert.deepEqual(
|
||||
view.legend.map((entry) => entry.key),
|
||||
['recent', 'older', 'pano', 'selected'],
|
||||
['mapillary:recent', 'mapillary:older', 'mapillary:pano', 'selected'],
|
||||
);
|
||||
});
|
||||
|
||||
test('the meta line never mixes the visible-sequence count with the selected sequence', () => {
|
||||
assert.equal(presentStreetLevelPanel(snapshot()).meta, '');
|
||||
const browsing = presentStreetLevelPanel(
|
||||
snapshot({ enabled: true, coverage: { zoom: 14, sequences: 812 } }),
|
||||
snapshot({ enabled: true, coverage: { count: 812 } }),
|
||||
);
|
||||
assert.match(browsing.meta, /^812 sequences in view · click a line/);
|
||||
const selected = presentStreetLevelPanel(
|
||||
snapshot({
|
||||
enabled: true,
|
||||
coverage: { zoom: 14, sequences: 812 },
|
||||
coverage: { count: 812 },
|
||||
sequence: { selectedId: 'abc', images: 33 },
|
||||
}),
|
||||
);
|
||||
@@ -131,17 +207,22 @@ test('the meta line never mixes the visible-sequence count with the selected seq
|
||||
assert.equal(hinted.meta, 'Point the camera at the globe');
|
||||
});
|
||||
|
||||
test('viewer caption reads "Image by" left and date right, with a deep link', () => {
|
||||
test('viewer caption reads "Image by" left, date right, and links to the provider', () => {
|
||||
const view = presentStreetLevelPanel(
|
||||
snapshot({
|
||||
enabled: true,
|
||||
street: {
|
||||
open: true,
|
||||
providerId: 'mapillary',
|
||||
providerName: 'Mapillary',
|
||||
providerLabel: 'MAPILLARY',
|
||||
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',
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -151,6 +232,7 @@ test('viewer caption reads "Image by" left and date right, with a deep link', ()
|
||||
view.viewer.link,
|
||||
'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo',
|
||||
);
|
||||
assert.equal(view.viewer.linkLabel, 'MAPILLARY ↗');
|
||||
assert.equal(view.viewer.follow.disabled, false);
|
||||
assert.equal(view.wantsOpen, true);
|
||||
});
|
||||
@@ -163,12 +245,6 @@ test('an image without a creator name leaves the left caption empty', () => {
|
||||
);
|
||||
assert.equal(view.viewer.captionLeft, '');
|
||||
assert.equal(view.viewer.captionRight, '2024-01-02');
|
||||
});
|
||||
|
||||
test('image deep links match the mapillary.com share format', () => {
|
||||
assert.equal(
|
||||
mapillaryImageUrl(1814275685699406),
|
||||
'https://www.mapillary.com/app/?pKey=1814275685699406&focus=photo',
|
||||
);
|
||||
assert.equal(mapillaryImageUrl(' '), 'https://www.mapillary.com/app/');
|
||||
assert.equal(view.viewer.link, null);
|
||||
assert.equal(view.viewer.linkLabel, '');
|
||||
});
|
||||
@@ -339,6 +339,9 @@
|
||||
#right-context-rail[data-rail-measuring]
|
||||
> [data-panel-id]:not(.collapsed)
|
||||
> .cctv-panel-inner,
|
||||
#right-context-rail[data-rail-measuring]
|
||||
> [data-panel-id]:not(.collapsed)
|
||||
> .street-level-panel-inner,
|
||||
#right-context-rail[data-rail-measuring]
|
||||
> [data-panel-id]:not(.collapsed)
|
||||
> .weather-panel-inner,
|
||||
|
||||
@@ -43,19 +43,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sl-mark {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-dim);
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
#street-level-panel[data-sl-enabled='true'] .sl-mark {
|
||||
color: var(--sl-green);
|
||||
filter: drop-shadow(0 0 6px rgba(5, 203, 99, 0.7));
|
||||
}
|
||||
|
||||
.sl-status {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--sl-text-small);
|
||||
@@ -110,6 +97,25 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Providers ───────────────────────────────────────────────────────────── */
|
||||
.sl-providers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sl-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sl-chips .data-toggle-chip.chip-error {
|
||||
color: #ffb300;
|
||||
border-color: rgba(255, 179, 0, 0.55);
|
||||
}
|
||||
|
||||
/* ── Errors ──────────────────────────────────────────────────────────────── */
|
||||
.sl-error {
|
||||
font-family: var(--font-sans);
|
||||
|
||||
@@ -75,12 +75,8 @@
|
||||
runtime next to CCTV (see PanelLayoutController). -->
|
||||
<div id="street-level-panel" class="panel-collapsible collapsed" data-panel-id="street-level-panel">
|
||||
<div class="panel-glow"></div>
|
||||
<div class="street-level-panel-inner">
|
||||
<div class="street-level-panel-inner" data-rail-scroller>
|
||||
<div class="panel-header">
|
||||
<!-- Mapillary mark: path from the MapillaryJS attribution component
|
||||
(mapillary-js, MIT, © Mapillary). Mapillary is a trademark of
|
||||
Meta Platforms, Inc.; it is used here to identify the data source. -->
|
||||
<svg class="sl-mark" viewBox="0 0 13.5 13.8" aria-hidden="true" focusable="false"><path fill="currentColor" d="M6.38154274 9.6505067C6.14007412 9.52786404 6.08124006 9.31709453 6.21044037 9.08865565 6.27614941 8.97247561 6.41859234 8.74074422 6.53468168 8.5539822 6.69432724 8.29714786 6.92701434 8.29637588 7.05835866 8.37253362 7.18970298 8.44869135 9.04853604 9.42087063 9.16874871 9.48670656 9.4525926 9.64215711 9.87118629 9.28623741 9.68342251 8.97441326 9.59375332 8.82549731 8.8103938 7.4044783 8.5614978 6.93610438 8.44825156 6.72299696 8.47921005 6.53132251 8.731647 6.40377463 8.85656436 6.3406581 9.08004239 6.20890798 9.27394762 6.09190586 9.47189219 5.9724664 9.7353488 6.06223885 9.84245555 6.24022621 9.94956229 6.41821357 13.1209182 12.2294019 13.2221517 12.4098169 13.4231294 12.7679928 13.0322853 13.1511137 12.662949 12.953094 12.4781436 12.8540105 6.62301136 9.77314936 6.38154274 9.6505067zM.200146062 6.40081205C-.111554282 6.23274106-.0545442328 5.79604052.338782648 5.68466036.672030083 5.59029317 3.37820647 4.6882634 4.19715115 4.4149007 4.34479443 4.36561757 4.46005809 4.25228232 4.50965757 4.1075867 4.78858853 3.29386671 5.72024556.575961334 5.82468911.27126979 5.9477048-.087601979 6.39304077-.0889184867 6.57454671.258502251 6.63901566.381902346 9.08940484 4.79813543 9.17465946 4.98547051 9.25991408 5.17280559 9.20498731 5.38031434 9.01176406 5.50000515 8.8185408 5.61969596 8.50343522 5.79872065 8.3936507 5.86654323 8.19489463 5.98933057 8.00439135 5.90384324 7.91365501 5.71364816 7.82291866 5.52345307 7.12387828 4.29626886 6.83882952 3.77527795 6.69634941 3.51486341 6.2671132 3.41492796 6.12283721 3.83582159 6.01848488 4.14024669 5.77526264 4.84979439 5.64864557 5.219172 5.60020375 5.3604904 5.47741944 5.48212554 5.33398124 5.52937001 4.9780163 5.64661478 4.30162944 5.86939744 3.90672375 5.99946818 3.62491469 6.0922881 3.48793523 6.52150257 3.86841776 6.70224674 3.94789525 6.74000167 5.66097153 7.65404391 5.83772054 7.73764558 6.01446954 7.82124724 6.11275281 8.06257945 6.00925316 8.23355004 5.8680804 8.46675268 5.68734796 8.76272052 5.6217862 8.85880815 5.50815838 9.02534163 5.28495077 9.08080286 5.09748996 8.98251076 4.91002915 8.88421866.311522289 6.46086689.200146062 6.40081205z"/></svg>
|
||||
<span class="panel-title">STREET LEVEL</span>
|
||||
<span class="panel-divider"></span>
|
||||
<span id="sl-status" class="sl-status" role="status" aria-live="polite">OFF</span>
|
||||
@@ -90,7 +86,11 @@
|
||||
<fieldset id="sl-controls" class="sl-controls">
|
||||
<div class="cctv-controls">
|
||||
<button id="sl-enable-btn" class="scene-btn" type="button" aria-pressed="false" title="Turn the Street Level layer on or off">STREET LEVEL OFF</button>
|
||||
<button id="sl-look-btn" class="scene-btn" type="button" title="Open the nearest Mapillary photo to the view centre">OPEN NEAREST PHOTO</button>
|
||||
<button id="sl-look-btn" class="scene-btn" type="button" title="Open the nearest street-level photo to the view centre">OPEN NEAREST PHOTO</button>
|
||||
</div>
|
||||
<div class="sl-providers" role="group" aria-label="Imagery providers">
|
||||
<span class="cctv-summary-label">PROVIDERS</span>
|
||||
<div id="sl-provider-chips" class="sl-chips"></div>
|
||||
</div>
|
||||
<div id="sl-error" class="sl-error" role="alert" hidden><span aria-hidden="true">⚠</span> <span id="sl-error-text"></span></div>
|
||||
<div class="sl-filters" role="group" aria-label="Imagery filters">
|
||||
@@ -106,9 +106,7 @@
|
||||
<option value="365">last year</option>
|
||||
<option value="730">2 years</option>
|
||||
<option value="1826">5 years</option>
|
||||
<option value="year:2024">2024</option>
|
||||
<option value="year:2022">2022</option>
|
||||
<option value="year:2020">2020</option>
|
||||
<option value="3652">10 years</option>
|
||||
</select>
|
||||
</div>
|
||||
<ul id="sl-legend" class="sl-legend" aria-label="Coverage colours"></ul>
|
||||
@@ -127,12 +125,12 @@
|
||||
<button id="sl-viewer-close" class="sl-viewer-x" type="button" title="Close the image and deselect it on the map" aria-label="Close the street-level image">×</button>
|
||||
</div>
|
||||
<div id="sl-viewer-placeholder" class="sl-viewer-placeholder" hidden></div>
|
||||
<div id="sl-viewer" class="sl-viewer" aria-label="Mapillary street-level viewer"></div>
|
||||
<div id="sl-viewer" class="sl-viewer" aria-label="Street-level viewer"></div>
|
||||
<div id="sl-image-meta" class="sl-image-meta">
|
||||
<span id="sl-image-by" class="sl-image-by"></span>
|
||||
<span class="sl-image-right">
|
||||
<span id="sl-image-when" class="sl-image-when"></span>
|
||||
<a id="sl-image-link" class="sl-image-link" href="https://www.mapillary.com/app/" target="_blank" rel="noopener noreferrer" title="Open this image on mapillary.com">MAPILLARY ↗</a>
|
||||
<a id="sl-image-link" class="sl-image-link" href="#" target="_blank" rel="noopener noreferrer" title="Open this image on the provider's site" hidden></a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user