ship-gis/src/hooks/useRadiusFilter.js
HeungTak Lee 83f5f72b0e feat: 추적 모드 반경 필터링 구현
- useTrackingMode 훅 (함정 중심 지도 이동 + 반경 원)
- useRadiusFilter 훅 (Bounding Box + Haversine 거리 계산)
- shipStore 반경 필터 연동

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 06:37:38 +09:00

168 lines
4.9 KiB
JavaScript

/**
* 반경 필터링 훅
* 선박 모드일 때 추적 함정 중심으로 반경 내 선박만 필터링
*
* 성능 최적화:
* - 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);
}