FOLLOW is gone: no setFollow, follow state, map-stack availability or
handoff listener. Opening a photo still frames it with one flight through
the application's deferred navigation (begin/reassert), so a slow photo
never overrides newer navigation and tracking is released as before.
Ground casting is gone: groundCast.js, meshSampler.js, the surface mode,
the draped-to-cast swap and remesh. Coverage lines are always draped
GroundPolylinePrimitives (ClassificationType.BOTH); cones and the marker
clamp to the ground and keep the horizon cull. The terrain/meshFloor/ground
services and meshFloor.visibleTilesetLoaded are no longer needed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
From gekh's review of db79027, each with a test that fails before:
- P2: image cones and the photo marker no longer disappear where the
ground lies below the WGS84 ellipsoid (NYC -22 m, Colombo -95 m). The
horizon cull's occluder is WGS84 shrunk by 1 km (HORIZON_CULL_DEPTH_M),
below any real surface, so a camera near or under the ellipsoid no
longer treats every lower point as behind the horizon; the far side of
the Earth is still hidden from orbit (tested).
- Mesh range across the date line: the camera's longitude is taken to the
box's side of +-180 before clamping (meshBoxInRange), so a tile just
across the line is not measured from its far edge.
- MapillaryJS no longer tracks window resizes (trackResize: false): the
panel's ResizeObserver resizes the viewer and skips it while hidden, so
a window resize after closing a photo cannot ask for a tile at z=NaN.
- A key-less install sends no tile requests before the key status is
known: coverage waits for the first status answer and the provider
refreshes it once the status lands.
- constructCatalog takes the source method list from MAPILLARY_SOURCE_
METHODS; sequences.js imports view.js once; the provider's unused
re-exports are gone.
Verified: unit 5970 pass; strict fixture gate on a production build 28 ok
+ 2 "no Google 3D" skips and the panel gate 6/6 (QA_FAIL_ON_RETRY=1);
live gate with a real token 25 ok + 3 fixture-only skips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the IC8 review of #768, each with tests that fail before:
- P1: closing a photo, switching the layer off or destroying it stops the
1.6 s framing flight toward that photo. The flight is claimed with a
token before flyToBoundingSphere and released by its complete/cancel
callbacks, so cancelFraming() calls camera.cancelFlight() only while
the flight is still Street Level's: a newer navigation flight (a search
result, a scene) is never cancelled. Turning FOLLOW on also stops it, so
the tween cannot overwrite the follow view.
- P2: an older nearest-photo lookup can no longer replace a newer choice.
The core owns one AbortController per lookup, aborted by a newer lookup,
a direct open or cone click, closing the viewer, disable and destroy;
the signal reaches source.nearestImages through the provider, and an
aborted lookup never surfaces as an error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From gekh's review of #768, each bug with a test that fails before:
- P2: a failed sequence load clears the previous sequence's cones, resets
the selection and can be retried; a second click while loading starts no
second lookup. sequences.js had no tests: sequences.test.mjs now covers
failure and retry, a superseded late answer, the cache and clearSelection.
- P2: closing a photo stops MapillaryJS sequence playback, which kept the
hidden viewer stepping and downloading images.
- openNearest without the panel reports it instead of loading forever, and
stops if the layer (or its provider) went off during the lookup; the
mesh sampler's misses are bounded and forgotten with its samples; the
sequence count is the union of ids across tiles; a provider withdraws
only its own error, so a sequence error cannot mask KEY REJECTED; hover
stays on (throttled) under an orbiting camera and re-picks when it
stops; "since N days" follows the clock in a long-open tab.
- A prewarm waits for one in flight and then builds if it gave up.
- New coverage for the layer core (credits, KEY REJECTED vs REQUIRED,
switching a provider off), the refresh wiring of the tilted-view fix and
the draped-to-cast swap completion.
- Cleanup: loading is derived from pending; freshStreet(); tile ranking
computes distances once and shares one path for the date line; shared
metresBetween / whenIdle / redrawTile; one NEAREST_RADIUS_M and one
source method list; unused contract fields, exports, getImage and the
decoder's image branch removed; PROVIDER_COLORS lists only Mapillary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tilted views: the visible box came from screen rays against the bare
ellipsoid and the 9 tiles kept were the ones nearest its centre, so a view
over high ground (Denver, 1,600 m up) or near the horizon dropped the
streets in front of the camera. At street zooms the rays now meet an
ellipsoid raised to the ground under the camera, hits beyond a range tied
to camera height are ignored, tilted views also keep the ground around the
camera, and tiles are ranked along the line of sight (nadir to screen
centre). Simulated with real Cesium ray maths over Sacramento and Denver
from 2 m to 1,000 m: the nearest visible ground and the screen centre are
always loaded.
Rejected token: Mapillary's 401/403 reached the panel as a raw "Mapillary
tiles HTTP 403" while the pill said ON, and every pan asked Mapillary again.
The proxy now answers a refusal as {keyRejected: true} and holds it for that
token (5 min), and a 429 with its Retry-After (held until then). The panel
reads KEY REJECTED, names MAPILLARY_CLIENT_TOKEN and Provider Settings,
gates its controls like KEY REQUIRED, requests nothing more, and clears the
message when the layer goes off. A rate limit keeps the drawn tiles and
refreshes once the wait is over.
Found in the IC8 review of #768 (P1 tile selection, P2 rejected token).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
As more sources arrive, each one reads the same everywhere it appears: its
PROVIDERS chip, coverage lines, overview points, image cones and a single
legend swatch. The palette lives in one place, PROVIDER_COLORS in
src/layers/streetLevel/policy.js: Mapillary green (#05cb63), Panoramax
purple, Google Street View blue.
Mapillary drops its age dimming and magenta panoramas: lines and overview
points are Mapillary green, cones are green too and 360° images keep their
ring shape; the 360°/FLAT filter still separates them, and the selected
sequence stays GEV cyan.
The provider contract loses `legend`: `colors.coverage` is the source's one
colour, and the core composes the legend as one swatch per active source
(its name, its colour) followed by Selected. The chip group takes an
optional per-chip colour as a `--chip-color` CSS variable, written only
when it changes; Street Level chips show it as a swatch dot and, while on,
as the chip's border, text and a faint fill.
The feature gate checks the legend has one swatch per source plus Selected
and that the Mapillary chip carries #05cb63.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Draped ground polylines and clamped billboards land on the top of the
Google Photorealistic 3D mesh, so on a street under trees or an overpass
the coverage seemed to float at roof height.
At street zoom on Google 3D (camera below 1,400 m, back above 1,800 m)
the core switches to a `terrain` surface mode. groundCast.js samples the
existing terrain service on the ground-floor grid (0.001 deg), interpolates
bilinearly and adds 2 m; Mapillary lines become plain polylines at those
heights, and cones and the position marker stand on the street, so the
mesh hides what is behind buildings and trees. A tile draws draped first
and swaps to its cast lines once the heights arrive, keeping the old lines
until the new primitives are ready. Lookups are once per tile and aborted
when the tile is dropped; a geoid fallback counts as unknown and leaves
lines draped. Other map stacks and higher views are unchanged.
Providers get getSurface() and groundCaster in their context and an
optional setSurface(mode); getUIState() exposes `surface`. The QA gate
checks draped on Esri and terrain on Google 3D.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Street Level is now a provider-neutral layer modelled on the iD editor's
photo overlay. The core owns what every imagery provider shares: the
enable state and a switch per provider, the imagery filter (360°/flat and
captured-since in relative days), one click handler routed by pick prefix,
one viewer host that mounts whichever provider's viewer adapter owns the
open image, the position marker, camera follow, one on-globe credit per
active provider, and share-link parameters (one bit per provider plus the
filter). Providers implement the contract in registry.js: coverage,
sequences, nearest-image lookup and a viewer adapter emitting a common
pose. Mapillary is the only registered provider; Google Street View,
KartaView and Panoramax slot in beside it.
Panel: a PROVIDERS chip row (one chip per registered provider, keyless
chips read as errors), relative-day SINCE options, a provider-neutral
caption whose link label follows the provider, and no vendor mark in the
header. The panel inner is a rail scroller like CCTV's.
Tests cover the registry, params codec, filter, pick router, UI-state
composition, the provider definition and coverage-tile decoding; the QA
harness switches the provider off and on and checks the filter.
Co-Authored-By: Claude Fable 5.1 <[email protected]>