Merge origin/main: Google Street View, KartaView and Panoramax on the slim Street Level

Main gained three imagery providers (Google Street View, KartaView,
Panoramax), shared line-coverage, cone and photo-viewer modules, one source
lit at a time and the translated interface. Their shared modules still
carried what this branch removes, so the merge ports them onto it:

- lineCoverage.js is this branch's coverage.js made provider-neutral
  (draped lines near the ground only, the selection drawn over them, key
  and rate-limit blocking); sequenceCones.js is sequences.js the same way.
  Mapillary, KartaView and Panoramax all build on them; Mapillary moves
  back to providers/mapillary/ beside the others.
- No ground casting, mesh sampling, surface modes, FOLLOW, legend, prewarm,
  orbit overview points (Panoramax's density dots included), stale-tile
  swaps or water-filled point budget come back.
- Kept from main because the new providers need them: requiresMapStack and
  attachMapStackController (Street View on Google 3D only), ground clicks,
  openNearest with an optional provider nearestImage, openImage {frame},
  reportError, the view-cone marker (now an SVG glyph), filters /
  coverageLabel / sinceBasis, OPEN STREET VIEW, keyAlternatives and i18n.
- photoViewer.js treats the viewer as expanded when its wrap is full screen
  or the iPhone popover, as EXPAND now is.
- The fixture browser gate keeps its 8 steps and adds one each for
  KartaView and Panoramax; catalogs, docs, boundaries and format scope
  follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-19 13:51:45 +04:00
commit 6390a3346f
809 files changed
+81825 -6035

No files matched your search

@@ -0,0 +1,155 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { PbfWriter } from 'pbf';
import { decodeCoverageTile } from './decode.js';
const zigzag = (value) => (value << 1) ^ (value >> 31);
/** One line part: MoveTo the first point, LineTo the rest (relative). */
function linePart(points) {
const out = [9, zigzag(points[0][0]), zigzag(points[0][1])];
out.push(((points.length - 1) << 3) | 2);
for (let i = 1; i < points.length; i++)
out.push(
zigzag(points[i][0] - points[i - 1][0]),
zigzag(points[i][1] - points[i - 1][1]),
);
return out;
}
/**
* A minimal mly1_public-shaped tile: one sequence line, plus one point in the
* z14 `image` layer the decoder skips.
*/
function sampleTile({ geometry = null } = {}) {
const writer = new PbfWriter();
writer.writeMessage(
3,
(_, layer) => {
layer.writeVarintField(15, 2);
layer.writeStringField(1, 'sequence');
layer.writeVarintField(5, 4096);
for (const key of ['id', 'captured_at', 'is_pano'])
layer.writeStringField(3, key);
layer.writeMessage(
4,
(__, value) => value.writeStringField(1, 'seq-42'),
null,
);
layer.writeMessage(
4,
(__, value) => value.writeVarintField(5, 1_700_000_000),
null,
);
layer.writeMessage(
4,
(__, value) => value.writeBooleanField(7, true),
null,
);
layer.writeMessage(
2,
(__, feature) => {
feature.writeVarintField(1, 7);
feature.writePackedVarint(2, [0, 0, 1, 1, 2, 2]);
feature.writeVarintField(3, 2); // LineString
feature.writePackedVarint(
4,
geometry || [
9,
zigzag(100),
zigzag(100), // MoveTo
10,
zigzag(200),
zigzag(200), // LineTo ×1
],
);
},
null,
);
},
null,
);
writer.writeMessage(
3,
(_, layer) => {
layer.writeVarintField(15, 2);
layer.writeStringField(1, 'image');
layer.writeVarintField(5, 4096);
layer.writeStringField(3, 'is_pano');
layer.writeMessage(
4,
(__, value) => value.writeBooleanField(7, false),
null,
);
layer.writeMessage(
2,
(__, feature) => {
feature.writeVarintField(1, 3);
feature.writePackedVarint(2, [0, 0]);
feature.writeVarintField(3, 1); // Point
feature.writePackedVarint(4, [9, zigzag(2048), zigzag(2048)]);
},
null,
);
},
null,
);
return new Uint8Array(writer.finish());
}
test('sequences decode into lon/lat records; image points are skipped', () => {
const decoded = decodeCoverageTile(sampleTile(), { x: 0, y: 0, z: 1 });
assert.equal(decoded.sequences.length, 1);
const [sequence] = decoded.sequences;
assert.equal(sequence.id, 'seq-42');
assert.equal(sequence.capturedAt, 1_700_000_000);
assert.equal(sequence.isPano, true);
assert.equal(sequence.parts.length, 1);
assert.equal(sequence.parts[0].length, 2);
const [[lon0, lat0], [lon1, lat1]] = sequence.parts[0];
assert.ok(
lon0 > -180 && lon0 < 0 && lon1 > lon0,
'west half of tile 0/0 at z1',
);
assert.ok(lat0 > 0 && lat1 < lat0, 'northern hemisphere, moving south');
assert.deepEqual(Object.keys(decoded), ['sequences']);
});
test('an empty tile decodes to empty lists', () => {
assert.deepEqual(
decodeCoverageTile(new Uint8Array(0), { x: 0, y: 0, z: 0 }),
{ sequences: [] },
);
});
test('a sequence with a capture gap keeps its parts as separate lines', () => {
// Two parts with a gap between (300,300) and (1000,1000); the second part's
// MoveTo is relative to the end of the first, as in the MVT encoding.
const first = [
[100, 100],
[300, 300],
];
const second = [
[700, 700],
[900, 700],
];
const geometry = [
...linePart(first),
9,
zigzag(second[0][0] - first[1][0]),
zigzag(second[0][1] - first[1][1]),
((second.length - 1) << 3) | 2,
zigzag(second[1][0] - second[0][0]),
zigzag(second[1][1] - second[0][1]),
];
const [sequence] = decodeCoverageTile(sampleTile({ geometry }), {
x: 0,
y: 0,
z: 1,
}).sequences;
assert.equal(sequence.parts.length, 2);
assert.equal(sequence.parts[0].length, 2);
assert.equal(sequence.parts[1].length, 2);
// No part ends where the next begins: the gap is not bridged.
assert.notDeepEqual(sequence.parts[0].at(-1), sequence.parts[1][0]);
});