// Simple map hook for displaying a single station on the info page import { isValidCoordinate } from "../map_helpers"; declare const L: typeof import("leaflet"); // ~1.1 m at the equator — enough to ignore float noise but still notice any // real position change from the server. const POSITION_EPSILON = 0.00001; interface InfoMapContext { el: HTMLElement; map: import("leaflet").Map | null; marker: import("leaflet").Marker | null; lastSymbolHtml: string | null; initializing: boolean; resizeTimer: ReturnType | null; destroyed?: boolean; } export const InfoMap = { mounted(this: InfoMapContext) { this.map = null; this.marker = null; this.lastSymbolHtml = null; this.initializing = false; this.resizeTimer = null; this.destroyed = false; initializeMap.call(this); }, updated(this: InfoMapContext) { const lat = parseFloat(this.el.dataset.lat || ""); const lon = parseFloat(this.el.dataset.lon || ""); const symbolHtml = this.el.dataset.symbolHtml || null; const callsign = this.el.dataset.callsign || "unknown"; if (!isValidCoordinate(lat, lon)) { console.warn( `InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign} during update`, ); return; } if ( !this.map || !(this.map as any)._container || !(this.map as any)._container.parentNode ) { initializeMap.call(this); return; } if (this.marker) { const currentPos = this.marker.getLatLng(); if ( Math.abs(currentPos.lat - lat) > POSITION_EPSILON || Math.abs(currentPos.lng - lon) > POSITION_EPSILON ) { this.marker.setLatLng([lat, lon]); this.marker.setPopupContent( `${callsign}
Lat: ${lat.toFixed(6)}
Lon: ${lon.toFixed(6)}`, ); this.map!.panTo([lat, lon], { animate: true, duration: 1 }); } if (symbolHtml !== this.lastSymbolHtml) { const markerIcon = createMarkerIcon(symbolHtml); this.marker.setIcon(markerIcon); this.lastSymbolHtml = symbolHtml; } } }, destroyed(this: InfoMapContext) { this.destroyed = true; if (this.resizeTimer) { clearTimeout(this.resizeTimer); this.resizeTimer = null; } if (this.map) { this.map.remove(); this.map = null; } this.marker = null; this.lastSymbolHtml = null; this.initializing = false; }, }; function initializeMap(this: InfoMapContext) { if (this.initializing || this.destroyed) return; if (typeof L === "undefined") { console.error("Leaflet not loaded for InfoMap"); return; } this.initializing = true; const lat = parseFloat(this.el.dataset.lat || ""); const lon = parseFloat(this.el.dataset.lon || ""); const zoom = parseInt(this.el.dataset.zoom || "13") || 13; const callsign = this.el.dataset.callsign || "unknown"; const symbolHtml = this.el.dataset.symbolHtml || null; if (!isValidCoordinate(lat, lon)) { console.warn( `InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign}`, ); this.initializing = false; return; } const loadingEl = this.el.querySelector( `#${this.el.id}-loading`, ) as HTMLElement | null; if (loadingEl) { loadingEl.style.display = "none"; } try { this.map = L.map(this.el, { center: [lat, lon], zoom: zoom, scrollWheelZoom: false, zoomControl: true, attributionControl: true, }); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap', maxZoom: 19, }).addTo(this.map); const markerIcon = createMarkerIcon(symbolHtml); this.lastSymbolHtml = symbolHtml; this.marker = L.marker([lat, lon], { icon: markerIcon }) .addTo(this.map) .bindPopup( `${callsign}
Lat: ${lat.toFixed(6)}
Lon: ${lon.toFixed(6)}`, ); const map = this.map; this.resizeTimer = setTimeout(() => { if (!this.destroyed && this.el.isConnected && map && this.map === map) { map.invalidateSize(); } this.resizeTimer = null; }, 250); this.initializing = false; } catch (error) { console.error("Error initializing InfoMap:", error); this.initializing = false; } } function createMarkerIcon(symbolHtml: string | null): import("leaflet").DivIcon { if (symbolHtml) { return L.divIcon({ html: symbolHtml, className: "aprs-info-marker", iconSize: [32, 32], iconAnchor: [16, 16], }); } else { return L.divIcon({ html: `
`, className: "", iconSize: [24, 24], iconAnchor: [12, 12], }); } }