Click a row's info area to expand a Leaflet mini-map centered on that
pin, defaulting to ESRI World_Imagery satellite (zoom up to 21) with
OSM/Topo toggles in a top-right layer control.
Adds a checkbox in the rover sidebar that constrains the suggested
candidate spots to lat/lon points tagged as :ideal in /rover-locations.
When enabled, Compute.run replaces grid cells with synthetic cells at
each ideal location's exact coordinates, inheriting the propagation
score from the nearest grid cell so atmospheric forecast still factors
in. Path-clearance, terrain, building, and canopy penalties run
unchanged on those snapped candidates.
New globally-shared list of rover-friendly (or off-limits) parking
spots. Anyone can view; logged-in users can add new locations and
edit/delete their own. Each location stores lat/lon, free-text notes,
and a status enum (:ideal | :off_limits).
* Microwaveprop.Rover.Location schema + migration
* Rover.list_locations/0, create_location/2, update_location/3,
delete_location/2 (mutations require User; ownership-scoped)
* /rover-locations LiveView in the public live_session — Add button
is disabled for anonymous visitors, Edit/Delete buttons render
only on the user's own entries
* Tests for context + LiveView covering anonymous read, owner-only
edit/delete, and form submission
Rover ranking now applies a per-cell tree-canopy clutter penalty
(1 dB / 8 m, capped at 4 dB) so cells in dense forests get down-ranked
even when their per-station path clearance survives. PathTerrain
already accounts for trees ON the link path; this surfaces "I'm in a
forest, every direction is foliage."
Canopy.BulkFetch downloads Lang et al. 2020 10 m global canopy-height
3-degree COGs and slices each into 1° × 1° uint8 .canopy tiles via
gdal_translate, the runtime image now ships gdal-bin. Run on prod via:
bin/microwaveprop rpc 'Microwaveprop.Canopy.BulkFetch.run(32.8, -97.0, 200)'
Tile cache lives under /data/canopy/. Already-sliced tiles are skipped.
Adds Microwaveprop.Canopy module that reads 1° × 1° uint8 per-degree
canopy-height tiles (mirrors SRTM .hgt naming/layout, 30 m resolution).
Returns 0 m for areas with no tile on disk so the lookup is safe to
thread through the existing path-clearance pipeline before any data
lands.
Wires canopy into:
- PathTerrain.obstacle_top: per-sample blocker = ground +
max(building_m, canopy_m), so the rover ranks paths through forests
as obstructed even when no buildings sit on the line
- CandidateDetail profile: each sample now carries canopy_m alongside
building_m
- Rover-detail SVG: green "trees" polygon stacked on terrain, under
the red building polygon
- Path calculator elevation chart: green Tree Canopy dataset between
terrain and buildings
Data prep (download Potapov 2019 / Lang 2022 GeoTIFF, slice to per-degree
uint8 tiles, drop in /data/canopy/) is a separate one-shot operation —
without tiles, lookups return 0 and the existing behavior is unchanged.
Path calculator now queries Microsoft Building Footprints for the
tallest structure within 80m of each profile sample and renders a red
"Buildings" dataset on top of the terrain layer in the elevation chart.
Also normalize on-disk grid HRRR cells to include the legacy
:min_refractivity_gradient / :surface_refractivity / :ducting_detected
keys (mapped from :native_min_gradient and :duct_count). Path-calculator
crashed in prod with KeyError when a path's HRRR points came from the
new on-disk grid format instead of the DB profile shape.
Compute.run now accepts a `progress` callback and reports human-readable
labels before each pipeline step (loading grid, looking up elevation,
checking road access, scoring cells, etc.). RoverLive feeds it via
send/2 to itself and renders the latest label as small text to the left
of the Calculate button while it's running.
Each profile sample now carries the tallest building height within 80m
of the path point. The candidate-detail SVG renders these as a red
polygon stacked on top of the terrain so blockage from buildings is
visible alongside ridges. max_obstacle_m now reflects building tops too,
so the clearance label downgrades when buildings sit on the link.
Adds a per-cell building-clutter penalty so the algorithm down-ranks
spots in built environments where buildings on multiple sides
scatter/block signal regardless of which station you're aiming at.
Penalty = max_height_within_75m / 5 dB, capped at 6 dB. Path-clearance
already accounts for buildings ON the link path; this is the
"surrounded by stuff" signal.
Phase 2/3/5 of the building-blockage support:
* Parser streams csv.gz tiles into compact records (centroid, radius,
height) — keeps RAM ~32 B per polygon, drops -1.0-height entries.
* Index buckets records into 0.01° (~1 km) grid cells in a public ETS
table for concurrent reads; max_height_near/3 and records_near/3
scan only the 9 nearest buckets.
* Loader lazily parses any cached quadkey before a Calculate so the
scorer is terrain-only when tiles aren't on disk yet.
* Rover.PathTerrain now treats each path-sample obstacle as
terrain_elev + max_local_building_height, so blocked LOS through
recent construction actually reduces clearance.
* Selecting a candidate pushes a candidate_buildings event with the
buildings near each rover→station path; the hook renders them as
height-coloured circles (yellow <10 m, orange 10-30 m, red >=30 m)
with a tooltip showing height in meters.
Mix tasks don't run in production, so the orchestration lives in
Microwaveprop.Buildings.BulkFetch — callable from an attached iex on
the prod pod via:
iex> Microwaveprop.Buildings.BulkFetch.run(32.8, -97.0, 500)
Default 500 mi radius around DFW yields ~440 candidate quadkeys at
zoom 9; only those present in the MS dataset index (typically a few
hundred over CONUS) get downloaded. Concurrency 4, per-task timeout
3 min, results logged with downloaded/cached/failed/missing counts.
The mix.task is kept as a dev-only thin wrapper.
Phase 1 of building-blockage support: implements quadkey math (Bing/MS
tile encoding), looks up the MS dataset-links index for UnitedStates,
and downloads csv.gz quadkey tiles to /data/buildings (overridable via
:buildings_cache_dir). Index is cached 24h; tiles are written once and
reused. No path-analysis wiring yet — that's the next slice.
Prod logs showed road_proximity took 186 s on a single Calculate.
Req's receive_timeout only catches TCP idle, not slow streaming, so a
trickling Overpass response could not be cancelled. Wrap the call in a
Task.yield/shutdown deadline and cache successful results by 0.05°-rounded
bbox so adjacent Calculates skip Overpass entirely.
- Detail panel now reads as an aiming card: "Aim 220° (SW) · clearance +12 ft"
with the precise bearing in degrees, plus rover/obstacle elev on a second line
- Elevation profile labels swapped from min/max-of-profile to start (rover) and
end (station) elevations, since their positions in the chart now match
- Map draws a dashed indigo polyline from the candidate to each selected
station when a candidate is opened; cleared when the panel closes
Calculate occasionally hung in production for ~30s. Adds per-phase
timing logs to Compute.run so the slow step is visible in pod logs,
and trims the Overpass road-proximity request to a 10s server-side
timeout / 12s receive timeout (no Req retries) so a slow road API
fails soft to no road penalty instead of stalling Calculate.
- Local prominence: each cell's elevation vs. its 8-neighbor ring
(~1.3 km radius) feeds a small additive bonus so broad hilltops
rank above isolated SRTM voxels
- Road proximity: one Overpass call per Calculate fetches drivable
ways inside the bbox; cells beyond ~0.5 km of any road get a
light dB penalty, gated off in tests