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().
This commit is contained in:
Graham McIntire 2026-04-13 16:31:53 -05:00
parent 2afaeb56a3
commit 2b323189d0
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 130 additions and 2 deletions

View file

@ -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<typeof setInterval> | 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 &copy; 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) {

View file

@ -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)}
</div>
<div class="flex flex-col gap-1 border-t border-base-300 pt-2">
<label class="flex items-center gap-2 cursor-pointer px-1">
<input
type="checkbox"
class="toggle toggle-sm toggle-primary"
checked={@grid_visible}
phx-click="toggle_grid"
/>
<span class="text-sm">Grid squares</span>
</label>
<label class="flex items-center gap-2 cursor-pointer px-1">
<input
type="checkbox"
class="toggle toggle-sm toggle-primary"
checked={@radar_visible}
phx-click="toggle_radar"
/>
<span class="text-sm">Weather radar</span>
</label>
</div>
<div class="flex flex-col gap-1 border-t border-base-300 pt-2">
<.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)}
</div>
<%!-- Overlay toggles --%>
<div class="flex flex-col gap-1 border-t border-base-300 pt-2">
<label class="flex items-center gap-2 cursor-pointer px-1">
<input
type="checkbox"
class="toggle toggle-sm toggle-primary"
checked={@grid_visible}
phx-click="toggle_grid"
/>
<span class="text-sm">Grid squares</span>
</label>
<label class="flex items-center gap-2 cursor-pointer px-1">
<input
type="checkbox"
class="toggle toggle-sm toggle-primary"
checked={@radar_visible}
phx-click="toggle_radar"
/>
<span class="text-sm">Weather radar</span>
</label>
</div>
<%!-- Navigation --%>
<ul class="menu menu-xs border-t border-base-300 pt-2 px-0">
<li>