From the test-engineering review of #768: the rendering and panel code had
thin unit coverage (coverage.js 29% of lines, sequences/viewer/selection
~30%, cameraFollow 50%, streetLevelControls only source-text checks), and
two shipped bugs were caught only by the manual browser gate.
- coverage.test.mjs drives the real createCoverage against a fake scene and
a source whose tile requests the test resolves. It reproduces gekh's P0 #2:
a request superseded by retire() finishes after a newer one for the same
tile. Without the ownership checks it drops the newer request's key and a
refresh starts a duplicate whose lines nothing can remove (fails on the old
loadTile, passes now). It also covers the draped-to-cast swap keeping both
line sets on the globe until the cast one is built and clear() removing
both mid-swap, and a tile with nothing castable not being rebuilt.
- coverageFixture.mjs encodes Mapillary-shaped coverage tiles (sequence
lines, overview points) from lon/lat; the tests and the hermetic gate use it.
- cameraFollow.test.mjs pins the underground-camera fix: a -14,886 m sample
is ignored for bare earth, a plausible sample wins, and following stands at
eye height. Three of the four fail on the pre-fix module (the first reports
"centre at -14884 m").
- streetLevelControls.test.mjs is a contract test through the real
DataLayerManager and LayerStateCoordinator: a 360 deg click and a SINCE
release reach the durable layer state and the encoded share link under
Street Level's token. Against a stand-in layer it also covers the chip rule
(darkening the last lit chip turns the layer off, provider kept on; lighting
a dark chip is a user params request, then enable), the legend rebuilding
on a same-count swap (review #9) and the viewer resizing only on open
(review #8). With the old direct-to-layer routing, three of these fail,
including both share-link tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Norway's ~850 working road cameras load from one keyless GeoJSON request
to the agency's OGC API view of its DATEX 3.1 CCTV table, so no DATEX
account is needed. Faulty cameras are dropped, every camera sits under a
"Norway" category, and the ~135 that publish HLS play as live video
through the existing in-memory relay with the still as fallback. Frames
and manifests are pinned to each camera's own path on the agency hosts.
CCTV_VEGVESEN_ENABLED=0 disables the pack, CCTV_VEGVESEN_MAX_SOURCES
caps it (nearest the largest cities first) and CCTV_VEGVESEN_VIDEO=0
keeps stills only. Attributed under NLOD 2.0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in 123 commits from main, including the v0.2.0 and v0.2.1 releases
(MCP server and tool catalog, server security hardening, build hygiene).
Conflicts:
- package.json: kept main's script order and added this PR's
qa:panel-resize and qa:street-level; exports keep both sides (main's
tools/view entries and this PR's street-level and Mapillary entries).
- src/tooling/viteBuild.test.mjs: kept this PR's Mapillary-token define
assertion and main's new wildcard-host allowedHosts assertions.
- CHANGELOG.md: main now has release sections; this PR's entries go under
[Unreleased].
Checks after the merge: doctor, format:check, check:boundaries,
layer-token:check (28 published, 1 new: street-level 0), npm test (5726
pass), build, the Windows job's focused tests (70 pass), qa:panel-resize,
and the keyless street-level gate. The new CSP allows Mapillary (https:
connect/img, blob workers). The keyed gate stops at coverage locally
because the dev .env Mapillary token is invalid (upstream 403), not because
of the merge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Binding HOST=0.0.0.0 no longer sets Vite's allowedHosts to true. The dev and
preview servers accept IP addresses, localhost and the LAN hostnames listed
in GEV_ALLOWED_HOSTS; suffix and wildcard entries are ignored. The
configuration factory takes the resolved list as an input, so build/vite.js
still reads no environment.
Ported from #97 to the current build/vite.js layout. Fixes#21.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The OpenAI endpoints (/api/realtime/token, /api/openai/hud-summary) now
allow 30 requests per minute per client IP and the Google Places endpoints
120, the caps the Pinokio build already ships. GEV_RATELIMIT_OPENAI_PER_MIN
and GEV_RATELIMIT_GOOGLE_PER_MIN still override them and exactly 0 disables
them; an unreadable value falls back to the default rather than to
unlimited.
Salvaged from #683 onto current main.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The cost-bearing endpoints (/api/realtime/token, /api/openai/hud-summary,
/api/google/nearby-places) and the voice debug log refuse cross-site browser
requests: a foreign or opaque Origin, a Sec-Fetch-Site other than
same-origin/none, or proxy forwarding headers. Loopback tools without an
Origin and the LAN opt-in keep working. The dev and preview servers send one
shared Content-Security-Policy, and embed-mode framing rewrites only the
frame-ancestors directive of it.
Salvaged from #242 onto current main.
Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
show_on_globe replaces open_in_gods_eye_view and names an MCP Apps view, a
ui:// panel that loads the app in embed mode and posts each later view to it,
so the globe in the conversation changes without reloading. It also takes a
view another answer returned. Tools can declare a UI resource, and the MCP
adapter serves resources/list and resources/read and lists each tool's
resource in _meta.ui.resourceUri.
?embed=1 shows the globe alone: clean view with the HUD, panels, welcome and
setup prompts hidden, and provider attribution kept. The page that frames it
sends views with postMessage; the app applies each through its own actions
and reports every step. Annotations in any link are drawn once it restores.
Only embed-mode documents may be framed; every other page keeps
X-Frame-Options DENY and frame-ancestors 'none', and
GEV_EMBED_FRAME_ANCESTORS can restrict who frames them.
Tool answers with something to show now include data.view: the view with
the matching layers on, the area framed from above or a single aircraft or
satellite followed, and the link that opens it. Area framing moves to
src/tools/views.js so links and answers frame alike.
A view is a camera, data layers, visual style, map imagery and an aircraft,
military aircraft or satellite to follow, written in the share-link format
the app already restores. gods-eye-view/view builds, encodes and decodes one,
and share links take their style names from it. open_in_gods_eye_view now
builds its link from a full view: an area or camera, layers, style, map and
something to follow.
Brief sections received a named place as its bounding box, so they centered
on the box's middle: Tokyo's aircraft were sorted from a point in the
Pacific. Sections now receive the place name and resolve it like a direct
query, and place lookups are cached briefly so a brief resolves it once.
The raycast challenge on #768 showed bare-earth casting misplacing roads
that are not at grade: lines floated over freeways in trenches and steep
streets (rendered line pixels floating: Austin 8.5%, SF 27.1%, LA 43.8%),
because a ~111 m terrain grid cannot see a 30 m-wide cut or a slope between
its corners.
meshSampler.js samples the rendered Google 3D surface (scene.sampleHeight,
overlays excluded) once per ~11 m cell within 900 m of the camera, nearest
first, in idle time within a 6 ms budget; misses are retried, not latched.
groundCast.refineHeights blends it in, per point along a line:
- mesh up to 3 m above bare earth is the road: follow it, 1 m above;
- higher mesh (canopy, deck, roof): carry the road's offset from bare earth
across from the road samples on both sides (up to 8 points);
- otherwise bare earth + 2 m, as before.
Lines, cones and the marker use it; tiles touched by new samples are
redrawn one per idle slice with the no-blink swap.
Rendered line pixels on the surface / floating, before -> after:
Austin 90.0 -> 95.3% / 8.5 -> 0.7%; SF 69.0 -> 86.4% / 27.1 -> 9.1%;
LA 52.3 -> 91.0% / 43.8 -> 4.0%; Innsbruck 95.0 -> 95.7% / 0.2 -> 0.2%;
Sacramento 80.7 -> 79.3% / 3.8 -> 2.7%; buried lines stay under 1%.
Cones floating in SF 44 -> 5%, buried at LA 59 -> 28%. Main-thread long
tasks added during refinement: at most 134 ms (Austin, densest tile).
Roads on elevated decks are still drawn at ground level under the deck.
Unit tests cover the blend (trench, slope, canopy bridge, deck, bad probe),
the finer cast with meshAt, and the sampler (once per cell, nearest first,
overlays excluded, far cells skipped, misses not latched, idle when off).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From the code review of #768:
- view.js measured the camera's height above ground with globe.getHeight,
but Google 3D hides the globe, so coverage picked its zoom from the
ellipsoidal height. Over a high city (Denver, ~1,600 m) a camera 500 m up
read 2,100 m: coverage stayed at z12/z13, whose tiles are too big to cast,
and lines stayed draped on the roofs. With the globe hidden, the bare-earth
height from the ground caster now answers; coverage re-picks its zoom when
the surface mode changes. Verified in Denver: terrain mode, z14, cast lines.
The core's surface check uses the same helper (and no longer subtracts the
2 m lift from the camera height).
- castTile rebuilt a tile even when no line could be cast (terrain proxy
down, tile too big); it now leaves such tiles as they are.
- The feature gate read Street Level's share token as a literal `0`; it now
takes it from LAYER_STATE_REGISTRY, since a rebase can reassign it.
New view.test.mjs covers the shown-globe, hidden-globe and no-sample cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merge of main (c451478) appended main's new entries directly after
this branch's last entries in scripts/format-scope.json (after
streetLevel/index.test.mjs) and in two groups of
scripts/package-boundaries.json (after app/layers/streetLevel.js), without
the separating comma. Both files stopped parsing, so every CI job failed
at its first JSON read, the "Check adopted formatting" step.
With the commas back, the CI steps pass locally: doctor, format:check,
check:boundaries, layer-token:check against main, npm test (5475 pass),
build, and the Windows job's focused onboarding tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
src/tools/surfaces.js lists the tools a surface leaves out, and the MCP
server, the voice session's tool list and the browser catalog voice calls
all filter through it. Voice leaves out tools that answer with images, link
to the app, or repeat what its app actions already answer.
Add find_alpr_cameras over the layer's OpenStreetMap camera extracts:
cameras in a US or Canadian area of at most 3° per side, nearest first,
with operator, manufacturer and facing direction, and the source's
attribution. Areas outside coverage or too wide are reported.
Add find_submarine_cables over the bundled TeleGeography data: cables
passing through or landing in an area, or matching a name, with landing
points and the dataset's attribution. The local MCP server's fetch reads
the bundled data files a browser would load by URL, and nothing outside
that directory.
Add get_recent_imagery. It searches the recent Harmonized Landsat and
Sentinel-2 catalog for an area, picks the image the layer would show
with the layer's own ranking, and returns it with its date, sensor and
cloud cover, falling back to the VIIRS daily overview.
Add get_weather_map, which returns the latest NOAA radar, satellite or
lightning map over an area as an image, choosing a window the image
route accepts and reporting areas outside the product's coverage, and
get_wind, which samples GFS or IFS wind at a location with the wind
layer's own grid sampling.
Mount the local MCP server at /mcp on the development and preview
servers for clients that connect by URL. The route accepts only
requests from this machine that name a loopback host and, when an
Origin is sent, come from a loopback origin, and drains oversized
bodies so the 413 response is delivered. The provider-order regression
test now pins the route between the providers and the API fallback.
Add open_in_gods_eye_view. It builds a version 2 share link looking
straight down on an area, with altitude chosen from the area's size, on
the app address the application supplies.
Add vessels_in_area, find_vessel and get_vessel_track over the live
vessel source, and a ships section in situation_brief. Without an AIS
key the source's own message reaches clients as an unavailable feed.
Add get_weather, get_regional_brief, get_cyclones, get_fire_perimeters,
get_terrain_height, find_military_installations and get_map_features
over the application request services and the layers' sources.
situation_brief combines weather, earthquakes, fires, aircraft and
cyclones for an area by reusing those tools, marking sections that are
unavailable instead of failing. get_hud_caption returns the app's
heads-up display caption for the same overview.
Add get_bike_share over the public GBFS systems and get_transit_vehicles
over the GTFS-Realtime feeds. Each finds the systems or feeds whose
coverage reaches the area, reads up to three, and reports any that were
unavailable instead of failing the whole answer. Transit results carry
each feed's attribution and license.
Add getFeeds() to the transit source. It reads the public catalog route
the server already serves, so code outside the layer can find feeds
near a location without loading the layer.
Add search_places and places_nearby over the app's Google Places routes,
and plan_route over its OpenStreetMap routing. The place search service
reports when no search key is configured, so an unconfigured server is
not mistaken for an empty result, and rate limits become retry_later.
Route endpoints accept a place name or coordinates.
Add find_cctv_cameras, get_cctv_snapshot and find_radio_stations. A
snapshot returns the camera's current frame as image content with its
credit. Tool results may now carry base64 images, which the MCP adapter
sends as image content.
Add aircraft_in_area, find_aircraft, get_aircraft_track and
get_aircraft_info over the live aircraft sources, with an optional
military feed, and next_satellite_pass and satellites_overhead over the
satellite catalog and the existing pass predictor.
Source failures from the live contract now become tool errors with the
matching code, so a rate limit reaches clients as retry_later with a
wait time. Tool schemas may use pattern.
Define tools that answer questions from the app's data once, independent
of the surface that exposes them. A tool declares its arguments and the
services it reads; applications compose tools with their own services and
optional call interceptors. Queries reuse the layers' portable source
factories, so they run wherever an application routes /api requests.
The first queries are get_earthquakes, get_active_fires and
get_recent_launches, with a shared area argument (place name, bounding
box, or point and radius) and capped lists.
gods-eye-view/tools/mcp adapts a catalog to the Model Context Protocol
with a small JSON-RPC handler and a stateless HTTP transport, adding no
dependencies. npm run mcp serves Core's tools over stdio to local clients
such as Claude Code, reading from a running app. Both exports are
portable and checked by check:boundaries; the app does not import them.
Traffic roads come from OpenFreeMap vector tiles, or TomTom flow tiles with a
key (Hybrid by default). Military areas use OpenFreeMap polygons with a bundled
worldwide name index; wide views show named points. ALPR reads an hourly
OpenStreetMap extract. Cockpit place context and country, state and US county
outlines use bundled Natural Earth and US Census data. Overpass is used only
when an operator configures their own endpoint.
Also: camera arrivals no longer land below ground, keyless terrain retries on
429, bounded relation geometry for configured Overpass, one shared
OpenStreetMap credit, and separate tile and API transports for embedders.
Co-authored-by: theGaryLarson <[email protected]>
Co-authored-by: Milan Khanal <[email protected]>
Brings in #744 (share-link token ledger). package.json: keep the branch's
sorted scripts and add layer-token:next and layer-token:check.
Street Level's token moves from the provisional `3` to `0`: following
CONTRIBUTING, the provisional entry was removed and
`npm run layer-token:next -- street-level` assigned `0` (free digits in
order). It is added to the registry and to
src/data/layerStateTokenReservations.json, and
`npm run layer-token:check -- --base-ref upstream/main` passes (28
published, 1 new). No link with the provisional token was ever published.
Pinned token tests move with the ledger as CONTRIBUTING asks: 29
reservations, next free digit `3`, and the competing-PR and retired-digit
scenarios shift up one free digit (0->3, 3->4) so they still test the same
rules. The README share examples and the free-digit notes in
CONTRIBUTING and CURRENT-STATE now read `0`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Draped ground polylines and clamped billboards land on the top of the
Google Photorealistic 3D mesh, so on a street under trees or an overpass
the coverage seemed to float at roof height.
At street zoom on Google 3D (camera below 1,400 m, back above 1,800 m)
the core switches to a `terrain` surface mode. groundCast.js samples the
existing terrain service on the ground-floor grid (0.001 deg), interpolates
bilinearly and adds 2 m; Mapillary lines become plain polylines at those
heights, and cones and the position marker stand on the street, so the
mesh hides what is behind buildings and trees. A tile draws draped first
and swaps to its cast lines once the heights arrive, keeping the old lines
until the new primitives are ready. Lookups are once per tile and aborted
when the tile is dropped; a geoid fallback counts as unknown and leaves
lines draped. Other map stacks and higher views are unchanged.
Providers get getSurface() and groundCaster in their context and an
optional setSurface(mode); getUIState() exposes `surface`. The QA gate
checks draped on Esri and terrain on Google 3D.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FOLLOW puts the globe camera at eye height inside the street-level view; on
flat imagery or terrain there is nothing to stand among. The layer now takes
the application map stack controller (attachMapStackController), subscribes
to stack switches, offers FOLLOW only while the stack is 'photoreal', disables
the button with a MAP SOURCE hint elsewhere, and stops following when the
stack leaves Google 3D. Selection uninstall is a no-op when never installed.
The QA gate checks the disabled path and, when Google 3D is available, that
leaving it stops following; its caption-timeout dump now forces one render to
tell a missed update from a render fault.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Only a user's own collapse docks a floating window; cockpit entry,
restores and the Cyber accordion no longer dock it or delete its saved
place, and the accordion leaves floating windows alone.
- The viewer host records an adapter as active only after its mount
succeeds, so a library that failed to load once is retried; concurrent
opens share one mount, and unmounting mid-mount leaves nothing active.
- openNearest tries every active provider; one provider's error (no key,
offline) no longer hides the others' imagery.
- The SINCE readout shows its cut-off date as soon as the committed value
matches the thumb, even while the slider keeps focus.
- A floating window that was only dragged is bounded to the viewport and
scrolls instead of running off-screen.
- getStats summarises coverage (summarizeCoverage) instead of building a
full UI snapshot every second; disable() uses credits.hideAll(); smaller
cleanups in the viewer host and sequence snapshot.
- Unit tests: panel docking and header double press, viewer-host mount
retry/sharing/cancel, coverage summary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bring in portable, resizable rail panels. The Street Level rail rules get
the same :not(.panel-floating) guard as CCTV's so the panel can float too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Street Level is now a provider-neutral layer modelled on the iD editor's
photo overlay. The core owns what every imagery provider shares: the
enable state and a switch per provider, the imagery filter (360°/flat and
captured-since in relative days), one click handler routed by pick prefix,
one viewer host that mounts whichever provider's viewer adapter owns the
open image, the position marker, camera follow, one on-globe credit per
active provider, and share-link parameters (one bit per provider plus the
filter). Providers implement the contract in registry.js: coverage,
sequences, nearest-image lookup and a viewer adapter emitting a common
pose. Mapillary is the only registered provider; Google Street View,
KartaView and Panoramax slot in beside it.
Panel: a PROVIDERS chip row (one chip per registered provider, keyless
chips read as errors), relative-day SINCE options, a provider-neutral
caption whose link label follows the provider, and no vendor mark in the
header. The panel inner is a rail scroller like CCTV's.
Tests cover the registry, params codec, filter, pick router, UI-state
composition, the provider definition and coverage-tile decoding; the QA
harness switches the provider off and on and checks the filter.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Pure move: the layer id, panel id, CSS prefix, classes, exports and config
paths become provider-neutral (street-level, #street-level-panel, sl-);
the Mapillary-specific modules move under providers/mapillary/. Behaviour
is unchanged. The two sprite collections now register under distinct ids
instead of overwriting each other under 'mapillary'.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Street Level is imagery only from here on: coverage, sequences and the
embedded viewer. The Claude planner, map-feature and traffic-sign tiles,
the NDJSON feature stream, sprites, detection outlines and analyst records
go, along with @anthropic-ai/sdk, zod and the Anthropic key entry.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Preserve all published token assignments and add regression coverage for
all-enabled URL restoration, longer token lists, and manual provisional
token recovery after competing merges.
Builds on daikaginza's digits-first and merge-time allocation proposal.
Thanks to KnottyDyes for the review and regression-test suggestions.
Upstream took share token '2' for fire perimeters, so Street Level moves to
'3'. mapillary-js joins upstream's optimizeDeps include list instead of
overriding it; catalog and registry tests count the new upstream layers.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The Natural Earth and neighborhood packs were loaded with a JSON-attributed
import(). The Vite dev server answers that request with a JavaScript module,
which the browser rejects, so every analyst region scope failed and fell
through to a geocode lookup that could take 12-28 s.
- src/data/bundledJson.js fetches the pack from a new URL(..., import.meta.url)
asset in the browser; under Node the caller's attributed import runs.
- resolveRegionRingForQuery caps the geocode + admin-boundary rung at 3 s and
returns { error: 'region-timeout' }; the lookup keeps running and fills the
cache. The analyst query reports it as code 'region-timeout'. The Nepal
locator opts out of the budget.
- Unit tests for the loader, the timeout and the analyst error; the behavior
harness checks the Gulf of Mexico scope and retries Texas on region-timeout.
Co-authored-by: Bilawal Sidhu <[email protected]>
Adds MODIS NRT to Active Fires and a keyless Fire Perimeters layer (NIFC WFIGS) with
InciWeb incident links, served through a capped, cached /api/fire-perimeters proxy.
Co-authored-by: Gustavo Beneduzi <[email protected]>
Co-authored-by: James Cooke <[email protected]>