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