import type { Feature, FeatureCollection, LineString, MultiLineString } from "geojson"; import type { Map as MapLibreMap } from "maplibre-gl"; import type { MapboxOverlay } from "@deck.gl/mapbox"; import { WORKBENCH_INTERACTION_BEFORE_ID } from "./layers"; type FlowFeature = Feature; export type FlowOverlayState = { data: FlowFeature[] | null; animationFrameId: number | null; overlay: MapboxOverlay | null; }; export function createFlowOverlayState(): FlowOverlayState { return { data: null, animationFrameId: null, overlay: null }; } export async function showFlowOverlay( map: MapLibreMap, state: FlowOverlayState, loadData: () => Promise, reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches ) { if (!state.data) { const collection = await loadData(); state.data = collection.features.filter(isFlowFeature); } const [{ MapboxOverlay }, { PathLayer }, { FlowPathExtension }] = await Promise.all([ import("@deck.gl/mapbox"), import("@deck.gl/layers"), import("./flow-path-extension") ]); const extension = new FlowPathExtension(); let renderingError: Error | null = null; const render = (phase: number) => { const layer = new PathLayer({ id: "workbench-network-flow", beforeId: WORKBENCH_INTERACTION_BEFORE_ID, data: state.data ?? [], getPath: (feature: FlowFeature) => feature.geometry.type === "LineString" ? feature.geometry.coordinates : feature.geometry.coordinates.flat(), getColor: [15, 118, 110, 210], getWidth: (feature: FlowFeature) => Math.min(5, Math.max(1, Number(feature.properties?.diameter ?? 200) / 180)), widthUnits: "pixels", widthMinPixels: 1, widthMaxPixels: 5, capRounded: true, jointRounded: true, pickable: false, parameters: { depthTest: false }, extensions: [extension], flowPhase: phase } as never); state.overlay?.setProps({ layers: [layer], onError: (error) => { renderingError = error; } }); }; if (!state.overlay) { const overlay = new MapboxOverlay({ interleaved: true, layers: [] }); map.addControl(overlay); state.overlay = overlay; } stopFlowAnimation(state); render(0.2); await new Promise((resolve) => window.requestAnimationFrame(() => resolve())); if (renderingError) throw renderingError; if (!reducedMotion) { const startedAt = performance.now(); const tick = (now: number) => { render(((now - startedAt) / 1800) % 1); state.animationFrameId = window.requestAnimationFrame(tick); }; state.animationFrameId = window.requestAnimationFrame(tick); } } export function hideFlowOverlay(map: MapLibreMap, state: FlowOverlayState) { stopFlowAnimation(state); if (state.overlay) { map.removeControl(state.overlay as never); state.overlay = null; } } export function stopFlowAnimation(state: FlowOverlayState) { if (state.animationFrameId !== null) { window.cancelAnimationFrame(state.animationFrameId); state.animationFrameId = null; } } function isFlowFeature(feature: Feature): feature is FlowFeature { return feature.geometry?.type === "LineString" || feature.geometry?.type === "MultiLineString"; }