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.
139 lines
4 KiB
Elixir
139 lines
4 KiB
Elixir
defmodule Microwaveprop.Terrain.ElevationClient do
|
|
@moduledoc false
|
|
|
|
alias Microwaveprop.Terrain.Srtm
|
|
|
|
@batch_size 100
|
|
|
|
@spec fetch_elevation_profile(float(), float(), float(), float(), pos_integer()) ::
|
|
{:ok, list(map())} | {:error, String.t()}
|
|
def fetch_elevation_profile(lat1, lon1, lat2, lon2, n \\ 64) do
|
|
case srtm_tiles_dir() do
|
|
nil ->
|
|
fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n)
|
|
|
|
tiles_dir ->
|
|
case Srtm.fetch_elevation_profile(lat1, lon1, lat2, lon2, tiles_dir, n) do
|
|
{:ok, _profile} = ok -> ok
|
|
{:error, _} -> fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp fetch_elevation_profile_api(lat1, lon1, lat2, lon2, n) do
|
|
pts = sample_path(lat1, lon1, lat2, lon2, n)
|
|
dist_km = haversine_km(lat1, lon1, lat2, lon2)
|
|
|
|
with {:error, e1} <- fetch_batched(pts, :open_meteo),
|
|
{:error, e2} <- fetch_batched(pts, :open_topo) do
|
|
{:error, "Elevation fetch failed: #{e1} | #{e2}"}
|
|
else
|
|
{:ok, elevs} ->
|
|
profile =
|
|
pts
|
|
|> Enum.zip(elevs)
|
|
|> Enum.map(fn {pt, elev} ->
|
|
%{lat: pt.lat, lon: pt.lon, d: pt.d, elev: elev || 0.0, dist_km: pt.d * dist_km}
|
|
end)
|
|
|
|
{:ok, profile}
|
|
end
|
|
end
|
|
|
|
defp srtm_tiles_dir, do: Application.get_env(:microwaveprop, :srtm_tiles_dir)
|
|
|
|
def sample_path(lat1, lon1, lat2, lon2, n) do
|
|
for i <- 0..n do
|
|
f = i / n
|
|
|
|
%{
|
|
lat: lat1 + f * (lat2 - lat1),
|
|
lon: lon1 + f * (lon2 - lon1),
|
|
d: f
|
|
}
|
|
end
|
|
end
|
|
|
|
defp fetch_batched(pts, source) do
|
|
pts
|
|
|> Enum.chunk_every(@batch_size)
|
|
|> Enum.reduce_while({:ok, []}, fn batch, {:ok, acc} ->
|
|
case fetch_batch(source, batch) do
|
|
{:ok, elevs} -> {:cont, {:ok, acc ++ elevs}}
|
|
{:error, _} = err -> {:halt, err}
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp fetch_batch(:open_meteo, batch) do
|
|
lats = Enum.map_join(batch, ",", &Float.to_string(Float.round(&1.lat, 6)))
|
|
lons = Enum.map_join(batch, ",", &Float.to_string(Float.round(&1.lon, 6)))
|
|
url = "https://api.open-meteo.com/v1/elevation?latitude=#{lats}&longitude=#{lons}"
|
|
|
|
case Req.get(url, req_options()) do
|
|
{:ok, %{status: 200, body: %{"elevation" => elevs}}} when is_list(elevs) ->
|
|
{:ok, elevs}
|
|
|
|
{:ok, %{status: status}} ->
|
|
{:error, "Open-Meteo HTTP #{status}"}
|
|
|
|
{:error, reason} ->
|
|
{:error, "Open-Meteo error: #{inspect(reason)}"}
|
|
end
|
|
end
|
|
|
|
defp fetch_batch(:open_topo, batch) do
|
|
locations =
|
|
Enum.map_join(batch, "|", fn p ->
|
|
"#{Float.round(p.lat, 6)},#{Float.round(p.lon, 6)}"
|
|
end)
|
|
|
|
url = "https://api.opentopodata.org/v1/srtm90m?locations=#{URI.encode(locations)}"
|
|
|
|
case Req.get(url, req_options()) do
|
|
{:ok, %{status: 200, body: %{"status" => "OK", "results" => results}}} ->
|
|
{:ok, Enum.map(results, fn r -> r["elevation"] || 0.0 end)}
|
|
|
|
{:ok, %{status: status}} ->
|
|
{:error, "OpenTopo HTTP #{status}"}
|
|
|
|
{:error, reason} ->
|
|
{:error, "OpenTopo error: #{inspect(reason)}"}
|
|
end
|
|
end
|
|
|
|
defp haversine_km(lat1, lon1, lat2, lon2) do
|
|
dlat = deg_to_rad(lat2 - lat1)
|
|
dlon = deg_to_rad(lon2 - lon1)
|
|
rlat1 = deg_to_rad(lat1)
|
|
rlat2 = deg_to_rad(lat2)
|
|
|
|
a =
|
|
:math.sin(dlat / 2) ** 2 +
|
|
:math.cos(rlat1) * :math.cos(rlat2) * :math.sin(dlon / 2) ** 2
|
|
|
|
2 * 6371.0 * :math.asin(:math.sqrt(a))
|
|
end
|
|
|
|
defp deg_to_rad(deg), do: deg * :math.pi() / 180
|
|
|
|
defp req_options do
|
|
defaults = [retry: &retry?/2, max_retries: 5, retry_delay: &retry_delay/1]
|
|
overrides = Application.get_env(:microwaveprop, :elevation_req_options, [])
|
|
Keyword.merge(defaults, overrides)
|
|
end
|
|
|
|
defp retry?(_request, response) do
|
|
case response do
|
|
%Req.Response{status: status} when status in [429, 500, 502, 503, 504] -> true
|
|
%{__exception__: true} -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
defp retry_delay(n) do
|
|
base = Integer.pow(2, n) * 1_000
|
|
jitter = :rand.uniform(1_000)
|
|
base + jitter
|
|
end
|
|
end
|