133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
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<maplibregl.Map | null>,
|
|
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<string>('');
|
|
|
|
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<string, unknown>)['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');
|
|
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]);
|
|
}
|