main grew the Street Level provider registry while this branch removed it
(#982–#985: injected providers, an empty composition, the `r` switch field).
The registry, params, uiState and pick router come back as main has them and
the slimmed layer sits behind the provider contract:
- The core (index.js) drives providers as on main, without surface modes,
ground casting, FOLLOW, prewarm and nearest-image lookups. A provider's key
status is asked with a 10 s timeout and asked again on a switch-on while no
key is known.
- Mapillary is mapillaryProvider.js over the slim coverage, sequences and
viewer modules, which read the filter and the active flag from the provider
context.
- The viewer host mounts one provider adapter at a time, no prewarm.
- The panel has a chip per provider again (the pill switches the layer);
no legend, EXPAND stays the Fullscreen API.
- The contract drops what no provider implements: groundCaster, meshSampler,
getSurface/setSurface, nearestImage, prewarm, reportError, the legend and
surface fields of the UI snapshot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
- 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]>
- A failed or missing key status is asked again on the next switch-on, and
the check gives up after 10 s instead of holding LOADING forever.
- Waiting for the key status reads as LOADING while the layer is on.
- Esc shrinks the photo in full screen too, before the panel's own Esc.
- Destroying the panel with the iPhone popover open leaves no popover behind.
- An empty view says whether the filter emptied it.
- A freed upstream tile slot passes straight to the oldest waiter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- visibleBbox: no view-rectangle fallback or grid option; its one caller
always ranges the rays
- tilesForBbox returns the tiles; normalizeBbox takes arrays only
- viewerHost: no try/catch around adapter calls that catch for themselves
- coverage: counts come from the drawn sequences; no sequenceCount export
- one keyStatusLabel for the pill and the layer list
- no constructor source check, double-init guard or selectSequence /
clearSequence on the layer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The browser gate is one pass over the remaining flows: rail strip and
expand, coverage with fixtures, the ON/OFF pill, 360° and SINCE filters,
KEY REJECTED, nearest photo with caption, × close, phone fit, keyless
KEY REQUIRED on a second page and no page errors. Panel controls are
pressed through the DOM; pick-point search, line/cone clicks, the
framing-cancel, reload/share and status-route probes, the second
viewport and pointer-settling retries are gone (unit tests cover them).
Tests that pinned constants, copy, CSS or source text, or repeated
another test, are removed or merged. CSS loses an unused variable and
duplicate rules. Docs describe the slimmed feature; unused exports and
the layer's dead setCoverageFilter/refreshCoverage surface are removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The layer now owns its Mapillary parts directly: the provider registry,
pick router, multi-provider UI summary, params codec helper and provider
context are gone. createStreetLevelLayer({ source }) builds coverage,
sequences, the nearest-image lookup and the MapillaryJS viewer itself; a
single providerOn switch keeps the share link's `mapillary` option (off
draws nothing), and getParams/setParams keep { mapillary, pano, sinceDays }.
No z0-5 overview points: above 60 km nothing is drawn and the panel says to
zoom in; the proxy now serves only coverage z11-14. Tiles come from the
visible ground within range of the camera, ranked from the ground under it
(no line-of-sight ranking or near-range padding). MapillaryJS loads on the
first photo; viewer prewarm is removed. Key status, a rejected key and a
rate limit share one `blocked` state on coverage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
- A photo takes a deferred navigation ticket when it starts opening
(begin/reassert, the application's _beginDeferredNavigation and
_reassertNavigationHandoff) and frames only if that ticket still owns
the camera once loaded, so a slow photo no longer flies the camera back
over newer navigation or clears newer aircraft tracking.
- A failed mesh refresh keeps the last good sample and the distance it was
taken from, and is retried after the miss cooldown, instead of recording
the failed distance and never refreshing again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Photo framing and FOLLOW claim the camera through the application's
navigation, injected with attachNavigation({ run, subscribeHandoff }):
aircraft and satellite tracking is released first, the cockpit refuses,
and FOLLOW stops when another feature takes the camera. No UI imports
in the layer.
- The mesh sampler follows the mesh floor's rules: it waits for the
visible tileset to finish streaming (visibleTilesetLoaded, now exposed
by the mesh floor service), needs a real bare-earth prior and keeps to
the shared meshFloorSampleWithinPrior window, which cameraFollow now
uses too. A cell is probed again once the camera is half as far from
it (down to 40 m), so a coarse early sample is replaced and redrawn.
- The MCP globe panel's CSP allows the Mapillary Graph API and its image
CDN (https://*.xx.fbcdn.net).
- The tile proxy reads bodies with the shared readResponseBytesCapped.
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 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]>
The raycast challenge on #768 showed bare-earth casting misplacing roads
that are not at grade: lines floated over freeways in trenches and steep
streets (rendered line pixels floating: Austin 8.5%, SF 27.1%, LA 43.8%),
because a ~111 m terrain grid cannot see a 30 m-wide cut or a slope between
its corners.
meshSampler.js samples the rendered Google 3D surface (scene.sampleHeight,
overlays excluded) once per ~11 m cell within 900 m of the camera, nearest
first, in idle time within a 6 ms budget; misses are retried, not latched.
groundCast.refineHeights blends it in, per point along a line:
- mesh up to 3 m above bare earth is the road: follow it, 1 m above;
- higher mesh (canopy, deck, roof): carry the road's offset from bare earth
across from the road samples on both sides (up to 8 points);
- otherwise bare earth + 2 m, as before.
Lines, cones and the marker use it; tiles touched by new samples are
redrawn one per idle slice with the no-blink swap.
Rendered line pixels on the surface / floating, before -> after:
Austin 90.0 -> 95.3% / 8.5 -> 0.7%; SF 69.0 -> 86.4% / 27.1 -> 9.1%;
LA 52.3 -> 91.0% / 43.8 -> 4.0%; Innsbruck 95.0 -> 95.7% / 0.2 -> 0.2%;
Sacramento 80.7 -> 79.3% / 3.8 -> 2.7%; buried lines stay under 1%.
Cones floating in SF 44 -> 5%, buried at LA 59 -> 28%. Main-thread long
tasks added during refinement: at most 134 ms (Austin, densest tile).
Roads on elevated decks are still drawn at ground level under the deck.
Unit tests cover the blend (trench, slope, canopy bridge, deck, bad probe),
the finer cast with meshAt, and the sampler (once per cell, nearest first,
overlays excluded, far cells skipped, misses not latched, idle when off).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the code review of #768:
- view.js measured the camera's height above ground with globe.getHeight,
but Google 3D hides the globe, so coverage picked its zoom from the
ellipsoidal height. Over a high city (Denver, ~1,600 m) a camera 500 m up
read 2,100 m: coverage stayed at z12/z13, whose tiles are too big to cast,
and lines stayed draped on the roofs. With the globe hidden, the bare-earth
height from the ground caster now answers; coverage re-picks its zoom when
the surface mode changes. Verified in Denver: terrain mode, z14, cast lines.
The core's surface check uses the same helper (and no longer subtracts the
2 m lift from the camera height).
- castTile rebuilt a tile even when no line could be cast (terrain proxy
down, tile too big); it now leaves such tiles as they are.
- The feature gate read Street Level's share token as a literal `0`; it now
takes it from LAYER_STATE_REGISTRY, since a rebase can reassign it.
New view.test.mjs covers the shown-globe, hidden-globe and no-sample cases.
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]>
FOLLOW puts the globe camera at eye height inside the street-level view; on
flat imagery or terrain there is nothing to stand among. The layer now takes
the application map stack controller (attachMapStackController), subscribes
to stack switches, offers FOLLOW only while the stack is 'photoreal', disables
the button with a MAP SOURCE hint elsewhere, and stops following when the
stack leaves Google 3D. Selection uninstall is a no-op when never installed.
The QA gate checks the disabled path and, when Google 3D is available, that
leaving it stops following; its caption-timeout dump now forces one render to
tell a missed update from a render fault.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Only a user's own collapse docks a floating window; cockpit entry,
restores and the Cyber accordion no longer dock it or delete its saved
place, and the accordion leaves floating windows alone.
- The viewer host records an adapter as active only after its mount
succeeds, so a library that failed to load once is retried; concurrent
opens share one mount, and unmounting mid-mount leaves nothing active.
- openNearest tries every active provider; one provider's error (no key,
offline) no longer hides the others' imagery.
- The SINCE readout shows its cut-off date as soon as the committed value
matches the thumb, even while the slider keeps focus.
- A floating window that was only dragged is bounded to the viewport and
scrolls instead of running off-screen.
- getStats summarises coverage (summarizeCoverage) instead of building a
full UI snapshot every second; disable() uses credits.hideAll(); smaller
cleanups in the viewer host and sequence snapshot.
- Unit tests: panel docking and header double press, viewer-host mount
retry/sharing/cancel, coverage summary.
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]>
Pure move: the layer id, panel id, CSS prefix, classes, exports and config
paths become provider-neutral (street-level, #street-level-panel, sl-);
the Mapillary-specific modules move under providers/mapillary/. Behaviour
is unchanged. The two sprite collections now register under distinct ids
instead of overwriting each other under 'mapillary'.
Co-Authored-By: Claude Fable 5.1 <[email protected]>