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 (
- Real-time view of all devices and their detection status -
++ Real-time view of all devices and drone detections +
+