diff --git a/client/src/pages/MapView.jsx b/client/src/pages/MapView.jsx index 6cba513..bd78eb6 100644 --- a/client/src/pages/MapView.jsx +++ b/client/src/pages/MapView.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'; +import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet'; import { Icon } from 'leaflet'; import { useSocket } from '../contexts/SocketContext'; import api from '../services/api'; @@ -45,20 +45,85 @@ const createDeviceIcon = (status, hasDetections) => { }); }; +// 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.3293, 18.0686]); // Stockholm default const [mapZoom, setMapZoom] = useState(10); + 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'); @@ -90,6 +155,20 @@ const MapView = () => { 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 (
@@ -100,18 +179,38 @@ const MapView = () => { return (
-
-

- Device Map -

-

- Real-time view of all devices and their detection status -

+
+
+

+ Device Map +

+

+ Real-time view of all devices and drone detections +

+
+ +
+ + + {droneDetectionHistory.length > 0 && ( +
+ {droneDetectionHistory.length} recent detection{droneDetectionHistory.length > 1 ? 's' : ''} +
+ )} +
{/* Map */}
-
+
{ ); })} + + {/* 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 */} +
+
Legend
+
+
+
+ Device Online +
+
+
+ Device Detecting +
+
+
+ Device Offline +
+ {showDroneDetections && ( + <> +
+
Drone Detections:
+
+
+
+ Strong Signal (>-60dBm) +
+
+
+ Medium Signal (-60 to -70dBm) +
+
+
+ Weak Signal (<-70dBm) +
+ + )} +
+
@@ -238,6 +412,73 @@ const DevicePopup = ({ device, status, detections }) => (
); +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
+
+
+ +
+
+
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 }) => (