diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index c0b9f19f..47d12b0e 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -4,9 +4,11 @@ defmodule Microwaveprop.Weather do import Ecto.Query alias Microwaveprop.Propagation.ProfilesFile + alias Microwaveprop.Radio alias Microwaveprop.Repo alias Microwaveprop.Weather.GefsProfile alias Microwaveprop.Weather.GridCache + alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.HrrrNativeProfile alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.IemClient @@ -865,6 +867,33 @@ defmodule Microwaveprop.Weather do |> Repo.exists?() end + @doc """ + Returns `true` when every path point for `contact` (pos1 → midpoint → pos2, + or just pos1 for one-endpoint contacts) already has an HRRR profile at the + nearest HRRR hour. Lets the enrichment enqueuer skip :queued → :queued + churn when the backing data is already present. + + Returns `false` if `pos1` is nil — a contact with no position can't be + looked up. + """ + @spec hrrr_data_fully_present?(map()) :: boolean() + def hrrr_data_fully_present?(%{pos1: nil}), do: false + + def hrrr_data_fully_present?(contact) do + rounded = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp) + + case Radio.contact_path_points(contact) do + [] -> + false + + points -> + Enum.all?(points, fn {lat, lon} -> + {rlat, rlon} = round_to_hrrr_grid(lat, lon) + has_hrrr_profile?(rlat, rlon, rounded) + end) + end + end + @spec hrrr_for_contact(map()) :: HrrrProfile.t() | nil def hrrr_for_contact(%{pos1: nil}), do: nil @@ -1018,7 +1047,7 @@ defmodule Microwaveprop.Weather do def hrrr_profiles_for_path(contact) do contact - |> Microwaveprop.Radio.contact_path_points() + |> Radio.contact_path_points() |> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, contact.qso_timestamp) end) |> Enum.reject(&is_nil/1) end @@ -1093,7 +1122,7 @@ defmodule Microwaveprop.Weather do def narr_profiles_for_path(contact) do contact - |> Microwaveprop.Radio.contact_path_points() + |> Radio.contact_path_points() |> Enum.map(fn {lat, lon} -> find_nearest_narr(lat, lon, contact.qso_timestamp) end) |> Enum.reject(&is_nil/1) end @@ -1261,7 +1290,7 @@ defmodule Microwaveprop.Weather do def iemre_for_path(contact) do contact - |> Microwaveprop.Radio.contact_path_points() + |> Radio.contact_path_points() |> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, contact.qso_timestamp) end) |> Enum.reject(&is_nil/1) end diff --git a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex index 9781b670..ddee2e87 100644 --- a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex +++ b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex @@ -88,25 +88,12 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do cond do is_nil(contact.pos1) -> [] NarrClient.in_coverage?(contact.qso_timestamp) -> [] - hrrr_data_fully_present?(contact) -> [] + Weather.hrrr_data_fully_present?(contact) -> [] true -> [contact.id] end end) end - defp hrrr_data_fully_present?(contact) do - alias Microwaveprop.Weather.HrrrClient - - rounded = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp) - - contact - |> Radio.contact_path_points() - |> Enum.all?(fn {lat, lon} -> - {rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon) - Weather.has_hrrr_profile?(rlat, rlon, rounded) - end) - end - defp mark_enrichment_statuses(contact, types, jobs_by_type) do ids = [contact.id] diff --git a/test/microwaveprop/weather_test.exs b/test/microwaveprop/weather_test.exs index 6056c6c6..b4c84cc7 100644 --- a/test/microwaveprop/weather_test.exs +++ b/test/microwaveprop/weather_test.exs @@ -656,6 +656,72 @@ defmodule Microwaveprop.WeatherTest do end end + describe "hrrr_data_fully_present?/1" do + # nearest_hrrr_hour rounds <:30 down; pick 18:15 so it maps to 18:00 to + # match the valid_time in @hrrr_profile_attrs. + @contact_time ~U[2026-03-28 18:15:00Z] + + test "returns true when a pos1-only contact's snapped point has a profile" do + Weather.upsert_hrrr_profile(@hrrr_profile_attrs) + + contact = %{ + pos1: %{"lat" => 32.9047, "lon" => -97.0382}, + pos2: nil, + qso_timestamp: @contact_time + } + + assert Weather.hrrr_data_fully_present?(contact) + end + + test "returns false when a pos1-only contact's snapped point has no profile" do + contact = %{ + pos1: %{"lat" => 40.0, "lon" => -80.0}, + pos2: nil, + qso_timestamp: @contact_time + } + + refute Weather.hrrr_data_fully_present?(contact) + end + + test "returns true only when every path point (endpoint + midpoint + endpoint) has a profile" do + # Contact from (32.9, -97.04) to (33.1, -96.96) → midpoint ≈ (33.0, -97.0) + for {lat, lon} <- [{32.9, -97.04}, {33.0, -97.0}, {33.1, -96.96}] do + Weather.upsert_hrrr_profile(%{@hrrr_profile_attrs | lat: lat, lon: lon}) + end + + contact = %{ + pos1: %{"lat" => 32.9, "lon" => -97.04}, + pos2: %{"lat" => 33.1, "lon" => -96.96}, + qso_timestamp: @contact_time + } + + assert Weather.hrrr_data_fully_present?(contact) + end + + test "returns false when the midpoint profile is missing" do + # Only endpoints present — midpoint (33.0, -97.0) missing + for {lat, lon} <- [{32.9, -97.04}, {33.1, -96.96}] do + Weather.upsert_hrrr_profile(%{@hrrr_profile_attrs | lat: lat, lon: lon}) + end + + contact = %{ + pos1: %{"lat" => 32.9, "lon" => -97.04}, + pos2: %{"lat" => 33.1, "lon" => -96.96}, + qso_timestamp: @contact_time + } + + refute Weather.hrrr_data_fully_present?(contact) + end + + test "returns false when pos1 is nil" do + refute Weather.hrrr_data_fully_present?(%{ + pos1: nil, + pos2: nil, + qso_timestamp: @contact_time + }) + end + end + describe "round_to_hrrr_grid/2" do test "rounds coordinates to 2 decimal places" do assert Weather.round_to_hrrr_grid(32.9047, -97.0382) == {32.90, -97.04}