feat(rover): snap top candidates to SRTM hilltops + add hillshade overlay
This commit is contained in:
parent
3c877b8b8e
commit
7d80babb18
4 changed files with 122 additions and 11 deletions
|
|
@ -18,6 +18,7 @@ interface RoverMapHook extends ViewHook {
|
|||
map: L.Map
|
||||
osmLayer: L.TileLayer
|
||||
topoLayer: L.TileLayer | null
|
||||
hillshadeLayer: L.TileLayer
|
||||
layersControl: L.Control.Layers
|
||||
stations: Station[]
|
||||
stationMarkers: L.LayerGroup
|
||||
|
|
@ -108,12 +109,17 @@ export const RoverMap: Partial<RoverMapHook> = {
|
|||
this.topoLayer = L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 17
|
||||
})
|
||||
this.hillshadeLayer = L.tileLayer(
|
||||
"https://services.arcgisonline.com/ArcGIS/rest/services/Elevation/World_Hillshade/MapServer/tile/{z}/{y}/{x}",
|
||||
{ maxZoom: 16, opacity: 0.45 }
|
||||
)
|
||||
|
||||
this.osmLayer.addTo(map)
|
||||
this.hillshadeLayer.addTo(map)
|
||||
|
||||
this.layersControl = L.control.layers(
|
||||
{ "OSM": this.osmLayer, "Topo": this.topoLayer },
|
||||
undefined,
|
||||
{ "Hillshade": this.hillshadeLayer },
|
||||
{ position: "topright" }
|
||||
).addTo(map)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ defmodule Microwaveprop.Rover.Compute do
|
|||
alias Microwaveprop.Rover.Aggregator
|
||||
alias Microwaveprop.Rover.DriveTime
|
||||
alias Microwaveprop.Rover.Elevation
|
||||
alias Microwaveprop.Rover.Hilltop
|
||||
alias Microwaveprop.Rover.LinkMargin
|
||||
alias Microwaveprop.Rover.PathTerrain
|
||||
alias Microwaveprop.Rover.Prominence
|
||||
|
|
@ -73,6 +74,7 @@ defmodule Microwaveprop.Rover.Compute do
|
|||
clearance_lookup = Keyword.get(deps, :clearance_lookup, &PathTerrain.clearance_map/2)
|
||||
prominence_lookup = Keyword.get(deps, :prominence_lookup, &Prominence.prominence_map/1)
|
||||
road_lookup = Keyword.get(deps, :road_lookup, &RoadProximity.road_distances/2)
|
||||
hilltop_snap = Keyword.get(deps, :hilltop_snap, &Hilltop.snap/1)
|
||||
|
||||
%{
|
||||
home: home,
|
||||
|
|
@ -131,7 +133,7 @@ defmodule Microwaveprop.Rover.Compute do
|
|||
cells
|
||||
|> Enum.sort_by(& &1.score, :desc)
|
||||
|> Enum.take(@top_n)
|
||||
|> Enum.map(&candidate_payload(&1, home))
|
||||
|> Enum.map(&candidate_payload(&1, home, hilltop_snap))
|
||||
|
||||
warnings = build_warnings(raw_cells, in_radius, elev_map, cells)
|
||||
|
||||
|
|
@ -242,24 +244,31 @@ defmodule Microwaveprop.Rover.Compute do
|
|||
db |> max(-@prominence_cap_db) |> min(@prominence_cap_db)
|
||||
end
|
||||
|
||||
defp candidate_payload(cell, home) do
|
||||
drive_min = DriveTime.drive_min(cell.distance_km)
|
||||
bearing = DriveTime.bearing_compass({home.lat, home.lon}, {cell.lat, cell.lon})
|
||||
grid = Maidenhead.from_latlon(cell.lat, cell.lon, 10)
|
||||
defp candidate_payload(cell, home, hilltop_snap) do
|
||||
{lat, lon, elev_m} =
|
||||
case hilltop_snap.({cell.lat, cell.lon}) do
|
||||
{hl_lat, hl_lon, hl_elev} -> {hl_lat, hl_lon, hl_elev}
|
||||
_ -> {cell.lat, cell.lon, cell.elev_m}
|
||||
end
|
||||
|
||||
distance_km = DriveTime.haversine_km({home.lat, home.lon}, {lat, lon})
|
||||
drive_min = DriveTime.drive_min(distance_km)
|
||||
bearing = DriveTime.bearing_compass({home.lat, home.lon}, {lat, lon})
|
||||
grid = Maidenhead.from_latlon(lat, lon, 10)
|
||||
|
||||
%{
|
||||
grid: grid,
|
||||
lat: cell.lat,
|
||||
lon: cell.lon,
|
||||
elev_m: cell.elev_m,
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
elev_m: elev_m,
|
||||
prominence_m: cell.prominence_m,
|
||||
road_km: cell.road_km,
|
||||
drive_min: drive_min,
|
||||
score: cell.score,
|
||||
tier_color: cell.tier_color,
|
||||
distance_km: cell.distance_km,
|
||||
distance_km: distance_km,
|
||||
bearing_compass: bearing,
|
||||
name: "#{grid} — #{round(cell.distance_km)} km #{bearing} of home"
|
||||
name: "#{grid} — #{round(distance_km)} km #{bearing} of home"
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
55
lib/microwaveprop/rover/hilltop.ex
Normal file
55
lib/microwaveprop/rover/hilltop.ex
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
defmodule Microwaveprop.Rover.Hilltop do
|
||||
@moduledoc """
|
||||
Snaps a propagation-grid cell center to the highest SRTM point inside
|
||||
its footprint. The propagation grid is on a 0.125° lattice (~14 km),
|
||||
but a rover wants to park on the actual hilltop inside that cell, not
|
||||
at the nominal cell center. Sample SRTM at sub-cell resolution and
|
||||
return the highest point.
|
||||
"""
|
||||
|
||||
alias Microwaveprop.Rover.Elevation
|
||||
|
||||
@default_half_deg 0.0625
|
||||
@default_step_deg 0.01
|
||||
|
||||
@type latlon :: {float(), float()}
|
||||
|
||||
@doc """
|
||||
Returns `{best_lat, best_lon, best_elev}` for the highest SRTM point
|
||||
inside `±half_deg` of `cell_center`, or `nil` if SRTM coverage is
|
||||
missing across the whole footprint.
|
||||
"""
|
||||
@spec snap(latlon(), keyword()) :: {float(), float(), integer()} | nil
|
||||
def snap({lat, lon}, opts \\ []) do
|
||||
half = Keyword.get(opts, :half_deg, @default_half_deg)
|
||||
step = Keyword.get(opts, :step_deg, @default_step_deg)
|
||||
elev_lookup = Keyword.get(opts, :elev_lookup, &Elevation.lookup_many/1)
|
||||
|
||||
points = sample_grid(lat, lon, half, step)
|
||||
elev_map = elev_lookup.(points)
|
||||
|
||||
points
|
||||
|> Enum.flat_map(fn pt ->
|
||||
case Map.get(elev_map, pt) do
|
||||
nil -> []
|
||||
elev -> [{pt, elev}]
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
[] ->
|
||||
nil
|
||||
|
||||
list ->
|
||||
{{best_lat, best_lon}, best_elev} = Enum.max_by(list, fn {_, e} -> e end)
|
||||
{best_lat, best_lon, best_elev}
|
||||
end
|
||||
end
|
||||
|
||||
defp sample_grid(lat, lon, half, step) do
|
||||
n = max(trunc(half / step), 1)
|
||||
|
||||
for i <- -n..n, j <- -n..n do
|
||||
{Float.round(lat + i * step, 4), Float.round(lon + j * step, 4)}
|
||||
end
|
||||
end
|
||||
end
|
||||
41
test/microwaveprop/rover/hilltop_test.exs
Normal file
41
test/microwaveprop/rover/hilltop_test.exs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
defmodule Microwaveprop.Rover.HilltopTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Microwaveprop.Rover.Hilltop
|
||||
|
||||
test "snap/2 returns the highest sampled SRTM point inside the cell" do
|
||||
cell_center = {33.0, -96.0}
|
||||
|
||||
# Stub elev lookup: highest elevation NE of cell center.
|
||||
elev_lookup = fn points ->
|
||||
Map.new(points, fn {lat, lon} ->
|
||||
elev = round((lat - 33.0) * 1000 + (lon + 96.0) * 1000)
|
||||
{{lat, lon}, elev}
|
||||
end)
|
||||
end
|
||||
|
||||
{lat, lon, elev} = Hilltop.snap(cell_center, elev_lookup: elev_lookup)
|
||||
|
||||
# Best point should be the corner with the largest +lat, +lon offsets
|
||||
# (NE corner of the sample grid).
|
||||
assert lat > 33.0
|
||||
assert lon > -96.0
|
||||
assert is_integer(elev)
|
||||
assert elev > 0
|
||||
end
|
||||
|
||||
test "snap/2 returns nil when SRTM lookup yields no values" do
|
||||
elev_lookup = fn points -> Map.new(points, fn p -> {p, nil} end) end
|
||||
|
||||
assert Hilltop.snap({33.0, -96.0}, elev_lookup: elev_lookup) == nil
|
||||
end
|
||||
|
||||
test "snap/2 stays within the requested half-degree radius" do
|
||||
elev_lookup = fn points -> Map.new(points, fn p -> {p, 100} end) end
|
||||
|
||||
{lat, lon, _} = Hilltop.snap({33.0, -96.0}, elev_lookup: elev_lookup, half_deg: 0.0625)
|
||||
|
||||
assert_in_delta lat, 33.0, 0.0625 + 1.0e-6
|
||||
assert_in_delta lon, -96.0, 0.0625 + 1.0e-6
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue