import { useEffect, useRef, type MutableRefObject } from 'react'; import type maplibregl from 'maplibre-gl'; import type { BaseMapId, MapProjectionId } from '../types'; import { kickRepaint, onMapStyleReady, getLayerId } from '../lib/mapCore'; import { ensureSeamarkOverlay } from '../layers/seamark'; import { applyBathymetryZoomProfile, resolveMapStyle } from '../layers/bathymetry'; export function useBaseMapToggle( mapRef: MutableRefObject, opts: { baseMap: BaseMapId; projection: MapProjectionId; showSeamark: boolean; mapSyncEpoch: number; pulseMapSync: () => void; }, ) { const { baseMap, projection, showSeamark, mapSyncEpoch, pulseMapSync } = opts; const showSeamarkRef = useRef(showSeamark); const bathyZoomProfileKeyRef = useRef(''); useEffect(() => { showSeamarkRef.current = showSeamark; }, [showSeamark]); // Base map style toggle useEffect(() => { const map = mapRef.current; if (!map) return; let cancelled = false; const controller = new AbortController(); let stop: (() => void) | null = null; (async () => { try { const style = await resolveMapStyle(baseMap, controller.signal); if (cancelled) return; map.setStyle(style, { diff: false }); stop = onMapStyleReady(map, () => { kickRepaint(map); requestAnimationFrame(() => kickRepaint(map)); pulseMapSync(); }); } catch (e) { if (cancelled) return; console.warn('Base map switch failed:', e); } })(); return () => { cancelled = true; controller.abort(); stop?.(); }; }, [baseMap]); // Bathymetry zoom profile + water layer visibility useEffect(() => { const map = mapRef.current; if (!map) return; const apply = () => { if (!map.isStyleLoaded()) return; const seaVisibility = 'visible' as const; const seaRegex = /(water|sea|ocean|river|lake|coast|bay)/i; const nextProfileKey = `bathyZoomV1|${baseMap}|${projection}`; if (bathyZoomProfileKeyRef.current !== nextProfileKey) { applyBathymetryZoomProfile(map, baseMap, projection); bathyZoomProfileKeyRef.current = nextProfileKey; kickRepaint(map); } try { const style = map.getStyle(); const styleLayers = style && Array.isArray(style.layers) ? style.layers : []; for (const layer of styleLayers) { const id = getLayerId(layer); if (!id) continue; const sourceLayer = String((layer as Record)['source-layer'] ?? '').toLowerCase(); const source = String((layer as { source?: unknown }).source ?? '').toLowerCase(); const type = String((layer as { type?: unknown }).type ?? '').toLowerCase(); const isSea = seaRegex.test(id) || seaRegex.test(sourceLayer) || seaRegex.test(source); const isRaster = type === 'raster'; if (!isSea) continue; if (!map.getLayer(id)) continue; if (isRaster && id === 'seamark') continue; try { map.setLayoutProperty(id, 'visibility', seaVisibility); } catch { // ignore } } } catch { // ignore } }; const stop = onMapStyleReady(map, apply); return () => { stop(); }; }, [projection, baseMap, mapSyncEpoch]); // Seamark toggle useEffect(() => { const map = mapRef.current; if (!map) return; if (showSeamark) { try { ensureSeamarkOverlay(map, 'bathymetry-lines-coarse'); map.setPaintProperty('seamark', 'raster-opacity', 0.85); } catch { // ignore until style is ready } return; } try { if (map.getLayer('seamark')) map.removeLayer('seamark'); } catch { // ignore } try { if (map.getSource('seamark')) map.removeSource('seamark'); } catch { // ignore } }, [showSeamark]); }