import React, { useState, useEffect } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet'; import { Icon } from 'leaflet'; import { useSocket } from '../contexts/SocketContext'; import api from '../services/api'; import { format } from 'date-fns'; import { ServerIcon, ExclamationTriangleIcon, SignalIcon, EyeIcon } from '@heroicons/react/24/outline'; // Fix for default markers in React Leaflet import 'leaflet/dist/leaflet.css'; import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png'; import iconUrl from 'leaflet/dist/images/marker-icon.png'; import shadowUrl from 'leaflet/dist/images/marker-shadow.png'; delete Icon.Default.prototype._getIconUrl; Icon.Default.mergeOptions({ iconRetinaUrl, iconUrl, shadowUrl, }); // Custom icons const createDeviceIcon = (status, hasDetections) => { let color = '#6b7280'; // gray for offline/inactive if (status === 'online') { color = hasDetections ? '#ef4444' : '#22c55e'; // red if detecting, green if online } return new Icon({ iconUrl: `data:image/svg+xml;base64,${btoa(` `)}`, iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -16], }); }; // Drone detection icon const createDroneIcon = (rssi, droneType) => { // Color based on signal strength let color = '#ff6b6b'; // Default red if (rssi > -50) color = '#ff4757'; // Very strong - bright red else if (rssi > -60) color = '#ff6b6b'; // Strong - red else if (rssi > -70) color = '#ffa726'; // Medium - orange else if (rssi > -80) color = '#ffca28'; // Weak - yellow else color = '#66bb6a'; // Very weak - green return new Icon({ iconUrl: `data:image/svg+xml;base64,${btoa(` `)}`, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14], }); }; const MapView = () => { const [devices, setDevices] = useState([]); const [selectedDevice, setSelectedDevice] = useState(null); const [loading, setLoading] = useState(true); const [mapCenter, setMapCenter] = useState([59.4, 18.1]); // Stockholm area center const [mapZoom, setMapZoom] = useState(11); // Closer zoom for Stockholm area const [showDroneDetections, setShowDroneDetections] = useState(true); const [droneDetectionHistory, setDroneDetectionHistory] = useState([]); const { recentDetections, deviceStatus } = useSocket(); // Drone types mapping const droneTypes = { 1: "DJI Mavic", 2: "Racing Drone", 3: "DJI Phantom", 4: "Fixed Wing", 5: "Surveillance", 0: "Unknown" }; useEffect(() => { fetchDevices(); const interval = setInterval(fetchDevices, 30000); // Refresh every 30 seconds return () => clearInterval(interval); }, []); // Update drone detection history when new detections arrive useEffect(() => { if (recentDetections.length > 0) { const latestDetection = recentDetections[0]; // Add to history with timestamp for fade-out setDroneDetectionHistory(prev => [ { ...latestDetection, timestamp: Date.now(), id: `${latestDetection.device_id}-${latestDetection.drone_id}-${latestDetection.device_timestamp}` }, ...prev.slice(0, 19) // Keep last 20 detections ]); } }, [recentDetections]); // Clean up old detections (fade them out after 5 minutes) useEffect(() => { const cleanup = setInterval(() => { const fiveMinutesAgo = Date.now() - (5 * 60 * 1000); setDroneDetectionHistory(prev => prev.filter(detection => detection.timestamp > fiveMinutesAgo) ); }, 30000); // Clean up every 30 seconds return () => clearInterval(cleanup); }, []); const fetchDevices = async () => { try { const response = await api.get('/devices/map'); const deviceData = response.data.data; setDevices(deviceData); // Set map bounds to Stockholm area with all three detectors if (deviceData.length > 0 && devices.length === 0) { // Stockholm area bounds that include Arlanda, Naval Base, and Royal Castle const stockholmBounds = [ [59.2, 17.8], // Southwest [59.7, 18.4] // Northeast ]; setMapCenter([59.4, 18.1]); // Center of Stockholm area setMapZoom(11); } } catch (error) { console.error('Error fetching devices:', error); } finally { setLoading(false); } }; const getDeviceStatus = (device) => { const realtimeStatus = deviceStatus[device.id]; if (realtimeStatus) { return realtimeStatus.status; } return device.status || 'offline'; }; const getDeviceDetections = (deviceId) => { return recentDetections.filter(d => d.device_id === deviceId); }; const getDetectionAge = (detection) => { const ageMs = Date.now() - detection.timestamp; const ageMinutes = ageMs / (1000 * 60); return ageMinutes; }; const getDetectionOpacity = (detection) => { const age = getDetectionAge(detection); if (age < 1) return 1.0; // Full opacity for first minute if (age < 3) return 0.8; // 80% for 1-3 minutes if (age < 5) return 0.5; // 50% for 3-5 minutes return 0.2; // 20% for older detections }; if (loading) { return (
); } return (

Device Map

Real-time view of all devices and drone detections

{droneDetectionHistory.length > 0 && (
{droneDetectionHistory.length} recent detection{droneDetectionHistory.length > 1 ? 's' : ''}
)}
{/* Map */}
{ // Set bounds to Stockholm area to show all three detectors const stockholmBounds = [ [59.2, 17.8], // Southwest [59.7, 18.4] // Northeast ]; map.fitBounds(stockholmBounds, { padding: [20, 20] }); }} > {devices .filter(device => device.geo_lat && device.geo_lon) .map(device => { const status = getDeviceStatus(device); const detections = getDeviceDetections(device.id); const hasRecentDetections = detections.length > 0; return ( setSelectedDevice(device), }} > ); })} {/* Drone Detection Markers */} {showDroneDetections && droneDetectionHistory .filter(detection => detection.geo_lat && detection.geo_lon) .map(detection => { const opacity = getDetectionOpacity(detection); const age = getDetectionAge(detection); return ( {/* Detection range circle for recent detections */} {age < 2 && ( -60 ? '#ff4757' : '#ffa726', fillColor: detection.rssi > -60 ? '#ff4757' : '#ffa726', fillOpacity: 0.1 * opacity, weight: 2, opacity: opacity * 0.5 }} /> )} ); })} {/* Map Legend - Fixed positioning and visibility */}
Map Legend
Device Online
Device Detecting
Device Offline
{showDroneDetections && ( <>
Drone Detections:
Strong Signal (>-60dBm)
Medium Signal (-60 to -70dBm)
Weak Signal (<-70dBm)
Detection Range
)}
{/* Device List */}

Device Status

{devices.map(device => { const status = getDeviceStatus(device); const detections = getDeviceDetections(device.id); return ( setSelectedDevice(device)} /> ); })} {devices.length === 0 && (
No devices found
)}
); }; const DevicePopup = ({ device, status, detections }) => (

{device.name || `Device ${device.id}`}

{status}
{device.location_description && (

{device.location_description}

)}
ID: {device.id}
Coordinates: {device.geo_lat}, {device.geo_lon}
{device.last_heartbeat && (
Last seen: {format(new Date(device.last_heartbeat), 'MMM dd, HH:mm')}
)}
{detections.length > 0 && (
{detections.length} recent detection{detections.length > 1 ? 's' : ''}
{detections.slice(0, 3).map((detection, index) => (
Drone {detection.drone_id} • {detection.freq}MHz • {detection.rssi}dBm
))}
)}
); const DroneDetectionPopup = ({ detection, age, droneTypes }) => (

🚨 Drone Detection

{age < 1 ? 'LIVE' : `${Math.round(age)}m ago`}
Drone ID:
{detection.drone_id}
Type:
{droneTypes[detection.drone_type] || 'Unknown'}
RSSI:
-50 ? 'text-red-600' : detection.rssi > -70 ? 'text-orange-600' : 'text-green-600' }`}> {detection.rssi}dBm
Frequency:
{detection.freq}MHz
Confidence:
{(detection.confidence_level * 100).toFixed(0)}%
Duration:
{(detection.signal_duration / 1000).toFixed(1)}s
{/* Movement Analysis */} {detection.movement_analysis && (
Movement Analysis:
= 3 ? 'bg-red-100 text-red-800' : detection.movement_analysis.alertLevel >= 2 ? 'bg-orange-100 text-orange-800' : detection.movement_analysis.alertLevel >= 1 ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800' }`}> {detection.movement_analysis.description}
{detection.movement_analysis.rssiTrend && (
Trend: {detection.movement_analysis.rssiTrend.trend} {detection.movement_analysis.rssiTrend.change !== 0 && ( ({detection.movement_analysis.rssiTrend.change > 0 ? '+' : ''}{detection.movement_analysis.rssiTrend.change.toFixed(1)}dB) )}
)} {detection.movement_analysis.proximityLevel && (
Proximity: {detection.movement_analysis.proximityLevel.replace('_', ' ')}
)}
)}
Detected by: Device {detection.device_id}
Location: {detection.geo_lat?.toFixed(4)}, {detection.geo_lon?.toFixed(4)}
Time: {format(new Date(detection.device_timestamp), 'MMM dd, HH:mm:ss')}
); const DeviceListItem = ({ device, status, detections, onClick }) => (
0 ? 'bg-red-400 animate-pulse' : 'bg-green-400' : 'bg-gray-400' }`} />
{device.name || `Device ${device.id}`}
{device.location_description || `${device.geo_lat}, ${device.geo_lon}`}
{detections.length > 0 && (
{detections.length}
)} {status}
); export default MapView;