defmodule MicrowavepropWeb.WeatherMapLive do @moduledoc false use MicrowavepropWeb, :live_view alias Microwaveprop.Weather @initial_bounds %{ "south" => 29.5, "north" => 36.3, "west" => -101.5, "east" => -92.5 } @layers [ # Surface %{ id: "temperature", label: "Temperature", unit: "°C", group: "Surface", desc: "2m air temperature. Warm, humid air increases refractivity and ducting potential at lower frequencies." }, %{ id: "dewpoint_depression", label: "Td Depression", unit: "°C", group: "Surface", desc: "Temperature minus dewpoint. Small values (< 5°C) mean moist boundary layer — favorable for ducting at 10 GHz, but increases absorption above 24 GHz." }, %{ id: "surface_rh", label: "Humidity", unit: "%", group: "Surface", desc: "Relative humidity from surface T and Td. High RH supports refractivity gradients that bend microwave signals." }, %{ id: "pwat", label: "PWAT", unit: "mm", group: "Surface", desc: "Precipitable water — total moisture in the atmospheric column. High values signal rain fade risk for 24 GHz and above." }, %{ id: "surface_refractivity", label: "Refractivity", unit: "N", group: "Surface", desc: "Radio refractivity (N-units) at the surface. Typical values 280–380. Higher N means the atmosphere bends signals more toward the ground." }, %{ id: "refractivity_gradient", label: "N-Gradient", unit: "N/km", group: "Surface", desc: "Minimum refractivity gradient in the profile. Standard is −40 N/km. Below −157 N/km signals are trapped in a duct. More negative = stronger ducting." }, %{ id: "bl_height", label: "BL Height", unit: "m", group: "Surface", desc: "Planetary boundary layer height. Shallow BL (< 500m) concentrates moisture and heat near the surface, favoring temperature inversions and ducting." }, # Upper Air %{ id: "temp_850mb", label: "T @ 850mb", unit: "°C", group: "Upper Air", desc: "Temperature at 850 mb (~1500m altitude). Warm 850mb air over cool surface air indicates a capping inversion — a classic ducting setup." }, %{ id: "dewpoint_850mb", label: "Td @ 850mb", unit: "°C", group: "Upper Air", desc: "Dewpoint at 850 mb. A sharp moisture drop between the surface and 850mb creates an elevated refractivity gradient that can form ducts." }, %{ id: "lapse_rate", label: "Lapse Rate", unit: "°C/km", group: "Upper Air", desc: "Temperature decrease per km from surface to 700mb. Low rates (< 5 °C/km) mean stable air that preserves inversions. High rates (> 8) mean convective mixing that destroys them." }, %{ id: "inversion_strength", label: "Inversion", unit: "°C", group: "Upper Air", desc: "Strongest temperature increase between adjacent levels. Inversions trap microwave signals. > 3°C is significant, > 5°C is strong." }, %{ id: "inversion_base_m", label: "Inv. Base", unit: "m", group: "Upper Air", desc: "Height (m AGL) where the strongest inversion begins. Surface-based inversions (< 200m) create surface ducts. Elevated inversions create elevated ducts." }, # Ducting %{ id: "ducting", label: "Ducting", unit: "", group: "Ducting", desc: "Whether a trapping layer (modified refractivity decreasing with height) was detected in the profile. Green = duct present, signals can travel far beyond line of sight." }, %{ id: "duct_base_m", label: "Duct Base", unit: "m", group: "Ducting", desc: "Height of the lowest duct base. Surface ducts (< 100m) are most effective for ground-based stations. Elevated ducts require antennas near the duct height." }, %{ id: "duct_strength", label: "Duct Strength", unit: "M", group: "Ducting", desc: "Duct trapping strength in modified refractivity (M) units. > 10 M is moderate, > 20 M is strong. Stronger ducts trap a wider range of frequencies." } ] @impl true def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "weather:updated") end data = Weather.latest_weather_grid(@initial_bounds) valid_time = if data != [], do: hd(data).valid_time {:ok, assign(socket, page_title: "Weather Map", layers: @layers, selected_layer: "refractivity_gradient", initial_data_json: Jason.encode!(data), valid_time: valid_time, bounds: @initial_bounds )} end @impl true def handle_event("select_layer", %{"layer" => layer_id}, socket) do {:noreply, assign(socket, :selected_layer, layer_id)} end def handle_event("map_bounds", bounds, socket) do data = Weather.latest_weather_grid(bounds) socket = socket |> assign(:bounds, bounds) |> push_event("update_weather", %{data: data}) {:noreply, socket} end def handle_event("point_detail", %{"lat" => lat, "lon" => lon}, socket) do detail = if socket.assigns.valid_time do Weather.weather_point_detail(lat, lon, socket.assigns.valid_time) end payload = detail || %{} {:noreply, push_event(socket, "point_detail", payload)} end @impl true def handle_info({:weather_updated, _valid_time}, socket) do data = Weather.latest_weather_grid(socket.assigns.bounds) valid_time = if data != [], do: hd(data).valid_time socket = socket |> assign(:valid_time, valid_time) |> push_event("update_weather", %{data: data}) {:noreply, socket} end @group_order ["Surface", "Upper Air", "Ducting"] defp group_layers(layers) do layers |> Enum.group_by(& &1.group) |> Enum.sort_by(fn {group, _} -> Enum.find_index(@group_order, &(&1 == group)) || 99 end) end defp layer_description(layers, selected_id) do case Enum.find(layers, &(&1.id == selected_id)) do %{desc: desc} -> desc _ -> nil end end @impl true def render(assigns) do ~H"""