/** * 반경 필터링 훅 * 선박 모드일 때 추적 함정 중심으로 반경 내 선박만 필터링 * * 성능 최적화: * - Bounding Box 사전 필터링 (빠른 사각형 체크) * - 그 후 정확한 원형 거리 계산 */ import { useCallback, useMemo } from 'react'; import useTrackingModeStore, { isWithinRadius, NM_TO_METERS, } from '../stores/trackingModeStore'; /** * 경도 1도당 대략적인 미터 (위도에 따라 다름) * 중위도(35도) 기준 약 91km */ const LON_DEGREE_METERS = 91000; const LAT_DEGREE_METERS = 111000; // 위도 1도당 약 111km /** * 반경 필터링 훅 * @returns {Object} { filterByRadius, isRadiusFilterActive, getRadiusCenter, radiusNM } */ export default function useRadiusFilter() { const mode = useTrackingModeStore((s) => s.mode); const trackedShip = useTrackingModeStore((s) => s.trackedShip); const radiusNM = useTrackingModeStore((s) => s.radiusNM); // 반경 필터 활성화 여부 const isRadiusFilterActive = mode === 'ship' && trackedShip !== null; // 반경 중심 좌표 const radiusCenter = useMemo(() => { if (!isRadiusFilterActive || !trackedShip) return null; return { lon: trackedShip.longitude, lat: trackedShip.latitude, }; }, [isRadiusFilterActive, trackedShip]); /** * Bounding Box 계산 (사전 필터링용) * 반경을 감싸는 사각형 영역 */ const boundingBox = useMemo(() => { if (!radiusCenter) return null; const radiusMeters = radiusNM * NM_TO_METERS; const lonDelta = radiusMeters / LON_DEGREE_METERS; const latDelta = radiusMeters / LAT_DEGREE_METERS; return { minLon: radiusCenter.lon - lonDelta, maxLon: radiusCenter.lon + lonDelta, minLat: radiusCenter.lat - latDelta, maxLat: radiusCenter.lat + latDelta, }; }, [radiusCenter, radiusNM]); /** * 선박이 Bounding Box 내에 있는지 빠른 체크 */ const isInBoundingBox = useCallback((ship) => { if (!boundingBox) return true; if (!ship.longitude || !ship.latitude) return false; return ( ship.longitude >= boundingBox.minLon && ship.longitude <= boundingBox.maxLon && ship.latitude >= boundingBox.minLat && ship.latitude <= boundingBox.maxLat ); }, [boundingBox]); /** * 선박 배열을 반경 내 선박만 필터링 * @param {Array} ships - 선박 배열 * @returns {Array} 반경 내 선박만 */ const filterByRadius = useCallback((ships) => { // 반경 필터 비활성화 시 전체 반환 if (!isRadiusFilterActive || !radiusCenter) { return ships; } return ships.filter((ship) => { // 1단계: Bounding Box 체크 (빠른 사전 필터) if (!isInBoundingBox(ship)) return false; // 2단계: 정확한 원형 거리 체크 return isWithinRadius(ship, radiusCenter.lon, radiusCenter.lat, radiusNM); }); }, [isRadiusFilterActive, radiusCenter, radiusNM, isInBoundingBox]); /** * 단일 선박이 반경 내에 있는지 확인 * @param {Object} ship * @returns {boolean} */ const isShipInRadius = useCallback((ship) => { if (!isRadiusFilterActive || !radiusCenter) return true; if (!isInBoundingBox(ship)) return false; return isWithinRadius(ship, radiusCenter.lon, radiusCenter.lat, radiusNM); }, [isRadiusFilterActive, radiusCenter, radiusNM, isInBoundingBox]); /** * Map의 features를 반경 내 선박만 필터링 * @param {Map} featuresMap - featureId -> ship Map * @returns {Map} 반경 내 선박만 */ const filterFeaturesMapByRadius = useCallback((featuresMap) => { if (!isRadiusFilterActive || !radiusCenter) { return featuresMap; } const filteredMap = new Map(); featuresMap.forEach((ship, featureId) => { if (isInBoundingBox(ship) && isWithinRadius(ship, radiusCenter.lon, radiusCenter.lat, radiusNM)) { filteredMap.set(featureId, ship); } }); return filteredMap; }, [isRadiusFilterActive, radiusCenter, radiusNM, isInBoundingBox]); return { filterByRadius, filterFeaturesMapByRadius, isShipInRadius, isRadiusFilterActive, radiusCenter, radiusNM, boundingBox, }; } /** * 반경 필터 유틸리티 (비훅 버전) * shipStore나 다른 스토어에서 직접 사용 */ export function getRadiusFilterState() { const state = useTrackingModeStore.getState(); const { mode, trackedShip, radiusNM } = state; const isActive = mode === 'ship' && trackedShip !== null; if (!isActive || !trackedShip) { return { isActive: false, center: null, radiusNM: 0 }; } return { isActive: true, center: { lon: trackedShip.longitude, lat: trackedShip.latitude }, radiusNM, }; } /** * 선박이 반경 내에 있는지 확인 (비훅 버전) */ export function checkShipInRadius(ship) { const { isActive, center, radiusNM } = getRadiusFilterState(); if (!isActive || !center) return true; return isWithinRadius(ship, center.lon, center.lat, radiusNM); }