diff --git a/.env.example b/.env.example index 102b447b7..55cb367a0 100644 --- a/.env.example +++ b/.env.example @@ -207,6 +207,7 @@ AISSTREAM_API_KEY= # CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams # CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL # CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras +# ONTARIO_511_API_KEY= # free developer key: sign up at 511on.ca/my511/register, then request it at 511on.ca/developers/doc # CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511 # CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack # CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog @@ -227,6 +228,9 @@ AISSTREAM_API_KEY= # CCTV_CALGARY_ROWS_URL= # override the Open Calgary catalog URL # CCTV_CATALONIA_ENABLED=1 # set 0 to disable the Servei Català de Trànsit cameras # CCTV_CATALONIA_MAX_SOURCES=150 # cap cameras loaded from Catalonia (~130 exist today) +# CCTV_HK_ENABLED=1 # set 0 to disable the Hong Kong Transport Department cameras +# CCTV_HK_MAX_SOURCES=1100 # cap cameras loaded from Hong Kong (~1,013 exist today); min 8, max 1200 +# CCTV_HK_CAMERAS_URL= # override the DATA.GOV.HK camera locations XML URL # CCTV_KINGCOUNTY_ENABLED=1 # set 0 to disable the King County (Washington) road cameras # CCTV_KINGCOUNTY_MAX_SOURCES=125 # cap cameras loaded from the King County catalog # CCTV_WSDOT_ENABLED=1 # set 0 to disable the WSDOT (Washington State) highway cameras @@ -246,6 +250,9 @@ AISSTREAM_API_KEY= # CCTV_LITHUANIA_MAX_SOURCES=400 # cap Lithuanian cameras; no API key required # CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent; see DATA_SOURCES.md) # CCTV_FLORIDA_MAX_SOURCES=500 # cap cameras loaded from the FL511 catalog (nearest regional anchors first) +# CCTV_TAIWAN_ENABLED=1 # set 0 to skip Taiwan Freeway Bureau (MOTC) road cameras +# CCTV_TAIWAN_MAX_SOURCES=600 # cap Taiwan freeway cameras (1-2000); no API key required +# CCTV_TAIWAN_URL= # override the Freeway Bureau MOTC 2.0 CCTV.xml URL # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 491f2468d..ce3563685 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,6 @@ jobs: timeout-minutes: 30 env: GEV_REQUIRE_ALLOCATION_GATE: '0' - # Fail on a missed header press unless the panel itself moved. - QA_FAIL_ON_RETRY: '1' steps: - name: Check out source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -104,10 +102,6 @@ jobs: - name: Street Level gate with Mapillary fixtures run: npm run qa:street-level:fixtures -- --strict --url http://localhost:4173 - - name: Portable panel gate - if: success() || failure() - run: npm run qa:panel-resize -- --url http://localhost:4173 - - name: Preview server log if: failure() run: cat preview.log diff --git a/.gitignore b/.gitignore index d2f0107c0..11e081e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ output/ .gev-cache/ # Browser QA gate evidence (screenshots, console and network dumps). qa-artifacts/ +# Playwright MCP session logs: browser consoles can carry tokens in URLs. +.playwright-mcp/ +# Local Claude Code settings and preview configs. +.claude/ .DS_Store .gstack/ 3d-models/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d32365f..0ad3a7368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -317,9 +317,8 @@ [NotProniss](https://github.com/NotProniss) (#1084). - Street Level: Panoramax is the fourth provider, keyless. Light its chip - (orange; off by default, one provider at a time): from orbit dots show - where the federated commons has pictures, and below about 60 km its - sequences draw as lines from `api.panoramax.xyz` vector tiles. A click on a + (orange; off by default, one provider at a time): below about 60 km the + federated commons' sequences draw as lines from `api.panoramax.xyz` vector tiles. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it and ‹ › / ← → following the picture's own links. The 360° and since filters apply, @@ -377,10 +376,10 @@ Maps JavaScript API) and click a street, or press OPEN STREET VIEW, to open Google's own panorama in the panel, with its arrows, the address and month in the caption, a link - to Google Maps and FOLLOW. Share links carry it in the provider switches + to Google Maps. Share links carry it in the provider switches (`r.google-1`). One provider is lit at a time (lighting one switches the other off), Mapillary by default since it is free. Street View's panoramas - are all 360°, so the IMAGERY and SINCE filters and the legend apply to + are all 360°, so the IMAGERY and SINCE filters apply to Mapillary only and hide while GOOGLE STREET VIEW is lit; its globe marker is a Google-blue view cone. @@ -419,21 +418,16 @@ proposed diagnosis in #927, and [kvnloo](https://github.com/kvnloo) for identifying the repair race. Windows/Claude Desktop Cowork confirmation remains outstanding. -- Street Level: a street-level imagery layer modelled on the iD editor's photo - overlay, with Mapillary as its first provider (free client token, CC BY-SA 4.0 - imagery). One right-rail panel holds a chip per provider, shared 360°/flat and - captured-since filters, and an embedded viewer with EXPAND, FIT/FILL and - FOLLOW (Google 3D only). Coverage lines and image cones are drawn from orbit - down to a single street; on Google 3D they sit on the bare earth, refined - against the rendered mesh near the camera, so trees and buildings hide them. - Share links carry the provider switches, the filters and the panel's collapsed - state. A server proxy caches Mapillary coverage tiles (24 h, 1 GiB on disk), - refuses cross-site requests, rate-limits per IP and backs off when Mapillary - rejects the token or rate-limits. - -- The CCTV and Street Level panels are portable: drag the header to float the - panel, resize it from any edge or corner, and double-click the header to dock - it again. The position persists across reloads. +- Street Level: street-level imagery from providers, Mapillary first (free + client token, CC BY-SA 4.0). A right-rail panel holds a chip per provider, + 360°/flat and captured-since filters 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 provider switches, the filters and the panel's collapsed + state. A server proxy adds the token to Mapillary 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 city bikes to the Bike Share layer: Oslo, Bergen and Trondheim Bysykkel (Urban Sharing), Kolumbus Bysykkel in Stavanger and diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba0d38768..442b9c1bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,9 +54,8 @@ head -40 scripts/qa-radio.mjs # what this runnable gate proves and how to run i Runnable gate entrypoints document what they assert and how to invoke them. Most need a dev server; some also need a specific provider key or port. Follow the entrypoint's header rather than assuming every matching file accepts the -same arguments. Five have an `npm run` alias: `qa:map-source-tray`, -`qa:panel-resize`, `qa:street-level`, `qa:street-level:fixtures` and -`qa:transit`. +same arguments. Four have an `npm run` alias: `qa:map-source-tray`, +`qa:street-level`, `qa:street-level:fixtures` and `qa:transit`. If you aren't sure which gate covers your change, search `docs/CURRENT-STATE.md` for the feature: it names the gate for many of them, and it's the authoritative @@ -66,11 +65,10 @@ gate for the feature you changed. > **CI covers only two of these 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, and against the built preview -> it runs `qa:street-level:fixtures` and `qa:panel-resize`. It does not run -> `npm run test:track` or any other `qa-*.mjs` gate — those need a live dev -> server and a browser. Include the applicable local run in your PR's -> validation evidence. +> production build, plus a Windows onboarding job. It runs neither +> `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/DATA_SOURCES.md b/DATA_SOURCES.md index c99ec28d1..61b971b71 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -20,10 +20,10 @@ How to read this: | **NOAA nowCOAST observed weather** (`nowcoast.noaa.gov/geoserver/observations/{weather_radar,satellite}/ows`, WMS 1.1.1) | Rain radar (MRMS base reflectivity, contiguous US, about 4-minute updates) and Satellite clouds (GOES-19/18 Band 14 regional infrared, about 5-minute updates; global infrared mosaic, hourly). Keyless; layer metadata refreshed every 2 minutes, exact-time images cached for up to 24 hours | [NOAA disclaimer](https://oceanservice.noaa.gov/disclaimer.html) | "NOAA nowCOAST · NWS/OAR MRMS radar; NESDIS GOES and global satellite partners" | | **NOAA nowCOAST lightning density** (`nowcoast.noaa.gov/geoserver/observations/lightning_detection/ows`) | Lightning density: 15-minute accumulated strike density on an approximately 8 km grid, Americas and Pacific. Keyless; layer metadata refreshed every 10 minutes, exact-time images cached for up to 24 hours | NOAA Level-5 derived product from Vaisala NLDN/GLD360; the [product description](https://ocean.weather.gov/lightning/lightning_pdd.php) permits public distribution of this density product, not of raw Vaisala detections | "NOAA/NWS nowCOAST; derived from Vaisala NLDN/GLD360" | | **NOAA NHC / CPHC advisories** (`www.nhc.noaa.gov/CurrentStorms.json` + the `mapservices.weather.noaa.gov` tropical weather summary MapServer) | Cyclone advisories: storm positions, forecast tracks, points and cones for the Atlantic and eastern/central North Pacific. Keyless through `/api/cyclones`; cached for 5 minutes | [NWS public-data terms](https://www.weather.gov/disclaimer); no endorsement implied | "NOAA/NWS NHC / CPHC" | -| **Mapillary** (vector tiles, Graph API, MapillaryJS) | Street Level: coverage lines, image cones and the embedded viewer. Coverage tiles cached for 24 h | Imagery [CC BY-SA 4.0](https://www.mapillary.com/terms); API under the Mapillary terms with a client token | "© Mapillary contributors, CC BY-SA 4.0", shown on the globe while the provider is on | +| **Mapillary** (vector tiles, Graph API, MapillaryJS) | Street Level: coverage lines, image cones and the embedded viewer. Coverage tiles kept in server memory for 24 h | Imagery [CC BY-SA 4.0](https://www.mapillary.com/terms); API under the Mapillary terms with a client token | "© Mapillary contributors, CC BY-SA 4.0", shown on the globe while the provider is on | | **Google Street View** (Maps JavaScript API) | Street Level, on the Google 3D map only (Google's terms): Google's panorama viewer and nearest-panorama lookups; panorama dates and addresses kept for the session only | Google Maps Platform ToS (your own key + billing; every panorama load is billed) | "Street View imagery © Google" on the globe while the provider is on, and Google's logo and terms in the viewer | | **KartaView** (`api.kartaview.org` coverage tiles and photo API, `cdn.kartaview.org` image proxy, `storage*.openstreetcam.org` photos) | Street Level: coverage lines (z13–14 JSON tiles; with the imagery filter on, KartaView's filtered PNG tiles as a mask), nearest-photo lookups, the photo's sequence and the photo itself, fetched by the browser. Keyless; nothing cached beyond the session | Imagery [CC BY-SA 4.0](https://kartaview.org/terms) (KartaView terms §4); the public API needs no token | "© KartaView contributors, CC BY-SA 4.0", shown on the globe while the provider is on | -| **Panoramax** (`api.panoramax.xyz` meta-catalogue: vector tiles and STAC search; pictures from the federated instance holding each one, e.g. `panoramax.openstreetmap.fr`, `panoramax.ign.fr`) | Street Level: coverage lines and density points, nearest-picture lookups, a sequence's pictures as cones, and the picture itself, fetched by the browser. Keyless; tiles cached by the browser for an hour (the API's own header) | Per picture: mostly [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), IGN's [Licence Ouverte 2.0](https://www.etalab.gouv.fr/licence-ouverte-open-licence/); the API needs no token | "© Panoramax contributors, licence per picture" on the globe while the provider is on; the caption names each picture's author and licence | +| **Panoramax** (`api.panoramax.xyz` meta-catalogue: vector tiles and STAC search; pictures from the federated instance holding each one, e.g. `panoramax.openstreetmap.fr`, `panoramax.ign.fr`) | Street Level: coverage lines, nearest-picture lookups, a sequence's pictures as cones, and the picture itself, fetched by the browser. Keyless; tiles cached by the browser for an hour (the API's own header) | Per picture: mostly [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), IGN's [Licence Ouverte 2.0](https://www.etalab.gouv.fr/licence-ouverte-open-licence/); the API needs no token | "© Panoramax contributors, licence per picture" on the globe while the provider is on; the caption names each picture's author and licence | | **OpenStreetMap** | Map and place data (roads, military areas and names, ALPR, place search, outlines); via vector tiles, hourly ALPR extract, bundled data, Nominatim and optional self-configured Overpass | [ODbL 1.0](https://opendatacommons.org/licenses/odbl/1-0/) | © OpenStreetMap contributors | | **Google Map Tiles API** (Photorealistic 3D Tiles) + Places/Geocoding | The 3D globe, voice scene context, and on-demand nearby installation search | Google Maps Platform ToS (proprietary, your own key + billing) | "Google" / "Google Maps" logo — **shown in-app**, required | | **OpenSky Network** | Primary worldwide live-flight snapshot | Non-commercial research/education license | Schäfer et al., _"Bringing Up OpenSky"_, IPSN 2014 + opensky-network.org | @@ -63,6 +63,7 @@ How to read this: | **Servei Català de Trànsit** (+ Ajuntament de Barcelona, Ajuntament de Terrassa) | CCTV camera catalog + frames, Catalonia | [Llicència oberta d'ús d'informació - Catalunya](https://web.gencat.cat/ca/generalitat/dades-indicadors/dades-obertes/llicencies) — attribution REQUIRED | "Servei Català de Trànsit"; partner-supplied cameras also name their owner | | **King County Road Services** (`services.arcgis.com`, King County GIS) | CCTV camera catalog + frames, unincorporated King County WA road network | [KCGIS Center terms](https://kingcounty.gov/en/dept/kcit/data-information-services/gis-center/about/terms-conditions-copyrights) — attribution required; public ArcGIS layer | "King County Road Services" (partner-city cameras name their owner) | | **WSDOT** (`data.wsdot.wa.gov`) | CCTV camera catalog + frames, Washington State highways | [WSDOT travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer) — public "low volume" feed; catalog cached, frames on WSDOT's own image host only | "Washington State Department of Transportation (Travel Center)" (courtesy) | +| **Hong Kong Transport Department / DATA.GOV.HK** | CCTV camera catalog + frames, Hong Kong traffic snapshot cameras | [DATA.GOV.HK Terms of Use](https://data.gov.hk/en/terms-and-conditions) — attribution REQUIRED | "Hong Kong Transport Department / DATA.GOV.HK" | | **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" | | **Kartverket** (`ws.geonorge.no/hoydedata/v1/punkt`) | Terrain heights (NN2000) used as the ground prior for Statens vegvesen cameras | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — attribution REQUIRED | "© Kartverket", modified (heights over water set to 0 m, rounded to 0.1 m) | | **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) | @@ -112,6 +113,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei **Quebec is thinly represented**, and that is upstream rather than a filter here: the province runs its own Info-Smog program, so only a handful of Quebec stations appear in ECCC's national feed. The reporting station count also varies hour to hour as stations report or fall silent. - **Via Lietuva / Eismoinfo (Lithuania).** Public road-camera catalogs and periodically updated still images from [Eismoinfo](https://eismoinfo.lt/), credited to Via Lietuva / eismoinfo.lt in the app. **Off by default.** The camera endpoints are the eismoinfo.lt website backend and are not among the services listed in Via Lietuva's [open-data statement](https://eismoinfo.lt/eismoinfo-backend/content?id=44), whose conditions are attribution and reuse on the same terms. Until the statement is confirmed to cover the camera photos, the pack loads only when an operator sets `CCTV_LITHUANIA_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to Eismoinfo. `eismoinfo-backend/camera-info-table` is joined by ID to `layer-static-features/VKR`. Without `lks=true`, the latter returns WGS84 `[latitude, longitude]`; the table's `x/y` instead use LKS-94 metres. Only the official HTTPS `/eismoinfo-backend/image-provider/camera/last?id=…` image endpoint is registered. Catalogs use the shared 15-minute cache; frames are fetched on demand and never bundled or archived. These are periodic snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. `CCTV_LITHUANIA_MAX_SOURCES` defaults to 400. +- **Freeway Bureau, MOTC (Taiwan).** National freeway cameras from the Freeway Bureau's keyless [CCTV static table](https://tisvcloud.freeway.gov.tw/history/motc20/CCTV.xml) in the MOTC 2.0 standard, refreshed daily and fetched at runtime (never bundled). Freeway Bureau traffic data is published under the [Open Government Data License, Taiwan (Version 1.0)](https://data.gov.tw/license), which permits reuse, including commercial reuse, with attribution; the app credits the Freeway Bureau, MOTC, names the dataset and links the licence, as its attribution exhibit asks. The table publishes each camera's MJPEG stream on one of four official camera servers (`cctvn`, `cctvn5`, `cctvc`, `cctvs` under `freeway.gov.tw`). The frame proxy relays single images, so each published stream URL is mapped to the same server's single-JPEG path for the same camera number (`/abs2mjpg/bmjpg` → `/abs2mjpg/jpg`, `/live-view/mjpg/video.cgi` → `/live-view/jpg/image.cgi`); only those exact HTTPS pairs are registered, and the still path is not itself listed in the table. These are on-demand snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. The table host does not answer from some networks outside Taiwan (the camera servers do); there the pack is empty, logs one warning and retries hourly. On by default; `CCTV_TAIWAN_ENABLED=0` skips the pack, and `CCTV_TAIWAN_MAX_SOURCES` defaults to 600 (bounded to 1–2000). - **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)). - **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default. @@ -154,7 +156,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. - **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state. - **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover. -- **Ontario 511 CCTV.** The camera list comes from the keyless `511on.ca/api/v2/get/cameras` endpoint; frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. +- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. Frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does. - **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms. - **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch. - **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel. @@ -168,6 +170,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **Catalonia (Servei Català de Trànsit) cameras.** The camera list is the keyless `https://www.gencat.cat/transit/opendata/cameres.xml` WFS/GML feed, fetched over https with redirects refused and the body read capped at 1 MiB, since the list decides ids, coordinates and which allowlisted frame path is proxied. It carries a `font` field naming the real publisher per camera; this pack keeps SCT's own highway cameras plus hotlinked Barcelona (IMI) and Terrassa municipal cameras (~130 total), each confirmed to return a real frame, so frame URLs are checked against a small host allowlist (`mct.gencat.cat`, `www.bcn.cat`, `emap.terrassa.cat`) rather than one pinned origin (a link carrying `user:pass@` userinfo or an explicit port, or pointing outside that host's frame directory, is rejected even on an allowed host; an SCT `sctidcam` must be a bare `.gif`/`.jpg` filename), and every non-SCT camera carries its real owner as a `credit` shown beside the shared `provider`. The feed carries no compass heading, so every camera loads at low heading confidence (same as TfL and Fintraffic). SCT's own feed link (`RenderService`) needed a workaround: it always 302s to a second endpoint (`TransitCamera`) on a Location the upstream hardcodes as plain http even when requested over https — refused by `fetchWithinHost`'s same-origin (scheme-included) redirect policy — and separately, `mct.gencat.cat`'s TLS setup fails the handshake with Node's fetch outright (`ERR_SSL_WRONG_SIGNATURE_TYPE`) even though other HTTP clients complete it. The loader therefore builds the `TransitCamera` URL itself, over plain http, from `RenderService`'s `sctidcam` id, rather than registering the (broken) upstream link — the only transport this one host actually serves to this runtime. Camera ids are built from the SCT camera file or the partner image path, not the feed's `fid` (which embeds an upstream server-start stamp and changes on every upstream restart), and rows the feed lists more than once collapse to one camera. The feed carries no camera status field, so a camera its publisher has taken offline still registers and shows the publisher's own "image unavailable" still (SCT and Barcelona both serve one with HTTP 200), the same as other packs without a status field. Frames are fetched at request time and never stored or redistributed. The feed itself states no licence; the pack relies on the Generalitat de Catalunya open-data portal's terms for this dataset (the licence and attribution above). Disable with `CCTV_CATALONIA_ENABLED=0`. - **King County road cameras.** Keyless. The camera list is one public ArcGIS FeatureServer GeoJSON query (King County DOT Road Services, "King County and WSDOT Traffic Cameras"). The `outFields` list is explicit on purpose: the layer also carries asset-administration columns — including staff names — that are never fetched. Only live, active cameras with frames on the county's own `info.kingcounty.gov` image handler are registered (the feed writes `http://`; the handler serves HTTPS, so URLs are upgraded and pinned before registration). WSDOT-owned rows in the layer are skipped because their frames are WSDOT-hosted, outside the pinned image origin; partner-city cameras hosted on the county image handler (e.g. Newcastle) name their owner as the per-camera credit. The layer publishes no facing, so headings use the shared id-hash fallback at low confidence. Uniquely, the county publishes each camera's hardware `Manufacturer`/`Model` (Cohu 3950/3965, Bosch AUTODOME, Axis PTZ domes); both are carried on the source (and served by `/api/cctv/sources`) as `model` and `manufacturer` for identified-hardware consumers (vendor-generic strings such as `PTZ` name no model and are omitted). Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_KINGCOUNTY_ENABLED=0`. - **WSDOT highway cameras.** Keyless. The camera list is the Esri JSON layer `data.wsdot.wa.gov/travelcenter/Cameras.json` — the same layer WSDOT's own Travel Center map consumes (~1,700 point features, EPSG:3857 geometry converted to WGS84 on load; the body carries Windows-1252 bytes and is decoded as such). Only frames on WSDOT's own `images.wsdot.wa.gov` host are registered — the catalog also lists ~70 partner cameras (Oregon DOT, City of Seattle, lodges, airports) whose own terms apply, and those are skipped rather than proxied. WSDOT's layer metadata marks the feed for "low volume" use (see WSDOT's [travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer)). The catalog is fetched once per CCTV catalog refresh (every 15 minutes), and a camera's frame is fetched only when that camera is viewed; frames are never stored or redistributed. The live feed's `CompassDirection` is a single cardinal (N/E/S/W), `B` (both directions — most rows), `O`, or null. A single cardinal gives a high-confidence heading (the diagonals NE/SE/SW/NW are mapped too should they appear); `B`, `O`, null, and any other code carry no single facing and use the shared id-hash fallback at low confidence. By default the 250 cameras nearest Seattle and Spokane load; `CCTV_WSDOT_MAX_SOURCES` changes the cap (8–1,200) and `CCTV_WSDOT_ENABLED=0` turns the pack off. A keyed WSDOT Traveler Information API serves the same population for higher-volume use and is deliberately not required here. +- **Hong Kong Transport Department traffic snapshot cameras.** Keyless. The English camera list is the public DATA.GOV.HK XML at `static.data.gov.hk/td/traffic-snapshot-images/code/Traffic_Camera_Locations_En.xml`; frames are stills on `tdcctv.data.one.gov.hk`, and only that host is registered. Frame URLs are rebuilt from each camera key (`https://tdcctv.data.one.gov.hk/{KEY}.JPG`) rather than taken verbatim from the catalog. Stills were probed to answer directly on that origin with no redirect hop. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. By default nearly the full catalog loads (~1,013; pack cap 1,100), prioritized nearest Central, Tsim Sha Tsui, Sha Tin, Tsuen Wan, Tai Po, Fanling, Tuen Mun, Yuen Long and Tseung Kwan O so a lowered cap still spans Hong Kong Island, Kowloon and the New Territories. When the sum of packs exceeds the catalog-wide ceiling, shared round-robin keeps every lane represented. `CCTV_HK_MAX_SOURCES` (8–1200) changes the cap and `CCTV_HK_ENABLED=0` turns the pack off. The DATA.GOV.HK Terms of Use require attribution to the Government / Transport Department / DATA.GOV.HK. - **Statens vegvesen road cameras.** Keyless. The camera list is one GeoJSON request to the OGC API Features view of the official DATEX 3.1 CCTV site table (`ogckart-sn1.atlas.vegvesen.no/ogc/features/v1/collections/datex_3_1:CctvSimple/items`), so no DATEX account is needed; frames are stills on `kamera.atlas.vegvesen.no/api/images/`, and only that exact path per camera is registered. Cameras the feed reports as faulty are dropped. About 135 working cameras also publish live HLS (`kamera.vegvesen.no/public//manifest.m3u8`, MPEG-TS); those play as live video through the bounded in-memory HLS relay, with the still as the snapshot fallback, and only that exact manifest path per camera is registered. `CCTV_VEGVESEN_VIDEO=0` keeps every camera on stills. `orientationDescription` names the place a camera faces, not a bearing, so it goes into the label and headings use the id-hash fallback at low confidence. By default every live camera loads (~850); `CCTV_VEGVESEN_MAX_SOURCES` (8–1000) lowers the cap, keeping the cameras nearest Oslo, Bergen, Stavanger, Trondheim, Kristiansand, Bodø and Tromsø and `CCTV_VEGVESEN_ENABLED=0` turns the pack off. Frames are fetched at request time and never stored or redistributed. The NLOD attribution is registered in the Data attribution popover. - **Kartverket ground heights for Statens vegvesen cameras.** The camera feed's points are 2D, so the loader asks Kartverket's keyless point-height API (`ws.geonorge.no/hoydedata/v1/punkt`, CC BY 4.0) for the terrain height at each camera. Requests carry 20 points: the API accepts 50, but above ~20 it answers the extra points from contour lines (`hoydekurver`, metres to ~20 m off) or not at all. Eight requests run at a time, about 36 for the whole country: a cold lookup took 6–13 s in testing (single requests 0.6–9.5 s). Answers are matched by the coordinates the API echoes and recorded only for points that request asked about. Over water the API returns the sea-floor depth (`dybdekurver` or `Havflate`), which is replaced by 0 m, the water surface; for the ~14 bridge cameras that is below the deck, but much closer than the sea floor or the flat prior. Heights are cached in memory by position for the life of the server, and points with no height (a few ferry quays and bridges, or a point a 200 answer leaves out) for 6 h, so the 15-minute catalog refresh only asks about new or moved cameras. The first failing request (a 408, 429 or 5xx, a network error, a request over 10 s, or an unreadable or oversized body) stops the lookup, with no retries, and no new lookup starts until the backoff ends (Retry-After when sent, else 30 min, longer than the refresh; bounded to 1 min–1 h). Any other 4xx marks that request's points as having no height and the lookup carries on. The whole lookup has a 30 s deadline; a lookup cut short by it resumes on the next refresh without a backoff. The lane waits at most 12 s, counted from the start of the loader, and only when more than 10% of the cameras have no answer yet; a few new cameras or expired misses are looked up in the background. A slower lookup keeps filling the cache, and the cameras it had not reached keep the flat prior until the next 15-minute refresh (about 190 of 865 on one slow first run). Only coordinates are sent, with the same User-Agent as the other CCTV requests. `CCTV_VEGVESEN_HEIGHTS=0` turns the lookup off. The Kartverket credit, with a note that heights over water are set to 0 m and heights are rounded to 0.1 m, is shown beside the Statens vegvesen entry in the Data attribution popover. - **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `tmcdot@dot.nyc.gov` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras//image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover. diff --git a/README.md b/README.md index 3b005c8dc..00fb9c24f 100644 --- a/README.md +++ b/README.md @@ -381,9 +381,9 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | -| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | +| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | -| 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer and **FOLLOW**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | +| 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | | 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland, and nine in Ontario: Toronto (surface vehicles; the subway publishes no positions), Mississauga, Hamilton, Durham Region, York Region, Guelph, Burlington, Barrie and Kingston | Operator GTFS-Realtime feeds | 🟢 | | 🚲 **Bikeshare** | Live station availability | GBFS | 🟢 | @@ -425,9 +425,9 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED **Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it the panel reads **KEY REQUIRED**. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless. -**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters and legend are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it under the slider; KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds, and the unfiltered lines show meanwhile. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): from orbit its dots show where it has pictures, and below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND**, **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos), and **FOLLOW** (the globe camera follows the photo's view; Google 3D map only). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo. +**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo. -Drag the panel header to float the panel, and resize it from any edge. Double-click the header or collapse it to put it back in the rail. Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`. +Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`. ## 🎖️ Field Missions diff --git a/SECURITY.md b/SECURITY.md index b694d8dcd..30203ed94 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,7 +29,7 @@ These are designed to be used directly in the browser (like a Mapbox public toke 1. **Google Maps API key** — loads Photorealistic 3D Tiles directly and powers Street View (Maps JavaScript API; add it to the key's API restrictions). **Restrict it** (HTTP referrer + API restriction to the required Google APIs) in the Google Cloud Console. An unrestricted key in a public deployment can be abused and billed to you. Restrict it to the **Map Tiles API and Maps JavaScript API only**: a referrer restriction applies to the Maps JavaScript API and the client SDKs, not to the web services, so a key carrying Geocoding under one is refused with `API keys with referer restrictions cannot be used with this API`. Search does not need it — geocoding goes through the server proxies above (#363). 2. **Cesium ion token** (`CESIUM_ION_TOKEN`, optional — for ion-hosted Google Photorealistic 3D Tiles, Bing world imagery, and world terrain) — used as `Cesium.Ion.defaultAccessToken` client-side. Use a public **`assets:read`** token with **URL restrictions** for any hosted deployment. The Community plan has eligibility and usage limits; a public token is not a secret, but it can still consume the account's quota. -3. **Mapillary client token** (`MAPILLARY_CLIENT_TOKEN`, optional, Street Level) — used by the MapillaryJS viewer and Graph API lookups in the browser, and by the server's coverage-tile proxy. Mapillary client tokens are meant for browser use. +3. **Mapillary client token** (`MAPILLARY_CLIENT_TOKEN`, optional, Street Level) — used by the MapillaryJS viewer and Graph API lookups in the browser, and by the server's coverage-tile proxy, which sends it only to `tiles.mapillary.com` over HTTPS (an upstream redirect is refused and answered 502, never followed) and caches tiles in memory only. Mapillary client tokens are meant for browser use. > The explicit browser `define` block in `build/vite.js` controls exactly what reaches the client: only these three keys. Everything else stays server-side. diff --git a/changelog.d/1205-cctv-tool-routing.md b/changelog.d/1205-cctv-tool-routing.md new file mode 100644 index 000000000..ed7552591 --- /dev/null +++ b/changelog.d/1205-cctv-tool-routing.md @@ -0,0 +1,3 @@ +- Report actual outcomes when voice enables or disables CCTV through the shared + layer lifecycle, including failed and cancelled changes (manjunath22466, + #1205). diff --git a/changelog.d/1228-cctv-ontario-511-key.md b/changelog.d/1228-cctv-ontario-511-key.md new file mode 100644 index 000000000..d9dc7986f --- /dev/null +++ b/changelog.d/1228-cctv-ontario-511-key.md @@ -0,0 +1,4 @@ +- Load Ontario 511 cameras again. 511on.ca's camera endpoint now requires a + free developer key; set `ONTARIO_511_API_KEY` to enable the pack, which is + otherwise skipped with one log line saying where to get a key + (quanticsoul4772, #1228). diff --git a/changelog.d/1235-location-resolution.md b/changelog.d/1235-location-resolution.md new file mode 100644 index 000000000..160688005 --- /dev/null +++ b/changelog.d/1235-location-resolution.md @@ -0,0 +1,3 @@ +- Require a geocoded containment anchor for ambiguous analyst region names, + preserve boundary provenance and cancellation safety, and retain exact-name + precedence for natural outlines (manjunath22466, #1235). diff --git a/changelog.d/1237-taiwan-freeway-cctv.md b/changelog.d/1237-taiwan-freeway-cctv.md new file mode 100644 index 000000000..1578971d4 --- /dev/null +++ b/changelog.d/1237-taiwan-freeway-cctv.md @@ -0,0 +1,4 @@ +- Add Taiwan's national freeway cameras from the Freeway Bureau, MOTC to CCTV: + ~1,800 cameras read at runtime from the official MOTC 2.0 open-data table, + with on-demand snapshots and attribution. No API key is required; set + `CCTV_TAIWAN_ENABLED=0` to skip it (419vive, #1237). diff --git a/changelog.d/1283-satellite-cache-provenance.md b/changelog.d/1283-satellite-cache-provenance.md new file mode 100644 index 000000000..c9b0dd7dc --- /dev/null +++ b/changelog.d/1283-satellite-cache-provenance.md @@ -0,0 +1,3 @@ +- Report cached CelesTrak satellite records as stale in the Data Layers row, + preserving separate core and dense catalog provenance through refresh, + removal and lifecycle cleanup (rudycelekli, #1283). diff --git a/changelog.d/1284-route-plausibility-segment.md b/changelog.d/1284-route-plausibility-segment.md new file mode 100644 index 000000000..4ab211957 --- /dev/null +++ b/changelog.d/1284-route-plausibility-segment.md @@ -0,0 +1,3 @@ +- Keep aircraft route labels on the scheduled airport leg instead of accepting + its distant great-circle extensions, preserving generous endpoint tolerance + across the signed-angle branch cut (rudycelekli, #1284). diff --git a/changelog.d/1286-since-note-inline.md b/changelog.d/1286-since-note-inline.md new file mode 100644 index 000000000..f66467d67 --- /dev/null +++ b/changelog.d/1286-since-note-inline.md @@ -0,0 +1,4 @@ +- The Street Level SINCE note sits on the value's line, in its type and a + size smaller: with KartaView lit it reads ANY DATE (by upload date). It + had lost its size variable in the Street Level slim-down and rendered + larger than the value, on a line of its own (asturksever, #1286). diff --git a/changelog.d/1287-shared-sliders.md b/changelog.d/1287-shared-sliders.md new file mode 100644 index 000000000..b5e5fb215 --- /dev/null +++ b/changelog.d/1287-shared-sliders.md @@ -0,0 +1,3 @@ +- Give ordinary sliders a shared native control and consistent track, thumb, + value, focus and disabled styling while preserving feature behavior and + specialized controls (mazy1998, #1287). diff --git a/changelog.d/1301-shared-segmented-selectors.md b/changelog.d/1301-shared-segmented-selectors.md new file mode 100644 index 000000000..379b8c716 --- /dev/null +++ b/changelog.d/1301-shared-segmented-selectors.md @@ -0,0 +1,5 @@ +- Unify segmented selectors for model coverage, detection allocation, drawing + shape, Street View and imagery comparison while preserving their existing + behavior. Keep compact labels such as FILL and FLAT on one line, and let + collapsed Weather release space to the open rail panel. Reserve the full mode + button height in Recent Imagery so keyboard focus is not clipped (mazy1998, #1301). diff --git a/changelog.d/974-street-level-slim.md b/changelog.d/974-street-level-slim.md new file mode 100644 index 000000000..8bc2b5346 --- /dev/null +++ b/changelog.d/974-street-level-slim.md @@ -0,0 +1,6 @@ +- Street Level is rebuilt around one small provider registry: Mapillary, + KartaView, Panoramax and Google Street View share one coverage drawer and + one photo viewer, one source lit at a time. Floating and docked panels, + FOLLOW, surface modes, the legend and the Mapillary proxy's disk cache are + gone; `.gev-cache/mapillary/` from earlier runs is no longer read and can be + deleted (gekh, #974). diff --git a/changelog.d/990-hong-kong-cctv.md b/changelog.d/990-hong-kong-cctv.md new file mode 100644 index 000000000..a150b66ae --- /dev/null +++ b/changelog.d/990-hong-kong-cctv.md @@ -0,0 +1,7 @@ +- Add Hong Kong Transport Department traffic snapshot cameras as a keyless CCTV + source pack: the public DATA.GOV.HK English locations XML, frames pinned to + `tdcctv.data.one.gov.hk`, with DATA.GOV.HK Terms of Use attribution. By + default nearly the full TD catalog (~1,013 cameras; cap 1,100) loads, + prioritized across Hong Kong Island, Kowloon and the New Territories. Headings + use the shared id-hash fallback at low confidence. `CCTV_HK_MAX_SOURCES` sets + the cap (8–1,200) and `CCTV_HK_ENABLED=0` turns the pack off (JCY2515, #990). diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index 19a5ee372..eb393a2da 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -356,8 +356,8 @@ drag or final change. Feature adapters own data mapping and service subscription generic modules own DOM/listeners/timers and release them in `destroy()`. Keep persistence with panel chrome: collapse uses -`godsEyeView.v6.panelCollapsed.`, position uses -`godsEyeView.v8.panelPos.`, and share state uses the registered panel id. +`godsEyeView.v6.panelCollapsed.`, positions are +not stored (`godsEyeView.v8.panelPos.` stays reserved), and share state uses the registered panel id. Do not rename existing ids or reset persistence versions when adding a body. The WEATHER adapter is `src/ui/weatherPanel.js`; its observed-history selection remains transient. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 135543f3f..ce7d065b2 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,14 @@ # God's Eye View Current State +Analyst region queries resolve unique Natural Earth names offline without +geocoding, so Gold Coast and Ivory Coast name the West African natural regions. +Duplicate names require a geocoded containment anchor; when several entries for +the same name contain that anchor, the lookup stays unresolved rather than +selecting the largest entry. Natural outlines try exact names before aliases and +broader suffix variants. Follow-ups retain boundary provenance, and cancelled +lookups preserve the previous analyst cohort and boundary source. Navigation +search keeps a successful geocode, with no search or view-based region recovery. + Mission hover picking is limited to one drill-pick per 120 ms and suppressed during camera movement, hidden tabs, disabled layers, and pointer-owned tools. Motion, hiding, disable, and destroy clear the cursor. Click selection keeps its full overlap-aware drill-pick behavior. Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and @@ -626,6 +635,14 @@ The standalone app offers OpenAI and Gemini Live through an explicit provider ch Voice action argument schemas have one portable owner under `src/voice/actionSchemas.js`. The Realtime provider builds 30 tools using separate description-only metadata. Description customization cannot replace argument types, enum values or required fields. +`createGevActionRunner` routes `control_cctv` enable/disable through the shared +layer visibility lifecycle. Results distinguish successful changes from +manager-reported failures or cancellation and report enabled state and lifecycle +uncertainty. A caller-supplied abort signal reaches the manager; a late abort +preserves an already committed change. Missing-layer errors and other CCTV +actions retain their existing behavior. The exported `controlCctv` helper refuses direct +enable/disable requests and directs callers to `set_layer_visibility`. + `src/voice/layerManifest.js` declares each catalog layer's voice capabilities: spoken aliases, `toggle`, in-view `context`, `track`, and analyst `query` fields with type and unit, or `noQuery` with the reason. The `set_layer_visibility`, `show_data_layers_menu`, `get_entity_context` and `analyst_query` layer enums, the runner's alias table and the analyst field hints are generated from it. `src/voice/layerManifest.test.mjs` fails when a catalog layer is neither listed nor named in `VOICE_OFF_LAYERS` (today only the two scene-driven Bhote Koshi layers). Global Context is listed with `toggle: false`; `set_context_mode` reaches it. `set_panel_open` also opens `weather-panel` and `recent-imagery-panel`. Portable source exports provide Radio Browser station normalization, CCTV feed types and regional records independently of HTTP middleware. Radio normalization accepts an explicit URL policy; the standalone directory retains its existing HTTPS rules. Tile coordinate validation accepts explicit zoom bounds with unchanged traffic defaults. Cache, request and rendering owners remain unchanged. @@ -653,7 +670,7 @@ The UI facade composes separate owners for navigation authority, share restorati visual settings and panel chrome. Navigation owns request generations and follow handoff; restoration owns its coordinator, deferred notices, gestures and timer; visual settings own style/detection overrides and sensor cleanup; panel chrome -owns disclosure, docking and Cockpit panel snapshots. Each takes explicit +owns disclosure and Cockpit panel snapshots. Each takes explicit operations and reads instead of the complete shell. Existing controls, share formats and one-application-per-page behavior are preserved. @@ -819,7 +836,11 @@ Viewport limits, saturation retry, placement, query caps and controls are retain Satellite and mission layers expose separate factories through `./layers/satellites` and `./layers/launches`. Each owns its catalog, render state, tracking, interaction -and teardown. Source adapters retain the existing CelesTrak and Launch Library +and teardown. Satellite core and dense catalogs retain upstream stale-cache +provenance only for admitted records; duplicate or invalid entries cannot taint +a fresh winning record. Dense removal clears only its own provenance, and a +fresh core rebuild or lifecycle reset replaces the corresponding ownership. +Source adapters retain the existing CelesTrak and Launch Library endpoints; standalone entries provide scene services and the satellite dependency. The mission factory separates paths, replay, camera, panel, overlays and placement. Launch Library pad coordinates retain unknown values: missing fields use the @@ -979,10 +1000,9 @@ start with current state, isolate listener failures and stop on disposal. The standalone entry composes the UI with the application's existing layer, terrain, navigation and rendering operations. The shell receives those instances -and assembles the controls. Panel layout scheduling, position preferences and -active drags, loading notices, recording presentation and DOM lookup have focused +and assembles the controls. Panel layout scheduling, loading notices, recording presentation and DOM lookup have focused owners. Disposal revokes queued presentation and listeners before asynchronous -Context restoration, cancels an unfinished drag without saving it, restores the +Context restoration, restores the recording HUD and releases status decoration without replacing accessible text. The ordinary control snapshot includes the current 3D model toggle and mode. @@ -1095,6 +1115,26 @@ The images are periodically updated road-condition photographs, not continuous video; camera poses use the existing low-confidence heading prior. `CCTV_LITHUANIA_MAX_SOURCES` sets its cap (default 400, bounded to 1–1000). +## Taiwan CCTV source pack + +The CCTV catalog includes Taiwan national freeway cameras from the Freeway +Bureau, MOTC. One keyless request reads the daily MOTC 2.0 `CCTV.xml` table +(~1,800 cameras); the request refuses redirects, has a 15-second deadline and a +6 MiB response cap, and uses the shared 15-minute cache and fair catalog +allocation. A failed download serves the last good table (up to a day old), +or an empty pack, and is not retried for an hour: the catalog host does not +answer from some networks outside Taiwan, and without the backoff every refresh +there would wait out the connect timeout. The table +publishes MJPEG stream URLs; each is mapped to the same camera server's +single-JPEG path for the same camera number, and only those exact HTTPS pairs +on the four `freeway.gov.tw` camera servers are registered with the frame +proxy. Camera keys come from the server and camera number, because published +CCTV IDs contain Chinese place names and a few repeat across cameras. Poses use +the low-confidence heading prior. `CCTV_TAIWAN_ENABLED=0` disables the pack; +`CCTV_TAIWAN_MAX_SOURCES` sets its cap (default 600, bounded to 1–2000), and +`CCTV_TAIWAN_URL` overrides the table URL. +Gate: `src/data/cctvTaiwan.test.mjs`. + ## DriveBC CCTV source pack The CCTV catalog adds DriveBC highway cameras for British Columbia alongside the @@ -1186,6 +1226,32 @@ The application supplies actions and retains effect settings, restore claims and rendering. Native keyboard editing, model modes and current defaults are preserved. Destroying the UI removes these listeners before asynchronous teardown. +Ordinary sliders share the native range component in `ui/slider.js` and the +`gev-quantitative-slider` surface in `ui/styles/slider.css`. Display, shader +parameters, volume, replay speed, weather settings/history, imagery opacity and +Street View's date filter use the same rail, thumb, value styling, keyboard focus and disabled state. +Generated controls use `createSlider`; static templates apply the same class. +Feature owners retain bounds, steps, formatted values, input/change timing and +listener cleanup. The radio tuning dial, imagery comparison divider and event +comparison/progression controls keep their specialized presentation and behavior. + +## Segmented selectors + +Segmented selectors use the controlled DOM adapter in `ui/segmentedControl.js` +and the shared `gev-segmented` / `gev-segment` surface. Model coverage, label +allocation, drawing shape, Street View fit/panorama filters and imagery mode keep +their feature-owned values, actions and persistence. Painting a restored value +does not emit a change or move focus. Existing keyboard policies remain explicit: +Street View uses arrows/Home/End with one tab stop, imagery mode uses arrows from +its controlled selection, and Display keeps its existing native button behavior. +Pure value navigation is separate from DOM rendering for reuse by a future view. +Segments keep their word minimum width: compact inline groups such as FIT/FILL +and ALL/360°/FLAT must not break short labels mid-word. Collapsed Weather follows +the same exclusive-rail hiding rule as the other readouts. +The imagery mode row reserves 26px for the 24px buttons and group borders so +the complete button and inset keyboard-focus outline remain visible. +Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4173 node scripts/qa-segmented-controls.mjs`. + ## Application shortcuts and shader parameter controls `ui/input` supplies the bubbling application shortcut listener and generated @@ -2988,6 +3054,13 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > position getter reads only `_trackedDisplayCached`, the same authoritative > Cartesian already consumed by the tracked visual and camera, and never > advances dead reckoning from the host frame. +> - **Scheduled route plausibility:** Aircraft route labels and selected-route +> context reject the distant backwards, forwards and antipodal extensions of +> an airport pair's great circle. The existing 130 km endpoint admission and +> 200 km cross-track tolerance remain; a projected fix outside the finite +> origin-to-destination minor arc also has a generous 200 km extension at +> either end. Missing coordinates and coincident or antipodal endpoint pairs +> retain their loose, unjudgeable behavior; vertical-trend checks are unchanged. > - **Aircraft label convention (both flight layers, 2026-08-18):** every civil > and military label surface resolves **callsign → registration → icao24** > (`_contactLabel()` in `flights.js`; the same chain inline in @@ -3450,7 +3523,7 @@ its criteria cannot be silently ignored. | Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s | | Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min | | Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | `src/data/traffic.js` | browser-direct OpenFreeMap tiles; `/api/tomtom` for flow | viewport-driven; capped tile cache | -| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + FL511 (FL, opt-in) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | +| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Hong Kong Transport Department + FL511 (FL, opt-in) + Freeway Bureau, MOTC (Taiwan) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | | Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh | | Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) | | Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil), Bicing (Barcelona) and seven other Spanish cities + Urban Sharing, Entur, JCDecaux (Lillestrøm, Valencia, Seville) + nextbike (AMBici)) | `src/data/bikeshare.js` | `/api/gbfs` | 60s | @@ -3459,7 +3532,7 @@ its criteria cannot be silently ignored. | Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static | | Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static | | FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT + MODIS NRT, trailing 24h) | `src/data/firmsHeatmap.js` | `/api/firms` (`FIRMS_MAP_KEY`) | 10 min (proxy TTL 30 min) | -| Street Level 📷 | Street-level imagery; Mapillary is the first provider (vector tiles, Graph API, MapillaryJS) | `src/layers/streetLevel/` via `src/app/layers/streetLevel.js` | `/api/mapillary/status`, `/api/mapillary/tiles/coverage/{z}/{x}/{y}` (`MAPILLARY_CLIENT_TOKEN`) | camera-driven (320 ms debounce, ≤9 tiles, cached 24 h) | +| Street Level 📷 | Mapillary street-level imagery (vector tiles, Graph API, MapillaryJS) | `src/layers/streetLevel/` via `src/app/layers/streetLevel.js` | `/api/mapillary/status`, `/api/mapillary/tiles/coverage/{z}/{x}/{y}` (`MAPILLARY_CLIENT_TOKEN`) | camera-driven (320 ms debounce, ≤9 tiles, server memory cache 24 h) | | Wind 🌬 | NOAA GFS 10 m wind (keyless, 0.25°→1° grid; animated particles) | `src/data/wind.js` | `/api/wind` | 1 h (forecast cycle) | | GNSS Integrity 📡 (`gnss-integrity`) | adsb.lol point snapshot, low navigation accuracy binned per 0.5° cell (GEV NIC/NACp threshold + gpsjam.org formula and bands, keyless, ODbL) | `src/layers/gnss/` via `src/app/layers/gnss.js` | `/api/gnss-integrity` | camera-driven (600 ms debounce, minimum 2.5 s between reads) + 60s poll (server cache 60 s; 30 min rolling window) | | Fire Perimeters 🔥 | NIFC WFIGS current interagency perimeters (keyless, paged past the 2000-record cap); InciWeb catalog + incident page origin and update time checks for verified incident-page links | `src/layers/perimeters/` via `src/app/layers/perimeters.js` | `/api/fire-perimeters` + `/api/fire-perimeters/inciweb/*` | 5 min (server caches: catalog 1 h; publication 30 min) | @@ -3491,8 +3564,8 @@ by `scripts/qa-emergency-squawk.mjs`. Both accept `--timeout-ms` for slow boot. Fire Perimeters uses capped, timed server reads with stale-on-error caching and a per-client limit. Unchanged snapshots retain geometry; link checks cancel on disable or selection change, and the row legend shows reported containment. -Street Level is a collapsible right-rail panel (`#street-level-panel`, layer -token `0`, option owner `street-level`, panel `ui` token `t`) that starts +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 has one chip per registered provider, @@ -3504,31 +3577,30 @@ sequence is GEV cyan. Share options: `m` (Mapillary), `r` (the other providers' e.g. `google-1` for Street View), `p` (`a`/`p`/`f` panoramas), `s` (since, days). Mapillary, Google Street View, KartaView and Panoramax are registered. Without `MAPILLARY_CLIENT_TOKEN` the panel reads KEY REQUIRED and the controls are -disabled. With it, coverage draws as z0–5 overview points from orbit and z11–14 -sequence lines below 60 km; the proxy strips the unused `image` layer from z14 +disabled. With it, coverage draws as z11–14 sequence lines below 60 km; the proxy strips the unused `image` layer from z14 tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent requests for a tile. -Mapillary coverage ranks unique tile addresses before applying the 16-tile -overview budget. Overlapping dateline halves in a polar view therefore do not -waste overview slots on the same tile; loading already skips duplicate keys. +Coverage is z11–14 sequence lines below 60 km; above that nothing is drawn and +the panel says to zoom in. Tiles come from the visible ground within range of +the camera, nearest first, at most 9 (the halves of a view across +the date line count a shared tile once). Lines are draped +`GroundPolylinePrimitive`s (`ClassificationType.BOTH`), so they follow terrain +and Google 3D tiles alike; cones and the photo marker are ground-clamped +billboards with a horizon cull. Without `MAPILLARY_CLIENT_TOKEN` the panel +reads KEY REQUIRED, says which key to add and disables the filters; a rejected +key reads KEY REJECTED and stops tile requests until the layer goes off; a 429 +pauses them for its Retry-After. -On Google 3D (`photoreal`) at street zoom (in below 1,400 m above ground, out -above 1,800 m) the surface mode switches from `draped` to `terrain`: draped -lines land on roofs and tree tops, so `groundCast.js` instead places lines, -cones and the marker 2 m above the bare earth from `/api/terrain/heights`, and -the mesh hides what is behind buildings and trees. A tile is drawn draped until -its heights arrive; a geoid fallback leaves it draped. Within 900 m of the -camera, `meshSampler.js` samples the rendered surface (`scene.sampleHeight`, -~11 m cells, nearest first, 6 ms idle budget) so lines also follow trenches, -steep streets and roads under trees. It uses the mesh floor's rules (the -visible tileset has finished streaming, a real bare-earth prior, the shared -mesh window) and probes a cell again once the camera is half as far from it, -down to 40 m, so coarse early samples are replaced as finer tiles load. A -failed probe keeps the last good sample and distance and is retried after the -miss cooldown. Roads on elevated decks are still drawn at -ground level. Framing a photo ignores mesh samples far below the bare earth -(unloaded tiles). +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, 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 +(a body too big to be a trimmed tile is served but not kept). While Mapillary +rate-limits, the proxy answers 429 itself until the Retry-After (seconds or an +HTTP date, at most 10 minutes) is over, so the rest of a view does not go on +asking. The header pill is the layer switch. Lighting a chip swaps the source; clicking the lit chip turns the layer off. A chip without a usable key @@ -3539,19 +3611,18 @@ their keys but a switched-off one would draw with a key already set (no Mapillary token, Street View on Google 3D with a Google key), the layer reports it (`keyAlternatives`), and the panel's KEY REQUIRED, its coverage line and the layer row's tooltip say to light it. The -viewer sits under the header with EXPAND, FIT/FILL, FOLLOW and close above the -image. FOLLOW needs the Google 3D map stack (`attachMapStackController`) and -stops when the stack changes. Camera moves go through the application's -navigation (`attachNavigation`), which releases aircraft and satellite tracking -and is refused in the cockpit: turning FOLLOW on claims the camera at once, and -FOLLOW stops when another feature takes it; a photo takes a deferred ticket -when it starts opening and frames only if nothing newer took the camera while -it loaded. SINCE is a stepped slider whose readout names the -cut-off date. The panel is portable (`street-level-panel` spec, minimum -320 × 280, `dockOnCollapse`): a floating window gives spare height to the -viewer, and collapsing it, double-clicking the header or SHRINK after EXPAND -docks it. At phone width (≤720 px) the viewer height is derived from the rail -band so the whole photo fits. +viewer sits under the header with EXPAND, FIT/FILL and close above the +image. SINCE is a stepped slider whose readout names the cut-off date. + +MapillaryJS loads on the first photo. Opening one frames it with a single +camera flight through the application's deferred navigation (`begin` / +`reassert`): it releases tracking, is refused in the cockpit, and is skipped +if newer navigation took the camera while the photo loaded; closing the photo +cancels it. EXPAND is the browser's Fullscreen API on the viewer element; on +iPhone, which has no element fullscreen, the viewer opens as a top-layer +popover instead (hidden where neither API exists). Clean View, recording and +the cockpit take an expanded photo out of full screen. At phone width the +viewer height comes from the rail band so the whole photo fits. Providers implement the contract in `src/layers/streetLevel/registry.js`. The core routes clicks by pick prefix, swaps viewer adapters in the one host, @@ -3560,9 +3631,10 @@ add a provider: implement the definition, register it in `src/app/layers/streetLevel.js` and add its modules to `scripts/package-boundaries.json`; its share switch travels in `r` by id. A provider with line coverage builds it on `lineCoverage.js` (camera-driven -XYZ tiles, draped or cast to bare earth, selection recolouring) and its cones +XYZ tiles near the ground, draped, the selected sequence drawn again over +them) and its cones on `sequenceCones.js`, passing its tile fetch, zoom ladder, colours and pick -prefixes; Mapillary's `coverage.js` and `sequences.js` are such wrappers. With providers on different keys the +prefixes; Mapillary, KartaView and Panoramax all do. With providers on different keys the layer's `requiresKeyId` is null and each chip reports its own key. Google Street View (`providers/google/`) runs on the Maps JavaScript API with @@ -3571,7 +3643,7 @@ on first use (`mapsLoader.js`, CSP `script-src https://maps.googleapis.com`). Its chip starts off (`defaultOn: false`; a link without its switch keeps it off) because every panorama load is billed to the key. Google's terms forbid Street View beside non-Google maps, so the provider declares `requiresMapStack: 'photoreal'`: -off Google 3D it does not appear at all (no chip, legend swatch, globe +off Google 3D it does not appear at all (no chip, globe credit, layer source or button, and Google's script is not loaded), lookups and opens skip it, and leaving Google 3D closes its panorama; the share option keeps the user's choice for when Google 3D returns. Google offers no coverage layer @@ -3593,9 +3665,8 @@ marker is a Google-blue view cone (`marker: {kind: 'view-cone'}`) in place of GEV's amber pin. Its panoramas are all 360° and it draws no coverage, so it declares `filters: []` (a provider's `filters` default to both `pano` and `since`): lookups ignore -the IMAGERY and SINCE filters, the panel hides each filter row no lit provider -honours (`filterControls`), and the legend lists only providers that draw -lines, with "Selected" only beside one of them. The viewer is Google's `StreetViewPanorama` in its own +the IMAGERY and SINCE filters, and the panel hides each filter row no lit provider +honours (`filterControls`). The viewer is Google's `StreetViewPanorama` in its own element in the viewer host, without Google's fullscreen, close and address controls (the address goes in the caption); a pose goes out at once and again when the panorama's month, photographer and address arrive (one @@ -3622,8 +3693,7 @@ are no KartaView photos in it. A tile's lines are OSM way segments with photos and carry no sequence, date or projection, so `decode.js` chains segments that meet end to start (about four times fewer lines; linear time, at most 20,000 segments a tile and 5,000 points a line), they are -drawn as `kv:line:`, up to 1,600 per tile, and never highlighted -(`highlightsSequence: false`, so the legend has no "Selected"), and the +drawn as `kv:line:`, up to 1,600 per tile, and never highlighted, and the provider declares `filters: ['pano', 'since']` and `coverageLabel` ("N streets in view · click a line for its nearest photo"). KartaView filters coverage only in its PNG tiles (`/sequence/tiles/{x}/{y}/{z}.png?fieldOfView=360|plane&startDate=YYYY-MM-DD`; @@ -3632,8 +3702,7 @@ asked for), so a filtered tile is drawn from the cached JSON segments that lie on the filtered PNG's drawn pixels (`maskedSegments`: at least half the points sampled every 2 px have a drawn pixel within one; on Jakarta's 360° filter, 332 of 1,041 segments lie on it, 676 miss it, 33 fall between). -A filter change reloads the lines (`lineCoverage.reload`: the drawn ones show -until the new ones are in), keeps the JSON segments, and caches masks per +A filter change reloads the lines (`lineCoverage.reload`), keeps the JSON segments, and caches masks per filter and tile. A dense filtered PNG can take KartaView over a minute the first time (it answers 504 at 60 s and keeps drawing), so a timeout or 5xx is asked again 4 s later, four tries in all; then the panel says it did not @@ -3644,8 +3713,8 @@ lookup asks the photo API for the projection (`SPHERE`/`PLANE`; it ignores `fieldOfView`) and upload date and checks each answer the same way (when photos are near but none passes, the panel says so), and the cones pass on each photo's projection and upload date. The provider declares -`sinceBasis: 'upload'`, so while it is lit the panel shows "KartaView: by -upload date" under the SINCE slider (`sinceNote` in the UI state). Tiles come uncompressed and +`sinceBasis: 'upload'`, so while it is lit the panel shows "(by +upload date)" beside the SINCE value, as ANY DATE (by upload date) (`sinceNote` in the UI state). Tiles come uncompressed and uncacheable, so the last 24 decoded tiles are kept for 10 minutes while KartaView is lit (`createTileCache`, dropped when it is switched off); one that does not answer in 20 s is asked once more, and one that is not a line @@ -3660,7 +3729,7 @@ remembered; a storage timeout or 5xx is asked again. The open photo's sequence draws as cones from the 250-photo page holding it (`/2.0/sequence/{id}/photos`); the panel counts the drive's own photos (`countActivePhotos`, asked once per sequence). The camera direction (the -marker, cones, FOLLOW, and a panorama's centre) is the direction of travel +marker, cones and a panorama's centre) is the direction of travel to the next photo a few metres on (`assignCourses`): KartaView is dashcam imagery, while its `heading` is the device compass, which reads 0 for whole drives without one and can sit a steady 110° off inside a car. Until the @@ -3717,23 +3786,18 @@ to 30 km and z10 to 60 km, as Mapillary's (a dense z10 tile over Paris is line (Douglas–Peucker to the segment, 4 units of a 4,096 tile: about a pixel where it is drawn, 6–10% of a GPS track's points), and the tiles in view draw their longest lines within 90,000 points (`maxPoints`, -`keepOrder`), shared out once loading settles so a sparse tile keeps all -it has and a dense one (a city centre) takes the rest; the selected -sequence is always drawn. Every point becomes ~8 ground-line vertices: +`keepOrder`), split evenly between the tiles in view (10,000 points +each). Every point becomes ~8 ground-line vertices: unsimplified, the lines hung the tab. Decoded tiles are kept while the -provider is on (16, ten minutes, `tileCache.js`, shared with KartaView), -and above -60 km the `grid` layer's 0.1° -density cells, binned to at most 96×96 points a tile (a z0 tile holds -~70,000 cells) and not counted as sequences: the panel says to zoom in. +provider is on (16, ten minutes, `tileCache.js`, shared with KartaView). +Above 60 km nothing is drawn and the panel says to zoom in. A tile decodes in slices of about 8 ms, the page getting a turn between them (a 2 MB z10 tile is ~200 ms of work). A busy API (429/503) holds the tiles back for its Retry-After, which the browser cannot read here (the API exposes no headers), so the wait doubles from 5 s to 5 minutes until it answers; an answer may take as long as it keeps arriving (silent for 15 s is given up), so a slow link still gets its megabyte tiles. Both -filters apply: lines carry their day and camera type, and a cell stays while -it holds pictures of the kind asked for (cells are undated). A click on a +filters apply: lines carry their day and camera type. A click on a line asks the core (`actions.openNearest`) for that sequence's picture nearest the click, within the camera height / 100 (50–400 m: from high up a click lands well off a line), without framing, then any sequence's within @@ -3782,8 +3846,9 @@ runs against real Mapillary and needs `MAPILLARY_CLIENT_TOKEN`. `npm run qa:street-level:fixtures -- --url ` answers every Mapillary request from fixtures (`providers/mapillary/coverageFixture.mjs`, `scripts/fixtures/street-level/`), and KartaView's and Panoramax's too, so it -needs no network and covers the whole photo flow. CI runs the fixture gate and `qa:panel-resize` -against a production build served by `vite preview` with a dummy token. +needs no network. It opens and closes a photo from Mapillary, KartaView and +Panoramax; stepping, EXPAND, FIT/FILL, the filters and Street View stay with +the unit tests. CI runs the fixture gate against a production build served by `vite preview` with a dummy token. 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 @@ -4443,8 +4508,10 @@ silently demoting every later lookup for the session. default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of 1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego, Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default - sources. Ontario 511 (2026-09-12) adds keyless highway cameras including Kitchener-area routes - (cap 1,000, all enabled rows from the current ~944-camera catalog). All sources are RAW PRIOR + sources. Ontario 511 (2026-09-12) adds highway cameras including Kitchener-area routes + (cap 1,000, all enabled rows from the current ~944-camera catalog). The 511on.ca API now + requires a developer key, so the pack needs `ONTARIO_511_API_KEY` and is skipped without one + (#1228). All sources are RAW PRIOR poses, and the layer is stills-first. sources. Fintraffic Finland road weather cameras (2026-09-13; cap 300, `CCTV_FINTRAFFIC_MAX_SOURCES`, kill switch `CCTV_FINTRAFFIC_ENABLED=0`) are the fourth pack: one keyless GeoJSON station list @@ -5087,7 +5154,7 @@ pins and share links already supply complete boxes. Pixel-mode pinch deltas receive bounded amplification before reaching the canvas; ordinary wheel, line/page-mode and native touch-pinch paths are left unchanged. The application scene owns and removes the relay listener. -- Z ladder: panels promote within 100–139 (renormalized on wrap), voice pill 150, toast 200, clean-view exit 300. +- Z ladder: panels around 100, voice pill 150, toast 200, clean-view exit 300. - Panel POSITION keys are versioned `v8` (`godsEyeView.v8.panelPos.`); collapsed-state keys remain `v6`. The one-time position reset clears stale DISPLAY placements that could overlap the Context rail. - Map Source lives in the bottom Visual Presets tray. The left accordion contains no MAP STACK panel, and the `k` panel token that addressed it is gone from the share registry, so legacy `ui=k...` state takes the ordinary unknown-token skip. - A dock popover (Visual Presets, Location) auto-dismisses on mouse-away unless pinned. Focus inside the tray defers that dismissal only when the browser reports `:focus-visible` — keyboard focus and typed-into fields hold the tray open; a mouse-clicked tile does not, because Chromium focuses a `