release: Phase 3 완료 (React 19 + MapLibre GL JS 전환) #2
@ -1,13 +1,24 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import Feature from 'ol/Feature';
|
||||
import Polygon from 'ol/geom/Polygon';
|
||||
import type { Geometry } from 'ol/geom';
|
||||
import { Style, Fill, Stroke, Text } from 'ol/style';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import type { Feature, FeatureCollection, Polygon } from 'geojson';
|
||||
import useFavoriteStore from '../stores/favoriteStore';
|
||||
import { useMapStore } from '../stores/mapStore';
|
||||
|
||||
/** MapLibre source/layer ID */
|
||||
const REALM_SOURCE_ID = 'realm-source';
|
||||
const REALM_FILL_LAYER_ID = 'realm-fill-layer';
|
||||
const REALM_LINE_SOLID_LAYER_ID = 'realm-line-solid-layer';
|
||||
const REALM_LINE_DOTTED_LAYER_ID = 'realm-line-dotted-layer';
|
||||
const REALM_LABEL_LAYER_ID = 'realm-label-layer';
|
||||
|
||||
const ALL_REALM_LAYER_IDS = [
|
||||
REALM_LABEL_LAYER_ID,
|
||||
REALM_LINE_DOTTED_LAYER_ID,
|
||||
REALM_LINE_SOLID_LAYER_ID,
|
||||
REALM_FILL_LAYER_ID,
|
||||
] as const;
|
||||
|
||||
/** 관심구역 데이터 (API 응답 형태) */
|
||||
interface RealmData {
|
||||
coordinates: number[] | number[][] | number[][][];
|
||||
@ -23,144 +34,232 @@ interface RealmData {
|
||||
seaRelmId?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관심구역 OpenLayers 레이어 관리 훅
|
||||
* 참조: mda-react-front/src/services/commonService.ts - getRealmLayer()
|
||||
*/
|
||||
export default function useRealmLayer(): void {
|
||||
const map = useMapStore((s) => s.map);
|
||||
const layerRef = useRef<VectorLayer<Feature<Geometry>> | null>(null);
|
||||
const sourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
// MapLibre 전환 후 OL VectorLayer는 호환 불가 — Session E에서 MapLibre 네이티브 레이어로 마이그레이션
|
||||
if (typeof map.addControl === 'function' && typeof map.getCanvas === 'function') {
|
||||
console.warn('[useRealmLayer] MapLibre 맵 감지 — OL 관심구역 레이어 비활성화 (Session E에서 마이그레이션)');
|
||||
return;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 50,
|
||||
});
|
||||
|
||||
map.addLayer(layer);
|
||||
layerRef.current = layer;
|
||||
sourceRef.current = source;
|
||||
|
||||
// 초기 데이터 렌더링
|
||||
const { realmList, isRealmVisible } = useFavoriteStore.getState();
|
||||
console.log(`[useRealmLayer] 초기화: realmList=${realmList.length}건, visible=${isRealmVisible}`);
|
||||
layer.setVisible(isRealmVisible);
|
||||
if (realmList.length > 0) {
|
||||
renderRealms(source, realmList as unknown as RealmData[]);
|
||||
}
|
||||
|
||||
// realmList 변경 구독
|
||||
const unsubRealmList = useFavoriteStore.subscribe(
|
||||
(state) => state.realmList,
|
||||
(newRealmList) => {
|
||||
console.log(`[useRealmLayer] realmList 변경 감지: ${newRealmList.length}건`);
|
||||
if (newRealmList.length > 0) {
|
||||
console.log('[useRealmLayer] 첫 번째 realm 샘플:', JSON.stringify(newRealmList[0]).slice(0, 300));
|
||||
}
|
||||
renderRealms(source, newRealmList as unknown as RealmData[]);
|
||||
}
|
||||
);
|
||||
|
||||
// isRealmVisible 변경 구독
|
||||
const unsubVisible = useFavoriteStore.subscribe(
|
||||
(state) => state.isRealmVisible,
|
||||
(isVisible: boolean) => {
|
||||
console.log(`[useRealmLayer] visible 토글: ${isVisible}, layer=${!!layerRef.current}, features=${sourceRef.current?.getFeatures()?.length || 0}`);
|
||||
if (layerRef.current) {
|
||||
layerRef.current.setVisible(isVisible);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubRealmList();
|
||||
unsubVisible();
|
||||
if (map && layerRef.current) {
|
||||
map.removeLayer(layerRef.current);
|
||||
}
|
||||
layerRef.current = null;
|
||||
sourceRef.current = null;
|
||||
};
|
||||
}, [map]);
|
||||
}
|
||||
/** 빈 FeatureCollection */
|
||||
const EMPTY_FC: FeatureCollection<Polygon> = { type: 'FeatureCollection', features: [] };
|
||||
|
||||
/**
|
||||
* 좌표 배열 정규화
|
||||
* API 응답 형식에 관계없이 Polygon 생성에 적합한 형식으로 변환
|
||||
* @param {Array} coordinates - 좌표 데이터
|
||||
* @returns {Array} Polygon rings 배열 [[lon,lat], ...]
|
||||
* API 응답 형식에 관계없이 Polygon ring에 적합한 형식으로 변환
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function normalizeCoordinates(coordinates: any): number[][] | null {
|
||||
if (!Array.isArray(coordinates) || coordinates.length === 0) return null;
|
||||
|
||||
// coordinates[0]이 숫자 배열이면 → 이미 ring 형태: [[lon,lat], ...]
|
||||
if (Array.isArray(coordinates[0]) && typeof coordinates[0][0] === 'number') {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
// coordinates[0][0]이 숫자 배열이면 → 이미 rings 형태: [[[lon,lat], ...]]
|
||||
if (Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0]) && typeof coordinates[0][0][0] === 'number') {
|
||||
return coordinates[0]; // 첫 번째 ring만 사용
|
||||
return coordinates[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관심구역 Feature 렌더링
|
||||
* @param {VectorSource} source - OL VectorSource
|
||||
* @param {Array} realmList - 관심구역 데이터 배열
|
||||
* RealmData 배열 → GeoJSON FeatureCollection 변환
|
||||
* properties에 스타일 정보를 포함하여 data-driven expression으로 개별 스타일링
|
||||
*/
|
||||
function renderRealms(source: VectorSource, realmList: RealmData[]): void {
|
||||
source.clear();
|
||||
function buildRealmGeoJSON(realmList: RealmData[]): FeatureCollection<Polygon> {
|
||||
if (!realmList || realmList.length === 0) return EMPTY_FC;
|
||||
|
||||
if (!realmList || realmList.length === 0) return;
|
||||
const features: Feature<Polygon>[] = [];
|
||||
|
||||
let count = 0;
|
||||
realmList.forEach((realm) => {
|
||||
const ring = normalizeCoordinates(realm.coordinates);
|
||||
if (!ring) return;
|
||||
if (!ring || ring.length < 3) return;
|
||||
|
||||
try {
|
||||
const polygon = new Polygon([ring]).transform('EPSG:4326', 'EPSG:3857');
|
||||
const feature = new Feature({ geometry: polygon });
|
||||
|
||||
feature.setStyle(
|
||||
new Style({
|
||||
text: new Text({
|
||||
text: realm.seaRelmNameYn === 'Y' ? (realm.seaRelmName || '') : '',
|
||||
fill: new Fill({ color: realm.fontColor || '#000000' }),
|
||||
font: `${realm.fontSize || 12}px ${realm.fontKind || 'sans-serif'}`,
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: realm.outlineColor || '#333333',
|
||||
width: Number(realm.outlineWidth) || 2,
|
||||
lineDash: realm.outlineType === 'dot' ? [15, 15] : undefined,
|
||||
}),
|
||||
fill: new Fill({ color: realm.fillColor || 'rgba(0,0,0,0.1)' }),
|
||||
})
|
||||
);
|
||||
|
||||
if (realm.seaRelmId) {
|
||||
feature.setId(realm.seaRelmId);
|
||||
// GeoJSON Polygon ring은 닫혀야 함 (first == last)
|
||||
const coords = [...ring];
|
||||
const first = coords[0];
|
||||
const last = coords[coords.length - 1];
|
||||
if (first[0] !== last[0] || first[1] !== last[1]) {
|
||||
coords.push([first[0], first[1]]);
|
||||
}
|
||||
source.addFeature(feature);
|
||||
count++;
|
||||
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
id: realm.seaRelmId != null ? Number(realm.seaRelmId) : undefined,
|
||||
properties: {
|
||||
fillColor: realm.fillColor || 'rgba(0,0,0,0.1)',
|
||||
outlineColor: realm.outlineColor || '#333333',
|
||||
outlineWidth: Number(realm.outlineWidth) || 2,
|
||||
isDotted: realm.outlineType === 'dot',
|
||||
showLabel: realm.seaRelmNameYn === 'Y',
|
||||
labelText: realm.seaRelmName || '',
|
||||
fontColor: realm.fontColor || '#000000',
|
||||
fontSize: realm.fontSize || 12,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [coords],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[useRealmLayer] Feature 생성 실패:', realm.seaRelmName, err);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[useRealmLayer] ${count}/${realmList.length}건 렌더링 완료`);
|
||||
return { type: 'FeatureCollection', features };
|
||||
}
|
||||
|
||||
/**
|
||||
* Realm source/layer를 맵에 추가 (멱등)
|
||||
*/
|
||||
function ensureRealmLayers(map: maplibregl.Map): void {
|
||||
if (!map.getSource(REALM_SOURCE_ID)) {
|
||||
map.addSource(REALM_SOURCE_ID, {
|
||||
type: 'geojson',
|
||||
data: EMPTY_FC,
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getLayer(REALM_FILL_LAYER_ID)) {
|
||||
map.addLayer({
|
||||
id: REALM_FILL_LAYER_ID,
|
||||
type: 'fill',
|
||||
source: REALM_SOURCE_ID,
|
||||
paint: {
|
||||
'fill-color': ['get', 'fillColor'],
|
||||
'fill-opacity': 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 실선 레이어 (line-dasharray가 data-driven 미지원이므로 실선/점선 분리)
|
||||
if (!map.getLayer(REALM_LINE_SOLID_LAYER_ID)) {
|
||||
map.addLayer({
|
||||
id: REALM_LINE_SOLID_LAYER_ID,
|
||||
type: 'line',
|
||||
source: REALM_SOURCE_ID,
|
||||
filter: ['!=', ['get', 'isDotted'], true],
|
||||
paint: {
|
||||
'line-color': ['get', 'outlineColor'],
|
||||
'line-width': ['get', 'outlineWidth'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 점선 레이어
|
||||
if (!map.getLayer(REALM_LINE_DOTTED_LAYER_ID)) {
|
||||
map.addLayer({
|
||||
id: REALM_LINE_DOTTED_LAYER_ID,
|
||||
type: 'line',
|
||||
source: REALM_SOURCE_ID,
|
||||
filter: ['==', ['get', 'isDotted'], true],
|
||||
paint: {
|
||||
'line-color': ['get', 'outlineColor'],
|
||||
'line-width': ['get', 'outlineWidth'],
|
||||
'line-dasharray': [4, 4],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 라벨 레이어
|
||||
if (!map.getLayer(REALM_LABEL_LAYER_ID)) {
|
||||
map.addLayer({
|
||||
id: REALM_LABEL_LAYER_ID,
|
||||
type: 'symbol',
|
||||
source: REALM_SOURCE_ID,
|
||||
filter: ['==', ['get', 'showLabel'], true],
|
||||
layout: {
|
||||
'text-field': ['get', 'labelText'],
|
||||
'text-size': ['get', 'fontSize'],
|
||||
'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
|
||||
'text-allow-overlap': true,
|
||||
},
|
||||
paint: {
|
||||
'text-color': ['get', 'fontColor'],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관심구역 MapLibre 레이어 관리 훅
|
||||
* - GeoJSON source + fill/line/symbol layer로 폴리곤 및 라벨 표시
|
||||
* - favoriteStore 구독으로 realmList/visibility 변경 반영
|
||||
* - style.load 이벤트로 배경지도 전환 후 레이어 복구
|
||||
*/
|
||||
export default function useRealmLayer(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 마이그레이션 기간 mapStore.map: any
|
||||
const map = useMapStore((s) => s.map);
|
||||
const currentDataRef = useRef<FeatureCollection<Polygon>>(EMPTY_FC);
|
||||
const isVisibleRef = useRef<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
if (typeof map.getCanvas !== 'function') return;
|
||||
|
||||
const mlMap = map as maplibregl.Map;
|
||||
|
||||
/** 현재 상태를 맵에 적용 (초기화 + style.load 복구 공용) */
|
||||
const applyState = () => {
|
||||
ensureRealmLayers(mlMap);
|
||||
|
||||
const source = mlMap.getSource(REALM_SOURCE_ID) as GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(currentDataRef.current);
|
||||
}
|
||||
|
||||
const visibility = isVisibleRef.current ? 'visible' : 'none';
|
||||
ALL_REALM_LAYER_IDS.forEach((layerId) => {
|
||||
if (mlMap.getLayer(layerId)) {
|
||||
mlMap.setLayoutProperty(layerId, 'visibility', visibility as 'visible' | 'none');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 배경지도 전환(setStyle) 후 레이어 복구
|
||||
mlMap.on('style.load', applyState);
|
||||
|
||||
// 초기 데이터 로드
|
||||
const { realmList, isRealmVisible } = useFavoriteStore.getState();
|
||||
isVisibleRef.current = isRealmVisible;
|
||||
if (realmList.length > 0) {
|
||||
currentDataRef.current = buildRealmGeoJSON(realmList as unknown as RealmData[]);
|
||||
}
|
||||
|
||||
if (mlMap.isStyleLoaded()) {
|
||||
applyState();
|
||||
}
|
||||
|
||||
// realmList 변경 구독
|
||||
const unsubRealmList = useFavoriteStore.subscribe(
|
||||
(state) => state.realmList,
|
||||
(newRealmList) => {
|
||||
currentDataRef.current = buildRealmGeoJSON(newRealmList as unknown as RealmData[]);
|
||||
const source = mlMap.getSource(REALM_SOURCE_ID) as GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(currentDataRef.current);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// isRealmVisible 변경 구독
|
||||
const unsubVisible = useFavoriteStore.subscribe(
|
||||
(state) => state.isRealmVisible,
|
||||
(isVisible: boolean) => {
|
||||
isVisibleRef.current = isVisible;
|
||||
const visibility = isVisible ? 'visible' : 'none';
|
||||
ALL_REALM_LAYER_IDS.forEach((layerId) => {
|
||||
if (mlMap.getLayer(layerId)) {
|
||||
mlMap.setLayoutProperty(layerId, 'visibility', visibility as 'visible' | 'none');
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubRealmList();
|
||||
unsubVisible();
|
||||
mlMap.off('style.load', applyState);
|
||||
|
||||
ALL_REALM_LAYER_IDS.forEach((layerId) => {
|
||||
if (mlMap.getLayer(layerId)) mlMap.removeLayer(layerId);
|
||||
});
|
||||
if (mlMap.getSource(REALM_SOURCE_ID)) mlMap.removeSource(REALM_SOURCE_ID);
|
||||
};
|
||||
}, [map]);
|
||||
}
|
||||
|
||||
@ -1,14 +1,34 @@
|
||||
/**
|
||||
* 추적 모드 훅
|
||||
* - 선박 모드일 때 추적 함정 중심으로 지도 이동
|
||||
* - 반경 원 레이어 생성 및 업데이트 (OL → MapLibre 마이그레이션 Session E에서 구현)
|
||||
* - 반경 원 레이어: MapLibre GeoJSON source + fill/line layer (@turf/circle)
|
||||
*/
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import * as turf from '@turf/turf';
|
||||
import type { Feature, FeatureCollection, Polygon } from 'geojson';
|
||||
import { useMapStore } from '../stores/mapStore';
|
||||
import useShipStore from '../stores/shipStore';
|
||||
import useTrackingModeStore from '../stores/trackingModeStore';
|
||||
import type { ShipFeature } from '../types/ship';
|
||||
|
||||
/** MapLibre source/layer ID */
|
||||
const RADIUS_SOURCE_ID = 'tracking-radius-source';
|
||||
const RADIUS_FILL_LAYER_ID = 'tracking-radius-fill-layer';
|
||||
const RADIUS_LINE_LAYER_ID = 'tracking-radius-line-layer';
|
||||
|
||||
const ALL_RADIUS_LAYER_IDS = [RADIUS_LINE_LAYER_ID, RADIUS_FILL_LAYER_ID] as const;
|
||||
|
||||
/** NM → km 변환 */
|
||||
const NM_TO_KM = 1.852;
|
||||
|
||||
/** 빈 FeatureCollection */
|
||||
const EMPTY_FC: FeatureCollection<Polygon> = { type: 'FeatureCollection', features: [] };
|
||||
|
||||
/** 반경 원 색상 (shipLayer 추적 선박 마커 색상과 통일: [0, 212, 255]) */
|
||||
const RADIUS_COLOR = '#00D4FF';
|
||||
|
||||
/** useTrackingMode 반환 타입 */
|
||||
interface UseTrackingModeReturn {
|
||||
isTrackingActive: boolean;
|
||||
@ -16,11 +36,58 @@ interface UseTrackingModeReturn {
|
||||
radiusNM: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 반경 원 GeoJSON 생성
|
||||
*/
|
||||
function buildRadiusGeoJSON(lon: number, lat: number, radiusNM: number): Feature<Polygon> {
|
||||
const radiusKm = radiusNM * NM_TO_KM;
|
||||
return turf.circle([lon, lat], radiusKm, { steps: 64, units: 'kilometers' }) as Feature<Polygon>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 반경 원 source/layer를 맵에 추가 (멱등)
|
||||
*/
|
||||
function ensureRadiusLayers(map: maplibregl.Map): void {
|
||||
if (!map.getSource(RADIUS_SOURCE_ID)) {
|
||||
map.addSource(RADIUS_SOURCE_ID, {
|
||||
type: 'geojson',
|
||||
data: EMPTY_FC,
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getLayer(RADIUS_FILL_LAYER_ID)) {
|
||||
map.addLayer({
|
||||
id: RADIUS_FILL_LAYER_ID,
|
||||
type: 'fill',
|
||||
source: RADIUS_SOURCE_ID,
|
||||
paint: {
|
||||
'fill-color': RADIUS_COLOR,
|
||||
'fill-opacity': 0.05,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!map.getLayer(RADIUS_LINE_LAYER_ID)) {
|
||||
map.addLayer({
|
||||
id: RADIUS_LINE_LAYER_ID,
|
||||
type: 'line',
|
||||
source: RADIUS_SOURCE_ID,
|
||||
paint: {
|
||||
'line-color': RADIUS_COLOR,
|
||||
'line-width': 1.5,
|
||||
'line-opacity': 0.6,
|
||||
'line-dasharray': [4, 2],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 추적 모드 훅
|
||||
* MapContainer에서 호출
|
||||
*/
|
||||
export default function useTrackingMode(): UseTrackingModeReturn {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 마이그레이션 기간 mapStore.map: any
|
||||
const map = useMapStore((s) => s.map);
|
||||
const features = useShipStore((s) => s.features);
|
||||
|
||||
@ -33,7 +100,8 @@ export default function useTrackingMode(): UseTrackingModeReturn {
|
||||
// 이전 좌표 (중복 업데이트 방지)
|
||||
const prevCoordsRef = useRef<string | null>(null);
|
||||
|
||||
// TODO: Session E — 반경 원 레이어를 MapLibre 네이티브 GeoJSON source + circle-layer로 구현
|
||||
// 반경 원 데이터 (style.load 복구용)
|
||||
const radiusDataRef = useRef<FeatureCollection<Polygon>>(EMPTY_FC);
|
||||
|
||||
/**
|
||||
* 지도 중심 이동 (MapLibre API)
|
||||
@ -57,11 +125,9 @@ export default function useTrackingMode(): UseTrackingModeReturn {
|
||||
useEffect(() => {
|
||||
if (mode !== 'ship' || !trackedShipId) return;
|
||||
|
||||
// features에서 추적 함정의 최신 데이터 가져오기
|
||||
const latestShip = features.get(trackedShipId);
|
||||
if (!latestShip) return;
|
||||
|
||||
// 좌표가 변경되었는지 확인
|
||||
const newCoords = `${latestShip.longitude},${latestShip.latitude}`;
|
||||
if (prevCoordsRef.current === newCoords) return;
|
||||
|
||||
@ -79,10 +145,74 @@ export default function useTrackingMode(): UseTrackingModeReturn {
|
||||
const { longitude, latitude } = trackedShip;
|
||||
if (!longitude || !latitude) return;
|
||||
|
||||
// 지도 중심 이동
|
||||
centerMapOnShip(longitude, latitude);
|
||||
}, [mode, trackedShip, map, centerMapOnShip]);
|
||||
|
||||
// 반경 원 레이어 관리
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
if (typeof map.getCanvas !== 'function') return;
|
||||
|
||||
const mlMap = map as maplibregl.Map;
|
||||
|
||||
/** 반경 원 상태를 맵에 적용 */
|
||||
const applyState = () => {
|
||||
ensureRadiusLayers(mlMap);
|
||||
const source = mlMap.getSource(RADIUS_SOURCE_ID) as GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(radiusDataRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
/** 추적 상태에 따라 반경 원 갱신 */
|
||||
const updateRadiusCircle = () => {
|
||||
const { mode: currentMode, trackedShip: currentShip, radiusNM: currentRadius } =
|
||||
useTrackingModeStore.getState();
|
||||
|
||||
if (currentMode !== 'ship' || !currentShip?.longitude || !currentShip?.latitude) {
|
||||
radiusDataRef.current = EMPTY_FC;
|
||||
} else {
|
||||
const circle = buildRadiusGeoJSON(currentShip.longitude, currentShip.latitude, currentRadius);
|
||||
radiusDataRef.current = { type: 'FeatureCollection', features: [circle] };
|
||||
}
|
||||
|
||||
const source = mlMap.getSource(RADIUS_SOURCE_ID) as GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(radiusDataRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
// 배경지도 전환(setStyle) 후 레이어 복구
|
||||
mlMap.on('style.load', applyState);
|
||||
|
||||
// 초기 설정
|
||||
if (mlMap.isStyleLoaded()) {
|
||||
ensureRadiusLayers(mlMap);
|
||||
}
|
||||
|
||||
// trackingModeStore 구독: mode, trackedShip, radiusNM 변경 시 원 갱신
|
||||
const unsubTracking = useTrackingModeStore.subscribe(
|
||||
(state) => [state.mode, state.trackedShip, state.radiusNM] as const,
|
||||
() => { updateRadiusCircle(); },
|
||||
{
|
||||
equalityFn: (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2],
|
||||
},
|
||||
);
|
||||
|
||||
// 초기 상태 반영
|
||||
updateRadiusCircle();
|
||||
|
||||
return () => {
|
||||
unsubTracking();
|
||||
mlMap.off('style.load', applyState);
|
||||
|
||||
ALL_RADIUS_LAYER_IDS.forEach((layerId) => {
|
||||
if (mlMap.getLayer(layerId)) mlMap.removeLayer(layerId);
|
||||
});
|
||||
if (mlMap.getSource(RADIUS_SOURCE_ID)) mlMap.removeSource(RADIUS_SOURCE_ID);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
return {
|
||||
isTrackingActive: mode === 'ship' && trackedShip !== null,
|
||||
trackedShip,
|
||||
|
||||
@ -99,7 +99,7 @@ export default function MapContainer() {
|
||||
// 측정 도구 (MapLibre 전환 후 별도 세션에서 수정)
|
||||
useMeasure();
|
||||
|
||||
// 추적 모드 (MapLibre 전환 후 별도 세션에서 수정)
|
||||
// 추적 모드 (지도 추적 + 반경 원 레이어)
|
||||
useTrackingMode();
|
||||
|
||||
// 배경지도 타입 변경 시 스타일 교체
|
||||
|
||||
@ -11,6 +11,7 @@ type BaseMapType = 'normal' | 'enc' | 'dark';
|
||||
|
||||
const OSM_RASTER_STYLE: StyleSpecification = {
|
||||
version: 8,
|
||||
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
||||
sources: {
|
||||
osm: {
|
||||
type: 'raster',
|
||||
@ -25,6 +26,7 @@ const OSM_RASTER_STYLE: StyleSpecification = {
|
||||
|
||||
const DARK_RASTER_STYLE: StyleSpecification = {
|
||||
version: 8,
|
||||
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
||||
sources: {
|
||||
'carto-dark': {
|
||||
type: 'raster',
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user