import type { ViewHook } from "phoenix_live_view" import { updateGridOverlay } from "./maidenhead_grid" interface LocationMapHook extends ViewHook { map: L.Map marker: L.Marker gridLayer: L.LayerGroup gridVisible: boolean visibilityHandler: (() => void) | null } // Same red dot the old CircleMarker rendered, recreated as a divIcon so // we can drop it on a real L.Marker (CircleMarker has no `dragging` // handler and can't be made draggable). const dotIcon = L.divIcon({ html: '
', className: "rover-location-dot", iconSize: [16, 16], iconAnchor: [8, 8] }) const draggingDotIcon = L.divIcon({ html: '
', className: "rover-location-dot rover-location-dot-edit", iconSize: [18, 18], iconAnchor: [9, 9] }) 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) // Draggable Marker (not CircleMarker — that one can't be dragged). // Server flips draggability via `set_marker_draggable` push_event. this.marker = L.marker([lat, lon], { icon: dotIcon, draggable: false }).addTo(map) this.marker.bindPopup(`${lat.toFixed(6)}, ${lon.toFixed(6)}`) this.marker.on("drag", (e: L.LeafletEvent) => { const ll = (e.target as L.Marker).getLatLng() this.marker.setPopupContent(`${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}`) }) // dragend pushes the new coords up so the LiveView can update its // `working_lat` / `working_lon` and render the live grid preview. this.marker.on("dragend", (e: L.DragEndEvent) => { const ll = (e.target as L.Marker).getLatLng() this.pushEvent("location_dragged", { lat: ll.lat, lon: ll.lng }) }) // 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) // Server → hook events for edit mode. this.handleEvent("set_marker_draggable", ({ draggable }: { draggable: boolean }) => { if (draggable) { this.marker.setIcon(draggingDotIcon) this.marker.dragging?.enable() } else { this.marker.setIcon(dotIcon) this.marker.dragging?.disable() } }) this.handleEvent("reset_marker", ({ lat, lon }: { lat: number; lon: number }) => { this.marker.setLatLng([lat, lon]) this.marker.setPopupContent(`${lat.toFixed(6)}, ${lon.toFixed(6)}`) }) // 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() } }