From d50c78102dbff7d6ec172c39becd2e9461adfe49 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 29 Mar 2026 14:16:38 -0500 Subject: [PATCH] Backfill missing QSO distances during weather enqueue scan Adds haversine_km/4 to Radio context and backfill_distances/1 which calculates distance_km from pos1/pos2 for QSOs missing it. Called by QsoWeatherEnqueueWorker before enqueueing weather fetches. --- lib/microwaveprop/radio.ex | 36 ++++++++++++++++ .../workers/qso_weather_enqueue_worker.ex | 2 + test/microwaveprop/radio_test.exs | 42 +++++++++++++++++++ .../qso_weather_enqueue_worker_test.exs | 11 +++++ 4 files changed, 91 insertions(+) diff --git a/lib/microwaveprop/radio.ex b/lib/microwaveprop/radio.ex index 5d3cd947..4a403b08 100644 --- a/lib/microwaveprop/radio.ex +++ b/lib/microwaveprop/radio.ex @@ -57,6 +57,42 @@ defmodule Microwaveprop.Radio do |> Repo.update_all(set: [weather_queued: true]) end + @earth_radius_km 6371.0 + + def 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 * @earth_radius_km * :math.asin(:math.sqrt(a)) + end + + def backfill_distances(qsos) do + Enum.each(qsos, fn qso -> + if is_nil(qso.distance_km) && qso.pos1 && qso.pos2 do + lat1 = qso.pos1["lat"] + lon1 = qso.pos1["lon"] || qso.pos1["lng"] + lat2 = qso.pos2["lat"] + lon2 = qso.pos2["lon"] || qso.pos2["lng"] + + if lat1 && lon1 && lat2 && lon2 do + dist = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new() + + Qso + |> where([q], q.id == ^qso.id) + |> Repo.update_all(set: [distance_km: dist]) + end + end + end) + end + + defp deg_to_rad(deg), do: deg * :math.pi() / 180 + def get_qso!(id) do Repo.get!(Qso, id) end diff --git a/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex b/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex index ffecc3dd..67e98461 100644 --- a/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex +++ b/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex @@ -14,6 +14,8 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do qsos = Radio.unprocessed_qsos() if qsos != [] do + Radio.backfill_distances(qsos) + jobs = build_weather_jobs(qsos) if jobs != [] do diff --git a/test/microwaveprop/radio_test.exs b/test/microwaveprop/radio_test.exs index d66baefd..8d13cd12 100644 --- a/test/microwaveprop/radio_test.exs +++ b/test/microwaveprop/radio_test.exs @@ -162,6 +162,48 @@ defmodule Microwaveprop.RadioTest do end end + describe "haversine_km/4" do + test "calculates distance between two known points" do + # Dallas to Austin ~roughly 295 km + dist = Radio.haversine_km(32.9, -97.0, 30.3, -97.7) + assert_in_delta dist, 295, 5 + end + + test "returns 0 for same point" do + assert Radio.haversine_km(32.9, -97.0, 32.9, -97.0) == 0.0 + end + end + + describe "backfill_distances/1" do + test "calculates and stores distance for QSOs missing distance_km" do + qso = create_qso(%{distance_km: nil}) + assert is_nil(qso.distance_km) + + Radio.backfill_distances([qso]) + + updated = Repo.get!(Qso, qso.id) + assert_in_delta Decimal.to_float(updated.distance_km), 295, 5 + end + + test "skips QSOs that already have a distance" do + qso = create_qso(%{distance_km: Decimal.new("100")}) + + Radio.backfill_distances([qso]) + + updated = Repo.get!(Qso, qso.id) + assert updated.distance_km == Decimal.new("100") + end + + test "skips QSOs missing pos2" do + qso = create_qso(%{distance_km: nil, pos2: nil}) + + Radio.backfill_distances([qso]) + + updated = Repo.get!(Qso, qso.id) + assert is_nil(updated.distance_km) + end + end + describe "get_qso!/1" do test "returns a QSO by ID" do qso = create_qso() diff --git a/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs b/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs index 964b8959..407d95e2 100644 --- a/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs +++ b/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs @@ -147,6 +147,17 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}}) end + test "backfills distance_km for QSOs missing it" do + _station = create_asos_station("KDFW", 32.90, -97.04) + qso = create_qso(%{distance_km: nil}) + + assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}}) + + updated = Repo.get!(Qso, qso.id) + assert updated.distance_km + assert_in_delta Decimal.to_float(updated.distance_km), 295, 5 + end + test "skips QSOs without positions" do _station = create_asos_station("KDFW", 32.90, -97.04) _qso = create_qso(%{pos1: nil})