Line-of-Sight and Area-of-Sight terrain analysis#122
Merged
Conversation
Adds a new "Line of Sight" entry to the Measure menu. After placing an observer with the first click, the LoS recomputes on every pointermove against the Mapbox-RGB terrain tile source and renders the visible segment in green, the blocked segment as a dashed red line and the first blocker as a red diamond. Distance, eye-height delta and first-blocker distance are shown in the OSD. Range is clamped to 10 km (clip marker drawn at the cap); the second click finalises the LoS and leaves it on the map, so multiple Line-of-Sights can coexist in a session. Earth curvature and atmospheric refraction (k=0.13) are applied unconditionally to keep results realistic at the relevant distances. The command is gated on terrain availability and WebGL2 support (the latter is a prerequisite for the planned Area-of-Sight feature and is checked here for consistency). Observer/target heights default to 1.70 m AGL; an editor for these plus persistence will follow in a separate change.
Adds a LOS scope (los:{uuid}) to the id catalogue and rewires the
line-of-sight module to be store-driven. Finalised LoS results are
written to the store, restored on app start, and rebuilt incrementally
on subsequent put/del operations.
The in-progress (live-preview) overlay is unchanged. On finalisation
the live features are handed over directly to the persistent map so
there is no flicker, and a fresh losId is inserted into the store. A
small race-safe initial-load path waits for a terrain layer to be
available before rendering the persisted set.
Heights are still fixed to the 1.70 m defaults; the per-feature
height editor will land alongside selection support in a separate
change.
Selecting a finalised LoS on the map (click) now opens a Properties panel with editable observer/target height fields and a read-only distance display. Edits flow through store.update, our batch handler detects the change and rebuilds the visual representation; the existing clipboard-delete pipeline removes a selected LoS without any extra wiring. All sub-features of a LoS (observer point, visible/blocked segments, blocker diamond, clip marker) carry the same losId as OpenLayers feature id, so a click on any of them selects the whole document. The LoS layer is tagged selectable; the map-wide Select interaction is suspended while the placement tool is live so the first/second click never doubles as a feature select. Multi-select shows M/V in the height fields and accepts a value to apply to all selected LoS at once, mirroring how the other property panels behave.
- ElevationService samples at a data-driven analysis zoom (finest zoom resolving <= 15 m/cell) instead of the current view zoom: LoS and elevation profile results no longer change with zoom or after reload - promise-based tile cache deduplicates concurrent downloads; tiles are decoded once into Float32 elevations - profileAlongLine fetches required tiles in parallel, then samples synchronously - getGrid(extent) stitches a tile-aligned Float32 elevation grid (foundation for Area-of-Sight), coarsening zoom to fit a cell budget - drop the WebGL2 gate from the LoS command (LoS is CPU sampling only)
- R2 viewshed engine: WebGPU compute shader (one thread per ray), 10 km @ 10 m in single-digit ms; viewshedCPU as reference and fallback when no GPU adapter is available - tool follows the cursor with a live preview (latest-wins), click places the observer and persists the document in the aos: scope - visibility mask rendered as circular raster overlay (ImageCanvas), green/red, no-data cells stay transparent and never block - selectable observer point with properties panel for radius (default 2500 m, max 10 km) and observer/target heights; changes recompute - map clicks no longer deselect LoS/AoS results
LoS and AoS documents are now plain GeoJSON features flowing through featureSource — one feature per document, like measure. This replaces the parallel rendering paths (private layers, own store sync, multiple OL features sharing one id) and brings the standard machinery for free: delete, undo, hide/show, lock, vertex modify and selection. - LoS: LineString observer→target; the async sight-line analysis lives in the style orchestrator (ol/style/los.js) — renders as pending line until the profile arrives, recomputes on geometry/height changes and once terrain becomes available (bridge: ol/style/losCompute.js) - AoS: Point observer with radius/height properties; observer point and radius rim are pipeline styles (ol/style/aos.js), the visibility raster stays in the interaction and now mirrors hidden state (including temporary reveal while highlighted in search) - sidebar/search: options/documents handlers for both scopes with name, distance/radius description, rename and tagging - properties panels operate on GeoJSON properties - pre-pipeline documents are migrated to GeoJSON on startup
- LoS: arrow up/down adjusts observer height, shift+arrows target height while placing; live preview recomputes, values shown in OSD - AoS: arrow up/down adjusts radius (250 m steps), shift/alt+arrows observer/target height; preview recomputes on change - Escape cancels an active placement - last-used values persist per project (session store) and become the defaults for the next placement; placed objects remain editable via the properties panel
getTileGrid() returns null until a TileJSON source has fetched its metadata. setSource treated "terrain layer present" as "terrain ready" and the AoS initial load crashed on the null tile grid at startup. - setSource returns false while the tile grid is not available yet - new onTerrainReady(map, attempt) retries when a layer is added AND when a pending source finishes loading; LoS/AoS use it for computer registration and initial document rendering - getGrid/profileAlongLine/elevationAt guard against a missing grid
The emitter dispatches handlers via setImmediate, so the OSD clear from idle tools' command/draw/cancel resets landed after the freshly shown placement hint and erased it — the hint only reappeared after the first arrow-key press. - reset() clears the OSD only when the tool was actually active - the hint is re-asserted on pointer move (LoS placing phase, AoS preview) so it survives any remaining dispatch-order race
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two terrain-analysis tools: Line-of-Sight (observer→target sight line with blocker detection) and Area-of-Sight (real-time viewshed). Both are backed by a reworked elevation service that samples deterministically from Mapbox-Terrain-RGB tiles, and both integrate into the standard feature pipeline like measurements.
The AoS engine runs the R2 viewshed algorithm as a WebGPU compute shader — one thread per ray — computing a 10 km radius at 10 m resolution in single-digit milliseconds, so the viewshed follows the cursor in real time during placement (benchmarked against single/multi-threaded CPU and a naive WebGL2 fragment shader; the CPU implementation remains as reference and fallback).
Changes
getGrid(extent)stitches tile-aligned elevation grids; handles TileJSON sources that are still loading their metadatafeatureSource— delete, undo, hide/show, lock, vertex modify (dragging endpoints recomputes live) and selection come from the standard machinery; pre-pipeline documents are migrated on startup