gc-wing/apps/web/src/widgets/map3d/hooks/useBaseMapToggle.ts

133 lines
3.9 KiB
TypeScript
Raw Normal View 히스토리

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]);
}