prop/test/microwaveprop/terrain/srtm_test.exs
Graham McIntire e74adf0036 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.
2026-04-11 16:30:25 -05:00

200 lines
6.2 KiB
Elixir

defmodule Microwaveprop.Terrain.SrtmTest do
use ExUnit.Case, async: true
import ExUnit.CaptureLog
alias Microwaveprop.Terrain.Srtm
@tiles_dir Path.expand("~/srtm/tiles")
@has_tiles File.exists?(Path.join(@tiles_dir, "N32W097.hgt"))
# A full SRTM tile: 3601 * 3601 samples, each 2 bytes big-endian signed
@samples 3601
@test_elevation 150
@synthetic_hgt :binary.copy(<<@test_elevation::signed-big-integer-size(16)>>, @samples * @samples)
describe "tile_filename/2" do
test "northern hemisphere, western hemisphere" do
assert Srtm.tile_filename(32.78, -96.8) == "N32W097.hgt"
end
test "southern hemisphere, eastern hemisphere" do
assert Srtm.tile_filename(-0.5, 10.3) == "S01E010.hgt"
end
test "equator, prime meridian boundary" do
assert Srtm.tile_filename(0.5, -0.5) == "N00W001.hgt"
end
test "exact integer coordinates" do
assert Srtm.tile_filename(32.0, -97.0) == "N32W097.hgt"
end
test "negative lon floor wrapping" do
assert Srtm.tile_filename(35.2, -106.7) == "N35W107.hgt"
end
end
describe "download_tile/3" do
test "downloads and decompresses tile on success" do
tmp_dir = Path.join(System.tmp_dir!(), "srtm_test_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp_dir)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
gzipped = :zlib.gzip(@synthetic_hgt)
Req.Test.stub(Srtm, fn conn ->
Plug.Conn.send_resp(conn, 200, gzipped)
end)
assert {:ok, path} = Srtm.download_tile(32.0, -97.0, tmp_dir)
assert path == Path.join(tmp_dir, "N32W097.hgt")
assert File.exists?(path)
assert File.read!(path) == @synthetic_hgt
end
test "returns {:error, :not_available} on 404" do
tmp_dir = Path.join(System.tmp_dir!(), "srtm_test_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp_dir)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
Req.Test.stub(Srtm, fn conn ->
Plug.Conn.send_resp(conn, 404, "Not Found")
end)
capture_log(fn ->
assert {:error, :not_available} = Srtm.download_tile(80.0, 0.0, tmp_dir)
end)
end
test "returns error message on other HTTP status" do
tmp_dir = Path.join(System.tmp_dir!(), "srtm_test_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp_dir)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
Req.Test.stub(Srtm, fn conn ->
Plug.Conn.send_resp(conn, 500, "Internal Server Error")
end)
capture_log(fn ->
assert {:error, "SRTM download HTTP 500"} = Srtm.download_tile(32.0, -97.0, tmp_dir)
end)
end
end
describe "lookup/3 with auto-download" do
test "auto-downloads missing tile and returns elevation" do
tmp_dir = Path.join(System.tmp_dir!(), "srtm_test_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp_dir)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
gzipped = :zlib.gzip(@synthetic_hgt)
Req.Test.stub(Srtm, fn conn ->
Plug.Conn.send_resp(conn, 200, gzipped)
end)
assert {:ok, @test_elevation} = Srtm.lookup(32.0, -97.0, tmp_dir, download: true)
end
test "returns {:error, :no_tile} without download opt" do
tmp_dir = Path.join(System.tmp_dir!(), "srtm_test_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp_dir)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
assert {:error, :no_tile} = Srtm.lookup(32.0, -97.0, tmp_dir)
end
test "returns {:error, :no_tile} when download fails" do
tmp_dir = Path.join(System.tmp_dir!(), "srtm_test_#{System.unique_integer([:positive])}")
File.mkdir_p!(tmp_dir)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
Req.Test.stub(Srtm, fn conn ->
Plug.Conn.send_resp(conn, 404, "Not Found")
end)
capture_log(fn ->
assert {:error, :no_tile} = Srtm.lookup(80.0, 0.0, tmp_dir, download: true)
end)
end
end
describe "lookup/3" do
@describetag :srtm
@tag skip: !@has_tiles && "SRTM tiles not available at #{@tiles_dir}"
test "returns elevation for Dallas area" do
assert {:ok, elev} = Srtm.lookup(32.78, -96.8, @tiles_dir)
# Dallas is roughly 120-200m elevation
assert is_number(elev)
assert elev > 100
assert elev < 300
end
@tag skip: !@has_tiles && "SRTM tiles not available at #{@tiles_dir}"
test "returns {:error, :no_tile} for missing tile" do
Req.Test.stub(Srtm, fn conn ->
Plug.Conn.send_resp(conn, 404, "Not Found")
end)
capture_log(fn ->
assert {:error, :no_tile} = Srtm.lookup(80.0, 0.0, @tiles_dir)
end)
end
end
describe "fetch_elevation_profile/6" do
@describetag :srtm
@tag skip: !@has_tiles && "SRTM tiles not available at #{@tiles_dir}"
test "returns profile with correct number of points" do
# Dallas to a point within the same tile
assert {:ok, profile} =
Srtm.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, @tiles_dir, 10)
assert length(profile) == 11
first = hd(profile)
assert Map.has_key?(first, :lat)
assert Map.has_key?(first, :lon)
assert Map.has_key?(first, :d)
assert Map.has_key?(first, :elev)
assert Map.has_key?(first, :dist_km)
end
@tag skip: !@has_tiles && "SRTM tiles not available at #{@tiles_dir}"
test "all elevations are positive for Dallas area path" do
assert {:ok, profile} =
Srtm.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, @tiles_dir, 8)
Enum.each(profile, fn pt ->
assert pt.elev > 0, "Expected positive elevation, got #{pt.elev}"
end)
end
@tag skip: !@has_tiles && "SRTM tiles not available at #{@tiles_dir}"
test "first and last dist_km are correct" do
assert {:ok, profile} =
Srtm.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, @tiles_dir, 4)
first = hd(profile)
last = List.last(profile)
assert_in_delta first.dist_km, 0.0, 0.01
assert last.dist_km > 0
end
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