prop/lib/microwaveprop/geo.ex
Graham McIntire 8a7719b953 Add /rover planner for microwave contest rovers
Interactive map-based tool for planning rover operating positions.
Rovers enter stationary stations they want to work, then click the
map to evaluate candidate operating grids.

Features:
- Add stationary stations by callsign or grid square
- Click map to add operating positions (snaps to Maidenhead grid)
- Async SRTM terrain analysis: each stop computes terrain profile
  to every station, shows CLEAR/BLOCKED verdict + diffraction loss
- Terrain lines on map: green=workable, red=blocked, dashed=marginal
- Propagation score and 18-hour forecast sparkline per stop
- Route summary: driving distance, unique grid-station pairs
- Band selector affects range limits and propagation scores
- Numbered waypoint markers with score-colored backgrounds

New files:
- Geo module (shared haversine/bearing, used by PathLive too)
- RoverLive with fullscreen map + sidebar
- RoverMap JS hook with grid overlay, station/stop markers, route line
- Nav links in header and map control panel
2026-04-07 14:17:48 -05:00

48 lines
1.6 KiB
Elixir

defmodule Microwaveprop.Geo do
@moduledoc "Geodetic helper functions: distances, bearings, coordinate math."
@earth_radius_km 6371.0
@doc "Great-circle distance between two points in km."
def haversine_km(lat1, lon1, lat2, lon2) do
dlat = deg_to_rad(lat2 - lat1)
dlon = deg_to_rad(lon2 - lon1)
a =
:math.sin(dlat / 2) ** 2 +
:math.cos(deg_to_rad(lat1)) * :math.cos(deg_to_rad(lat2)) * :math.sin(dlon / 2) ** 2
2 * @earth_radius_km * :math.asin(:math.sqrt(a))
end
@doc "Initial bearing from point 1 to point 2 in degrees (0-360)."
def bearing_deg(lat1, lon1, lat2, lon2) do
lat1r = deg_to_rad(lat1)
lat2r = deg_to_rad(lat2)
dlonr = deg_to_rad(lon2 - lon1)
y = :math.sin(dlonr) * :math.cos(lat2r)
x = :math.cos(lat1r) * :math.sin(lat2r) - :math.sin(lat1r) * :math.cos(lat2r) * :math.cos(dlonr)
b = :math.atan2(y, x) * 180 / :math.pi()
Float.round(:math.fmod(b + 360, 360), 1)
end
@doc "Center of a 4-character Maidenhead grid square."
def maidenhead_center(grid) when is_binary(grid) and byte_size(grid) >= 4 do
case Microwaveprop.Radio.Maidenhead.to_latlon(grid) do
{:ok, {lat, lon}} -> {lat, lon}
:error -> nil
end
end
@doc "4-character Maidenhead grid for a lat/lon."
def latlon_to_grid4(lat, lon) do
lon_field = trunc((lon + 180) / 20)
lat_field = trunc((lat + 90) / 10)
lon_sq = trunc(rem(trunc(lon + 180), 20) / 2)
lat_sq = trunc(rem(trunc(lat + 90), 10) / 1)
<<?A + lon_field, ?A + lat_field, ?0 + lon_sq, ?0 + lat_sq>>
end
defp deg_to_rad(d), do: d * :math.pi() / 180
end