prop/lib/microwaveprop/workers/terrain_profile_worker.ex
Graham McIntire 45e2e69361
Add SRTM terrain path profiles for QSOs
Fetch elevation data along the path between two stations via the
Open-Meteo Elevation API (with Open-Topo-Data fallback), compute
Fresnel zone clearance, earth bulge, and knife-edge diffraction loss,
and store the results per QSO.

- terrain_profiles table with migration and TerrainProfile schema
- ElevationClient with batched API calls and fallback
- TerrainAnalysis with Fresnel/diffraction physics (ITU-R P.526-15)
- TerrainProfileWorker on Oban :terrain queue
- QsoWeatherEnqueueWorker enqueues terrain jobs automatically
- QSO show page displays verdict badge and collapsible elevation table
- Reorder show page: terrain, soundings, solar, HRRR, surface obs
- Fix Dockerfile wgrib2 build (add cmake dependency)
2026-03-29 16:22:29 -05:00

57 lines
1.7 KiB
Elixir

defmodule Microwaveprop.Workers.TerrainProfileWorker do
@moduledoc false
use Oban.Worker, queue: :terrain, max_attempts: 3
alias Microwaveprop.Radio
alias Microwaveprop.Terrain
alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Terrain.TerrainAnalysis
@impl Oban.Worker
def perform(%Oban.Job{args: %{"qso_id" => qso_id}}) do
if Terrain.has_terrain_profile?(qso_id) do
:ok
else
qso = Radio.get_qso!(qso_id)
lat1 = qso.pos1["lat"]
lon1 = qso.pos1["lon"] || qso.pos1["lng"]
lat2 = qso.pos2["lat"]
lon2 = qso.pos2["lon"] || qso.pos2["lng"]
dist_km = Decimal.to_float(qso.distance_km)
freq_ghz = Decimal.to_float(qso.band) / 1000
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2) do
{:ok, profile} ->
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz)
path_points =
Enum.map(profile, fn p ->
%{
"lat" => p.lat,
"lon" => p.lon,
"d" => p.d,
"elev" => p.elev,
"dist_km" => p.dist_km
}
end)
Terrain.upsert_terrain_profile(%{
qso_id: qso_id,
sample_count: length(profile),
path_points: path_points,
max_elevation_m: analysis.max_elevation_m,
min_clearance_m: analysis.min_clearance_m,
diffraction_db: analysis.diffraction_db,
fresnel_hit_count: analysis.fresnel_hit_count,
obstructed_count: analysis.obstructed_count,
verdict: analysis.verdict
})
:ok
{:error, reason} ->
{:error, reason}
end
end
end
end