/*
* SPDX-FileCopyrightText: 2023 The Refinery Authors
*
* SPDX-License-Identifier: EPL-2.0
*/
import Box from '@mui/material/Box';
import * as d3 from 'd3';
import { zoom as d3Zoom } from 'd3-zoom';
import React, { useCallback, useRef, useState } from 'react';
import ZoomButtons from './ZoomButtons';
declare module 'd3-zoom' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Redeclaring type parameters.
interface ZoomBehavior {
// `@types/d3-zoom` does not contain the `center` function, because it is
// only available as a pull request for `d3-zoom`.
center(callback: (event: MouseEvent | Touch) => [number, number]): this;
// Custom `centroid` method added via patch.
centroid(centroid: [number, number]): this;
}
}
interface Transform {
x: number;
y: number;
k: number;
}
export default function ZoomCanvas({
children,
fitPadding,
transitionTime,
}: {
children?: React.ReactNode;
fitPadding?: number;
transitionTime?: number;
}): JSX.Element {
const canvasRef = useRef();
const elementRef = useRef();
const zoomRef = useRef<
d3.ZoomBehavior | undefined
>();
const fitPaddingOrDefault = fitPadding ?? ZoomCanvas.defaultProps.fitPadding;
const transitionTimeOrDefault =
transitionTime ?? ZoomCanvas.defaultProps.transitionTime;
const [zoom, setZoom] = useState({ x: 0, y: 0, k: 1 });
const setCanvas = useCallback(
(canvas: HTMLDivElement | null) => {
canvasRef.current = canvas ?? undefined;
if (canvas === null) {
return;
}
const zoomBehavior = d3Zoom()
.duration(transitionTimeOrDefault)
.center((event) => {
const { width, height } = canvas.getBoundingClientRect();
const [x, y] = d3.pointer(event, canvas);
return [x - width / 2, y - height / 2];
})
.centroid([0, 0]);
zoomBehavior.on(
'zoom',
(event: d3.D3ZoomEvent) =>
setZoom(event.transform),
);
d3.select(canvas).call(zoomBehavior);
zoomRef.current = zoomBehavior;
},
[transitionTimeOrDefault],
);
const makeTransition = useCallback(
(element: HTMLDivElement) =>
d3.select(element).transition().duration(transitionTimeOrDefault),
[transitionTimeOrDefault],
);
const changeZoom = useCallback(
(event: React.MouseEvent, factor: number) => {
if (canvasRef.current === undefined || zoomRef.current === undefined) {
return;
}
const zoomTransition = makeTransition(canvasRef.current);
const center: [number, number] = [0, 0];
zoomRef.current.scaleBy(zoomTransition, factor, center);
event.preventDefault();
event.stopPropagation();
},
[makeTransition],
);
const fitZoom = useCallback(
(event: React.MouseEvent) => {
if (
canvasRef.current === undefined ||
zoomRef.current === undefined ||
elementRef.current === undefined
) {
return;
}
const { width: canvasWidth, height: canvasHeight } =
canvasRef.current.getBoundingClientRect();
const { width: scaledWidth, height: scaledHeight } =
elementRef.current.getBoundingClientRect();
const currentFactor = d3.zoomTransform(canvasRef.current).k;
const width = scaledWidth / currentFactor;
const height = scaledHeight / currentFactor;
if (width > 0 && height > 0) {
const factor = Math.min(
1.0,
(canvasWidth - fitPaddingOrDefault) / width,
(canvasHeight - fitPaddingOrDefault) / height,
);
const zoomTransition = makeTransition(canvasRef.current);
zoomRef.current.transform(
zoomTransition,
d3.zoomIdentity.scale(factor),
);
}
event.preventDefault();
event.stopPropagation();
},
[fitPaddingOrDefault, makeTransition],
);
return (
{children}
);
}
ZoomCanvas.defaultProps = {
children: undefined,
fitPadding: 64,
transitionTime: 250,
};