mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
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]>
101 lines
3.4 KiB
JavaScript
101 lines
3.4 KiB
JavaScript
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;
|
||
}
|