From 85036eff2160a9bfc6266fc64515dc00eaad88e6 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 12 Apr 2026 15:36:28 -0500 Subject: [PATCH] Add toggleable weather radar overlay, disable rain-scatter display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Weather radar" toggle in /map's control panel (mobile + desktop) layers an ECCC GeoMet WMS composite dBZ mosaic over the propagation heatmap. The Radar_1km_dBZ-Extrapolation layer covers CONUS + Canada at 1 km resolution, updated every ~6 minutes. Tiles are lazy-created on first toggle so visitors who never open the panel never hit the WMS service. Refresh is driven by a setInterval that calls setParams with a cache-busting timestamp, matching ECCC's publish cadence. Rain-scatter display (NEXRAD cell markers + "Rain Scatter" section in the point-detail panel) is disabled for now. The live radar overlay answers "is there rain near this point?" more directly, and the scatter propagation feature needs more work before it's useful. The Elixir start_async, handle_async, and fetch_rain_scatter helpers are removed. The JS drawScatterMarkers / buildScatterBlock paths remain as harmless dead code since they're guarded by a `rain_scatter` field the server no longer sends — easy to re-enable later by restoring the payload key. --- assets/js/propagation_map_hook.ts | 37 ++++++++++++ lib/microwaveprop_web/live/map_live.ex | 78 +++++++++++++------------- 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/assets/js/propagation_map_hook.ts b/assets/js/propagation_map_hook.ts index 6a7b182f..fc33cc45 100644 --- a/assets/js/propagation_map_hook.ts +++ b/assets/js/propagation_map_hook.ts @@ -132,6 +132,8 @@ interface PropagationMapHook extends ViewHook { rangeCircles: L.LayerGroup gridLayer: L.LayerGroup gridVisible: boolean + radarLayer: L.TileLayer.WMS | null + radarRefreshTimer: ReturnType | null detailPanel: HTMLElement | null viewshedLoading: boolean lastDetail: PointDetail | null @@ -750,6 +752,41 @@ export const PropagationMap: Record & { } }) + // Weather radar overlay (ECCC GeoMet WMS — CONUS + Canada composite dBZ). + // Lazy-created on first toggle so we don't hit the service until asked. + this.radarLayer = null + this.radarRefreshTimer = null + + this.handleEvent("toggle_radar", ({ visible }: { visible: boolean }) => { + if (visible) { + if (!this.radarLayer) { + this.radarLayer = L.tileLayer.wms("https://geo.weather.gc.ca/geomet", { + layers: "Radar_1km_dBZ-Extrapolation", + format: "image/png", + transparent: true, + opacity: 0.55, + version: "1.3.0", + attribution: "Radar © Environment and Climate Change Canada" + }) + } + this.radarLayer.addTo(this.map) + // ECCC radar updates every ~6 minutes; force a tile refresh on that + // cadence so the overlay stays current without a page reload. + this.radarRefreshTimer = setInterval(() => { + if (this.radarLayer) { + // @ts-expect-error setParams exists on WMS TileLayer but isn't in @types/leaflet yet + this.radarLayer.setParams({ _: Date.now() }) + } + }, 6 * 60 * 1000) + } else { + if (this.radarLayer) this.radarLayer.remove() + if (this.radarRefreshTimer) { + clearInterval(this.radarRefreshTimer) + this.radarRefreshTimer = null + } + } + }) + // Click on map to request viewshed + factor detail from server this.map.on("click", (e: L.LeafletMouseEvent) => { this.rangeCircles.clearLayers() diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 0d7e8acf..6f517d43 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -52,6 +52,7 @@ defmodule MicrowavepropWeb.MapLive do initial_center: center, initial_zoom: @default_zoom, grid_visible: false, + radar_visible: false, antenna_height_ft: 33 )} end @@ -144,6 +145,17 @@ defmodule MicrowavepropWeb.MapLive do {:noreply, socket} end + def handle_event("toggle_radar", _params, socket) do + visible = !socket.assigns.radar_visible + + socket = + socket + |> assign(:radar_visible, visible) + |> push_event("toggle_radar", %{visible: visible}) + + {:noreply, socket} + end + def handle_event("point_detail", %{"lat" => lat, "lon" => lon}, socket) do band = socket.assigns.selected_band detail = Propagation.point_detail(band, lat, lon, socket.assigns.selected_time) @@ -154,22 +166,17 @@ defmodule MicrowavepropWeb.MapLive do %{time: DateTime.to_iso8601(f.valid_time), score: f.score} end) - # Push the detail immediately with rain_scatter as :pending — NEXRAD - # fetching is slow on a cold 5-min window so we don't want to block the - # LiveView process. The :rain_scatter async task pushes its own event. + # Rain-scatter (NEXRAD cell overlay + scatter-dB readings in the detail + # panel) is disabled for now — the live radar WMS toggle covers the + # "is there rain?" question and the scatter-propagation feature needs + # more work before it's useful. To re-enable: restore the :rain_scatter + # payload key and the start_async/handle_async pair below. payload = if detail, - do: detail |> Map.put(:forecast, forecast_data) |> Map.put(:rain_scatter, :pending), + do: Map.put(detail, :forecast, forecast_data), else: %{} - socket = - socket - |> push_event("point_detail", payload) - |> start_async({:rain_scatter, lat, lon, band}, fn -> - fetch_rain_scatter(lat, lon, band) - end) - - {:noreply, socket} + {:noreply, push_event(socket, "point_detail", payload)} end def handle_event("map_bounds", bounds, socket) do @@ -273,15 +280,6 @@ defmodule MicrowavepropWeb.MapLive do {:noreply, socket} end - def handle_async({:rain_scatter, lat, lon, _band}, {:ok, scatter}, socket) do - {:noreply, push_event(socket, "rain_scatter_update", %{lat: lat, lon: lon, rain_scatter: scatter})} - end - - def handle_async({:rain_scatter, _lat, _lon, _band}, {:exit, reason}, socket) do - Logger.warning("Rain scatter fetch failed: #{inspect(reason)}") - {:noreply, socket} - end - defp push_timeline(socket) do times = Enum.map(socket.assigns.valid_times, fn t -> @@ -317,23 +315,6 @@ defmodule MicrowavepropWeb.MapLive do end end - defp fetch_rain_scatter(lat, lon, band_mhz) do - alias Microwaveprop.Propagation.RainScatter - alias Microwaveprop.Weather.NexradClient - - freq_ghz = band_mhz / 1000.0 - - case NexradClient.fetch_rain_cells(lat, lon) do - {:ok, rain_cells} -> - cells = RainScatter.find_scatter_cells(rain_cells, lat, lon, freq_ghz) - classification = RainScatter.classify(cells) - %{cells: cells, classification: to_string(classification)} - - {:error, _} -> - %{cells: [], classification: "none"} - end - end - defp band_info(band_mhz) do config = BandConfig.get(band_mhz) @@ -450,6 +431,16 @@ defmodule MicrowavepropWeb.MapLive do Grid squares + +
Grid squares + <%!-- Weather radar toggle --%> + + <%!-- Antenna height --%>