From e74adf0036b251bd9e6a2aaa13bf8380d3c2c6ee Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 11 Apr 2026 16:30:17 -0500 Subject: [PATCH] Auto-download missing SRTM tiles, treat water areas as 0m elevation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- k8s/deployment.yaml | 2 -- lib/microwaveprop/terrain/elevation_client.ex | 10 ++----- lib/microwaveprop/terrain/srtm.ex | 27 +++++++----------- lib/microwaveprop/terrain/viewshed.ex | 28 ++++++++----------- .../terrain/elevation_client_test.exs | 15 +++------- test/microwaveprop/terrain/srtm_test.exs | 6 ++-- 6 files changed, 32 insertions(+), 56 deletions(-) diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 426a07f5..a92ae324 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -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 diff --git a/lib/microwaveprop/terrain/elevation_client.ex b/lib/microwaveprop/terrain/elevation_client.ex index e1f841e9..46f99dc1 100644 --- a/lib/microwaveprop/terrain/elevation_client.ex +++ b/lib/microwaveprop/terrain/elevation_client.ex @@ -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 diff --git a/lib/microwaveprop/terrain/srtm.ex b/lib/microwaveprop/terrain/srtm.ex index 44b75a53..c77be730 100644 --- a/lib/microwaveprop/terrain/srtm.ex +++ b/lib/microwaveprop/terrain/srtm.ex @@ -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 diff --git a/lib/microwaveprop/terrain/viewshed.ex b/lib/microwaveprop/terrain/viewshed.ex index e1a8dfd2..a07f77d1 100644 --- a/lib/microwaveprop/terrain/viewshed.ex +++ b/lib/microwaveprop/terrain/viewshed.ex @@ -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) diff --git a/test/microwaveprop/terrain/elevation_client_test.exs b/test/microwaveprop/terrain/elevation_client_test.exs index 4cf61651..bb47f45f 100644 --- a/test/microwaveprop/terrain/elevation_client_test.exs +++ b/test/microwaveprop/terrain/elevation_client_test.exs @@ -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 diff --git a/test/microwaveprop/terrain/srtm_test.exs b/test/microwaveprop/terrain/srtm_test.exs index 7fea0be5..72f1f87b 100644 --- a/test/microwaveprop/terrain/srtm_test.exs +++ b/test/microwaveprop/terrain/srtm_test.exs @@ -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