diff --git a/lib/microwaveprop_web/live/path_live.ex b/lib/microwaveprop_web/live/path_live.ex index fe9afddb..f4dad3b7 100644 --- a/lib/microwaveprop_web/live/path_live.ex +++ b/lib/microwaveprop_web/live/path_live.ex @@ -11,9 +11,11 @@ defmodule MicrowavepropWeb.PathLive do alias Microwaveprop.Propagation.SporadicE alias Microwaveprop.Radio.CallsignClient alias Microwaveprop.Radio.Maidenhead + alias Microwaveprop.Repo alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.TerrainAnalysis alias Microwaveprop.Weather + alias Microwaveprop.Weather.Station @band_options BandConfig.band_options() @@ -246,22 +248,25 @@ defmodule MicrowavepropWeb.PathLive do nil end - # HRRR profiles along path + # HRRR profiles along path — 9 evenly-spaced samples so mid-path + # ducts that endpoint-only queries miss show up in the duct count. now = DateTime.utc_now() midlat = (src.lat + dst.lat) / 2 midlon = (src.lon + dst.lon) / 2 hrrr_points = - [ - {"Source", src.lat, src.lon}, - {"Midpoint", midlat, midlon}, - {"Destination", dst.lat, dst.lon} - ] + src + |> path_sample_points(dst, 9) |> Enum.map(&label_hrrr_point(&1, now)) |> Enum.reject(&is_nil/1) hrrr_profiles = Enum.map(hrrr_points, & &1.profile) + # Independent duct signal: the nearest RAOB within 300 km / ±3 h of + # the midpoint. HRRR pressure levels systematically under-read thin + # surface ducts that sounding data resolves. + sounding = build_sounding_readout(midlat, midlon, now) + # Build conditions and score {conditions, scoring} = build_scoring(hrrr_profiles, src, now, band_config) @@ -294,7 +299,8 @@ defmodule MicrowavepropWeb.PathLive do forecast: forecast, hrrr_count: length(hrrr_profiles), hrrr_points: hrrr_points, - ionosphere: ionosphere + ionosphere: ionosphere, + sounding: sounding }} end end @@ -379,6 +385,53 @@ defmodule MicrowavepropWeb.PathLive do end end + # Linear interpolation along the great-circle-approximate path. Good + # enough at the path-calculator's typical range (<2000 km); for longer + # paths the path deviates from a rhumb line but we're picking + # HRRR grid cells, not doing precise bearing. + defp path_sample_points(src, dst, count) when count >= 2 do + Enum.map(1..count, fn i -> + t = (i - 1) / (count - 1) + lat = src.lat + (dst.lat - src.lat) * t + lon = src.lon + (dst.lon - src.lon) * t + + label = + cond do + i == 1 -> "Source" + i == count -> "Destination" + i * 2 == count + 1 -> "Midpoint" + true -> "#{round(t * 100)}%" + end + + {label, lat, lon} + end) + end + + defp build_sounding_readout(midlat, midlon, now) do + case Weather.nearest_sounding_to(midlat, midlon, now) do + {:ok, sounding} -> + station = if Ecto.assoc_loaded?(sounding.station), do: sounding.station + station = station || Repo.get(Station, sounding.station_id) + + distance_km = + if station, do: haversine_km(midlat, midlon, station.lat, station.lon) + + %{ + station_code: station && station.station_code, + station_name: station && station.name, + observed_at: sounding.observed_at, + ducting_detected: sounding.ducting_detected, + min_refractivity_gradient: sounding.min_refractivity_gradient, + boundary_layer_depth_m: sounding.boundary_layer_depth_m, + precipitable_water_mm: sounding.precipitable_water_mm, + distance_km: distance_km + } + + {:error, :not_found} -> + nil + end + end + defp build_scoring([], _src, _now, _band_config), do: {nil, nil} defp build_scoring(profiles, src, now, band_config) do @@ -523,6 +576,22 @@ defmodule MicrowavepropWeb.PathLive do defp format_number(n) when is_float(n), do: :erlang.float_to_binary(n, decimals: 1) defp format_number(n), do: to_string(n) + # Displays as `123 km (76 mi)`. Distances under 10 km keep one decimal + # so near-LOS paths don't round to zero; everything larger rounds to + # the nearest integer for easier reading. + defp format_km_mi(nil), do: "—" + + defp format_km_mi(km) when is_number(km) do + mi = km / 1.609344 + + {km_str, mi_str} = + if km < 10.0, do: {format_1d(km), format_1d(mi)}, else: {to_string(round(km)), to_string(round(mi))} + + "#{km_str} km (#{mi_str} mi)" + end + + defp format_1d(n), do: :erlang.float_to_binary(n * 1.0, decimals: 1) + defp format_utc_hour(%DateTime{} = dt), do: Calendar.strftime(dt, "%H:%M") defp format_utc_hour(_), do: "?" @@ -702,7 +771,7 @@ defmodule MicrowavepropWeb.PathLive do