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

230 lines
7.6 KiB
TypeScript
Raw Normal View 히스토리

import { useEffect, useRef, type MutableRefObject } from 'react';
import maplibregl, { type LayerSpecification } from 'maplibre-gl';
import type { SubcableGeoJson } from '../../../entities/subcable/model/types';
import type { MapToggleState } from '../../../features/mapToggles/MapToggles';
import type { MapProjectionId } from '../types';
import { ensureGeoJsonSource, ensureLayer, setLayerVisibility, cleanupLayers } from '../lib/layerHelpers';
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';
const SRC_ID = 'subcables-src';
const LINE_ID = 'subcables-line';
const LINE_HOVER_ID = 'subcables-line-hover';
const LABEL_ID = 'subcables-label';
export function useSubcablesLayer(
mapRef: MutableRefObject<maplibregl.Map | null>,
projectionBusyRef: MutableRefObject<boolean>,
reorderGlobeFeatureLayers: () => void,
opts: {
subcableGeo: SubcableGeoJson | null;
overlays: MapToggleState;
projection: MapProjectionId;
mapSyncEpoch: number;
hoveredCableId: string | null;
onHoverCable: (cableId: string | null) => void;
onClickCable: (cableId: string | null) => void;
},
) {
const { subcableGeo, overlays, projection, mapSyncEpoch, hoveredCableId, onHoverCable, onClickCable } = opts;
const onHoverRef = useRef(onHoverCable);
const onClickRef = useRef(onClickCable);
onHoverRef.current = onHoverCable;
onClickRef.current = onClickCable;
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const ensure = () => {
if (projectionBusyRef.current) return;
const visibility = overlays.subcables ? 'visible' : 'none';
setLayerVisibility(map, LINE_ID, overlays.subcables);
setLayerVisibility(map, LINE_HOVER_ID, overlays.subcables);
setLayerVisibility(map, LABEL_ID, overlays.subcables);
if (!subcableGeo) return;
if (!map.isStyleLoaded()) return;
try {
ensureGeoJsonSource(map, SRC_ID, subcableGeo);
const before = map.getLayer('zones-fill')
? 'zones-fill'
: map.getLayer('deck-globe')
? 'deck-globe'
: undefined;
ensureLayer(
map,
{
id: LINE_ID,
type: 'line',
source: SRC_ID,
paint: {
'line-color': ['get', 'color'],
'line-opacity': ['interpolate', ['linear'], ['zoom'], 2, 0.4, 6, 0.55, 10, 0.7],
'line-width': ['interpolate', ['linear'], ['zoom'], 2, 0.8, 6, 1.2, 10, 1.8],
},
layout: { visibility, 'line-cap': 'round', 'line-join': 'round' },
} as unknown as LayerSpecification,
{ before },
);
ensureLayer(
map,
{
id: LINE_HOVER_ID,
type: 'line',
source: SRC_ID,
paint: {
'line-color': ['get', 'color'],
'line-opacity': 0,
'line-width': ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 5, 10, 8],
},
filter: ['==', ['get', 'id'], ''],
layout: { visibility, 'line-cap': 'round', 'line-join': 'round' },
} as unknown as LayerSpecification,
{ before },
);
ensureLayer(
map,
{
id: LABEL_ID,
type: 'symbol',
source: SRC_ID,
layout: {
visibility,
'symbol-placement': 'line',
'text-field': ['get', 'name'],
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 9, 8, 11, 12, 13],
'text-font': ['Noto Sans Regular', 'Open Sans Regular'],
'text-allow-overlap': false,
'text-padding': 8,
'text-rotation-alignment': 'map',
},
paint: {
'text-color': 'rgba(210,225,240,0.78)',
'text-halo-color': 'rgba(2,6,23,0.85)',
'text-halo-width': 1.0,
'text-halo-blur': 0.6,
'text-opacity': ['interpolate', ['linear'], ['zoom'], 4, 0, 5, 0.7, 8, 0.85],
},
minzoom: 4,
} as unknown as LayerSpecification,
);
// Update hover highlight
if (hoveredCableId) {
if (map.getLayer(LINE_ID)) {
map.setPaintProperty(LINE_ID, 'line-opacity', [
'case',
['==', ['get', 'id'], hoveredCableId],
0.95,
['interpolate', ['linear'], ['zoom'], 2, 0.25, 6, 0.35, 10, 0.45],
] as never);
map.setPaintProperty(LINE_ID, 'line-width', [
'case',
['==', ['get', 'id'], hoveredCableId],
['interpolate', ['linear'], ['zoom'], 2, 2.0, 6, 2.8, 10, 3.5],
['interpolate', ['linear'], ['zoom'], 2, 0.6, 6, 0.9, 10, 1.4],
] as never);
}
if (map.getLayer(LINE_HOVER_ID)) {
map.setFilter(LINE_HOVER_ID, ['==', ['get', 'id'], hoveredCableId] as never);
map.setPaintProperty(LINE_HOVER_ID, 'line-opacity', 0.25);
}
} else {
if (map.getLayer(LINE_ID)) {
map.setPaintProperty(
LINE_ID,
'line-opacity',
['interpolate', ['linear'], ['zoom'], 2, 0.4, 6, 0.55, 10, 0.7] as never,
);
map.setPaintProperty(
LINE_ID,
'line-width',
['interpolate', ['linear'], ['zoom'], 2, 0.8, 6, 1.2, 10, 1.8] as never,
);
}
if (map.getLayer(LINE_HOVER_ID)) {
map.setFilter(LINE_HOVER_ID, ['==', ['get', 'id'], ''] as never);
map.setPaintProperty(LINE_HOVER_ID, 'line-opacity', 0);
}
}
} catch (e) {
console.warn('Subcables layer setup failed:', e);
} finally {
reorderGlobeFeatureLayers();
kickRepaint(map);
}
};
const stop = onMapStyleReady(map, ensure);
return () => {
stop();
};
}, [subcableGeo, overlays.subcables, projection, mapSyncEpoch, hoveredCableId, reorderGlobeFeatureLayers]);
// Mouse events
useEffect(() => {
const map = mapRef.current;
if (!map) return;
if (!overlays.subcables) return;
const onMouseEnter = (e: maplibregl.MapMouseEvent & { features?: maplibregl.MapGeoJSONFeature[] }) => {
const cableId = e.features?.[0]?.properties?.id;
if (typeof cableId === 'string' && cableId) {
map.getCanvas().style.cursor = 'pointer';
onHoverRef.current(cableId);
}
};
const onMouseLeave = () => {
map.getCanvas().style.cursor = '';
onHoverRef.current(null);
};
const onClick = (e: maplibregl.MapMouseEvent & { features?: maplibregl.MapGeoJSONFeature[] }) => {
const cableId = e.features?.[0]?.properties?.id;
if (typeof cableId === 'string' && cableId) {
onClickRef.current(cableId);
}
};
const addEvents = () => {
if (!map.getLayer(LINE_ID)) return;
map.on('mouseenter', LINE_ID, onMouseEnter);
map.on('mouseleave', LINE_ID, onMouseLeave);
map.on('click', LINE_ID, onClick);
};
if (map.isStyleLoaded() && map.getLayer(LINE_ID)) {
addEvents();
} else {
map.once('idle', addEvents);
}
return () => {
try {
map.off('mouseenter', LINE_ID, onMouseEnter);
map.off('mouseleave', LINE_ID, onMouseLeave);
map.off('click', LINE_ID, onClick);
} catch {
// ignore
}
};
}, [overlays.subcables, mapSyncEpoch]);
// Cleanup on unmount
useEffect(() => {
const mapInstance = mapRef.current;
return () => {
if (!mapInstance) return;
cleanupLayers(mapInstance, [LABEL_ID, LINE_HOVER_ID, LINE_ID], [SRC_ID]);
};
}, []);
}