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:
@@ -15,9 +15,9 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
|
||||
const tnpPosition = [tnpLat || 53.9, tnpLon || 27.56];
|
||||
const distance = maxDistance || 5.0;
|
||||
|
||||
// Функция для создания точек сектора
|
||||
const createSectorPoints = (centerLat, centerLon, radiusKm, startAngle, endAngle, points = 20) => {
|
||||
const coords = [[centerLat, centerLon]]; // Начинаем с центра
|
||||
// Функция для создания точек дуги
|
||||
const createArcPoints = (centerLat, centerLon, radiusKm, startAngle, endAngle, points = 24) => {
|
||||
const coords = [];
|
||||
|
||||
// Конвертируем км в градусы (приблизительно)
|
||||
const latPerKm = 1 / 111.0;
|
||||
@@ -34,10 +34,16 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
|
||||
coords.push([lat, lon]);
|
||||
}
|
||||
|
||||
coords.push([centerLat, centerLon]); // Замыкаем в центре
|
||||
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 = {
|
||||
'N': 0,
|
||||
@@ -50,45 +56,41 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
|
||||
'NW': 315
|
||||
};
|
||||
|
||||
// Создаем секторы на основе зон
|
||||
const sectors = zones && zones.length > 0 ? zones.slice(0, 8).map((zone, idx) => {
|
||||
const angle = directionAngles[zone.direction] || 0;
|
||||
const startAngle = angle - 22.5; // ±22.5° = 45° сектор
|
||||
// Подсветка приоритетных направлений: 45°-сектор ПОЛНОГО радиуса поверх кругов.
|
||||
// Приоритет задаёт прозрачность (топ-зона заметнее), цвет — палитра ЦОУ.
|
||||
const highlightSectors = zones && zones.length > 0 ? zones.map((zone, idx) => {
|
||||
const angle = directionAngles[zone.direction];
|
||||
if (angle === undefined) return null;
|
||||
const startAngle = 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;
|
||||
if (idx === 0) {
|
||||
color = '#ff4d00';
|
||||
fillOpacity = 0.35;
|
||||
fillOpacity = 0.30;
|
||||
} else if (idx === 1) {
|
||||
color = '#ff9f1a';
|
||||
fillOpacity = 0.25;
|
||||
fillOpacity = 0.22;
|
||||
} else if (idx === 2) {
|
||||
color = '#ffd166';
|
||||
fillOpacity = 0.2;
|
||||
fillOpacity = 0.18;
|
||||
} else {
|
||||
color = '#8b949e';
|
||||
fillOpacity = 0.1;
|
||||
fillOpacity = 0.10;
|
||||
}
|
||||
|
||||
return {
|
||||
points: createSectorPoints(tnpPosition[0], tnpPosition[1], radius, startAngle, endAngle),
|
||||
points: createSectorPoints(tnpPosition[0], tnpPosition[1], distance, startAngle, endAngle),
|
||||
color,
|
||||
fillOpacity,
|
||||
weight: idx < 3 ? 2 : 1,
|
||||
zone
|
||||
};
|
||||
}) : [];
|
||||
}).filter(Boolean) : [];
|
||||
|
||||
// Если зон нет, создаем базовые концентрические круги
|
||||
const createCirclePoints = (centerLat, centerLon, radiusKm, points = 40) => {
|
||||
// Концентрические круги — возможная зона поиска (базовый слой).
|
||||
// Классика ПСО: ядро (0.5R) — наибольшая плотность вероятности.
|
||||
const createCirclePoints = (centerLat, centerLon, radiusKm, points = 48) => {
|
||||
const coords = [];
|
||||
const latPerKm = 1 / 111.0;
|
||||
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),
|
||||
color: '#ffd166',
|
||||
fillOpacity: 0.15,
|
||||
label: `Зона 3 (${distance.toFixed(1)} км)`,
|
||||
probability: 10
|
||||
fillOpacity: 0.12,
|
||||
label: `Внешняя зона (до ${distance.toFixed(1)} км)`,
|
||||
probability: 'низкая'
|
||||
},
|
||||
{
|
||||
points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance * 0.75),
|
||||
color: '#ff9f1a',
|
||||
fillOpacity: 0.2,
|
||||
label: `Зона 2 (${(distance * 0.75).toFixed(1)} км)`,
|
||||
probability: 20
|
||||
fillOpacity: 0.16,
|
||||
label: `Средняя зона (до ${(distance * 0.75).toFixed(1)} км)`,
|
||||
probability: 'средняя'
|
||||
},
|
||||
{
|
||||
points: createCirclePoints(tnpPosition[0], tnpPosition[1], distance * 0.5),
|
||||
color: '#ff4d00',
|
||||
fillOpacity: 0.3,
|
||||
label: `Зона 1 (${(distance * 0.5).toFixed(1)} км)`,
|
||||
probability: 70
|
||||
fillOpacity: 0.20,
|
||||
label: `Ядро зоны (до ${(distance * 0.5).toFixed(1)} км)`,
|
||||
probability: 'высокая'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -192,46 +194,46 @@ const SearchMap = ({ tnpLat, tnpLon, maxDistance, zones, waterFeatures }) => {
|
||||
</Polyline>
|
||||
))}
|
||||
|
||||
{/* Рисуем секторы или круги */}
|
||||
{sectors.length > 0 ? (
|
||||
sectors.map((sector, idx) => (
|
||||
<Polygon
|
||||
key={idx}
|
||||
positions={sector.points}
|
||||
pathOptions={{
|
||||
color: sector.color,
|
||||
fillColor: sector.color,
|
||||
fillOpacity: sector.fillOpacity,
|
||||
weight: 2
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<strong>#{idx + 1} {sector.zone.name}</strong><br/>
|
||||
Направление: {sector.zone.direction}<br/>
|
||||
Приоритет: {sector.zone.score}%<br/>
|
||||
{sector.zone.reasoning}
|
||||
</Popup>
|
||||
</Polygon>
|
||||
))
|
||||
) : (
|
||||
defaultZones.map((zone, idx) => (
|
||||
<Polygon
|
||||
key={idx}
|
||||
positions={zone.points}
|
||||
pathOptions={{
|
||||
color: zone.color,
|
||||
fillColor: zone.color,
|
||||
fillOpacity: zone.fillOpacity,
|
||||
weight: 2
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<strong>{zone.label}</strong><br/>
|
||||
Вероятность: {zone.probability}%
|
||||
</Popup>
|
||||
</Polygon>
|
||||
))
|
||||
)}
|
||||
{/* Базовый слой: концентрические круги возможной зоны поиска */}
|
||||
{defaultZones.map((zone, idx) => (
|
||||
<Polygon
|
||||
key={`circle-${idx}`}
|
||||
positions={zone.points}
|
||||
pathOptions={{
|
||||
color: zone.color,
|
||||
fillColor: zone.color,
|
||||
fillOpacity: zone.fillOpacity,
|
||||
weight: 1.5,
|
||||
dashArray: idx === 0 ? '6 4' : undefined
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<strong>{zone.label}</strong><br/>
|
||||
Плотность вероятности: {zone.probability}
|
||||
</Popup>
|
||||
</Polygon>
|
||||
))}
|
||||
|
||||
{/* Подсветка приоритетных направлений (поверх кругов) */}
|
||||
{highlightSectors.map((sector, idx) => (
|
||||
<Polygon
|
||||
key={`hl-${idx}`}
|
||||
positions={sector.points}
|
||||
pathOptions={{
|
||||
color: sector.color,
|
||||
fillColor: sector.color,
|
||||
fillOpacity: sector.fillOpacity,
|
||||
weight: sector.weight
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<strong>#{idx + 1} {sector.zone.name}</strong><br/>
|
||||
Направление: {sector.zone.direction}<br/>
|
||||
Приоритет: {sector.zone.score}%<br/>
|
||||
{sector.zone.reasoning}
|
||||
</Popup>
|
||||
</Polygon>
|
||||
))}
|
||||
|
||||
{/* ТНП - красный маркер */}
|
||||
<Marker
|
||||
|
||||
Reference in New Issue
Block a user