Import Vector lab project

This commit is contained in:
root
2026-06-06 18:31:55 +00:00
commit fcc22063ed
105 changed files with 31639 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
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: "<div class=\"tnp-marker-inner\">ТНП</div>",
iconSize: [40, 40],
iconAnchor: [20, 20],
});
const marker = L.marker([tnpLat, tnpLon], { icon: tnpIcon }).addTo(mapInstanceRef.current);
marker.bindPopup("<b>Точка начала поиска</b><br>" + 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: "<div style=\"transform: rotate(" + angle + "deg)\">➤</div>",
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(
"<b>" + zone.name + "</b><br>" +
"Приоритет: " + zone.priority + "<br>" +
"Оценка: " + zone.score + "<br>" +
"Расстояние: " + zone.distance_km.toFixed(2) + " км<br>" +
"<small>" + zone.reasoning + "</small>"
);
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 (
<div className="map-view-container">
<div className="map-controls">
<button
className={heatmapVisible ? "control-btn active" : "control-btn"}
onClick={() => setHeatmapVisible(!heatmapVisible)}
disabled={heatmapData.length === 0}
>
{heatmapVisible ? "🔥 Скрыть тепловую карту" : "🔥 Показать тепловую карту"}
</button>
</div>
<div ref={mapRef} className="map-view" />
</div>
);
};
export default MapView;