feat: 선택 어구그룹 하이라이트 폴리곤 + 모선 강조 마커
- 선택된 어구그룹: 진한 주황 fill(0.25) + 굵은 경계선(3px) - 모선 존재 시: 28px 주황 원 + glow + 'M' 라벨 + 선박명 - zoom 시 자동 선택 + 펼침 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
83f1e8f387
커밋
8c008c69ec
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { Source, Layer } from 'react-map-gl/maplibre';
|
||||
import { Source, Layer, Marker } from 'react-map-gl/maplibre';
|
||||
import type { GeoJSON } from 'geojson';
|
||||
import type { Ship, VesselAnalysisDto } from '../../types';
|
||||
import { fetchFleetCompanies } from '../../services/vesselAnalysis';
|
||||
@ -89,6 +89,7 @@ export function FleetClusterLayer({ ships, analysisMap, clusters, onShipSelect,
|
||||
const [expandedFleet, setExpandedFleet] = useState<number | null>(null);
|
||||
const [hoveredFleetId, setHoveredFleetId] = useState<number | null>(null);
|
||||
const [expandedGearGroup, setExpandedGearGroup] = useState<string | null>(null);
|
||||
const [selectedGearGroup, setSelectedGearGroup] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFleetCompanies().then(setCompanies).catch(() => {});
|
||||
@ -199,6 +200,8 @@ export function FleetClusterLayer({ ships, analysisMap, clusters, onShipSelect,
|
||||
}, [gearGroupMap]);
|
||||
|
||||
const handleGearGroupZoom = useCallback((parentName: string) => {
|
||||
setSelectedGearGroup(prev => prev === parentName ? null : parentName);
|
||||
setExpandedGearGroup(parentName);
|
||||
const entry = gearGroupMap.get(parentName);
|
||||
if (!entry) return;
|
||||
const all: Ship[] = [...entry.gears];
|
||||
@ -383,6 +386,60 @@ export function FleetClusterLayer({ ships, analysisMap, clusters, onShipSelect,
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* 선택된 어구 그룹 하이라이트 + 모선 마커 */}
|
||||
{selectedGearGroup && (() => {
|
||||
const entry = gearGroupMap.get(selectedGearGroup);
|
||||
if (!entry) return null;
|
||||
const points: [number, number][] = entry.gears.map(g => [g.lng, g.lat]);
|
||||
if (entry.parent) points.push([entry.parent.lng, entry.parent.lat]);
|
||||
|
||||
const hlFeatures: GeoJSON.Feature[] = [];
|
||||
if (points.length >= 3) {
|
||||
const hull = convexHull(points);
|
||||
const padded = padPolygon(hull, 0.01);
|
||||
padded.push(padded[0]);
|
||||
hlFeatures.push({
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: { type: 'Polygon', coordinates: [padded] },
|
||||
});
|
||||
}
|
||||
const hlGeoJson: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: hlFeatures };
|
||||
|
||||
return (
|
||||
<>
|
||||
{hlFeatures.length > 0 && (
|
||||
<Source id="gear-cluster-selected" type="geojson" data={hlGeoJson}>
|
||||
<Layer id="gear-selected-fill" type="fill" paint={{ 'fill-color': '#f97316', 'fill-opacity': 0.25 }} />
|
||||
<Layer id="gear-selected-line" type="line" paint={{ 'line-color': '#f97316', 'line-width': 3, 'line-opacity': 0.9 }} />
|
||||
</Source>
|
||||
)}
|
||||
{entry.parent && (
|
||||
<Marker longitude={entry.parent.lng} latitude={entry.parent.lat} anchor="center">
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: '50%',
|
||||
border: '3px solid #f97316',
|
||||
backgroundColor: 'rgba(249, 115, 22, 0.3)',
|
||||
boxShadow: '0 0 12px rgba(249, 115, 22, 0.6)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<span style={{ fontSize: 9, fontWeight: 900, color: '#fff' }}>M</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 8, fontWeight: 700, color: '#f97316',
|
||||
textShadow: '0 0 3px #000, 0 0 3px #000',
|
||||
textAlign: 'center', marginTop: 2, whiteSpace: 'nowrap',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
{entry.parent.name || entry.parent.mmsi}
|
||||
</div>
|
||||
</Marker>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 비허가 어구 클러스터 폴리곤 */}
|
||||
<Source id="gear-clusters" type="geojson" data={gearClusterGeoJson}>
|
||||
<Layer
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user