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 --%>