230 lines
7.6 KiB
TypeScript
230 lines
7.6 KiB
TypeScript
|
|
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]);
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
}
|