From 2b323189d09c163a0420b502ac85c7357815f6d0 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 13 Apr 2026 16:31:53 -0500 Subject: [PATCH] Add grid squares and weather radar toggles to the weather map Ports the same two overlay toggles from /map to /weather: - Maidenhead grid squares via the shared updateGridOverlay helper, redrawn on moveend when visible. - ECCC GeoMet WMS radar (CONUS + Canada dBZ), lazy-created on first toggle with a 6-minute tile refresh timer. Both toggles live in the desktop sidebar below the layer description and in the mobile pulldown panel. Hook cleanup clears the radar refresh timer on destroyed(). --- assets/js/weather_map_hook.ts | 63 ++++++++++++++++- .../live/weather_map_live.ex | 69 ++++++++++++++++++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/assets/js/weather_map_hook.ts b/assets/js/weather_map_hook.ts index 1bb364ed..50ff031d 100644 --- a/assets/js/weather_map_hook.ts +++ b/assets/js/weather_map_hook.ts @@ -1,5 +1,7 @@ // assets/js/weather_map_hook.ts +import { updateGridOverlay } from "./maidenhead_grid" + // --- Type definitions --- interface RGB { @@ -79,6 +81,10 @@ interface WeatherMapHook extends ViewHook { escHint: HTMLDivElement clickMarker: L.LayerGroup legend: L.Control + gridLayer: L.LayerGroup + gridVisible: boolean + radarLayer: L.TileLayer.WMS | null + radarRefreshTimer: ReturnType | null _escHandler: (e: KeyboardEvent) => void _layerObserver: MutationObserver @@ -438,8 +444,62 @@ export const WeatherMap: WeatherMapHook = { L.DomEvent.disableScrollPropagation(controls) } + // Maidenhead grid overlay + this.gridLayer = L.layerGroup() + this.gridVisible = false + + this.handleEvent("toggle_grid", ({ visible }: { visible: boolean }) => { + this.gridVisible = visible + if (visible) { + this.gridLayer.addTo(this.map) + updateGridOverlay(this.map, this.gridLayer) + } else { + this.gridLayer.remove() + } + }) + + // 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 + } + } + }) + requestAnimationFrame(() => this.sendBounds()) - this.map.on("moveend", () => this.sendBounds()) + this.map.on("moveend", () => { + this.sendBounds() + if (this.gridVisible) { + updateGridOverlay(this.map, this.gridLayer) + } + }) this._layerObserver = new MutationObserver(() => { const newLayer = this.el.dataset.selectedLayer as LayerId | undefined @@ -458,6 +518,7 @@ export const WeatherMap: WeatherMapHook = { destroyed(this: WeatherMapHook) { if (this._layerObserver) this._layerObserver.disconnect() if (this._escHandler) document.removeEventListener("keydown", this._escHandler) + if (this.radarRefreshTimer) clearInterval(this.radarRefreshTimer) }, sendBounds(this: WeatherMapHook) { diff --git a/lib/microwaveprop_web/live/weather_map_live.ex b/lib/microwaveprop_web/live/weather_map_live.ex index 60276e67..5e9b23ea 100644 --- a/lib/microwaveprop_web/live/weather_map_live.ex +++ b/lib/microwaveprop_web/live/weather_map_live.ex @@ -153,7 +153,9 @@ defmodule MicrowavepropWeb.WeatherMapLive do initial_data_json: Jason.encode!(data), valid_time: valid_time, initial_utc_clock: Calendar.strftime(DateTime.utc_now(), "%H:%M UTC"), - bounds: @initial_bounds + bounds: @initial_bounds, + grid_visible: false, + radar_visible: false )} end @@ -162,6 +164,28 @@ defmodule MicrowavepropWeb.WeatherMapLive do {:noreply, assign(socket, :selected_layer, layer_id)} end + def handle_event("toggle_grid", _params, socket) do + visible = !socket.assigns.grid_visible + + socket = + socket + |> assign(:grid_visible, visible) + |> push_event("toggle_grid", %{visible: visible}) + + {: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("map_bounds", bounds, socket) do data = Weather.latest_weather_grid(bounds) @@ -296,6 +320,27 @@ defmodule MicrowavepropWeb.WeatherMapLive do {layer_description(@layers, @selected_layer)} +
+ + +
+
<.link navigate="/map" class="btn btn-xs btn-ghost justify-start"> Propagation Map @@ -415,6 +460,28 @@ defmodule MicrowavepropWeb.WeatherMapLive do {layer_description(@layers, @selected_layer)}
+ <%!-- Overlay toggles --%> +
+ + +
+ <%!-- Navigation --%>