feat(street-level): KartaView as a third, keyless imagery provider

Coverage lines from KartaView's z13-14 JSON tiles (OSM ways with photos,
chained end to start), the photo nearest a clicked line, cones along its
sequence, and a viewer for flat photos and 360° panoramas (a small WebGL
equirectangular renderer, no new dependency) with ‹ › along the sequence.

Mapillary's camera-driven line coverage and sequence cones move to the
provider-neutral core (lineCoverage.js, sequenceCones.js) with the tile
fetch, zoom ladder, colours and pick prefixes injected; Mapillary keeps
thin wrappers and its tests pass unchanged.

KartaView quirks handled: api.kartaview.org (the documented
api.openstreetcam.org serves empty tiles), photo URLs that name storage1
for photos stored elsewhere, and a photo API that answers 429 to more than
about three requests at once (two in flight, retried with backoff).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-13 18:33:14 +01:00
1 parent 9beadbda63
commit cb01014e36
33 files changed
+4046 -1196

No files matched your search

+1 -1
View File
@@ -193,7 +193,7 @@ function presentMeta(state) {
const hint = state.coverage.hint || '';
if (state.coverage.count > 0)
return [
`${state.coverage.count.toLocaleString()} sequences in view · click a line for its photos`,
`${state.coverage.count.toLocaleString()} ${state.coverage.label?.noun || 'sequences'} in view · ${state.coverage.label?.action || 'click a line for its photos'}`,
hint,
]
.filter(Boolean)
+80
View File
@@ -613,6 +613,86 @@ button.sl-status:focus-visible {
font-size: 1.3rem;
}
/* ── KartaView photo viewer ──────────────────────────────────────────────── */
.sl-kv-viewer {
position: absolute;
inset: 0;
background: #000;
outline: none;
overflow: hidden;
}
.sl-kv-viewer:focus-visible {
box-shadow: inset 0 0 0 2px var(--sl-kartaview, #635bff);
}
.sl-kv-photo,
.sl-kv-pano {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
user-select: none;
}
.sl-kv-photo {
object-fit: contain;
}
.sl-kv-pano {
cursor: grab;
touch-action: none;
}
.sl-kv-pano.sl-kv-dragging {
cursor: grabbing;
}
.sl-kv-photo[hidden],
.sl-kv-pano[hidden] {
display: none;
}
.sl-kv-step {
position: absolute;
top: 50%;
z-index: 2;
width: 34px;
height: 44px;
margin-top: -22px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
background: rgba(10, 10, 15, 0.62);
color: #fff;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
}
.sl-kv-step-prev {
left: 6px;
}
.sl-kv-step-next {
right: 6px;
}
.sl-kv-step:hover:not(:disabled),
.sl-kv-step:focus-visible {
background: rgba(99, 91, 255, 0.85);
outline: none;
}
.sl-kv-step:disabled {
opacity: 0.3;
cursor: default;
}
/* Nothing to step through until a photo shows. */
.sl-kv-viewer:not([data-kind='flat']):not([data-kind='pano']) .sl-kv-step {
display: none;
}
/* ── Short viewports ─────────────────────────────────────────────────────── */
@media (max-height: 640px) {
.street-level-panel-inner {