Street Level: Panoramax review follow-ups (gekh, #1134)

Image loading (photoViewer.js)
- One AbortController per open: a new open, close or unmount stops the
  previous photo's first and sharper downloads (an <img> by src = '').
  A collapsed viewer never falls back to the original; Panoramax's flat
  fallback is the thumbnail. A superseded open resolves quietly.
- Flat HD tracked as "loading" and "shown" apart: a discarded response no
  longer blocks later upgrades, and resizes while it loads ask once.
- No upgrade of the photo still shown while another opens.
- Size limits before decoding: imageSize.js reads JPEG/PNG/WebP/GIF
  headers; more than 64 MP or 16,384 px a side is refused undecoded, an
  <img> that decodes larger is refused and let go, flat HD needs a known
  width of at most 8,192 px, and panoramas always decode with a resize.

Panoramax provider
- Retry-After parsed as seconds or an HTTP date; the browser cannot read
  it here, so busy answers back off 5 s doubling to 300 s, reset once
  answered.
- Headers timed, then the body by an idle timeout (15 s of silence), so a
  slow link still gets its megabyte tiles.
- Cones centre only on the picture shown (onShowing), never on a late
  lookup; lookups nobody waits for are aborted (shared, refcounted).
- onReopen: a GPU reset redraws in the viewer, not through the core.
- Cones draw the home z15 tile at once and add neighbours as they arrive;
  a neighbour that failed is not counted as drawn.
- Coverage tiles decode in ~8 ms slices (a 2 MB z10 tile over Paris was
  one ~85-200 ms task); sorted by measured length without a Map.
- Box search asks for 20, not 100 (the API returns them nearest first).
- looksLikeEmail checks every '@'; creatorOf de-duplicates with a Set.

lineCoverage.js
- A redraw while a swap still builds keeps what is on screen (the swap's
  old lines and its ready new ones) and drops only the unready ones.

Tests for each (mutation-checked), two vacuous tests made real, the
cones' pictures lane and readable()'s busy/timeout paths covered; the
fixture's box search is nearest first. Docs: README filters and share
links, CHANGELOG HD wording, CURRENT-STATE.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-15 16:55:59 +01:00
1 parent e3bc431f9a
commit b645a767c8
25 files changed
+1362 -135

No files matched your search

@@ -397,6 +397,63 @@ test('a tile redrawn for a new share keeps its old lines until the new ones are
);
});
test('a second redraw while a swap still builds keeps the lines on screen', async (t) => {
const { state, coverage, viewer } = setup(t, {
zoomFor: () => ({ zoom: 18, kind: 'sequence' }),
tuning: {
...TUNING,
maxTiles: 9,
maxSequences: 900,
maxPoints: 90,
keepOrder: true,
},
decoded: (centre, tile) => {
const dense = tile.x % 2 === 0 && tile.y % 2 === 0;
return {
sequences: dense
? Array.from({ length: 30 }, (_, i) =>
pointsLine(`d${tile.x}/${tile.y}/${i}`, 5, tile, centre),
)
: [pointsLine(`s${tile.x}/${tile.y}`, 2, tile, centre)],
overview: [],
};
},
});
coverage.refresh();
assert.ok(
await until(
() => state.coverage.tiles.size > 1 && !state.coverage.pending.size,
),
);
// A tile mid-swap (its new lines never become ready here) with a line
// the cap left out: selecting that line redraws the tile again.
const entry = [...state.coverage.tiles.values()].find(
(tile) =>
tile.swap &&
tile.sequenceList.some((sequence) => !tile.sequences.has(sequence.id)),
);
assert.ok(entry, 'a swapping tile with a line left out');
const onScreen = [...entry.swap.old];
const building = entry.primitives.map(({ primitive }) => primitive);
const left = entry.sequenceList.find(
(sequence) => !entry.sequences.has(sequence.id),
);
state.sequence.selectedId = left.id;
coverage.recolorSequence(left.id, true);
assert.ok(entry.sequences.has(left.id), 'the selected line is drawn');
const onGlobe = (primitive) =>
viewer.scene.groundPrimitives.items.has(primitive) ||
viewer.scene.primitives.items.has(primitive);
for (const { primitive } of onScreen)
assert.ok(
onGlobe(primitive),
'the lines on screen stay until the new ones are ready',
);
for (const primitive of building)
assert.ok(!onGlobe(primitive), 'the first swap’s unready lines are gone');
assert.ok(entry.swap, 'still swapping');
});
test('a selected line outside the cap does not make its tile redraw on every settle, nor does one the filter hides', async (t) => {
let pano = 'all';
const { state, coverage, viewer } = setup(t, {