Files
gods-eye-view/src/layers/streetLevel/providers/mapillary/decode.js
T
Said TurkseverandClaude Opus 5.5 a1f8d6ca25 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]>
2026-10-02 19:00:01 +01:00

101 lines
3.4 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 { PbfReader } from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';
import { tileLocalToLonLat } from '../../tileMath.js';
/**
* Decode a Mapillary coverage tile (`mly1_public`) in the browser.
* Sequences are LineStrings with capture metadata; the `image` layer is only
* present at z14 and is skipped unless asked for (it can hold >150k points).
* @param {Uint8Array} bytes
* @param {{x:number,y:number,z:number}} address
* @param {{images?: boolean}} [options]
* @returns {{sequences: Array<object>, images: Array<object>}}
*/
export function decodeCoverageTile(bytes, address, { images = false } = {}) {
const result = { sequences: [], images: [], overview: [] };
if (!bytes || !bytes.length) return result;
const tile = new VectorTile(new PbfReader(bytes));
const { x, y, z } = address;
const sequenceLayer = tile.layers.sequence;
if (sequenceLayer) {
for (let i = 0; i < sequenceLayer.length; i++) {
const feature = sequenceLayer.feature(i);
const props = feature.properties || {};
// A sequence with a capture gap is a multi-line: keep each part as its
// own line, so no straight segment is drawn across the gap.
const parts = [];
for (const line of feature.loadGeometry()) {
const part = line.map((point) =>
tileLocalToLonLat(point.x, point.y, sequenceLayer.extent, x, y, z),
);
if (part.length >= 2) parts.push(part);
}
if (!parts.length) continue;
result.sequences.push({
id: String(props.id ?? feature.id ?? `${x}/${y}/${i}`),
imageId: props.image_id != null ? String(props.image_id) : null,
capturedAt: Number(props.captured_at) || 0,
isPano: props.is_pano === true,
onFoot: props.foot === true,
quality: Number.isFinite(props.quality_score)
? props.quality_score
: null,
parts,
});
}
}
// z0–5 tiles carry an `overview` point layer instead of sequences.
const overviewLayer = tile.layers.overview;
if (overviewLayer) {
for (let i = 0; i < overviewLayer.length; i++) {
const feature = overviewLayer.feature(i);
const props = feature.properties || {};
const point = feature.loadGeometry()?.[0]?.[0];
if (!point) continue;
const [lon, lat] = tileLocalToLonLat(
point.x,
point.y,
overviewLayer.extent,
x,
y,
z,
);
result.overview.push({
id: String(props.id ?? feature.id ?? i),
lon,
lat,
capturedAt: Number(props.captured_at) || 0,
isPano: props.is_pano === true,
sequenceId: props.sequence_id ? String(props.sequence_id) : null,
});
}
}
const imageLayer = images ? tile.layers.image : null;
if (imageLayer) {
for (let i = 0; i < imageLayer.length; i++) {
const feature = imageLayer.feature(i);
const props = feature.properties || {};
const point = feature.loadGeometry()?.[0]?.[0];
if (!point) continue;
const [lon, lat] = tileLocalToLonLat(
point.x,
point.y,
imageLayer.extent,
x,
y,
z,
);
result.images.push({
id: String(props.id ?? feature.id),
lon,
lat,
compassAngle: Number(props.compass_angle) || 0,
capturedAt: Number(props.captured_at) || 0,
isPano: props.is_pano === true,
sequenceId: props.sequence_id ? String(props.sequence_id) : null,
});
}
}
return result;
}