diff --git a/CHANGELOG.md b/CHANGELOG.md index 744451e76..1cff5f3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,16 +71,14 @@ proposed diagnosis in #927, and [kvnloo](https://github.com/kvnloo) for identifying the repair race. Windows/Claude Desktop Cowork confirmation remains outstanding. -- Street Level: Mapillary street-level imagery modelled on the iD editor's - photo overlay (free client token, CC BY-SA 4.0 imagery). A docked right-rail - panel holds an ON/OFF pill, 360°/flat and captured-since filters, a legend - and an embedded viewer with full-screen EXPAND and FIT/FILL. Coverage lines - (below 60 km) and image cones drape on the terrain and Google 3D tiles; +- Street Level: Mapillary street-level imagery (free client token, CC BY-SA + 4.0). A right-rail panel holds an ON/OFF pill, 360°/flat and captured-since + filters, a legend and an embedded viewer with EXPAND and FIT/FILL. Coverage + lines (below 60 km) and image cones drape on terrain and Google 3D tiles; opening a photo frames it with one camera flight. Share links carry the switch, the filters and the panel's collapsed state. A server proxy adds the - token to coverage-tile requests, trims the tiles and caches them in memory - for 24 h; it refuses cross-site requests and upstream redirects, rate-limits - per IP and runs at most six Mapillary fetches at once. The panel reads + token to coverage tiles, trims and caches them for 24 h, refuses cross-site + requests and redirects, and limits load per IP and upstream. The panel reads KEY REQUIRED without a token and KEY REJECTED when Mapillary refuses it. - Add Norway's road cameras from Statens vegvesen as a CCTV pack. The camera diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ff596696..7e1adaf67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,10 +66,9 @@ gate for the feature you changed. > **CI does not cover this for you.** The workflow runs the setup policy > checks, the formatting and package-boundary checks, the unit suite and the > production build, plus a Windows onboarding job. It runs neither -> `npm run test:track` nor any other `qa-*.mjs` gate — both need a live dev -> server and a browser. The one exception is the hermetic Street Level fixture -> gate (`street-level-browser` job). Include the applicable local run in your PR's validation -> evidence. +> `npm run test:track` nor any `qa-*.mjs` gate except the Street Level fixture +> gate (`street-level-browser` job) — they need a live dev server and a +> browser. Include the applicable local run in your PR's validation evidence. ## Share-link layer tokens diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 8213c4859..da9d7027b 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3028,9 +3028,8 @@ Street Level is a docked, collapsible right-rail panel (`#street-level-panel`, layer token `0`, option owner `street-level`, panel `ui` token `t`) that starts collapsed. It opens itself only when the user, voice or a tool switches the layer on or a photo opens, never on a restore, and those opens are not stored. -Like the iD editor's photo overlay it shows Mapillary only: 360°/flat and -captured-since filters (relative days, so a link keeps its meaning), one -embedded viewer and an on-globe credit while the layer is on. Share options: +It has 360°/flat and captured-since filters (relative days, so a link keeps +its meaning), one embedded viewer and an on-globe credit while the layer is on. Share options: `m` (Mapillary on/off; off draws nothing), `p` (`a`/`p`/`f` panoramas), `s` (since, days). The header pill is the only layer switch. @@ -3047,9 +3046,8 @@ pauses them for its Retry-After. The server proxy (`server/providers/mapillary.js`) adds the token to coverage-tile requests (the browser holds it too, for MapillaryJS and the Graph API), serves only z11–14 coverage, refuses upstream redirects, strips the -unused `image` layer, coalesces concurrent requests, runs at most six Mapillary -fetches at once (a fetch every client has left is cancelled, or never started) -and keeps tiles in a 24 h memory cache. +unused `image` layer, shares one fetch per tile, runs at most six at once +(cancelling those every client has left) and keeps tiles in memory for 24 h. MapillaryJS loads on the first photo. Opening one frames it with a single camera flight through the application's deferred navigation (`begin` / @@ -3064,12 +3062,10 @@ Mapillary back on. At phone width the viewer height comes from the rail band so the whole photo fits. Browser gates: `npm run qa:street-level -- --url ` runs against real -Mapillary with a token; `npm run qa:street-level:fixtures` answers every -Mapillary request from fixtures (`providers/mapillary/coverageFixture.mjs`, -`scripts/fixtures/street-level/`); its server (default `:4173`) still needs a -`MAPILLARY_CLIENT_TOKEN`, and any value will do. CI runs the fixture gate with -`--strict` against a production build served by `vite preview` with a dummy -token. +Mapillary; `npm run qa:street-level:fixtures` answers every Mapillary request +from fixtures (`providers/mapillary/coverageFixture.mjs`, +`scripts/fixtures/street-level/`), but its server (default `:4173`) still needs +some `MAPILLARY_CLIENT_TOKEN`. CI runs it `--strict` against `vite preview`. Directions is a keyless front end to the routing the voice agent already uses. Its row chips are the whole interface: DRIVE / WALK / BIKE pick the diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md index e41623cc6..6afd37839 100644 --- a/docs/KNOWN-ISSUES.md +++ b/docs/KNOWN-ISSUES.md @@ -57,9 +57,8 @@ Related keys (current versions): - Panel collapsed state: `godsEyeView.v6.panelCollapsed.` — `'0'` open, `'1'` closed, absent means the panel's own default. A view opened from a share link ignores the stored value entirely. -- Panel positions: `godsEyeView.v8.panelPos.` — the versioned name for a - stored position. The current layout writes none, so deleting one changes - nothing. +- Panel positions: `godsEyeView.v8.panelPos.` — the current layout + writes none. - CCTV calibration: `godsEyeView.cctv.calibration.v2` --- diff --git a/server/providers/mapillary/tiles.js b/server/providers/mapillary/tiles.js index f244b4985..76971bb66 100644 --- a/server/providers/mapillary/tiles.js +++ b/server/providers/mapillary/tiles.js @@ -14,10 +14,7 @@ const DROP_LAYERS = ['image']; /** A z14 tile with its image layer is ~11 MB; anything past this is wrong. */ export const TILE_MAX_BYTES = 48 * 1024 * 1024; const FETCH_TIMEOUT_MS = 60_000; -/** - * Upstream fetches at once; more wait their turn. A fetch every client has - * left is cancelled, or never started, so the queue holds live tiles only. - */ +/** Upstream fetches at once; more wait their turn. */ export const TILE_UPSTREAM_CONCURRENCY = 6; /** Tiles change only when new imagery is processed. */ export const TILE_TTL_MS = 24 * 60 * 60 * 1000; diff --git a/src/layers/streetLevel/cameraFraming.js b/src/layers/streetLevel/cameraFraming.js index 432475411..c6bc9327a 100644 --- a/src/layers/streetLevel/cameraFraming.js +++ b/src/layers/streetLevel/cameraFraming.js @@ -4,10 +4,8 @@ import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js'; /** Fly the globe camera once to frame a photo after it opens. */ export function createCameraFraming({ state }) { /** - * The application's camera authority (`attachNavigation`): `begin(noun)` - * takes a deferred ticket (a generation, or false when refused) that - * `reassert(generation)` honours, releasing tracking, only if nothing newer - * took the camera. Without it, framing flies directly. + * The application's camera authority: `reassert(begin())` succeeds only if + * nothing newer took the camera. Without it, framing flies directly. */ let navigation = null; /** Our framing flight while it is current; Cesium calls `cancel` when another flight starts. */ diff --git a/src/layers/streetLevel/index.js b/src/layers/streetLevel/index.js index 9f99883a9..061344ae4 100644 --- a/src/layers/streetLevel/index.js +++ b/src/layers/streetLevel/index.js @@ -218,11 +218,7 @@ export function createStreetLevelLayer({ }; }, - /** - * The application's camera authority, the deferred `begin(noun)` / - * `reassert(generation)` pair: a photo claims the camera when opening - * starts and frames only if it still owns it once loaded. - */ + /** A photo frames itself only if it still owns the camera once loaded. */ attachNavigation(navigation) { parts.framing.attachNavigation(navigation); }, diff --git a/src/layers/streetLevel/view.js b/src/layers/streetLevel/view.js index b58388c51..0fe86c344 100644 --- a/src/layers/streetLevel/view.js +++ b/src/layers/streetLevel/view.js @@ -31,10 +31,9 @@ export function sampledSurfaceHeight(scene, carto) { } /** - * Ellipsoidal height (m) of the ground under the camera, or null. The globe - * answers where shown; Google 3D hides it, so the rendered surface does - * (roofs count: a building's height at most). A sample above the camera is - * ignored: a raised ground would put the camera underground. + * Ellipsoidal height (m) of the ground under the camera, or null. Under + * Google 3D (globe hidden) the rendered surface counts, minus any roof above + * the camera. */ export function groundUnderCamera(viewer) { const carto = viewer?.camera?.positionCartographic; @@ -81,11 +80,9 @@ function groundHit(camera, point, ellipsoid, maxRange) { const BBOX_GRID = 5; /** - * Visible [west, south, east, north] degrees, or null when fewer than four - * rays hit. Only hits count, so a horizon cannot inflate the box to the world. - * `groundHeight` raises the ellipsoid to the ground (rays to the bare - * ellipsoid under Denver land kilometres too far out) and `maxRange` (m) - * drops near-horizon hits. + * Visible [west, south, east, north] degrees from the rays that hit the + * ground, or null when fewer than four do. `groundHeight` raises the ellipsoid + * to the ground (Denver is 1,600 m up); `maxRange` (m) drops far hits. */ export function visibleBbox( viewer, @@ -158,10 +155,8 @@ export function cameraNadir(viewer) { } /** - * Where coverage tiles are ranked from: halfway between the ground under the - * camera and the ground at the centre of the screen, so a tilted view loads - * what it looks at as well as what is near. The nadir when the centre ray - * misses the ground within `maxRange`. Options as `visibleBbox`. + * Where coverage tiles are ranked from: halfway between the nadir and the + * screen centre's ground, or the nadir when that is out of `maxRange`. */ export function viewFocus( viewer, diff --git a/src/layers/streetLevel/viewerHost.js b/src/layers/streetLevel/viewerHost.js index b1c618c9b..3a11ffb18 100644 --- a/src/layers/streetLevel/viewerHost.js +++ b/src/layers/streetLevel/viewerHost.js @@ -1,10 +1,6 @@ import { freshStreet } from './state.js'; -/** - * Owns the panel's viewer element and the photo viewer mounted in it (the - * MapillaryJS adapter, or a stand-in). Its poses update `state.street` and - * the marker; MapillaryJS loads on the first open. - */ +/** The panel's photo viewer: its poses update `state.street` and the marker. */ export function createViewerHost({ state, parts, adapter }) { /** Set once mounted, until `unmount`. */ let unsubscribe = null; diff --git a/src/tooling/mapillaryProvider.test.mjs b/src/tooling/mapillaryProvider.test.mjs index c0b3b2368..ca0425742 100644 --- a/src/tooling/mapillaryProvider.test.mjs +++ b/src/tooling/mapillaryProvider.test.mjs @@ -45,7 +45,7 @@ const UNTRIMMED = tile([ { name: 'image', payload: Buffer.alloc(4000, 1) }, ]); -/** Mount the plugin and return `call(route, url, method, headers)`. */ +/** Mount the plugin and return `call(route, url, method, headers, onResponse)`. */ function install(mode = 'configureServer') { const routes = new Map(); mapillaryProxy()[mode]({ diff --git a/src/ui/panelPositionControls.js b/src/ui/panelPositionControls.js index 812122abe..1f67eb655 100644 --- a/src/ui/panelPositionControls.js +++ b/src/ui/panelPositionControls.js @@ -79,9 +79,8 @@ export class PanelPositionControls { this._restorePanelPosition(spec.id, spec.panel); this._makePanelDraggable(spec.id, spec.panel, spec.handle); } - // Keep a positioned panel on-screen when its HEIGHT changes after restore — it expands to its - // full row set a frame or two later, so the restore-time clamp used a stale (shorter) height and - // the panel could still hang off the bottom (audit U2). Re-clamp on every size change. + // A restored panel grows a frame or two later, so the restore-time clamp + // is stale: re-clamp on every size change. if (this._ppToggles && typeof ResizeObserver !== 'undefined') { this._draggableResizeObserver = new ResizeObserver(() => this._reclampDraggablePanels(), @@ -159,8 +158,7 @@ export class PanelPositionControls { const pos = JSON.parse(raw); if (!pos || typeof pos.left !== 'number' || typeof pos.top !== 'number') return; - // Clamp to the viewport: a position saved at one window size would otherwise land off-screen at - // another (audit U2 — observed a panel at x:-192). The drag handler clamps; restore must too. + // A position saved at another window size can land off-screen. const { left, top } = this._clampToViewport( Math.round(pos.left), Math.round(pos.top), diff --git a/src/ui/streetLevelControls.js b/src/ui/streetLevelControls.js index 5988963ab..140ddef72 100644 --- a/src/ui/streetLevelControls.js +++ b/src/ui/streetLevelControls.js @@ -105,10 +105,8 @@ export class StreetLevelControls { this._setParams({ sinceDays: sinceDays() }), ); this.listen(el.viewerClose, 'click', () => this.layer.closeViewer?.()); - // EXPAND is the browser's own fullscreen: it handles Esc, focus and the - // rest of the page. iPhone Safari has no element fullscreen, so there the - // viewer opens as a top-layer popover over the page instead (Esc and the - // button close it). With neither, the button is not offered. + // EXPAND is the browser's fullscreen. iPhone Safari has no element + // fullscreen, so there it is a top-layer popover; with neither, no button. this._expandMode = typeof el.viewerWrap?.requestFullscreen === 'function' ? 'fullscreen' @@ -195,9 +193,8 @@ export class StreetLevelControls { async _toggleEnabled() { const enabled = this.actions.isEnabled?.() === true; - // The pill is the only switch. A share link or tool can switch Mapillary - // off under a layer that is on; the pill then reads OFF, and switching on - // brings Mapillary back (and the layer, if it is off too). + // The pill is the only switch: it also brings back Mapillary that a link + // or tool switched off under a layer that is on. if (this._state?.providerOn === false) { this._setParams({ mapillary: true }); if (enabled) return; @@ -252,11 +249,7 @@ export class StreetLevelControls { this._syncExpanded(); } - /** - * While expanded, leave when Clean View, recording or the cockpit starts: - * they hide every panel, and the photo would stay over the globe (and in a - * recording). - */ + /** Clean View, recording and the cockpit hide every panel, so they end EXPAND too. */ _watchPanelHidingModes(on) { const body = document.body; if (!on || !body) {