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, mapRef: MutableRefObject, overlayRef: MutableRefObject, overlayInteractionRef: MutableRefObject, globeDeckLayerRef: MutableRefObject, baseMapRef: MutableRefObject, projectionRef: MutableRefObject, opts: { baseMap: BaseMapId; projection: MapProjectionId; showSeamark: boolean; onViewBboxChange?: (bbox: [number, number, number, number]) => void; setMapSyncEpoch: Dispatch>; }, ) { 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 }; }