116 lines
4.0 KiB
React
116 lines
4.0 KiB
React
|
|
/**
|
||
|
|
* 항적분석 호버 툴팁 컴포넌트
|
||
|
|
* - 선박 기본 정보 (선종, 선명, 신호원)
|
||
|
|
* - 구역별 진입/진출 시간 및 위치
|
||
|
|
*/
|
||
|
|
import { useAreaSearchStore } from '../stores/areaSearchStore';
|
||
|
|
import { ZONE_COLORS } from '../types/areaSearch.types';
|
||
|
|
import { getShipKindName, getSignalSourceName } from '../../tracking/types/trackQuery.types';
|
||
|
|
import './AreaSearchTooltip.scss';
|
||
|
|
|
||
|
|
const OFFSET_X = 14;
|
||
|
|
const OFFSET_Y = -20;
|
||
|
|
|
||
|
|
/** nationalCode → 국기 SVG URL */
|
||
|
|
function getNationalFlagUrl(nationalCode) {
|
||
|
|
if (!nationalCode) return null;
|
||
|
|
return `/ship/image/small/${nationalCode}.svg`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatTimestamp(ms) {
|
||
|
|
if (!ms) return '-';
|
||
|
|
const d = new Date(ms);
|
||
|
|
const pad = (n) => String(n).padStart(2, '0');
|
||
|
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatPosition(pos) {
|
||
|
|
if (!pos || pos.length < 2) return null;
|
||
|
|
const lon = pos[0];
|
||
|
|
const lat = pos[1];
|
||
|
|
const latDir = lat >= 0 ? 'N' : 'S';
|
||
|
|
const lonDir = lon >= 0 ? 'E' : 'W';
|
||
|
|
return `${Math.abs(lat).toFixed(4)}\u00B0${latDir} ${Math.abs(lon).toFixed(4)}\u00B0${lonDir}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function AreaSearchTooltip() {
|
||
|
|
const tooltip = useAreaSearchStore((s) => s.areaSearchTooltip);
|
||
|
|
const tracks = useAreaSearchStore((s) => s.tracks);
|
||
|
|
const hitDetails = useAreaSearchStore((s) => s.hitDetails);
|
||
|
|
const zones = useAreaSearchStore((s) => s.zones);
|
||
|
|
|
||
|
|
if (!tooltip) return null;
|
||
|
|
|
||
|
|
const { vesselId, x, y } = tooltip;
|
||
|
|
const track = tracks.find((t) => t.vesselId === vesselId);
|
||
|
|
if (!track) return null;
|
||
|
|
|
||
|
|
const hits = hitDetails[vesselId] || [];
|
||
|
|
const kindName = getShipKindName(track.shipKindCode);
|
||
|
|
const sourceName = getSignalSourceName(track.sigSrcCd);
|
||
|
|
const flagUrl = getNationalFlagUrl(track.nationalCode);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className="area-search-tooltip"
|
||
|
|
style={{ left: x + OFFSET_X, top: y + OFFSET_Y }}
|
||
|
|
>
|
||
|
|
<div className="area-search-tooltip__header">
|
||
|
|
<span className="area-search-tooltip__kind">{kindName}</span>
|
||
|
|
{flagUrl && (
|
||
|
|
<span className="area-search-tooltip__flag">
|
||
|
|
<img
|
||
|
|
src={flagUrl}
|
||
|
|
alt="국기"
|
||
|
|
onError={(e) => { e.target.style.display = 'none'; }}
|
||
|
|
/>
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
<span className="area-search-tooltip__name">
|
||
|
|
{track.shipName || track.targetId || '-'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="area-search-tooltip__info">
|
||
|
|
<span>{sourceName}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{zones.length > 0 && hits.length > 0 && (
|
||
|
|
<div className="area-search-tooltip__zones">
|
||
|
|
{zones.map((zone) => {
|
||
|
|
const hit = hits.find((h) => h.polygonId === zone.id);
|
||
|
|
if (!hit) return null;
|
||
|
|
const zoneColor = ZONE_COLORS[zone.colorIndex]?.label || '#ffd43b';
|
||
|
|
const entryPos = formatPosition(hit.entryPosition);
|
||
|
|
const exitPos = formatPosition(hit.exitPosition);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div key={zone.id} className="area-search-tooltip__zone">
|
||
|
|
<span
|
||
|
|
className="area-search-tooltip__zone-name"
|
||
|
|
style={{ color: zoneColor }}
|
||
|
|
>
|
||
|
|
{zone.name}
|
||
|
|
</span>
|
||
|
|
<div className="area-search-tooltip__zone-row">
|
||
|
|
<span className="area-search-tooltip__zone-label">IN</span>
|
||
|
|
<span>{formatTimestamp(hit.entryTimestamp)}</span>
|
||
|
|
{entryPos && (
|
||
|
|
<span className="area-search-tooltip__pos">{entryPos}</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="area-search-tooltip__zone-row">
|
||
|
|
<span className="area-search-tooltip__zone-label">OUT</span>
|
||
|
|
<span>{formatTimestamp(hit.exitTimestamp)}</span>
|
||
|
|
{exitPos && (
|
||
|
|
<span className="area-search-tooltip__pos">{exitPos}</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|