100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
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<LineString | MultiLineString, { diameter?: number | string; [key: string]: unknown }>;
|
|
|
|
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<FeatureCollection>,
|
|
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<FlowFeature>({
|
|
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<void>((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";
|
|
}
|