kcg-monitoring/frontend/src/utils/fleetDetection.ts

158 lines
5.0 KiB
TypeScript

// ═══ 중국어선 선단(Fleet) 탐지 — GC-KCG-2026-001 기반 ═══
import type { Ship } from '../types';
import { getMarineTrafficCategory } from './marineTraffic';
export type FleetRole = 'mothership' | 'subsidiary' | 'carrier' | 'lighting' | 'pair';
export interface FleetMember {
ship: Ship;
role: FleetRole;
roleKo: string;
distanceNm: number;
reason: string;
}
export interface FleetConnection {
selectedShip: Ship;
members: FleetMember[];
fleetType: 'trawl_pair' | 'purse_seine_fleet' | 'transship' | 'unknown';
fleetTypeKo: string;
}
/** 두 지점 사이 거리(NM) */
function distNm(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 3440.065; // 지구 반경 (해리)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
/**
* 선택한 중국어선 주변의 선단 구성을 탐지
*
* 보고서 기준:
* - PT 2척식 저인망: 본선+부속선 3NM 이내, 유사 속도(2~5kn), 유사 방향
* - PS 위망 선단: 3척+ 클러스터, 모선+운반선+조명선
* - FC 운반선 환적: 0.5NM 이내 접근, 양쪽 2kn 이하
*/
export function detectFleet(selectedShip: Ship, allShips: Ship[]): FleetConnection | null {
if (selectedShip.flag !== 'CN') return null;
const _mtCat = getMarineTrafficCategory(selectedShip.typecode, selectedShip.category);
const members: FleetMember[] = [];
// 주변 중국 선박 탐색 (10NM 반경)
const nearby = allShips.filter(s =>
s.mmsi !== selectedShip.mmsi &&
s.flag === 'CN' &&
distNm(selectedShip.lat, selectedShip.lng, s.lat, s.lng) < 10
);
for (const s of nearby) {
const d = distNm(selectedShip.lat, selectedShip.lng, s.lat, s.lng);
const sCat = getMarineTrafficCategory(s.typecode, s.category);
const speedDiff = Math.abs(selectedShip.speed - s.speed);
let headingDiff = Math.abs(selectedShip.heading - s.heading);
if (headingDiff > 180) headingDiff = 360 - headingDiff;
// === PT 본선-부속선 쌍 탐지 ===
// 3NM 이내 + 유사 속도(차이 1kn 미만) + 유사 방향(20° 미만) + 둘 다 2~5kn
if (d < 3 && speedDiff < 1 && headingDiff < 20 &&
selectedShip.speed >= 2 && selectedShip.speed <= 5 &&
s.speed >= 2 && s.speed <= 5) {
members.push({
ship: s,
role: 'pair',
roleKo: '부속선 (PT-S)',
distanceNm: d,
reason: `속도 ${s.speed.toFixed(1)}kn, 방향차 ${headingDiff.toFixed(0)}°, 거리 ${d.toFixed(1)}NM`,
});
continue;
}
// === FC 운반선 환적 탐지 ===
// 0.5NM 이내 + 양쪽 2kn 이하
if (d < 0.5 && selectedShip.speed <= 2 && s.speed <= 2) {
const isCarrier = sCat === 'cargo' || sCat === 'unspecified' || s.name.includes('运') || s.name.includes('冷');
if (isCarrier) {
members.push({
ship: s,
role: 'carrier',
roleKo: '운반선 (FC)',
distanceNm: d,
reason: `환적 의심 — ${d.toFixed(2)}NM, 양쪽 저속`,
});
continue;
}
}
// === PS 선단 멤버 탐지 ===
// 2NM 이내 중국어선 클러스터
if (d < 2 && sCat === 'fishing') {
// 속도 차이로 역할 추정
if (s.speed < 1 && selectedShip.speed > 5) {
members.push({
ship: s,
role: 'lighting',
roleKo: '조명선',
distanceNm: d,
reason: `정지 중 — 집어등 추정`,
});
} else {
members.push({
ship: s,
role: 'subsidiary',
roleKo: '선단 멤버',
distanceNm: d,
reason: `${d.toFixed(1)}NM, ${s.speed.toFixed(1)}kn`,
});
}
continue;
}
// === 일반 근접 중국 선박 (5NM 이내) ===
if (d < 5 && (sCat === 'fishing' || sCat === 'unspecified')) {
members.push({
ship: s,
role: 'subsidiary',
roleKo: '인근 어선',
distanceNm: d,
reason: `${d.toFixed(1)}NM`,
});
}
}
if (members.length === 0) return null;
// 선단 유형 판별
const hasPair = members.some(m => m.role === 'pair');
const hasCarrier = members.some(m => m.role === 'carrier');
const hasLighting = members.some(m => m.role === 'lighting');
let fleetType: FleetConnection['fleetType'] = 'unknown';
let fleetTypeKo = '인근 선박 그룹';
if (hasPair) {
fleetType = 'trawl_pair';
fleetTypeKo = '2척식 저인망 (본선·부속선)';
} else if (hasCarrier) {
fleetType = 'transship';
fleetTypeKo = '환적 의심 (운반선 접근)';
} else if (hasLighting || members.length >= 3) {
fleetType = 'purse_seine_fleet';
fleetTypeKo = '위망 선단 (모선·운반·조명)';
}
// 거리순 정렬, 최대 10개
members.sort((a, b) => a.distanceNm - b.distanceNm);
return {
selectedShip,
members: members.slice(0, 10),
fleetType,
fleetTypeKo,
};
}