defmodule MicrowavepropWeb.QsoLive.Show do @moduledoc false use MicrowavepropWeb, :live_view alias Microwaveprop.Radio alias Microwaveprop.Weather @impl true def mount(%{"id" => id}, _session, socket) do qso = Radio.get_qso!(id) weather = load_weather(qso) solar = load_solar(qso) {:ok, assign(socket, page_title: "#{qso.station1} / #{qso.station2}", qso: qso, surface_observations: weather.surface_observations, soundings: weather.soundings, solar: solar, obs_sort_by: "station_name", obs_sort_order: "asc", sounding_sort_by: "station_name", sounding_sort_order: "asc" )} end @impl true def handle_event("sort", %{"field" => field, "table" => "obs"}, socket) do {sort_by, sort_order} = toggle_sort(socket.assigns.obs_sort_by, socket.assigns.obs_sort_order, field) sorted = sort_observations(socket.assigns.surface_observations, sort_by, sort_order) {:noreply, assign(socket, surface_observations: sorted, obs_sort_by: sort_by, obs_sort_order: sort_order )} end def handle_event("sort", %{"field" => field, "table" => "soundings"}, socket) do {sort_by, sort_order} = toggle_sort(socket.assigns.sounding_sort_by, socket.assigns.sounding_sort_order, field) sorted = sort_soundings(socket.assigns.soundings, sort_by, sort_order) {:noreply, assign(socket, soundings: sorted, sounding_sort_by: sort_by, sounding_sort_order: sort_order )} end defp toggle_sort(current_field, current_order, new_field) do if current_field == new_field && current_order == "asc", do: {new_field, "desc"}, else: {new_field, "asc"} end defp sort_observations(observations, sort_by, sort_order) do sorter = obs_sort_key(sort_by) sorted = Enum.sort_by(observations, sorter) if sort_order == "desc", do: Enum.reverse(sorted), else: sorted end defp obs_sort_key("station_name"), do: fn obs -> obs.station.name || obs.station.station_code end defp obs_sort_key("observed_at"), do: & &1.observed_at defp obs_sort_key("temp_f"), do: &(&1.temp_f || 0) defp obs_sort_key("dewpoint_f"), do: &(&1.dewpoint_f || 0) defp obs_sort_key("relative_humidity"), do: &(&1.relative_humidity || 0) defp obs_sort_key("sea_level_pressure_mb"), do: &(&1.sea_level_pressure_mb || 0) defp obs_sort_key(_), do: fn obs -> obs.station.name || obs.station.station_code end defp sort_soundings(soundings, sort_by, sort_order) do sorter = sounding_sort_key(sort_by) sorted = Enum.sort_by(soundings, sorter) if sort_order == "desc", do: Enum.reverse(sorted), else: sorted end defp sounding_sort_key("station_name"), do: fn s -> s.station.name || s.station.station_code end defp sounding_sort_key("observed_at"), do: & &1.observed_at defp sounding_sort_key("surface_temp_c"), do: &(&1.surface_temp_c || 0) defp sounding_sort_key("surface_refractivity"), do: &(&1.surface_refractivity || 0) defp sounding_sort_key("k_index"), do: &(&1.k_index || 0) defp sounding_sort_key("lifted_index"), do: &(&1.lifted_index || 0) defp sounding_sort_key(_), do: fn s -> s.station.name || s.station.station_code end defp load_weather(qso) do case qso.pos1 do %{"lat" => lat, "lon" => lon} -> Weather.weather_for_qso(%{lat: lat, lon: lon, timestamp: qso.qso_timestamp}) _ -> %{surface_observations: [], soundings: []} end end defp load_solar(qso) do qso.qso_timestamp |> DateTime.to_date() |> Weather.get_solar_index() end @impl true def render(assigns) do ~H""" <.header> {@qso.station1} / {@qso.station2} <:subtitle>{Calendar.strftime(@qso.qso_timestamp, "%Y-%m-%d %H:%M UTC")} <:actions> <.link navigate={~p"/qsos"} class="btn btn-sm btn-ghost"> <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to QSOs

QSO Details

<.list> <:item title="Station 1">{@qso.station1} <:item title="Station 2">{@qso.station2} <:item title="Band">{@qso.band} MHz <:item title="Mode">{@qso.mode} <:item title="Grid 1">{@qso.grid1 || "—"} <:item title="Grid 2">{@qso.grid2 || "—"} <:item title="Distance">{@qso.distance_km || "—"} km <:item title="Timestamp">{Calendar.strftime(@qso.qso_timestamp, "%Y-%m-%d %H:%M UTC")}

Surface Observations

<%= 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 || "—"} <% end %>

Soundings

<%= if @soundings == [] do %>

No soundings found nearby.

<% else %> <.table id="soundings" rows={@soundings} row_id={fn s -> "sounding-#{s.id}" end} sort_by={@sounding_sort_by} sort_order={@sounding_sort_order} sort_target="soundings" > <:col :let={s} label="Station" sort_field="station_name"> {s.station.name || s.station.station_code} <:col :let={s} label="Time" sort_field="observed_at"> {Calendar.strftime(s.observed_at, "%H:%M")} <:col :let={s} label="Sfc Temp (C)" sort_field="surface_temp_c"> {s.surface_temp_c || "—"} <:col :let={s} label="Refractivity" sort_field="surface_refractivity"> {format_number(s.surface_refractivity)} <:col :let={s} label="Ducting">{if s.ducting_detected, do: "Yes", else: "No"} <:col :let={s} label="K-Index" sort_field="k_index">{format_number(s.k_index)} <:col :let={s} label="Lifted Index" sort_field="lifted_index"> {format_number(s.lifted_index)} <% end %>

Solar Conditions

<%= if @solar do %> <.list> <:item title="SFI">{@solar.sfi || "—"} <:item title="Sunspot Number">{@solar.sunspot_number || "—"} <:item title="Ap Index">{@solar.ap_index || "—"} <:item title="Kp Values">{format_kp(@solar.kp_values)} <% else %>

No solar data available for this date.

<% end %> """ end defp format_number(nil), 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) defp format_wind(obs) do case {obs.wind_direction_deg, obs.wind_speed_kts} do {nil, nil} -> "—" {dir, speed} -> "#{dir || "?"}° @ #{speed || "?"} kts" end end defp format_kp(nil), do: "—" defp format_kp(values) when is_list(values), do: Enum.map_join(values, ", ", &format_number/1) end