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 (
Real-time view of all devices and drone detections
{device.location_description}
)}