docs(street-level): shorten docs and comments before merge

Trim the doc text this PR adds (README, CHANGELOG, CURRENT-STATE,
KNOWN-ISSUES, SECURITY, DATA_SOURCES, .env.example, the CI job comment),
drop code comments that restate the code, cut design-note JSDoc to a line
or two, and drop review tags from test names. Comments only; no code
changes.

Also: README key count (seven keys, two metered), CURRENT-STATE's gate
paragraph (CI runs a production build with a dummy token), the stacked
doc above meshBoxInRange and the orphaned inMeshRange line in
coverage.js, and a duplicate ALPR row in DATA_SOURCES that a merge had
brought back.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-06 21:59:12 +01:00
1 parent c65feda4b1
commit 4ea6631425
85 files changed
+701 -1520

No files matched your search

@@ -41,11 +41,7 @@ const REMESH_INTERVAL_MS = 1500;
const REMESH_STAGGER_MS = 120;
/** Old-zoom tiles are kept at most this long after a zoom change. */
const STALE_TILE_MAX_MS = 6000;
/**
* Sequences per ground primitive. Ground polyline geometry is built on a
* worker in proportion to the instance count, so smaller batches put the
* first lines on screen sooner instead of one big batch arriving late.
*/
/** Sequences per primitive: smaller batches build, and show, sooner. */
const SEQUENCE_PRIMITIVE_BATCH = 120;
const PER_TILE_SEQUENCE_CAP = Math.floor(
@@ -71,12 +67,7 @@ function partIds(sequence) {
);
}
/**
* Sequence id for a picked line, whichever part was hit: `mly:seq:<id>` and
* `mly:seq:<id>~<part>` both give `<id>`; null for any other pick.
* @param {string} pickId
* @returns {string|null}
*/
/** Sequence id for any part's pick id (`mly:seq:<id>[~<part>]`), else null. */
export function sequenceIdFromPick(pickId) {
if (typeof pickId !== 'string' || !pickId.startsWith(PICK_PREFIX.sequence))
return null;
@@ -86,22 +77,11 @@ export function sequenceIdFromPick(pickId) {
}
/**
* Camera-driven coverage: z0–5 `overview` points from orbit down to 60 km,
* then z11–14 sequence polylines clamped to terrain and 3D tiles. Decoded
* tiles are kept so the imagery filter can rebuild without refetching.
*
* In the core's terrain surface mode (Google 3D at street zoom) a tile's
* lines are cast to the bare earth instead: draped lines would land on roofs
* and tree tops. A tile is drawn draped first and swapped for its cast lines
* once the terrain heights are in, so coverage never waits on the terrain.
*/
/**
* Whether any point of a line's bounding `box` is within the mesh sampler's
* range of the camera ground point `centre`, measured the short way round
* the date line.
* Whether any point of `box` ({west, south, east, north}) is within the mesh
* sampler's range of `centre`, the ground point under the camera.
*/
export function meshBoxInRange(box, centre) {
// Take the camera's longitude to the box's side of the date line first:
// Move the camera's longitude to the box's side of the date line first:
// clamping 179.999 into a box at -179.99 would pick its far edge.
const lon =
centre.lon +
@@ -113,6 +93,12 @@ export function meshBoxInRange(box, centre) {
return metresBetween(centre, nearest) <= MESH_SAMPLE_RADIUS_M;
}
/**
* Camera-driven coverage: overview points (z0–5) from orbit, sequence lines
* (z11–14) near the ground. In terrain surface mode a tile is drawn draped
* first, then swapped for lines cast to the bare earth (draped lines would
* land on roofs), so coverage never waits on terrain heights.
*/
export function createCoverage({ state, source }) {
const { render } = state.services;
@@ -142,9 +128,8 @@ export function createCoverage({ state, source }) {
}
/**
* The sequences a tile draws: newest first, past the imagery filter, then
* capped. Capping before the filter could leave a dense tile of newer flat
* captures with no 360° lines at all.
* A tile's sequences, filtered then capped: capping first could leave a
* dense tile of newer flat captures with no 360° lines at all.
*/
function drawnSequences(entry) {
const drawn = [];
@@ -157,11 +142,9 @@ export function createCoverage({ state, source }) {
}
/**
* Primitives for a tile's sequences, in draw batches: ground primitives
* draped on the globe, plus (in terrain mode) plain polylines at the cast
* heights for every part whose heights are cached. Each part of a sequence
* with a capture gap is its own line. Each batch remembers the selection
* its colours were built with.
* Batched primitives for a tile's sequence parts: cast polylines where
* terrain heights are cached, draped ground lines otherwise. Each batch
* records the selection its colours were built with.
*/
function buildSequencePrimitives(sequences) {
const ground = terrainMode() ? state.context.groundCaster : null;
@@ -233,13 +216,11 @@ export function createCoverage({ state, source }) {
}
/**
* Fetch the terrain heights a tile's lines need, then redraw the tile cast.
* `castRequested` holds while the tile's castable lines are cast; a cast
* that resolves nothing (terrain proxy down, tile too big) clears it, so
* the next refresh tries again rather than this retrying in a loop.
* Fetch the terrain heights a tile's lines need, then redraw it cast. A
* cast that resolves nothing clears `castRequested`, so the next refresh
* retries instead of this looping.
*/
function castTile(entry) {
// `castAbort` is set while a cast is in flight.
if (entry.castRequested || entry.castAbort || !terrainMode()) return;
entry.castRequested = true;
entry.castAbort = new AbortController();
@@ -264,10 +245,7 @@ export function createCoverage({ state, source }) {
});
}
/**
* Rebuild a tile's lines (cast, or with new mesh heights), keeping the old
* ones on screen until the new ones are built, so nothing blinks.
*/
/** Rebuild a tile's lines; the old ones stay up until the new are ready. */
function redrawTile(entry) {
const previous = entry.primitives;
entry.primitives = [];
@@ -275,7 +253,7 @@ export function createCoverage({ state, source }) {
removeWhenReady(entry, previous);
}
/** The point under the camera: where the mesh sampler measures its range from. */
/** Lon/lat under the camera, where mesh range is measured from. */
function meshCentre() {
const carto = state.viewer?.camera?.positionCartographic;
if (!carto) return null;
@@ -285,11 +263,9 @@ export function createCoverage({ state, source }) {
};
}
/** Whether any of a {west, south, east, north} box is within the sampler's range. */
/**
* A tile's drawn lines with their bounding boxes, built once per filter;
* each line's mesh points are densified the first time it is in range.
* A tile's drawn lines with bounding boxes, built once per filter; mesh
* points are densified the first time a line is in range.
*/
function meshParts(entry) {
if (entry.meshParts) return entry.meshParts;
@@ -332,10 +308,7 @@ export function createCoverage({ state, source }) {
if (points.length) sampler.request(points);
}
/**
* New mesh samples landed: redraw the tiles they fall in, throttled, with
* the same no-blink swap as the bare-earth cast.
*/
/** Redraw the tiles new mesh samples fall in, throttled. */
function onMeshSampled(batch) {
if (!terrainMode() || !state.context.isActive()) return;
for (const entry of state.coverage.tiles.values()) {
@@ -543,9 +516,8 @@ export function createCoverage({ state, source }) {
});
if (kind === 'sequence') await ensureTerrainReady();
const bytes = await fetching;
// A refresh that still wants this tile must not throw the bytes away;
// only an abort (tile no longer wanted, or zoom changed) does.
// A retire, clear or newer request for this tile owns it now.
// Only an abort, a retire, a clear or a newer request for this tile
// discards the bytes; a refresh that still wants the tile keeps them.
const current = () =>
state.coverage.pending.get(key) === controller &&
!state.coverage.tiles.has(key);
@@ -644,10 +616,7 @@ export function createCoverage({ state, source }) {
requestRender();
}
/**
* Move every loaded tile to the stale set instead of removing it, so the
* old zoom stays visible while the new zoom streams in (no blank globe).
*/
/** Move tiles to the stale set: the old zoom shows until the new one loads. */
function retire() {
for (const controller of state.coverage.pending.values())
controller.abort();
@@ -812,9 +781,8 @@ export function createCoverage({ state, source }) {
}
/**
* Redraw loaded tiles draped or cast after the core's surface mode changes,
* then re-pick the zoom: the bare-earth height under the camera that the
* change brings in can move it (a high city reads much closer to the street).
* Redraw tiles draped or cast for the new surface mode, then re-pick the
* zoom, since the ground height under the camera may have changed.
*/
function setSurface() {
if (!state.context.isActive()) return;
@@ -839,10 +807,7 @@ export function createCoverage({ state, source }) {
}
}
/**
* Every tile drawn on the globe: the current zoom's, and the old zoom's
* kept visible until the new one is in (see retire).
*/
/** Every tile on the globe: the current zoom's and the stale zoom's. */
function drawnEntries() {
return [...state.coverage.tiles.values(), ...state.coverage.stale.values()];
}
@@ -886,10 +851,8 @@ export function createCoverage({ state, source }) {
}
/**
* Colours are baked in when a tile's lines are built, and a primitive
* cannot be recoloured until it is ready. Cast swaps and remeshes rebuild
* tiles all the time, so while any primitive is building, catch each one
* up with the selection made or cleared meanwhile as it becomes ready.
* A primitive cannot be recoloured until it is ready, so while any is
* building, catch each one up with the selection as it becomes ready.
*/
function watchSelection() {
const scene = state.viewer?.scene;
@@ -171,7 +171,7 @@ function frame(listeners) {
for (const listener of [...listeners]) listener();
}
test('a superseded tile request cannot strand lines on the globe (review P0 #2)', async () => {
test('a superseded tile request cannot strand lines on the globe', async () => {
const { viewer, source, state, coverage, bytes, tileKey, onGlobe } = setup();
const forTile = () => source.calls.filter((call) => call.key === tileKey);
@@ -304,7 +304,7 @@ test('zoom 0 is a zoom: the whole-earth view still shows coverage', () => {
coverage.clear();
});
test('a rejected key stops coverage requests until the layer goes off (review IC8 P2)', async () => {
test('a rejected key stops coverage requests until the layer goes off', async () => {
const { viewer, source, state, coverage } = setup();
coverage.refresh();
const rejected = Object.assign(
@@ -330,7 +330,7 @@ test('a rejected key stops coverage requests until the layer goes off (review IC
coverage.clear();
});
test('a rate limit keeps the drawn tiles and asks again once the wait is over (review IC8 P2)', async (t) => {
test('a rate limit keeps the drawn tiles and asks again once the wait is over', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const { viewer, source, state, coverage } = setup();
coverage.refresh();
@@ -566,7 +566,7 @@ test('a selection made or cleared while a tile builds is applied once it is read
assert.equal(viewer.postRender.size, 0, 'the selection watch is gone');
});
test('a sequence that crosses a tile edge is counted once (review P3)', async () => {
test('a sequence that crosses a tile edge is counted once', async () => {
const { viewer, source, coverage, centre } = setup();
// Look straight down on the edge between this tile and its east neighbour.
const { east } = tileBounds(centre.tile.x, centre.tile.y, 14);
@@ -598,10 +598,7 @@ test('a sequence that crosses a tile edge is counted once (review P3)', async ()
coverage.clear();
});
/**
* In terrain mode with a cast tile mid-swap: the draped lines and the cast
* primitive whose readiness the test controls.
*/
/** A terrain-mode tile mid-swap, with a cast primitive the test readies. */
async function midSwap() {
// Drawn draped first; castLine only answers once the heights are in.
let heightsReady = false;
@@ -665,9 +662,8 @@ function tiltedCamera({ lon, lat, height, heading, pitch }) {
}
/**
* Denver, 300 m above a street 1,600 m above the ellipsoid, at the centre of
* a z14 tile and looking east with the screen centre `aheadM` metres out.
* Google 3D hides the globe, so only the bare-earth caster knows the ground.
* Denver: 300 m above a street 1,600 m up, at a z14 tile centre, looking east
* at ground `aheadM` metres out. Only the bare-earth caster knows the ground.
*/
function denverStreetView(aheadM) {
const z = 14;
@@ -735,7 +731,7 @@ test('a street view toward the horizon ranks only the ground within range', () =
coverage.clear();
});
test('a selection cleared while the old zoom is still shown uncolours its lines (review IC8 P2)', async () => {
test('a selection cleared while the old zoom is still shown uncolours its lines', async () => {
const { viewer, source, state, coverage, bytes } = setup();
coverage.refresh();
source.calls[0].resolve(bytes);
@@ -760,7 +756,7 @@ test('a selection cleared while the old zoom is still shown uncolours its lines
coverage.clear();
});
test('a selection made while the old zoom is still building is applied once it is ready (review IC8 P2)', async () => {
test('a selection made while the old zoom is still building is applied once it is ready', async () => {
const { viewer, source, state, coverage, bytes } = setup();
coverage.refresh();
source.calls[0].resolve(bytes);
@@ -784,7 +780,7 @@ test('a selection made while the old zoom is still building is applied once it i
assert.equal(viewer.postRender.size, 0, 'the selection watch is gone');
});
test('only lines within the mesh sampler range of the camera are asked for (review IC8 P2)', async () => {
test('only lines within the mesh sampler range of the camera are asked for', async () => {
let heightsReady = false;
const groundCaster = {
prepareLines: async () => (heightsReady = true),
@@ -874,7 +870,7 @@ test('the old zoom goes as soon as the new zoom has loaded, not after the stale
coverage.clear();
});
test('a rate limit holds every refresh inside its wait, not only the first (review IC8 P2)', async (t) => {
test('a rate limit holds every refresh inside its wait, not only the first', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
const { viewer, source, coverage } = setup();
coverage.refresh();
@@ -898,7 +894,7 @@ test('a rate limit holds every refresh inside its wait, not only the first (revi
coverage.resetErrors();
});
test('a line across the date line from the camera is in mesh range (gekh P3)', () => {
test('a line across the date line from the camera is in mesh range', () => {
// A z14 tile's width of lines just past -180, the camera 111 m short of it.
const camera = { lon: 179.999, lat: 0 };
const east = { west: -180, east: -179.978, south: -0.001, north: 0.001 };
@@ -1,11 +1,5 @@
import { PbfWriter } from 'pbf';
/**
* Encode a Mapillary-shaped coverage tile (mly1_public: `sequence` lines and
* `overview` points) from lon/lat features, for tests and the hermetic QA
* gate. The decoder in ./decode.js reads it like a real tile.
*/
const zigzag = (value) => (value << 1) ^ (value >> 31);
/** Tile-local [x, y] (0..extent) of a lon/lat inside tile (x, y, z). */
@@ -67,7 +61,7 @@ function layerWriter(name, extent) {
};
}
/** MoveTo/LineTo commands for line parts, with the cursor carried across parts. */
/** MVT MoveTo/LineTo commands; the cursor carries over between parts. */
function lineGeometry(parts) {
const out = [];
let cx = 0;
@@ -89,9 +83,10 @@ function lineGeometry(parts) {
}
/**
* Encode a `mly1_public`-shaped tile from lon/lat features, for tests and
* the hermetic QA gate.
* @param {{x: number, y: number, z: number}} tile
* @param {{sequences?: Array<{id: string, capturedAt?: number, isPano?: boolean, parts: Array<Array<[number, number]>>}>, overview?: Array<{id: string, lon: number, lat: number, capturedAt?: number, isPano?: boolean}>, extent?: number}} [content]
* @returns {Uint8Array}
*/
export function encodeCoverageTile(
tile,
@@ -3,13 +3,10 @@ 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; z0–5 tiles carry
* `overview` points instead. The z14 `image` layer (it can hold >150k
* points) is never read.
* Decode a `mly1_public` coverage tile into sequences and (z0–5) overview
* points. The z14 `image` layer, which can hold >150k points, is skipped.
* @param {Uint8Array} bytes
* @param {{x:number,y:number,z:number}} address
* @returns {{sequences: Array<object>, overview: Array<object>}}
*/
export function decodeCoverageTile(bytes, address) {
const result = { sequences: [], overview: [] };
@@ -44,7 +41,6 @@ export function decodeCoverageTile(bytes, address) {
});
}
}
// 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++) {
@@ -23,19 +23,16 @@ function createProviderState(context) {
services: context.services,
viewer: null,
keyRequired: false,
/**
* False until the first status answer: coverage asks for nothing before
* it, so a key-less install never sends tile requests that get 503.
*/
/** Coverage waits for the first status: no tile requests without a key. */
statusKnown: false,
/** Mapillary refused the configured token: nothing is requested until re-enabled. */
/** Mapillary refused the token: request nothing until re-enabled. */
keyRejected: false,
status: null,
coverage: {
zoom: null,
/** @type {Map<string, {primitive: object|null, sequences: Map<string, object>, count: number}>} */
/** Current zoom's tiles by `z/x/y` key. */
tiles: new Map(),
/** Tiles from the previous zoom, kept on screen until replacements land. */
/** Previous zoom's tiles, kept on screen until replacements land. */
stale: new Map(),
staleTimer: null,
/** Tile key → its request's controller; any entry means LOADING. */
@@ -53,7 +50,7 @@ function createProviderState(context) {
sequence: {
selectedId: null,
images: [],
/** @type {Map<string, Array<object>>} recent sequences' thinned images */
/** Recent sequences' thinned images, by sequence id. */
cache: new Map(),
collection: null,
loading: false,
@@ -63,9 +60,8 @@ function createProviderState(context) {
}
/**
* Mapillary as a Street Level provider: coverage from cached vector tiles,
* per-sequence image cones from the Graph API and the MapillaryJS viewer.
* @param {{source: object}} options A source from ./source.js (or a stand-in with the same methods).
* Mapillary Street Level provider: tile coverage, Graph API image cones and
* the MapillaryJS viewer. `source` is ./source.js or a stand-in.
* @returns {import('../../registry.js').StreetLevelProvider}
*/
export function createMapillaryProvider({ source }) {
@@ -24,10 +24,7 @@ export const PICK_PREFIX = Object.freeze({
image: 'mly:img:',
});
/**
* Mapillary draws in one colour, its brand green, for lines, overview points
* and cones alike (360° cones keep their ring shape); selection is GEV cyan.
*/
/** Mapillary green for lines, points and cones; GEV cyan for selection. */
export const COLORS = Object.freeze({
coverage: PROVIDER_COLORS.mapillary,
selected: SHARED_COLORS.selected,
@@ -45,9 +42,8 @@ export const KEY_REJECTED_MESSAGE =
export const RATE_LIMITED_MESSAGE =
'Coverage requests are being rate-limited — they resume on their own';
/**
* How far from the camera a street-zoom view may reach for coverage: a tilted
* view sees to the horizon, but only the ground within this range is ranked
* and boxed. Scales with camera height, never below the floor.
* Street-zoom coverage reaches this far from the camera: camera height times
* the factor, never below the floor, even when a tilted view sees further.
*/
export const SEQUENCE_VIEW_RANGE_MIN_M = 2_500;
export const SEQUENCE_VIEW_RANGE_PER_HEIGHT = 10;
@@ -65,10 +61,7 @@ export const IMAGE_CONE_MIN_SPACING_M = 3;
/** Images asked for in a nearest-image search (radius: NEAREST_RADIUS_M). */
export const NEAREST_LIMIT = 8;
/**
* On-globe credit shown while the provider is active. Mapillary imagery and
* derived data are CC BY-SA 4.0 and require visible attribution.
*/
/** On-globe credit: Mapillary imagery is CC BY-SA 4.0 and needs attribution. */
export const MAPILLARY_CREDIT_HTML =
'Street Level: imagery © <a href="https://www.mapillary.com" target="_blank" rel="noopener">Mapillary</a> contributors, CC BY-SA 4.0';
@@ -120,7 +120,7 @@ test('nearestImage picks the closest image, not the first the API returned', asy
assert.equal(await instance.nearestImage({ lat: 1, lon: 2 }), 'near');
});
test('a "since N days" window follows the clock in a long-open tab (review P3)', async (t) => {
test('a "since N days" window follows the clock in a long-open tab', async (t) => {
t.mock.timers.enable({ apis: ['Date'], now: Date.UTC(2026, 9, 1) });
const day = 86_400_000;
const capturedAt = Date.now() - day / 2;
@@ -140,7 +140,7 @@ test('a "since N days" window follows the clock in a long-open tab (review P3)',
);
});
test('nearestImage measures the short way round the date line (review IC8 P2)', async () => {
test('nearestImage measures the short way round the date line', async () => {
const at = (id, lon, lat) => ({
id,
is_pano: false,
@@ -167,7 +167,7 @@ test('nearestImage measures the short way round the date line (review IC8 P2)',
assert.equal(await west.nearestImage({ lat: 0, lon: -179.9999 }), 'across');
});
test('cone thinning across the date line drops images metres apart (review IC8 P2)', () => {
test('cone thinning across the date line drops images metres apart', () => {
const image = (id, lon) => ({ id, lon, lat: 0 });
// A sequence driving east over ±180° with images ~1 m apart, then ~11 m on.
const kept = thinImages(
@@ -220,7 +220,7 @@ function streetViewer() {
};
}
test('coverage asks for no tiles before the key status is known (gekh P3)', async () => {
test('coverage asks for no tiles before the key status is known', async () => {
for (const configured of [false, true]) {
let answer;
const tiles = [];
@@ -58,12 +58,8 @@ export function createSequences({ state, source, parts }) {
state.context.notify();
}
/**
* The cones on the globe: each drawn image's index, billboard and height
* (null while clamped), and the mesh cells under them, so a mesh sample
* moves only the cones it can affect.
*/
const nothingDrawn = () => ({ images: [], cones: [], cells: new Set() });
/** Drawn cones, plus the mesh cells under them, so a sample moves only those. */
let drawn = nothingDrawn();
// The cones skip the depth test: hide the ones behind the globe.
@@ -105,11 +101,8 @@ export function createSequences({ state, source, parts }) {
}
/**
* Heights for the cones in terrain mode (Google 3D at street zoom), so they
* stand on the street rather than on roofs and tree tops: bare earth,
* refined along the sequence by the sampled Google 3D surface. Null when
* draped, or when a terrain height is not cached yet; that fetches the
* heights and draws the cones again.
* Terrain-mode cone heights on the street, not on roofs: bare earth refined
* by mesh samples. Null when draped or while terrain heights are fetched.
*/
function coneHeights(images, { request = true } = {}) {
const ground = state.context.groundCaster;
@@ -173,10 +166,8 @@ export function createSequences({ state, source, parts }) {
}
/**
* New mesh samples landed: when any is under a cone, re-place the cones
* whose cast height changed, in place. A sample can move a neighbour too
* (refineHeights bridges across covered points), so every height is
* recomputed (cheap arithmetic) but only moved cones are touched.
* Re-place cones whose height new mesh samples changed. A sample can move
* a neighbour too, so every height is recomputed but only moved cones touched.
*/
function onMeshSampled(batch) {
if (state.context.getSurface?.() !== 'terrain' || !drawn.cones.length)
@@ -91,7 +91,7 @@ function setup() {
return { state, source, sequences, colours, errors, drawn };
}
test('a failed sequence load leaves no stale cones and can be retried (review P2-4)', async () => {
test('a failed sequence load leaves no stale cones and can be retried', async () => {
const { state, source, sequences, colours, errors, drawn } = setup();
sequences.select('A');
source.last('A').resolve(records('A'));
@@ -174,9 +174,8 @@ test('clearSelection cancels the lookup and uncolours the sequence', async () =>
});
/**
* Terrain mode (Google 3D at street zoom): bare earth 10 m everywhere, a
* mesh sampler whose samples the test sets and announces, and cones that
* log every change made to them or their collection.
* Terrain mode with bare earth at 10 m, a mesh sampler the test drives, and
* cones that log every change made to them.
*/
function terrainSetup() {
const context = setup();
@@ -240,7 +239,7 @@ function terrainSetup() {
return { ...context, sequences, changes, items, sample, heightOf };
}
test('a mesh sample away from the selected sequence changes no cone (review IC8 P1)', async () => {
test('a mesh sample away from the selected sequence changes no cone', async () => {
const { source, sequences, changes, items, sample } = terrainSetup();
sequences.select('A');
source.last('A').resolve(records('A', 3));
@@ -251,7 +250,7 @@ test('a mesh sample away from the selected sequence changes no cone (review IC8
assert.deepEqual(changes, [], 'nothing rebuilt or moved');
});
test('a mesh sample under a cone moves that cone in place, and no other (review IC8 P1)', async () => {
test('a mesh sample under a cone moves that cone in place, and no other', async () => {
const { source, sequences, changes, items, sample, heightOf } =
terrainSetup();
sequences.select('A');
@@ -282,7 +281,7 @@ test('a mesh sample under a cone moves that cone in place, and no other (review
assert.deepEqual([after[0], after[2]], [before[0], before[2]]);
});
test('cones behind the globe are hidden, and show again from the other side (review IC8 P2)', async () => {
test('cones behind the globe are hidden, and show again from the other side', async () => {
const { state, source, sequences } = setup();
const view = { lon: -121.49, lat: 38.58, height: 20_000_000 };
const preRender = new Set();
@@ -7,11 +7,7 @@ import {
SEQUENCE_IMAGES_LIMIT,
} from './policy.js';
/**
* Error carrying the HTTP status and any server-provided payload: no token
* (`keyRequired`), a token Mapillary rejected (`keyRejected`), or a rate
* limit with the seconds to wait (`retryAfterSec`).
*/
/** HTTP error flagged `keyRequired`, `keyRejected` or `retryAfterSec` (429). */
export class MapillarySourceError extends Error {
constructor(message, { status = 0, payload = null, retryAfterSec } = {}) {
super(message);
@@ -44,10 +40,8 @@ async function readJsonOrThrow(response, label) {
}
/**
* Every network path the Mapillary layer uses. Coverage tiles go through the
* local dev-server proxy (which adds the token and caches them); image and
* sequence lookups go straight to graph.mapillary.com with the client token,
* exactly as the embedded MapillaryJS viewer does.
* Mapillary network access: tiles via the local proxy, which adds the token
* and caches; Graph API lookups direct with the client token, as MapillaryJS.
* @param {{token?: string, fetchImpl?: Function, endpoints?: object}} options
*/
export function createMapillarySource({
@@ -110,7 +110,7 @@ test('the source exposes only imagery lookups', () => {
assert.equal(source[gone], undefined, gone);
});
test('a rejected key and a rate limit are told apart from other tile errors (review IC8 P2)', async () => {
test('a rejected key and a rate limit are told apart from other tile errors', async () => {
const rejectedSource = createMapillarySource({
token: 'MLY|1|abc',
fetchImpl: fakeFetch(() =>
@@ -1,9 +1,8 @@
import { MAPILLARY_PROVIDER_ID, mapillaryImageUrl } from './policy.js';
/**
* The Mapillary viewer adapter: lazy-load MapillaryJS, mount it in the host
* the core hands over, open images, and emit a provider-neutral pose for
* every image, point-of-view or position change.
* Viewer adapter: lazy-loads MapillaryJS into the core's host and emits a
* provider-neutral pose on every image, view or position change.
* @returns {import('../../registry.js').ViewerAdapter}
*/
export function createMapillaryViewer({ source, render } = {}) {
@@ -11,7 +10,6 @@ export function createMapillaryViewer({ source, render } = {}) {
let Library = null;
let container = null;
let pendingOpen = null;
/** The prewarm in flight, if any. */
let prewarming = null;
/** In-flight viewer construction, so pre-warm and open never build two. */
let creating = null;
@@ -34,10 +32,8 @@ export function createMapillaryViewer({ source, render } = {}) {
async function ensureLibrary() {
if (Library) return Library;
// The stylesheet comes with the lazy chunk: Vite applies a dynamically
// imported stylesheet before its import resolves, so the viewer's chrome
// is styled from the first frame without costing every page load ~90 KB
// of render-blocking CSS.
// Lazy CSS: Vite applies it before the import resolves, so the viewer is
// styled from its first frame without ~90 KB of render-blocking CSS.
const [library] = await Promise.all([
import('mapillary-js'),
import('mapillary-js/dist/mapillary.css'),
@@ -196,14 +192,11 @@ export function createMapillaryViewer({ source, render } = {}) {
}
},
/**
* Load the library and stand the viewer up ahead of the first image, so
* opening one only costs the image download. Safe to call repeatedly.
*/
/** Build the viewer ahead of the first image. Safe to call repeatedly. */
async prewarm(host) {
if (!host) return;
// A prewarm already in flight may give up (the layer went off and on
// while the library downloaded): wait for it, then build if it did not.
// An in-flight prewarm may give up (layer toggled mid-download): wait
// for it, then build if it did not.
if (prewarming) await prewarming;
if (viewer) return;
const run = (async () => {