prop/lib/microwaveprop/rover/prominence.ex
Graham McIntire eac817cd57
feat(rover): prefer broad hilltops near roads as ideal candidates
- 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
2026-04-25 17:45:48 -05:00

67 lines
1.9 KiB
Elixir

defmodule Microwaveprop.Rover.Prominence do
@moduledoc """
Local terrain prominence per rover cell: how far the cell sits above
the average elevation of its 8 immediate neighbors at SRTM tile scale.
Used by the rover scorer to favor *broad* hilltops over isolated SRTM
voxels that just happen to be tall — a hill big enough to actually
park on and run a station, not a tree poking through the data.
"""
alias Microwaveprop.Rover.Elevation
@ring_offset_deg 0.012
@ring_offsets [
{@ring_offset_deg, 0.0},
{-@ring_offset_deg, 0.0},
{0.0, @ring_offset_deg},
{0.0, -@ring_offset_deg},
{@ring_offset_deg, @ring_offset_deg},
{@ring_offset_deg, -@ring_offset_deg},
{-@ring_offset_deg, @ring_offset_deg},
{-@ring_offset_deg, -@ring_offset_deg}
]
@type latlon :: {float(), float()}
@doc """
Returns `%{ {lat, lon} => prominence_m | nil }` for each cell.
`prominence_m = cell_elev - mean(ring_elevs)`. ~1.3 km ring radius
matches the scale of the kind of hill a rover wants to park on.
"""
@spec prominence_map([map()], keyword()) :: %{latlon() => integer() | nil}
def prominence_map(cells, opts \\ []) do
elev_lookup = Keyword.get(opts, :elev_lookup, &Elevation.lookup_many/1)
cell_points = Enum.map(cells, fn c -> {c.lat, c.lon} end)
ring_points =
cells
|> Enum.flat_map(&ring_for/1)
|> Enum.uniq()
elev = elev_lookup.(cell_points ++ ring_points)
Map.new(cells, fn cell ->
key = {cell.lat, cell.lon}
cell_elev = Map.get(elev, key)
ring =
cell
|> ring_for()
|> Enum.map(&Map.get(elev, &1))
|> Enum.reject(&is_nil/1)
prominence =
if is_integer(cell_elev) and ring != [],
do: cell_elev - round(Enum.sum(ring) / length(ring))
{key, prominence}
end)
end
defp ring_for(%{lat: lat, lon: lon}) do
Enum.map(@ring_offsets, fn {dlat, dlon} -> {lat + dlat, lon + dlon} end)
end
end