Files
gods-eye-view/src/layers/streetLevel/providers/mapillary/decode.test.mjs
T
German KhokhlovandClaude Opus 5.5 6390a3346f 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]>
2026-10-19 13:51:45 +04:00

156 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]);
});