prop/test/microwaveprop/terrain/srtm_test.exs
Graham McIntire 2ae60d036b
Add SRTM auto-download on tile miss from AWS Terrain Tiles
When a local .hgt tile is missing, download it from the public AWS S3
skadi bucket, decompress with zlib, and write to the tiles directory
before retrying the lookup. Falls back to Open-Meteo/OpenTopo APIs if
the download fails.
2026-03-30 08:36:21 -05:00

180 lines
5.6 KiB
Elixir

defmodule Microwaveprop.Terrain.SrtmTest do
use ExUnit.Case, async: true
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)
assert {:error, :not_available} = Srtm.download_tile(80.0, 0.0, tmp_dir)
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)
assert {:error, "SRTM download HTTP 500"} = Srtm.download_tile(32.0, -97.0, tmp_dir)
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)
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)
assert {:error, :no_tile} = Srtm.lookup(80.0, 0.0, tmp_dir)
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)
assert {:error, :no_tile} = Srtm.lookup(80.0, 0.0, @tiles_dir)
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 error when tiles dir does not exist" do
assert {:error, _} =
Srtm.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, "/nonexistent", 4)
end
end
end