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, projectionBusyRef: MutableRefObject, 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]); }; }, []); }