Auto-download missing SRTM tiles, treat water areas as 0m elevation

SRTM profiles no longer abort on missing tiles — water/ocean areas
return 0m elevation instead of failing the entire profile and falling
back to the rate-limited API. Elevation client and viewshed now pass
download: true to auto-fetch missing tiles from S3. NFS mount changed
to writable so downloaded tiles persist across pods.
This commit is contained in:
Graham McIntire 2026-04-11 16:30:17 -05:00
parent 80d1169f71
commit e74adf0036
6 changed files with 32 additions and 56 deletions

View file

@ -105,10 +105,8 @@ spec:
volumeMounts:
- name: srtm-data
mountPath: /srtm
readOnly: true
volumes:
- name: srtm-data
nfs:
server: 204.110.191.8
path: /data/srtm
readOnly: true

View file

@ -16,14 +16,8 @@ defmodule Microwaveprop.Terrain.ElevationClient do
fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n)
tiles_dir ->
case Srtm.fetch_elevation_profile(lat1, lon1, lat2, lon2, tiles_dir, n, opts) do
{:ok, _profile} = ok ->
ok
{:error, reason} ->
Logger.warning("SRTM failed (#{inspect(reason)}), falling back to API")
fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n)
end
srtm_opts = Keyword.merge([download: true], opts)
Srtm.fetch_elevation_profile(lat1, lon1, lat2, lon2, tiles_dir, n, srtm_opts)
end
end

View file

@ -84,26 +84,19 @@ defmodule Microwaveprop.Terrain.Srtm do
pts = sample_path(lat1, lon1, lat2, lon2, n)
dist_km = haversine_km(lat1, lon1, lat2, lon2)
results =
Enum.reduce_while(pts, {:ok, []}, fn pt, {:ok, acc} ->
case lookup(pt.lat, pt.lon, tiles_dir, opts) do
{:ok, elev} ->
entry = %{
lat: pt.lat,
lon: pt.lon,
d: pt.d,
elev: elev,
dist_km: pt.d * dist_km
}
profile =
Enum.map(pts, fn pt ->
elev =
case lookup(pt.lat, pt.lon, tiles_dir, opts) do
{:ok, elev} -> elev
# Missing tile (water/ocean) or void value — treat as sea level
{:error, _} -> 0
end
{:cont, {:ok, acc ++ [entry]}}
{:error, reason} ->
{:halt, {:error, reason}}
end
%{lat: pt.lat, lon: pt.lon, d: pt.d, elev: elev, dist_km: pt.d * dist_km}
end)
results
{:ok, profile}
end
defp read_elevation(fd, lat, lon) do

View file

@ -151,25 +151,21 @@ defmodule Microwaveprop.Terrain.Viewshed do
terrain_check_km = min(radio_horizon_km(ant_height_m) * 2.0, max_range_km)
{end_lat, end_lon} = destination_point(origin_lat, origin_lon, bearing, terrain_check_km)
case Srtm.fetch_elevation_profile(origin_lat, origin_lon, end_lat, end_lon, tiles_dir) do
{:ok, profile} ->
analysis =
TerrainAnalysis.analyse(profile, terrain_check_km, freq_ghz, ant_ht_a: ant_height_m, ant_ht_b: ant_height_m)
{:ok, profile} =
Srtm.fetch_elevation_profile(origin_lat, origin_lon, end_lat, end_lon, tiles_dir, 64, download: true)
reach_km = effective_reach_km(analysis, max_range_km, score)
{reach_lat, reach_lon} = destination_point(origin_lat, origin_lon, bearing, reach_km)
analysis =
TerrainAnalysis.analyse(profile, terrain_check_km, freq_ghz, ant_ht_a: ant_height_m, ant_ht_b: ant_height_m)
%{
bearing: bearing,
reach_km: reach_km,
lat: reach_lat,
lon: reach_lon
}
reach_km = effective_reach_km(analysis, max_range_km, score)
{reach_lat, reach_lon} = destination_point(origin_lat, origin_lon, bearing, reach_km)
{:error, _} ->
{end_lat2, end_lon2} = destination_point(origin_lat, origin_lon, bearing, max_range_km)
%{bearing: bearing, reach_km: max_range_km, lat: end_lat2, lon: end_lon2}
end
%{
bearing: bearing,
reach_km: reach_km,
lat: reach_lat,
lon: reach_lon
}
end
# Radio horizon distance in km for a given antenna height (K=4/3 atmosphere)

View file

@ -148,21 +148,14 @@ defmodule Microwaveprop.Terrain.ElevationClientSrtmTest do
end
@tag :srtm
test "falls back to API when SRTM tiles missing" do
test "returns zero elevation when SRTM tiles missing (no API fallback)" do
Application.put_env(:microwaveprop, :srtm_tiles_dir, "/nonexistent/tiles")
Req.Test.stub(ElevationClient, fn conn ->
Req.Test.json(conn, %{"elevation" => [200.0, 250.0, 180.0]})
end)
assert {:ok, profile} =
ElevationClient.fetch_elevation_profile(32.9, -97.0, 30.3, -97.7, 2)
{result, _log} =
with_log(fn ->
ElevationClient.fetch_elevation_profile(32.9, -97.0, 30.3, -97.7, 2)
end)
assert {:ok, profile} = result
assert length(profile) == 3
assert hd(profile).elev == 200.0
assert Enum.all?(profile, fn p -> p.elev == 0 end)
end
end
end

View file

@ -190,9 +190,11 @@ defmodule Microwaveprop.Terrain.SrtmTest do
assert last.dist_km > 0
end
test "returns error when tiles dir does not exist" do
assert {:error, _} =
test "returns zero elevation when tiles dir does not exist" do
assert {:ok, profile} =
Srtm.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, "/nonexistent", 4)
assert Enum.all?(profile, fn p -> p.elev == 0 end)
end
end
end