// GigLez - Map Visualization let map; let markerLayer; let markerClusterGroup; let heatmapLayer; let capturesData = []; let heatmapEnabled = false; // Frequency color mapping const FREQUENCY_COLORS = { 315: '#10b981', // Green 433: '#3b82f6', // Blue 868: '#f59e0b', // Orange 915: '#ef4444', // Red default: '#6b7280' // Gray }; // Initialize map document.addEventListener('DOMContentLoaded', () => { initMap(); loadCaptures(); setupMapControls(); }); function initMap() { // Create map centered on US map = L.map('map').setView([39.8283, -98.5795], 4); // Add OpenStreetMap tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }).addTo(map); // Create marker cluster group markerClusterGroup = L.markerClusterGroup({ maxClusterRadius: 50, spiderfyOnMaxZoom: true, showCoverageOnHover: false, zoomToBoundsOnClick: true }); // Create regular marker layer markerLayer = L.layerGroup(); // Add cluster group by default map.addLayer(markerClusterGroup); // Export map to window for access from other modules window.map = map; } function setupMapControls() { // Cluster toggle document.getElementById('cluster-toggle').addEventListener('change', (e) => { if (e.target.checked) { map.removeLayer(markerLayer); map.addLayer(markerClusterGroup); } else { map.removeLayer(markerClusterGroup); map.addLayer(markerLayer); } renderMarkers(); }); // Frequency filter document.getElementById('frequency-filter').addEventListener('change', () => { if (heatmapEnabled) { renderHeatmap(); } else { renderMarkers(); } }); // Heatmap toggle document.getElementById('heatmap-toggle').addEventListener('change', (e) => { heatmapEnabled = e.target.checked; if (heatmapEnabled) { // Hide markers, show heatmap map.removeLayer(markerClusterGroup); map.removeLayer(markerLayer); renderHeatmap(); } else { // Hide heatmap, show markers if (heatmapLayer) { map.removeLayer(heatmapLayer); } // Re-add appropriate marker layer based on cluster toggle const clusterEnabled = document.getElementById('cluster-toggle').checked; if (clusterEnabled) { map.addLayer(markerClusterGroup); } else { map.addLayer(markerLayer); } } }); } async function loadCaptures() { try { const response = await fetch('/api/v1/query/captures?limit=1000'); if (!response.ok) { console.error('Failed to load captures'); return; } const data = await response.json(); capturesData = data.captures || []; renderMarkers(); updateStats(); } catch (error) { console.error('Error loading captures:', error); } } function renderMarkers() { // Clear existing markers markerClusterGroup.clearLayers(); markerLayer.clearLayers(); // Get frequency filter const frequencyFilter = document.getElementById('frequency-filter').value; // Filter captures let filtered = capturesData; if (frequencyFilter) { const targetFreq = parseInt(frequencyFilter) * 1e6; filtered = capturesData.filter(c => { const freq = c.frequency / 1e6; return Math.abs(freq - parseInt(frequencyFilter)) < 50; }); } // Create markers filtered.forEach(capture => { const marker = createMarker(capture); // Add to both layers (only one will be visible) markerClusterGroup.addLayer(marker); markerLayer.addLayer(marker); }); // Update count document.getElementById('total-captures').textContent = filtered.length; } function createMarker(capture) { // Determine color based on frequency const freqMHz = Math.round(capture.frequency / 1e6); let color = FREQUENCY_COLORS.default; for (const [freq, col] of Object.entries(FREQUENCY_COLORS)) { if (freq === 'default') continue; if (Math.abs(freqMHz - parseInt(freq)) < 50) { color = col; break; } } // Create custom icon const icon = L.divIcon({ className: 'custom-marker', html: `
`, iconSize: [12, 12], iconAnchor: [6, 6] }); // Create marker const marker = L.marker([capture.latitude, capture.longitude], { icon }); // Create popup const popupContent = createPopupContent(capture); marker.bindPopup(popupContent); return marker; } function createPopupContent(capture) { const freqMHz = (capture.frequency / 1e6).toFixed(2); const ts = capture.timestamp || capture.captured_at; const parsed = ts ? new Date(ts) : null; const date = parsed && !isNaN(parsed) ? parsed.toLocaleDateString() : 'Unknown'; let deviceInfo = 'Unknown Device'; if (capture.device_name) { deviceInfo = `${capture.device_name}`; if (capture.match_confidence) { deviceInfo += ` (${(capture.match_confidence * 100).toFixed(0)}% confidence)`; } } return `

${deviceInfo}

Frequency: ${freqMHz} MHz

Protocol: ${capture.protocol || 'RAW'}

Captured: ${date}

Location: ${capture.latitude.toFixed(6)}, ${capture.longitude.toFixed(6)}

${capture.accuracy ? `

Accuracy: ±${capture.accuracy.toFixed(1)}m

` : ''}
`; } function updateStats() { // Total captures document.getElementById('total-captures').textContent = capturesData.length; // Unique devices const uniqueDevices = new Set( capturesData .filter(c => c.device_name) .map(c => c.device_name) ).size; document.getElementById('unique-devices').textContent = uniqueDevices; } function renderHeatmap() { // Remove existing heatmap layer if it exists if (heatmapLayer) { map.removeLayer(heatmapLayer); } // Get frequency filter const frequencyFilter = document.getElementById('frequency-filter').value; // Filter captures let filtered = capturesData; if (frequencyFilter) { const targetFreq = parseInt(frequencyFilter) * 1e6; filtered = capturesData.filter(c => { const freq = c.frequency / 1e6; return Math.abs(freq - parseInt(frequencyFilter)) < 50; }); } // Convert captures to heatmap data format // Format: [[lat, lng, intensity], ...] const heatmapData = filtered.map(capture => { // Use match confidence as intensity (default to 0.5 if no match) const intensity = capture.match_confidence || 0.5; return [capture.latitude, capture.longitude, intensity]; }); console.log(`Rendering heatmap with ${heatmapData.length} points`); // Create heatmap layer heatmapLayer = L.heatLayer(heatmapData, { radius: 25, // Radius of each point blur: 15, // Amount of blur maxZoom: 17, // Zoom level where max intensity is reached max: 1.0, // Maximum point intensity gradient: { // Custom color gradient 0.0: 'blue', 0.3: 'cyan', 0.5: 'lime', 0.7: 'yellow', 1.0: 'red' } }); // Add to map map.addLayer(heatmapLayer); // Update count document.getElementById('total-captures').textContent = filtered.length; } // Export for external use window.reloadMapData = loadCaptures;