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

+60
View File
@@ -0,0 +1,60 @@
// DOM stand-ins for the KartaView viewer's tests (no browser in Node).
/** A DOM element with what the viewer touches: children, events, attributes. */
export function fakeElement(tag) {
const listeners = new Map();
const element = {
tagName: tag.toUpperCase(),
children: [],
parentNode: null,
style: {},
dataset: {},
attributes: {},
hidden: false,
disabled: false,
clientWidth: 400,
clientHeight: 250,
classList: {
values: new Set(),
add: (name) => element.classList.values.add(name),
remove: (name) => element.classList.values.delete(name),
},
append(...nodes) {
for (const node of nodes) {
node.parentNode = element;
element.children.push(node);
}
},
remove() {
const list = element.parentNode?.children;
if (list) list.splice(list.indexOf(element), 1);
element.parentNode = null;
},
setAttribute: (name, value) => (element.attributes[name] = value),
removeAttribute: (name) => {
delete element.attributes[name];
if (name === 'src') element.src = '';
},
addEventListener: (type, handler) => listeners.set(type, handler),
removeEventListener: (type) => listeners.delete(type),
setPointerCapture() {},
/** Fire an event at the element's listener. */
fire(type, event = {}) {
return listeners.get(type)?.({
preventDefault() {
this.defaultPrevented = true;
},
stopPropagation() {},
...event,
});
},
};
return element;
}
export function fakeHost() {
const doc = { createElement: (tag) => fakeElement(tag) };
const host = fakeElement('div');
host.ownerDocument = doc;
return host;
}