diff --git a/assets/js/elevation_profile_hook.js b/assets/js/elevation_profile_hook.js index e3351ddc..aa5f4753 100644 --- a/assets/js/elevation_profile_hook.js +++ b/assets/js/elevation_profile_hook.js @@ -40,7 +40,11 @@ const ductPlugin = { const labelY = Math.max(drawTop + 12, top + 12) ctx.fillStyle = "rgba(34, 197, 94, 0.8)" ctx.font = "9px sans-serif" - ctx.fillText(`Duct ${Math.round(duct.base_ft)}–${Math.round(duct.top_ft)} ft (${duct.strength} M-units)`, left + 4, labelY) + const labels = {hrrr: "Duct", sounding: "Duct (sounding)", inferred: "Est. duct"} + const units = {hrrr: "M-units", sounding: "M-units", inferred: "dN/dh÷10"} + const prefix = labels[duct.source] || "Duct" + const unit = units[duct.source] || "M-units" + ctx.fillText(`${prefix} ${Math.round(duct.base_ft)}–${Math.round(duct.top_ft)} ft (${duct.strength} ${unit})`, left + 4, labelY) } ctx.restore() } @@ -64,7 +68,8 @@ export const ElevationProfile = { const ducts = (data.ducts || []).map(d => ({ base_ft: d.base_m_msl * M2FT, top_ft: d.top_m_msl * M2FT, - strength: d.strength + strength: d.strength, + source: d.source || "hrrr" })) const allElevs = [...elevations, ...losLine] diff --git a/lib/microwaveprop_web/live/contact_live/show.ex b/lib/microwaveprop_web/live/contact_live/show.ex index 52daea54..dac0b96c 100644 --- a/lib/microwaveprop_web/live/contact_live/show.ex +++ b/lib/microwaveprop_web/live/contact_live/show.ex @@ -18,10 +18,11 @@ defmodule MicrowavepropWeb.ContactLive.Show do weather = load_weather(contact) solar = load_solar(contact) - hrrr = Weather.hrrr_for_contact(contact) + hrrr_path = Weather.hrrr_profiles_for_path(contact) + hrrr = List.first(hrrr_path) {hrrr, hrrr_status} = maybe_enqueue_hrrr(hrrr, contact) terrain = Terrain.get_terrain_profile(contact.id) - elevation_profile = compute_elevation_profile(contact, hrrr) + elevation_profile = compute_elevation_profile(contact, hrrr_path, weather.soundings) {:ok, assign(socket, @@ -133,13 +134,36 @@ defmodule MicrowavepropWeb.ContactLive.Show do defp sounding_sort_key(_), do: fn s -> s.station.name || s.station.station_code end defp load_weather(contact) do - lat = contact.pos1 && contact.pos1["lat"] - lon = contact.pos1 && (contact.pos1["lon"] || contact.pos1["lng"]) + lat1 = contact.pos1 && contact.pos1["lat"] + lon1 = contact.pos1 && (contact.pos1["lon"] || contact.pos1["lng"]) + lat2 = contact.pos2 && contact.pos2["lat"] + lon2 = contact.pos2 && (contact.pos2["lon"] || contact.pos2["lng"]) - if lat && lon do - Weather.weather_for_contact(%{lat: lat, lon: lon, timestamp: contact.qso_timestamp}, - radius_km: 300 - ) + if lat1 && lon1 do + # Search along the path midpoint with radius covering both endpoints + mid_lat = if lat2, do: (lat1 + lat2) / 2, else: lat1 + mid_lon = if lon2, do: (lon1 + lon2) / 2, else: lon1 + half_dist = if lat2 && lon2, do: haversine_km(lat1, lon1, lat2, lon2) / 2, else: 0 + radius = max(50, half_dist + 50) + + weather = + Weather.weather_for_contact( + %{lat: mid_lat, lon: mid_lon, timestamp: contact.qso_timestamp}, + radius_km: radius + ) + + # Keep only the 5 closest stations by distance to path midpoint + %{ + surface_observations: + weather.surface_observations + |> Enum.sort_by(fn obs -> + slat = obs.station.lat + slon = obs.station.lon + abs(slat - mid_lat) + abs(slon - mid_lon) + end) + |> Enum.take(5), + soundings: weather.soundings + } else %{surface_observations: [], soundings: []} end @@ -203,7 +227,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do if(@contact.flagged_invalid, do: "btn-warning", else: "btn-ghost") ]}> <.icon name="hero-flag" class="w-4 h-4" /> - {if @contact.flagged_invalid, do: "Unflag", else: "Flag Invalid"} + {if @contact.flagged_invalid, do: "Unflag", else: "Flag as Invalid"} <.link navigate={~p"/contacts"} class="btn btn-sm btn-ghost"> <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to Contacts @@ -506,33 +530,36 @@ defmodule MicrowavepropWeb.ContactLive.Show do <%= if @surface_observations == [] do %>
No surface observations found nearby.
<% else %> - <.table - id="surface-obs" - rows={@surface_observations} - row_id={fn obs -> "obs-#{obs.id}" end} - sort_by={@obs_sort_by} - sort_order={@obs_sort_order} - sort_target="obs" - > - <:col :let={obs} label="Station" sort_field="station_name"> - {obs.station.name || obs.station.station_code} - - <:col :let={obs} label="Time" sort_field="observed_at"> - {Calendar.strftime(obs.observed_at, "%H:%M")} - - <:col :let={obs} label="Temp (F)" sort_field="temp_f">{obs.temp_f || "—"} - <:col :let={obs} label="Dewpoint (F)" sort_field="dewpoint_f"> - {obs.dewpoint_f || "—"} - - <:col :let={obs} label="RH%" sort_field="relative_humidity"> - {format_number(obs.relative_humidity)} - - <:col :let={obs} label="Wind">{format_wind(obs)} - <:col :let={obs} label="Pressure (mb)" sort_field="sea_level_pressure_mb"> - {obs.sea_level_pressure_mb || "—"} - - <:col :let={obs} label="Sky">{obs.sky_condition || "—"} - +| Station | +Time | +Temp | +Dewpt | +RH% | +Wind | +Pres (mb) | +Sky | +
|---|---|---|---|---|---|---|---|
| {obs.station.name || obs.station.station_code} | +{Calendar.strftime(obs.observed_at, "%H:%M")} | +{obs.temp_f || "—"} | +{obs.dewpoint_f || "—"} | +{format_number(obs.relative_humidity)} | +{format_wind(obs)} | +{obs.sea_level_pressure_mb || "—"} | +{obs.sky_condition || "—"} | +