Skip to content

feat(web-standalone): antenna height from the tallest building within 50 m of a repeater - #128

Merged
khagele merged 2 commits into
mainfrom
roof-height
Sep 7, 2026
Merged

khagele merged 2 commits into
mainfrom
roof-height

Conversation

@khagele

@khagele khagele commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #74, the antenna-height half. The other half, building geometry in the line-of-sight profile, is measured and not built; numbers in #74 (comment): dense sampling calls 6 of 10 real receptions blocked or partial, so more resolution penalises the true cell under the current knife-edge model.

The line-of-sight check put every observer's antenna at 12 m. Sampled against 3D BAG, 32 of 60 fixture observers stand within 12 m of a building, roof median 10.2 m above ground, p90 25 m; the constant is right for about half of them and low for a quarter.

Change

  • bag3d.py (stdlib, tested): WGS84 to RD New and back (within 1 m on the Westertoren and the Martinitoren), the buildings in a 3D BAG CityJSON response with their footprints in metres via the response transform, distance from a point to a footprint (0 inside), and the tallest building whose footprint comes within a radius, ties to the nearer one.
  • server.py: /proxy/3dbag/roof?lat=&lon=, a 100 m box around the node, up to 4 pages of 100 features, radius 50 m, cached for a day per position; no roof outside the Netherlands; 400 on bad input.
  • On activation the page looks up the same nodes the link budget covers and sets roofHeightM and roofDistanceM; terrainLosFactor() starts the beam at roof plus a 3 m mast for those and at 12 m for the rest. The cluster status says how many; the observer popup shows the antenna height, its source and the distance to that roof; a dragged node drops its roof with its radius.
  • Dockerfile and .dockerignore ship the module; README and AGENTS name the source; changelog entry.

Why 50 m and the tallest, not the footprint underfoot: advertised positions are metres off and a mast can stand next to the building it serves. At 100 m, 3 of 8 observers on the test region picked a taller building 60 to 90 m away; at 50 m they keep the one beside them.

Verified

  • pytest web-standalone/tests: 13 passed.
  • Live route: 48.7 m in the Amsterdam centre (2 pages, 7.5 s first time, 0.01 s cached), 75.9 m on the Westertoren, 11.7 m at 79 m for a suburban node at 100 m radius; no roof for Paris; 400 for lat=x.
  • In the page, region 1 of the fixture deep link: 14 lookups, 13 of 14 observers have a building within 50 m, the farthest at 47.2 m; antennas from 10.6 m (a 7.6 m roof 23 m away) to 39 m (a 36 m roof underfoot); status "13 of 14 observers have a building within 50 m per 3D BAG and use its roof, the rest assume 12 m"; no console errors.

Not measurable

The effect on estimates: the accuracy harness does not run the terrain pass (#68, #72). This lands on the argument that a 25 m antenna at 25 m beats a 25 m antenna at 12 m in a diffraction profile, not on a number.

🤖 Generated with Claude Code

khagele and others added 2 commits September 7, 2026 23:17
The line-of-sight check put every observer's antenna at 12 m (#74).
Sampled against 3D BAG, 32 of 60 fixture observers stand within 12 m of
a building, roof median 10.2 m above ground, p90 25 m; the constant is
right for about half of them and low for a quarter.

bag3d.py (stdlib, tested): WGS84 to RD New and back (within 1 m on the
Westertoren and the Martinitoren), the buildings in a 3D BAG CityJSON
response with their footprints in metres via the response transform,
point-in-footprint, and roof_for_point(). server.py serves
/proxy/3dbag/roof?lat=&lon=, one 30 m bbox query per node cached for a
day, no roof outside the Netherlands, 400 on bad input.

On activation the page looks up the same nodes the link budget covers
and sets roofHeightM where the node stands inside a footprint;
terrainLosFactor() starts the beam at roof plus a 3 m mast for those and
at 12 m for the rest. The cluster status says how many, the observer
popup shows the antenna height and its source, and a dragged node drops
its roof with its radius. Dockerfile and .dockerignore ship the module;
README and AGENTS name the source.

The other half of #74, building geometry in the line-of-sight profile,
is measured and not built: dense sampling calls 6 of 10 real receptions
blocked or partial, so more resolution penalises the true cell under
the current knife-edge model. Numbers in the issue.

Verified: pytest 11 passed; the live route returns 35.5 m for the
fixture building, 75.9 m on the Westertoren, no roof for Paris; in the
page a locked cluster of 14 nodes makes 14 lookups, 3 stand on a
building, two of them active with antennas at 39 m and 13.8 m, the rest
at 12 m, status "3 of 14 observers stand on a building per 3D BAG, the
rest assume 12 m", no console errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the one underfoot

Advertised positions are metres off and a mast can stand next to the
building it serves, so "the footprint the point is inside" missed most
of them. The roof now comes from the tallest building whose footprint
comes within 50 m of the node, ties to the nearer one, and the popup
says how far that roof is.

bag3d.py: footprint_distance() (0 inside, else the nearest edge),
tallest_near() over one or more response pages, roof_for_point() with a
radius and a distance_m. server.py follows rel=next up to 4 pages of
100 features, radius 50 m. Tests for the distance, the radius cut-off
and the taller building on a later page winning: 13 passed.

Verified live: on the fixture deep link's region 1, 13 of 14 observers
have a building within 50 m, the farthest at 47.2 m; antennas range
from 10.6 m (a 7.6 m roof 23 m away) to 39 m (a 36 m roof underfoot).
The Amsterdam-centre lookup takes 7.5 s for two pages the first time
and 0.01 s from cache after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@khagele
khagele merged commit 40dbb5e into main Sep 7, 2026
2 checks passed
@khagele
khagele deleted the roof-height branch September 7, 2026 21:35
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.

Use 3D BAG building geometry for line of sight instead of raster sampling

1 participant