// Map and visualization management module export class MapManager { constructor() { this.map = null; this.coverageMap = null; this.mapOrigin = null; // Source markers and overlays this.sourceMarkers = new Map(); this.rangeCircles = new Map(); this.showSources = true; this.showRange = false; this.selectedSource = null; this.heatmapLayer = null; // Data references this.sourcesData = new Map(); // Map theme this.isDarkMode = false; this.currentTileLayer = null; this.coverageTileLayer = null; } async initializeMap() { // Get origin from server let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback try { const response = await fetch('/api/origin'); if (response.ok) { origin = await response.json(); } } catch (error) { console.warn('Could not fetch origin, using default:', error); } // Store origin for reset functionality this.mapOrigin = origin; this.map = L.map('map').setView([origin.latitude, origin.longitude], 10); // Light tile layer by default this.currentTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 19 }).addTo(this.map); // Add scale control for distance estimation L.control.scale({ metric: true, imperial: true, position: 'bottomright' }).addTo(this.map); return this.map; } async initializeCoverageMap() { if (!this.coverageMap) { // Get origin from server let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback try { const response = await fetch('/api/origin'); if (response.ok) { origin = await response.json(); } } catch (error) { console.warn('Could not fetch origin for coverage map, using default:', error); } this.coverageMap = L.map('coverage-map').setView([origin.latitude, origin.longitude], 10); this.coverageTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors' }).addTo(this.coverageMap); // Add scale control for distance estimation L.control.scale({ metric: true, imperial: true, position: 'bottomright' }).addTo(this.coverageMap); } return this.coverageMap; } updateSourcesData(data) { if (data.sources) { this.sourcesData.clear(); data.sources.forEach(source => { this.sourcesData.set(source.id, source); }); } } updateSourceMarkers() { if (!this.map || !this.showSources) return; // Remove markers for sources that no longer exist const currentSourceIds = new Set(this.sourcesData.keys()); for (const [id, marker] of this.sourceMarkers) { if (!currentSourceIds.has(id)) { this.map.removeLayer(marker); this.sourceMarkers.delete(id); } } // Update or create markers for current sources for (const [id, source] of this.sourcesData) { if (source.latitude && source.longitude) { if (this.sourceMarkers.has(id)) { // Update existing marker const marker = this.sourceMarkers.get(id); // Update marker style if status changed marker.setStyle({ radius: source.active ? 10 : 6, fillColor: source.active ? '#00d4ff' : '#666666', fillOpacity: 0.8 }); // Update popup content if it's open if (marker.isPopupOpen()) { marker.setPopupContent(this.createSourcePopupContent(source)); } } else { // Create new marker const marker = L.circleMarker([source.latitude, source.longitude], { radius: source.active ? 10 : 6, fillColor: source.active ? '#00d4ff' : '#666666', color: '#ffffff', weight: 2, fillOpacity: 0.8, className: 'source-marker' }).addTo(this.map); marker.bindPopup(this.createSourcePopupContent(source), { maxWidth: 300 }); this.sourceMarkers.set(id, marker); } } } this.updateSourcesLegend(); } updateRangeCircles() { if (!this.map || !this.showRange) return; // Clear existing circles this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); this.rangeCircles.clear(); // Add range circles for active sources for (const [id, source] of this.sourcesData) { if (source.active && source.latitude && source.longitude) { // Add multiple range circles (50km, 100km, 200km) const ranges = [50000, 100000, 200000]; ranges.forEach((range, index) => { const circle = L.circle([source.latitude, source.longitude], { radius: range, fillColor: 'transparent', color: '#00d4ff', weight: 2, opacity: 0.7 - (index * 0.15), dashArray: '8,4' }).addTo(this.map); this.rangeCircles.set(`${id}_${range}`, circle); }); } } } createSourcePopupContent(source, aircraftData) { const aircraftCount = aircraftData ? Array.from(aircraftData.values()) .filter(aircraft => aircraft.sources && aircraft.sources[source.id]).length : 0; return `
ID: ${source.id}
Location: ${source.latitude.toFixed(4)}°, ${source.longitude.toFixed(4)}°
Status: ${source.active ? 'Active' : 'Inactive'}
Aircraft: ${aircraftCount}
Messages: ${source.messages || 0}
Last Seen: ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}