UI карты: приоритет — концентрические круги + подсветка направлений

Дизайн Виктора (классика ПСО): возможная зона поиска — круг радиусом
max_distance от ТНП (ядро 0.5R / средняя 0.75R / внешняя R с градацией
плотности вероятности), а приоритетные направления ПОДСВЕЧИВАЮТСЯ
45°-секторами полного радиуса поверх кругов (топ-1 #ff4d00 op0.30,
топ-2 #ff9f1a 0.22, топ-3 #ffd166 0.18, прочие серые 0.10).
Раньше сектора были «пирогами» разного радиуса по score — зона выглядела
лоскутной. Теперь круги показывают охват, подсветка — приоритет.
This commit is contained in:
2026-09-09 19:58:05 +03:00
parent 2aa45e8932
commit b636389ee1
+75 -73
View File
@@ -15,9 +15,9 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
const tnpPosition = [tnpLat || 53.9, tnpLon || 27.56]; const tnpPosition = [tnpLat || 53.9, tnpLon || 27.56];
const distance = maxDistance || 5.0; const distance = maxDistance || 5.0;
// Функция для создания точек сектора // Функция для создания точек дуги
const createSectorPoints = (centerLat, centerLon, radiusKm, startAngle, endAngle, points = 20) => { const createArcPoints = (centerLat, centerLon, radiusKm, startAngle, endAngle, points = 24) => {
const coords = [[centerLat, centerLon]]; // Начинаем с центра const coords = [];
// Конвертируем км в градусы (приблизительно) // Конвертируем км в градусы (приблизительно)
const latPerKm = 1 / 111.0; const latPerKm = 1 / 111.0;
@@ -34,9 +34,15 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
coords.push([lat, lon]); coords.push([lat, lon]);
} }
coords.push([centerLat, centerLon]); // Замыкаем в центре
return coords; return coords;
}; };
// Функция для создания точек сектора (пирог: дуга + центр)
const createSectorPoints = (centerLat, centerLon, radiusKm, startAngle, endAngle, points = 24) => {
return [[centerLat, centerLon],
...createArcPoints(centerLat, centerLon, radiusKm, startAngle, endAngle, points),
[centerLat, centerLon]];
};
// Направления и их углы // Направления и их углы
const directionAngles = { const directionAngles = {
@@ -50,45 +56,41 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
'NW': 315 'NW': 315
}; };
// Создаем секторы на основе зон // Подсветка приоритетных направлений: 45°-сектор ПОЛНОГО радиуса поверх кругов.
const sectors = zones && zones.length > 0 ? zones.slice(0, 8).map((zone, idx) => { // Приоритет задаёт прозрачность (топ-зона заметнее), цвет — палитра ЦОУ.
const angle = directionAngles[zone.direction] || 0; const highlightSectors = zones && zones.length > 0 ? zones.map((zone, idx) => {
const startAngle = angle - 22.5; // ±22.5° = 45° сектор const angle = directionAngles[zone.direction];
if (angle === undefined) return null;
const startAngle = angle - 22.5;
const endAngle = angle + 22.5; const endAngle = angle + 22.5;
// Определяем радиус на основе score
let radius = distance;
if (zone.score >= 90) radius = distance * 1.0;
else if (zone.score >= 70) radius = distance * 0.8;
else if (zone.score >= 50) radius = distance * 0.6;
else radius = distance * 0.4;
// Цвет на основе приоритета
let color, fillOpacity; let color, fillOpacity;
if (idx === 0) { if (idx === 0) {
color = '#ff4d00'; color = '#ff4d00';
fillOpacity = 0.35; fillOpacity = 0.30;
} else if (idx === 1) { } else if (idx === 1) {
color = '#ff9f1a'; color = '#ff9f1a';
fillOpacity = 0.25; fillOpacity = 0.22;
} else if (idx === 2) { } else if (idx === 2) {
color = '#ffd166'; color = '#ffd166';
fillOpacity = 0.2; fillOpacity = 0.18;
} else { } else {
color = '#8b949e'; color = '#8b949e';
fillOpacity = 0.1; fillOpacity = 0.10;
} }
return { return {
points: createSectorPoints(tnpPosition[0], tnpPosition[1], radius, startAngle, endAngle), points: createSectorPoints(tnpPosition[0], tnpPosition[1], distance, startAngle, endAngle),
color, color,
fillOpacity, fillOpacity,
weight: idx < 3 ? 2 : 1,
zone zone
}; };
}) : []; }).filter(Boolean) : [];
// Если зон нет, создаем базовые концентрические круги // Концентрические круги — возможная зона поиска (базовый слой).
const createCirclePoints = (centerLat, centerLon, radiusKm, points = 40) => { // Классика ПСО: ядро (0.5R) — наибольшая плотность вероятности.
const createCirclePoints = (centerLat, centerLon, radiusKm, points = 48) => {
const coords = []; const coords = [];
const latPerKm = 1 / 111.0; const latPerKm = 1 / 111.0;
const lonPerKm = 1 / (111.0 * Math.cos(centerLat * Math.PI / 180)); const lonPerKm = 1 / (111.0 * Math.cos(centerLat * Math.PI / 180));
@@ -107,23 +109,23 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
{ {
points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance), points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance),
color: '#ffd166', color: '#ffd166',
fillOpacity: 0.15, fillOpacity: 0.12,
label: `Зона 3 (${distance.toFixed(1)} км)`, label: `Внешняя зона (до ${distance.toFixed(1)} км)`,
probability: 10 probability: 'низкая'
}, },
{ {
points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance * 0.75), points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance * 0.75),
color: '#ff9f1a', color: '#ff9f1a',
fillOpacity: 0.2, fillOpacity: 0.16,
label: `Зона 2 (${(distance * 0.75).toFixed(1)} км)`, label: `Средняя зона (до ${(distance * 0.75).toFixed(1)} км)`,
probability: 20 probability: 'средняя'
}, },
{ {
points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance * 0.5), points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance * 0.5),
color: '#ff4d00', color: '#ff4d00',
fillOpacity: 0.3, fillOpacity: 0.20,
label: `Зона 1 (${(distance * 0.5).toFixed(1)} км)`, label: `Ядро зоны (до ${(distance * 0.5).toFixed(1)} км)`,
probability: 70 probability: 'высокая'
} }
]; ];
@@ -192,46 +194,46 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
</Polyline> </Polyline>
))} ))}
{/* Рисуем секторы или круги */} {/* Базовый слой: концентрические круги возможной зоны поиска */}
{sectors.length > 0 ? ( {defaultZones.map((zone, idx) => (
sectors.map((sector, idx) => ( <Polygon
<Polygon key={`circle-${idx}`}
key={idx} positions={zone.points}
positions={sector.points} pathOptions={{
pathOptions={{ color: zone.color,
color: sector.color, fillColor: zone.color,
fillColor: sector.color, fillOpacity: zone.fillOpacity,
fillOpacity: sector.fillOpacity, weight: 1.5,
weight: 2 dashArray: idx === 0 ? '6 4' : undefined
}} }}
> >
<Popup> <Popup>
<strong>#{idx + 1} {sector.zone.name}</strong><br/> <strong>{zone.label}</strong><br/>
Направление: {sector.zone.direction}<br/> Плотность вероятности: {zone.probability}
Приоритет: {sector.zone.score}%<br/> </Popup>
{sector.zone.reasoning} </Polygon>
</Popup> ))}
</Polygon>
)) {/* Подсветка приоритетных направлений (поверх кругов) */}
) : ( {highlightSectors.map((sector, idx) => (
defaultZones.map((zone, idx) => ( <Polygon
<Polygon key={`hl-${idx}`}
key={idx} positions={sector.points}
positions={zone.points} pathOptions={{
pathOptions={{ color: sector.color,
color: zone.color, fillColor: sector.color,
fillColor: zone.color, fillOpacity: sector.fillOpacity,
fillOpacity: zone.fillOpacity, weight: sector.weight
weight: 2 }}
}} >
> <Popup>
<Popup> <strong>#{idx + 1} {sector.zone.name}</strong><br/>
<strong>{zone.label}</strong><br/> Направление: {sector.zone.direction}<br/>
Вероятность: {zone.probability}% Приоритет: {sector.zone.score}%<br/>
</Popup> {sector.zone.reasoning}
</Polygon> </Popup>
)) </Polygon>
)} ))}
{/* ТНП - красный маркер */} {/* ТНП - красный маркер */}
<Marker <Marker