fix(street-level): Panoramax IC8 review — line geometry budget, caches, attribution, tests

From a five-lens IC8 review (correctness, security, tests with mutation
testing, performance, UX):
- P0: over Paris at 12–60 km the unsimplified z10–z12 lines sent ~2M
  points to Cesium's ground-polyline builder (gigabytes of geometry; the
  tab hung). decode.js now simplifies lines (Douglas–Peucker, 4 tile
  units: 6–10% of the points), and lineCoverage gains an opt-in point
  budget (`maxPoints`, 10,000 a tile for Panoramax).
- Decoded coverage tiles are kept (KartaView's tile cache moves to the
  shared tileCache.js); a sequence's z15 picture lists are kept and load
  on their own connection; cones know their tiles from the start of a
  load, so a picture opening meanwhile re-centres them and ‹ › no longer
  restarts the load.
- The HD panorama loads only for the expanded viewer (a docked one on a
  dense screen asked for 3 MB originals) and is aborted when another
  photo opens; picture files over 48 MB or 16,384 px are refused; a
  bitmap no renderer took after a context loss is closed.
- A nearest lookup survives one of its two searches failing; only a
  timeout is retried; offline reads as unreachable; Retry-After is
  capped at five minutes; the search box stays on the map at ±180°;
  cones near both edges of a narrow (high-latitude) tile read both.
- Every producer is credited (app and photographer); with none to name,
  the caption says "via <instance>" rather than crediting the host.
- Esc anywhere in the docked viewer (toolbar, caption) clears the
  sequence before the panel takes it. A filter that hides every density
  dot says so; wording aligned ("coverage", "photo").
- Tests for every surviving mutant (Esc consumed, keepOrder and dot-size
  wiring, licence in the core, limiter aborts, radius floor, bbox corner,
  first-producer-only, byte and size caps), a limiter test, and a gate
  fixture that answers box searches and leaves out pictures facing away;
  the gate now clicks over the other sequence's pictures, so only the
  line click's own-sequence lookup can pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-14 15:24:13 +01:00
1 parent 5526666894
commit cf502429a3
29 files changed
+1153 -216

No files matched your search

+17 -1
View File
@@ -73,6 +73,7 @@ export function meshBoxInRange(box, centre) {
* @property {number} viewRangePerHeight …or camera height times this.
* @property {number} viewNearM Ground always covered around a tilted camera.
* @property {boolean} [keepOrder] Cap a tile's lines in the order decoded (default: newest first).
* @property {number} [maxPoints] Line points drawn at once, over every tile (default: no limit).
*/
/**
@@ -111,6 +112,9 @@ export function createLineCoverage({
}) {
const { render } = state.services;
const perTileSequenceCap = Math.floor(tuning.maxSequences / tuning.maxTiles);
const perTilePointCap = tuning.maxPoints
? Math.floor(tuning.maxPoints / tuning.maxTiles)
: Infinity;
/** Colour for a line: the provider's, GEV cyan while selected. */
function sequenceColor({ selected = false } = {}) {
@@ -151,13 +155,25 @@ export function createLineCoverage({
function drawnSequences(entry) {
const drawn = [];
const current = filter();
let points = 0;
for (const sequence of entry.sequenceList) {
if (drawn.length >= perTileSequenceCap) break;
if (passes(sequence, current)) drawn.push(sequence);
if (!passes(sequence, current)) continue;
// A line past the point budget is skipped; a shorter one may still fit.
const size = pointsOf(sequence);
if (points + size > perTilePointCap) continue;
points += size;
drawn.push(sequence);
}
return drawn;
}
function pointsOf(sequence) {
let count = 0;
for (const part of sequence.parts) count += part.length;
return count;
}
/**
* Batched primitives for a tile's sequence parts: cast polylines where
* terrain heights are cached, draped ground lines otherwise. Each batch