Skip to content

Observer siting: optimal observer placement for an area#123

Merged
ThomasHalwax merged 5 commits into
mainfrom
feature/observer-siting
Jul 6, 2026
Merged

Observer siting: optimal observer placement for an area#123
ThomasHalwax merged 5 commits into
mainfrom
feature/observer-siting

Conversation

@ThomasHalwax

Copy link
Copy Markdown
Member

Summary

Adds Observer Siting: given an area — drawn ad hoc, a selected polygon feature, or a selected (nearly) closed boundary line — find a small set of observer positions inside it whose combined viewsheds maximize coverage of that area. This answers the planning question "with how few observation posts can I overwatch this terrain, and where do they go?".

The optimization is a greedy max-coverage over candidate viewsheds ((1 − 1/e) approximation of the NP-hard set-cover optimum). Candidate positions come from block max-pooling over the elevation grid — each block contributes its highest cell, so candidates spread uniformly across the whole area (flat parts included) while hilly blocks contribute their local summit. Per-candidate viewsheds are computed by the WebGPU engine from #122, so a full run over ~100 candidates takes a few seconds with OSD progress.

Changes

  • Tool flow: toolbar command (binoculars) in the measurement group; with a polygon/boundary selected the tool arms and waits — sensor radius adjustable with arrow keys, Enter computes, Escape cancels; without a selection the area is drawn ad hoc (radius adjustable while drawing)
  • Algorithm (observer-siting/solve.js, pure and unit-tested): even-odd scanline polygon rasterization (holes supported), block max-pooling candidate generation with budget-adaptive block size, greedy max-coverage with stop criteria (95 % coverage, 12 observers, < 1 % marginal gain)
  • Boundaries: selected LineString graphics forming a (nearly) closed ring — endpoints within 100 m — are converted to polygons; unusable selections get an OSD notice and fall back to drawing
  • Results: observers are inserted as regular AoS documents (OP 1/4 - <timestamp>) — viewsheds render through the standard pipeline, each observer stays individually editable/deletable, one undo step removes the whole run
  • Group tag: all observers of a run share a random tag (e.g. OP-3f2a), reported in the OSD summary and filterable in the sidebar via #tag
  • Honest reporting: the OSD summary states coverage and sensor radius and suggests a larger radius when the 95 % target was missed
  • 15 unit tests including an end-to-end case (two hills with a deep valley → one observer per summit) and a regression test for candidate starvation of flat regions next to high terrain

Given an area (drawn ad hoc or a selected polygon feature), find a
small set of observer positions inside it whose combined viewsheds
maximize coverage of the area — greedy max-coverage over candidate
viewsheds ((1 - 1/e) approximation of the NP-hard set-cover optimum).

- candidates: local terrain maxima inside the polygon, thinned by
  non-maximum suppression (250 m spacing), lattice fallback for flat
  terrain; per-candidate viewsheds via the shared WebGPU engine
- polygon rasterization by even-odd scanline (holes supported)
- stops at 95 % coverage, 8 observers, or < 1 % marginal gain
- radius and heights come from the last-used AoS settings
- results are inserted as regular AoS documents (one undo step):
  viewsheds render through the standard pipeline, each observer stays
  individually editable; Escape cancels draw or a running computation
Candidates were local maxima sorted by elevation and capped — next to
high terrain, a lower flat part of the area received no candidates at
all and stayed uncovered (and dead-flat terrain flooded the peak
detector, every cell being >= its neighbours).

- candidates now come from block max-pooling: each block contributes
  its highest in-area cell, so candidates spread uniformly over the
  whole area while hilly blocks still yield their local summit; block
  size grows until the count fits the budget
- sensor radius is shown in the OSD, adjustable with arrow keys while
  drawing the area, and persisted as the shared AoS default
- result message reports coverage and radius, and suggests a larger
  radius when the coverage target was missed; observer cap raised to 12
- regression test: low flat plain next to mountains keeps its share
  of candidates
Starting the tool with a preselected polygon no longer computes right
away: it arms and shows the sensor radius in the OSD — arrow keys
adjust, Enter starts the computation, Escape cancels. Same radius
adjustment as in the draw phase.
Tactical boundaries are LineString graphics; when a selected line
forms a (nearly) closed ring — endpoints within 100 m — it is
converted to a polygon and used as the siting area. Unusable
selections get an OSD notice and fall back to drawing.
Observers placed by one siting run get a common random tag
(e.g. OP-3f2a) so they stay recognizable — and filterable in the
sidebar via #tag — as the set covering one defined area. The tag is
part of the same insert batch, so one undo still removes everything,
and it is reported in the OSD summary.
@ThomasHalwax
ThomasHalwax merged commit 858d47f into main Jul 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant