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

@@ -87,3 +87,43 @@ test('unmounting while a mount is in flight does not leave it active', async ()
assert.equal(adapter.calls.unmount, 1);
assert.equal(state.street.open, false);
});
test('a prewarmed viewer is released when the layer goes off, or at once if it went off mid-load', async () => {
const adapter = fakeAdapter();
let release = null;
adapter.prewarm = () =>
new Promise((resolve) => {
release = resolve;
});
const { state, host } = harness(adapter);
const entry = state.providers.get('mapillary');
entry.on = true;
// Warmed, then the layer is switched off: unmount releases the warm viewer.
const warming = host.prewarm([entry]);
release();
await warming;
assert.equal(adapter.calls.unmount, 0);
host.unmount();
assert.equal(adapter.calls.unmount, 1, 'the WebGL viewer is destroyed');
// Switched off while the library was still loading: released on arrival.
const late = host.prewarm([entry]);
state.enabled = false;
release();
await late;
assert.equal(adapter.calls.unmount, 2);
});
test('switching one provider off releases only its viewer', async () => {
const adapter = fakeAdapter();
adapter.prewarm = async () => {};
const { state, host } = harness(adapter);
const entry = state.providers.get('mapillary');
entry.on = true;
await host.prewarm([entry]);
host.unmount('panoramax');
assert.equal(adapter.calls.unmount, 0, 'another provider leaves it alone');
host.unmount('mapillary');
assert.equal(adapter.calls.unmount, 1);
});