refactor(street-level): drop hover, legend and byte-budget cache; flatten the provider directory

- No pointer cursor over coverage lines (no other layer has one); a click
  test replaces the hover tests.
- No legend: one source, one colour.
- The on-globe credit lives in index.js; credits.js is gone.
- Billboard glyphs are SVG data URLs with the same geometry, so tests need
  no canvas stub.
- The tile cache keeps the 120 most recently used tiles, same 24 h TTL,
  instead of a byte budget; listTileLayers was test-only.
- providers/mapillary/ is flattened into the layer and its policy merged
  into one policy.js; the source's method list is a literal in the catalog.
- One fakeCesiumViewer for the layer tests.
- The fixture gate keeps the steps the unit tests do not cover: coverage and
  credit, a rejected key, line -> cone -> photo, close, a keyless page.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-08 19:17:45 +04:00
1 parent 34d43c7e3f
commit e214129e45
41 files changed
+335 -909

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
import { PbfReader } from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';
import { tileLocalToLonLat } from './tileMath.js';
/**
* Decode a `mly1_public` coverage tile (z11–14) into sequences. The z14
* `image` layer, which can hold >150k points, is skipped.
* @param {Uint8Array} bytes
* @param {{x:number,y:number,z:number}} address
*/
export function decodeCoverageTile(bytes, address) {
const result = { sequences: [] };
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}`),
capturedAt: Number(props.captured_at) || 0,
isPano: props.is_pano === true,
parts,
});
}
}
return result;
}