Commit 084fe65
authored
Line-of-Sight and Area-of-Sight terrain analysis (#122)
* feat(los): add Line-of-Sight tool with live cursor tracking
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.
* feat(los): persist Line-of-Sight features across reloads
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.
* feat(los): selection + properties panel with height editor
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.
* feat(elevation): deterministic terrain sampling at fixed analysis zoom
- 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)
* feat(aos): real-time Area-of-Sight via WebGPU viewshed
- 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
* feat(analysis): integrate LoS/AoS into the standard feature pipeline
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
* feat(analysis): adjustable heights and radius during placement
- 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
* fix(elevation): handle TileJSON sources that are still loading
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
* fix(analysis): settings hint no longer swallowed by async OSD clears
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
* feat(sidebar): list LoS/AoS under the measurements scope
The measurements scope switch now covers '@measure @Los @aos' (scope
query tokens combine with OR). The active-state check handles
multi-token switches: active when all of its tokens are part of the
current search scope.1 parent 0611243 commit 084fe65
33 files changed
Lines changed: 2328 additions & 111 deletions
File tree
- src/renderer
- components
- map
- properties
- sidebar
- model
- commands
- sources
- ol
- interaction
- area-of-sight
- elevation-profile
- line-of-sight
- style
- store
- documents
- options
- test/renderer
- model
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | | - | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
83 | 85 | | |
84 | 86 | | |
85 | 87 | | |
| 88 | + | |
| 89 | + | |
86 | 90 | | |
87 | 91 | | |
88 | 92 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
Lines changed: 51 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
Lines changed: 58 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| |||
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
41 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
42 | 46 | | |
43 | 47 | | |
44 | 48 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
49 | 52 | | |
50 | 53 | | |
51 | | - | |
| 54 | + | |
52 | 55 | | |
53 | 56 | | |
54 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
| |||
45 | 47 | | |
46 | 48 | | |
47 | 49 | | |
| 50 | + | |
| 51 | + | |
48 | 52 | | |
49 | 53 | | |
50 | 54 | | |
| |||
106 | 110 | | |
107 | 111 | | |
108 | 112 | | |
| 113 | + | |
| 114 | + | |
109 | 115 | | |
110 | 116 | | |
111 | 117 | | |
| |||
173 | 179 | | |
174 | 180 | | |
175 | 181 | | |
| 182 | + | |
| 183 | + | |
176 | 184 | | |
177 | 185 | | |
178 | 186 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
| |||
0 commit comments