diff --git a/lib/microwaveprop_web/components/skew_t_chart.ex b/lib/microwaveprop_web/components/skew_t_chart.ex new file mode 100644 index 00000000..44271d6f --- /dev/null +++ b/lib/microwaveprop_web/components/skew_t_chart.ex @@ -0,0 +1,180 @@ +defmodule MicrowavepropWeb.Components.SkewTChart do + @moduledoc """ + Function component that renders a skew-T log-P diagram from a + pressure-level profile (list of `%{"pres", "tmpc", "dwpc"}` maps). + Returns empty output when the profile has fewer than three usable + levels. + """ + use Phoenix.Component + + alias MicrowavepropWeb.SkewT + + attr :profile, :list, required: true + + def skew_t_chart(assigns) do + chart = SkewT.build(assigns.profile) + usable_points = if chart, do: length(chart.t_points), else: 0 + assigns = assign(assigns, chart: chart, usable_points: usable_points) + + ~H""" +
= 2} class="mt-3"> +
+ Skew-T log-P +
+ + <%!-- Plot background --%> + + + <%!-- Clip background curves to the plot area so skewed lines + don't bleed over the axes. --%> + + + + + + + + <%!-- Dry adiabats (orange curves) --%> + + + <%!-- Mixing ratio lines (dashed teal) --%> + + + <%!-- Isotherms (gray) --%> + + + <%!-- Isobars (gray, across plot) --%> + + + <%!-- Dewpoint trace --%> + + + + <%!-- Temperature trace --%> + + + + + <%!-- Pressure axis labels (left side) --%> + + {bar.label} + + + <%!-- Temperature axis labels (bottom, only those inside the visible band) --%> + = -40 && iso.t_c <= 40 && rem(iso.t_c, 20) == 0} + x={iso.label_x} + y={iso.label_y} + font-size="10" + text-anchor="middle" + fill="currentColor" + class="text-base-content/60" + > + {iso.t_c}°C + + + <%!-- Legend --%> + + + + Temperature + + + + Dewpoint + + + +
+ Orange curves: dry adiabats · dashed teal: saturation mixing ratio · + thin gray: isotherms and isobars +
+
+ """ + end +end diff --git a/lib/microwaveprop_web/live/contact_live/show.ex b/lib/microwaveprop_web/live/contact_live/show.ex index ffd88896..43848277 100644 --- a/lib/microwaveprop_web/live/contact_live/show.ex +++ b/lib/microwaveprop_web/live/contact_live/show.ex @@ -2,6 +2,8 @@ defmodule MicrowavepropWeb.ContactLive.Show do @moduledoc false use MicrowavepropWeb, :live_view + import MicrowavepropWeb.Components.SkewTChart + alias Microwaveprop.Propagation.BandConfig alias Microwaveprop.Propagation.Scorer alias Microwaveprop.Radio @@ -16,7 +18,6 @@ defmodule MicrowavepropWeb.ContactLive.Show do alias Microwaveprop.Workers.HrrrFetchWorker alias Microwaveprop.Workers.SolarIndexWorker alias Microwaveprop.Workers.TerrainProfileWorker - alias MicrowavepropWeb.SkewT require Logger @@ -2171,176 +2172,4 @@ defmodule MicrowavepropWeb.ContactLive.Show do defp maybe_put_band(params, band) when is_integer(band), do: Map.put(params, "band", Integer.to_string(band)) defp maybe_put_band(params, band) when is_binary(band), do: Map.put(params, "band", band) defp maybe_put_band(params, _), do: params - - # Render a skew-T log-P diagram from a pressure-level profile - # (list of `%{"pres", "tmpc", "dwpc"}` maps). Returns empty output - # when the profile has fewer than three usable levels. - attr :profile, :list, required: true - - defp skew_t_chart(assigns) do - chart = SkewT.build(assigns.profile) - usable_points = if chart, do: length(chart.t_points), else: 0 - assigns = assign(assigns, chart: chart, usable_points: usable_points) - - ~H""" -
= 2} class="mt-3"> -
- Skew-T log-P -
- - <%!-- Plot background --%> - - - <%!-- Clip background curves to the plot area so skewed lines - don't bleed over the axes. --%> - - - - - - - - <%!-- Dry adiabats (orange curves) --%> - - - <%!-- Mixing ratio lines (dashed teal) --%> - - - <%!-- Isotherms (gray) --%> - - - <%!-- Isobars (gray, across plot) --%> - - - <%!-- Dewpoint trace --%> - - - - <%!-- Temperature trace --%> - - - - - <%!-- Pressure axis labels (left side) --%> - - {bar.label} - - - <%!-- Temperature axis labels (bottom, only those inside the visible band) --%> - = -40 && iso.t_c <= 40 && rem(iso.t_c, 20) == 0} - x={iso.label_x} - y={iso.label_y} - font-size="10" - text-anchor="middle" - fill="currentColor" - class="text-base-content/60" - > - {iso.t_c}°C - - - <%!-- Legend --%> - - - - Temperature - - - - Dewpoint - - - -
- Orange curves: dry adiabats · dashed teal: saturation mixing ratio · - thin gray: isotherms and isobars -
-
- """ - end end diff --git a/lib/microwaveprop_web/live/path_live.ex b/lib/microwaveprop_web/live/path_live.ex index 54f6c384..60757599 100644 --- a/lib/microwaveprop_web/live/path_live.ex +++ b/lib/microwaveprop_web/live/path_live.ex @@ -2,6 +2,8 @@ defmodule MicrowavepropWeb.PathLive do @moduledoc false use MicrowavepropWeb, :live_view + import MicrowavepropWeb.Components.SkewTChart + alias Microwaveprop.Propagation alias Microwaveprop.Propagation.BandConfig alias Microwaveprop.Propagation.Scorer @@ -230,11 +232,17 @@ defmodule MicrowavepropWeb.PathLive do midlat = (src.lat + dst.lat) / 2 midlon = (src.lon + dst.lon) / 2 - hrrr_profiles = - [{src.lat, src.lon}, {midlat, midlon}, {dst.lat, dst.lon}] - |> Enum.map(fn {lat, lon} -> Weather.find_nearest_hrrr(lat, lon, now) end) + hrrr_points = + [ + {"Source", src.lat, src.lon}, + {"Midpoint", midlat, midlon}, + {"Destination", dst.lat, dst.lon} + ] + |> Enum.map(&label_hrrr_point(&1, now)) |> Enum.reject(&is_nil/1) + hrrr_profiles = Enum.map(hrrr_points, & &1.profile) + # Build conditions and score {conditions, scoring} = build_scoring(hrrr_profiles, src, now, band_config) @@ -261,7 +269,8 @@ defmodule MicrowavepropWeb.PathLive do loss_budget: loss_budget, power_budget: power_budget, forecast: forecast, - hrrr_count: length(hrrr_profiles) + hrrr_count: length(hrrr_profiles), + hrrr_points: hrrr_points }} end end @@ -312,6 +321,13 @@ defmodule MicrowavepropWeb.PathLive do Regex.match?(~r/^[A-Ra-r]{2}[0-9]{2}/i, input) end + defp label_hrrr_point({label, lat, lon}, now) do + case Weather.find_nearest_hrrr(lat, lon, now) do + nil -> nil + profile -> %{label: label, profile: profile} + end + end + defp build_scoring([], _src, _now, _band_config), do: {nil, nil} defp build_scoring(profiles, src, now, band_config) do @@ -845,6 +861,11 @@ defmodule MicrowavepropWeb.PathLive do <% end %> + <%!-- Atmospheric Profile (HRRR) --%> + <%= if @result.hrrr_points != [] do %> + <.atmospheric_profile hrrr_points={@result.hrrr_points} /> + <% end %> + <%!-- 18-Hour Forecast --%> <%= if @result.forecast != [] do %> <.forecast_chart forecast={@result.forecast} band_config={@result.band_config} /> @@ -864,6 +885,87 @@ defmodule MicrowavepropWeb.PathLive do """ end + attr :hrrr_points, :list, required: true + + defp atmospheric_profile(assigns) do + mid = Enum.find(assigns.hrrr_points, &(&1.label == "Midpoint")) || hd(assigns.hrrr_points) + valid_time = mid.profile.valid_time + run_time = Map.get(mid.profile, :run_time) + any_ducting? = Enum.any?(assigns.hrrr_points, & &1.profile.ducting_detected) + + assigns = + assigns + |> assign(:mid, mid) + |> assign(:valid_time, valid_time) + |> assign(:run_time, run_time) + |> assign(:any_ducting, any_ducting?) + + ~H""" +
+
+

Atmospheric Profile

+ HRRR (3 km) + + Valid {Calendar.strftime(@valid_time, "%Y-%m-%d %H:%M UTC")} + + <%= if @run_time do %> + + Run {Calendar.strftime(@run_time, "%Y-%m-%d %H:%M UTC")} + + <% end %> + + {length(@hrrr_points)} / 3 path points + + <%= if @any_ducting do %> + Ducting detected + <% end %> +
+ +
+ + + + + + + + + + + + + + + + <%= for pt <- @hrrr_points do %> + + + + + + + + + + + + <% end %> + +
PointLat, LonTempDewptPressHPBLPWATNsdN/dh
{pt.label} + {format_number(pt.profile.lat)}, {format_number(pt.profile.lon)} + {format_number(pt.profile.surface_temp_c)}°C{format_number(pt.profile.surface_dewpoint_c)}°C{format_number(pt.profile.surface_pressure_mb)} mb{format_number(pt.profile.hpbl_m)} m{format_number(pt.profile.pwat_mm)} mm{format_number(pt.profile.surface_refractivity)}{format_number(pt.profile.min_refractivity_gradient)}
+
+ + <%= if @mid.profile.profile && @mid.profile.profile != [] do %> +
+ Midpoint vertical profile +
+ <.skew_t_chart profile={@mid.profile.profile} /> + <% end %> +
+ """ + end + attr :forecast, :list, required: true attr :band_config, :map, required: true