import type { ViewHook } from "phoenix_live_view" import { updateGridOverlay } from "./maidenhead_grid" interface LocationMapHook extends ViewHook { map: L.Map marker: L.CircleMarker gridLayer: L.LayerGroup gridVisible: boolean visibilityHandler: (() => void) | null } export const LocationMap: Partial = { mounted(this: LocationMapHook) { const lat = parseFloat(this.el.dataset.lat || "0") const lon = parseFloat(this.el.dataset.lon || "0") const osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19 }) const satellite = L.tileLayer( "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { maxZoom: 21, maxNativeZoom: 19 } ) const topo = L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", { maxZoom: 17 }) const map = L.map(this.el, { center: [lat, lon], zoom: 16, zoomControl: true, attributionControl: false }) this.map = map satellite.addTo(map) L.control.layers( { "Satellite": satellite, "OSM": osm, "Topo": topo }, {}, { position: "topright", collapsed: false } ).addTo(map) this.marker = L.circleMarker([lat, lon], { radius: 8, color: "#ffffff", weight: 2, fillColor: "#ef4444", fillOpacity: 0.95, pane: "markerPane" }).addTo(map) this.marker.bindPopup(`${lat.toFixed(6)}, ${lon.toFixed(6)}`) // Maidenhead grid overlay + toggle this.gridLayer = L.layerGroup().addTo(map) this.gridVisible = true updateGridOverlay(map, this.gridLayer) map.on("moveend", () => { if (this.gridVisible) updateGridOverlay(map, this.gridLayer) }) const GridToggle = L.Control.extend({ options: { position: "topright" as const }, onAdd: () => { const btn = L.DomUtil.create("button", "leaflet-bar leaflet-control") btn.innerHTML = "Grid" btn.title = "Toggle Maidenhead grid" btn.style.cssText = "background:#fff;padding:4px 8px;font-size:12px;cursor:pointer;font-weight:600;" L.DomEvent.disableClickPropagation(btn) btn.onclick = () => { this.gridVisible = !this.gridVisible if (this.gridVisible) { this.gridLayer.addTo(map) updateGridOverlay(map, this.gridLayer) btn.style.opacity = "1" } else { this.gridLayer.remove() btn.style.opacity = "0.5" } } return btn } }) new GridToggle().addTo(map) // LiveView reconnect / tab visibility — Leaflet needs invalidateSize. this.visibilityHandler = () => { if (document.visibilityState === "visible") this.map.invalidateSize() } document.addEventListener("visibilitychange", this.visibilityHandler) setTimeout(() => map.invalidateSize(), 50) }, reconnected(this: LocationMapHook) { this.map?.invalidateSize() }, destroyed(this: LocationMapHook) { if (this.visibilityHandler) { document.removeEventListener("visibilitychange", this.visibilityHandler) this.visibilityHandler = null } this.map?.remove() } }