import React, { useEffect, useRef, useState } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import "leaflet.heat"; import "./MapView.css"; // Fix Leaflet default marker icons delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png", iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", }); const MapView = ({ tnpLat, tnpLon, zones = [], direction = null, heatmapData = [], showHeatmap = false }) => { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const zonesLayerRef = useRef(null); const heatmapLayerRef = useRef(null); const tnpMarkerRef = useRef(null); const directionArrowRef = useRef(null); const [heatmapVisible, setHeatmapVisible] = useState(showHeatmap); // Цвета зон из §5 ЦОУ палитры const ZONE_COLORS = { 0: "#ff4d00", // Зона 1 (0-500м) - красный 1: "#ff9f1a", // Зона 2 (500-1500м) - оранжевый 2: "#ffd166", // Зона 3 (1500-2500м) - желтый }; // Направления в градусы const DIRECTION_ANGLES = { N: 0, NE: 45, E: 90, SE: 135, S: 180, SW: 225, W: 270, NW: 315, }; useEffect(() => { if (!mapRef.current || mapInstanceRef.current) return; // Инициализация карты const map = L.map(mapRef.current).setView([tnpLat || 53.9, tnpLon || 27.56], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors", maxZoom: 19, }).addTo(map); mapInstanceRef.current = map; return () => { if (mapInstanceRef.current) { mapInstanceRef.current.remove(); mapInstanceRef.current = null; } }; }, []); // Обновление ТНП маркера useEffect(() => { if (!mapInstanceRef.current || !tnpLat || !tnpLon) return; // Удаляем старый маркер if (tnpMarkerRef.current) { mapInstanceRef.current.removeLayer(tnpMarkerRef.current); } // Создаем красный маркер ТНП const tnpIcon = L.divIcon({ className: "tnp-marker", html: "
ТНП
", iconSize: [40, 40], iconAnchor: [20, 20], }); const marker = L.marker([tnpLat, tnpLon], { icon: tnpIcon }).addTo(mapInstanceRef.current); marker.bindPopup("Точка начала поиска
" + tnpLat.toFixed(6) + ", " + tnpLon.toFixed(6)); tnpMarkerRef.current = marker; // Центрируем карту mapInstanceRef.current.setView([tnpLat, tnpLon], 13); }, [tnpLat, tnpLon]); // Обновление стрелки направления useEffect(() => { if (!mapInstanceRef.current || !tnpLat || !tnpLon || !direction || direction === "unknown") { if (directionArrowRef.current) { mapInstanceRef.current.removeLayer(directionArrowRef.current); directionArrowRef.current = null; } return; } // Удаляем старую стрелку if (directionArrowRef.current) { mapInstanceRef.current.removeLayer(directionArrowRef.current); } const angle = DIRECTION_ANGLES[direction]; if (angle === undefined) return; // Создаем стрелку (линия 1км в направлении) const distance = 1000; // метров const endPoint = calculateDestination(tnpLat, tnpLon, angle, distance); const arrow = L.polyline( [[tnpLat, tnpLon], endPoint], { color: "#ff4d00", weight: 3, opacity: 0.8, dashArray: "10, 5", } ).addTo(mapInstanceRef.current); // Добавляем стрелку на конце const arrowHead = L.marker(endPoint, { icon: L.divIcon({ className: "direction-arrow", html: "
➤
", iconSize: [30, 30], iconAnchor: [15, 15], }), }).addTo(mapInstanceRef.current); directionArrowRef.current = L.layerGroup([arrow, arrowHead]).addTo(mapInstanceRef.current); }, [tnpLat, tnpLon, direction]); // Обновление зон (секторные полигоны) useEffect(() => { if (!mapInstanceRef.current || !tnpLat || !tnpLon || zones.length === 0) { if (zonesLayerRef.current) { mapInstanceRef.current.removeLayer(zonesLayerRef.current); zonesLayerRef.current = null; } return; } // Удаляем старые зоны if (zonesLayerRef.current) { mapInstanceRef.current.removeLayer(zonesLayerRef.current); } const zonePolygons = []; zones.forEach((zone, index) => { const color = ZONE_COLORS[index] || "#cccccc"; const directionAngle = DIRECTION_ANGLES[zone.direction] || 0; // Создаем секторный полигон (угол ±45° от направления) const sectorAngle = 90; // ширина сектора const startAngle = directionAngle - sectorAngle / 2; const endAngle = directionAngle + sectorAngle / 2; const radiusMeters = zone.distance_km * 1000; const sectorPoints = [[tnpLat, tnpLon]]; // Начало в ТНП // Генерируем точки по дуге for (let angle = startAngle; angle <= endAngle; angle += 5) { const point = calculateDestination(tnpLat, tnpLon, angle, radiusMeters); sectorPoints.push(point); } sectorPoints.push([tnpLat, tnpLon]); // Замыкаем полигон const polygon = L.polygon(sectorPoints, { color: color, fillColor: color, fillOpacity: 0.2, weight: 2, opacity: 0.6, }); polygon.bindPopup( "" + zone.name + "
" + "Приоритет: " + zone.priority + "
" + "Оценка: " + zone.score + "
" + "Расстояние: " + zone.distance_km.toFixed(2) + " км
" + "" + zone.reasoning + "" ); zonePolygons.push(polygon); }); zonesLayerRef.current = L.layerGroup(zonePolygons).addTo(mapInstanceRef.current); }, [tnpLat, tnpLon, zones]); // Обновление тепловой карты useEffect(() => { if (!mapInstanceRef.current) return; // Удаляем старый слой if (heatmapLayerRef.current) { mapInstanceRef.current.removeLayer(heatmapLayerRef.current); heatmapLayerRef.current = null; } if (!heatmapVisible || heatmapData.length === 0) return; // Создаем heatmap слой const heatPoints = heatmapData.map(point => [ point.lat, point.lon, point.intensity || 1.0 ]); const heatLayer = L.heatLayer(heatPoints, { radius: 25, blur: 15, maxZoom: 17, max: 1.0, gradient: { 0.0: "#0d1117", 0.3: "#238636", 0.5: "#ffd166", 0.7: "#ff9f1a", 1.0: "#ff4d00" } }).addTo(mapInstanceRef.current); heatmapLayerRef.current = heatLayer; }, [heatmapVisible, heatmapData]); // Вычисление точки назначения (lat, lon, angle, distance) const calculateDestination = (lat, lon, bearing, distance) => { const R = 6371000; // Радиус Земли в метрах const φ1 = (lat * Math.PI) / 180; const λ1 = (lon * Math.PI) / 180; const θ = (bearing * Math.PI) / 180; const φ2 = Math.asin( Math.sin(φ1) * Math.cos(distance / R) + Math.cos(φ1) * Math.sin(distance / R) * Math.cos(θ) ); const λ2 = λ1 + Math.atan2( Math.sin(θ) * Math.sin(distance / R) * Math.cos(φ1), Math.cos(distance / R) - Math.sin(φ1) * Math.sin(φ2) ); return [(φ2 * 180) / Math.PI, (λ2 * 180) / Math.PI]; }; return (
); }; export default MapView;