fix(street-level): address gekh's review of #768

P0
- tileMath: lonToTileX(180) wrapped to column 0, so a whole-world view at
  z1 loaded only the western column; 180 is now the last column.
- coverage.loadTile: a superseded request (after a retire or clear) still
  deleted the newer request's pending entry and loading count in `finally`,
  letting a duplicate load attach lines that nothing could remove. Only the
  request that still owns the key attaches its tile or settles the key.

P1
- view/tileMath: a view across the date line read as a ~358 deg box and
  loaded tiles on the far side of the globe. visibleBbox now returns
  west > east for such views and tilesForBbox splits them at +/-180;
  viewCentre averages across the line.
- decode: the parts of a multi-line sequence were joined into one line,
  drawing straight segments over capture gaps. Each part is now its own
  line (`parts`); later parts carry `~n` in their pick id, and picking or
  recolouring any part acts on the whole sequence.
- viewerHost: a prewarmed MapillaryJS viewer was never destroyed, leaking
  its WebGL context. Prewarmed adapters are tracked and released on
  unmount, at once when the layer went off mid-load, and per provider when
  only that provider is switched off.
- nearestImage took the first record the API returned; it now takes the
  closest one that passes the filter.

P2
- groundCast: the cache was cleared after a request counted its corners,
  dropping ones it relied on; it is now cleared first.
- streetLevelControls: the viewer was resized on every render (every frame
  while a panorama is dragged); the ResizeObserver handles size changes and
  the viewer is resized once when it opens. The legend now rebuilds when its
  entries change, not only their count.
- panelPositionControls: the portable-panel paths this PR added called the
  CCTV viewport sync directly; they go through onPanelResized, which the
  shell routes to CCTV. The "Compose camera panel controls" JSDoc is back on
  _initCctvPanel.

New tests: zoom-1 columns, date-line tiles and bbox, multi-part decode and
part pick ids, nearest image, prewarmed viewer release, and the cache
ordering (fails on the old order).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-02 19:00:01 +01:00
1 parent 514788303d
commit a1f8d6ca25
17 files changed
+435 -117

No files matched your search

@@ -5,8 +5,20 @@ 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 and one overview point. */
function sampleTile() {
function sampleTile({ geometry = null } = {}) {
const writer = new PbfWriter();
writer.writeMessage(
3,
@@ -37,14 +49,17 @@ function sampleTile() {
feature.writeVarintField(1, 7);
feature.writePackedVarint(2, [0, 0, 1, 1, 2, 2]);
feature.writeVarintField(3, 2); // LineString
feature.writePackedVarint(4, [
9,
zigzag(100),
zigzag(100), // MoveTo
10,
zigzag(200),
zigzag(200), // LineTo ×1
]);
feature.writePackedVarint(
4,
geometry || [
9,
zigzag(100),
zigzag(100), // MoveTo
10,
zigzag(200),
zigzag(200), // LineTo ×1
],
);
},
null,
);
@@ -86,8 +101,9 @@ test('sequences and overview points decode into lon/lat records', () => {
assert.equal(sequence.id, 'seq-42');
assert.equal(sequence.capturedAt, 1_700_000_000);
assert.equal(sequence.isPano, true);
assert.equal(sequence.coordinates.length, 2);
const [[lon0, lat0], [lon1, lat1]] = sequence.coordinates;
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',
@@ -110,3 +126,35 @@ test('an empty tile decodes to empty lists', () => {
},
);
});
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]);
});