gc-wing/apps/web/src/widgets/map3d/hooks/useMapInit.ts
htlee 3acda7432e refactor(map): UI 개선 — 3D 명칭, 수심 줌, 레거시 비활성
- "지구본" → "3D" 명칭 변경, 헤더 우측으로 이동
- 레거시 베이스맵 비활성 (주석처리)
- 수심 minzoom 통일: fill 3, borders 5, major 3
- NavigationControl 통합, 기어 버튼 겹침 수정
- constants.ts 미사용 BATHY_ZOOM_RANGES 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:04:31 +09:00

197 lines
6.1 KiB
TypeScript

import { useCallback, useEffect, useRef, type MutableRefObject, type Dispatch, type SetStateAction } from 'react';
import maplibregl, { type StyleSpecification } from 'maplibre-gl';
import { MapboxOverlay } from '@deck.gl/mapbox';
import { MaplibreDeckCustomLayer } from '../MaplibreDeckCustomLayer';
import type { BaseMapId, MapProjectionId } from '../types';
import { DECK_VIEW_ID } from '../constants';
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';
import { ensureSeamarkOverlay } from '../layers/seamark';
import { resolveMapStyle } from '../layers/bathymetry';
import { clearGlobeNativeLayers } from '../lib/layerHelpers';
export function useMapInit(
containerRef: MutableRefObject<HTMLDivElement | null>,
mapRef: MutableRefObject<maplibregl.Map | null>,
overlayRef: MutableRefObject<MapboxOverlay | null>,
overlayInteractionRef: MutableRefObject<MapboxOverlay | null>,
globeDeckLayerRef: MutableRefObject<MaplibreDeckCustomLayer | null>,
baseMapRef: MutableRefObject<BaseMapId>,
projectionRef: MutableRefObject<MapProjectionId>,
opts: {
baseMap: BaseMapId;
projection: MapProjectionId;
showSeamark: boolean;
onViewBboxChange?: (bbox: [number, number, number, number]) => void;
setMapSyncEpoch: Dispatch<SetStateAction<number>>;
},
) {
const { onViewBboxChange, setMapSyncEpoch, showSeamark } = opts;
const showSeamarkRef = useRef(showSeamark);
useEffect(() => {
showSeamarkRef.current = showSeamark;
}, [showSeamark]);
const ensureMercatorOverlay = useCallback(() => {
const map = mapRef.current;
if (!map) return null;
if (overlayRef.current) return overlayRef.current;
try {
const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
map.addControl(next);
overlayRef.current = next;
return next;
} catch (e) {
console.warn('Deck overlay create failed:', e);
return null;
}
}, []);
const clearGlobeNativeLayersCb = useCallback(() => {
const map = mapRef.current;
if (!map) return;
clearGlobeNativeLayers(map);
}, []);
const pulseMapSync = useCallback(() => {
setMapSyncEpoch((prev) => prev + 1);
requestAnimationFrame(() => {
kickRepaint(mapRef.current);
setMapSyncEpoch((prev) => prev + 1);
});
}, [setMapSyncEpoch]);
useEffect(() => {
if (!containerRef.current || mapRef.current) return;
let map: maplibregl.Map | null = null;
let cancelled = false;
const controller = new AbortController();
(async () => {
let style: string | StyleSpecification = '/map/styles/osm-seamark.json';
try {
style = await resolveMapStyle(baseMapRef.current, controller.signal);
} catch (e) {
console.warn('Map style init failed, falling back to local raster style:', e);
style = '/map/styles/osm-seamark.json';
}
if (cancelled || !containerRef.current) return;
map = new maplibregl.Map({
container: containerRef.current,
style,
center: [126.5, 34.2],
zoom: 7,
pitch: 45,
bearing: 0,
maxPitch: 85,
dragRotate: true,
pitchWithRotate: true,
touchPitch: true,
scrollZoom: { around: 'center' },
});
map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: true }), 'top-left');
map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: 'metric' }), 'bottom-left');
mapRef.current = map;
if (projectionRef.current === 'mercator') {
const overlay = ensureMercatorOverlay();
if (!overlay) return;
overlayRef.current = overlay;
} else {
globeDeckLayerRef.current = new MaplibreDeckCustomLayer({
id: 'deck-globe',
viewId: DECK_VIEW_ID,
deckProps: { layers: [] },
});
}
function applyProjection() {
if (!map) return;
const next = projectionRef.current;
if (next === 'mercator') return;
try {
map.setProjection({ type: next });
map.setRenderWorldCopies(next !== 'globe');
} catch (e) {
console.warn('Projection apply failed:', e);
}
}
onMapStyleReady(map, () => {
applyProjection();
const deckLayer = globeDeckLayerRef.current;
if (projectionRef.current === 'globe' && deckLayer && !map!.getLayer(deckLayer.id)) {
try {
map!.addLayer(deckLayer);
} catch {
// ignore
}
}
if (!showSeamarkRef.current) return;
try {
ensureSeamarkOverlay(map!, 'bathymetry-lines');
} catch {
// ignore
}
});
const emitBbox = () => {
const cb = onViewBboxChange;
if (!cb || !map) return;
const b = map.getBounds();
cb([b.getWest(), b.getSouth(), b.getEast(), b.getNorth()]);
};
map.on('load', emitBbox);
map.on('moveend', emitBbox);
map.once('load', () => {
if (showSeamarkRef.current) {
try {
ensureSeamarkOverlay(map!, 'bathymetry-lines');
} catch {
// ignore
}
try {
const opacity = showSeamarkRef.current ? 0.85 : 0;
map!.setPaintProperty('seamark', 'raster-opacity', opacity);
} catch {
// ignore
}
}
});
})();
return () => {
cancelled = true;
controller.abort();
try {
globeDeckLayerRef.current?.requestFinalize();
} catch {
// ignore
}
if (map) {
map.remove();
map = null;
}
if (overlayRef.current) {
overlayRef.current.finalize();
overlayRef.current = null;
}
if (overlayInteractionRef.current) {
overlayInteractionRef.current.finalize();
overlayInteractionRef.current = null;
}
globeDeckLayerRef.current = null;
mapRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { ensureMercatorOverlay, clearGlobeNativeLayers: clearGlobeNativeLayersCb, pulseMapSync };
}