ForFarm/frontend/components/map-component/undo-redo.ts

218 lines
6.7 KiB
TypeScript

import { Dispatch, MutableRefObject, useEffect } from "react";
import {
Action,
DrawResult,
DrawingActionKind,
Overlay,
Snapshot,
State,
isCircle,
isMarker,
isPolygon,
isPolyline,
isRectangle,
} from "@/types";
export default function reducer(state: State, action: Action) {
switch (action.type) {
// This action is called whenever anything changes on any overlay.
// We then take a snapshot of the relevant values of each overlay and
// save them as the new "now". The old "now" is added to the "past" stack
case DrawingActionKind.UPDATE_OVERLAYS: {
const overlays = state.now.map((overlay: Overlay) => {
const snapshot: Snapshot = {};
const { geometry } = overlay;
if (isCircle(geometry)) {
snapshot.center = geometry.getCenter()?.toJSON();
snapshot.radius = geometry.getRadius();
} else if (isMarker(geometry)) {
snapshot.position = geometry.getPosition()?.toJSON();
} else if (isPolygon(geometry) || isPolyline(geometry)) {
snapshot.path = geometry.getPath()?.getArray();
} else if (isRectangle(geometry)) {
snapshot.bounds = geometry.getBounds()?.toJSON();
}
return {
...overlay,
snapshot,
};
});
return {
now: [...overlays],
past: [...state.past, state.now],
future: [],
};
}
// This action is called when a new overlay is added to the map.
// We then take a snapshot of the relevant values of the new overlay and
// add it to the "now" state. The old "now" is added to the "past" stack
case DrawingActionKind.SET_OVERLAY: {
const { overlay } = action.payload;
const snapshot: Snapshot = {};
if (isCircle(overlay)) {
snapshot.center = overlay.getCenter()?.toJSON();
snapshot.radius = overlay.getRadius();
} else if (isMarker(overlay)) {
snapshot.position = overlay.getPosition()?.toJSON();
} else if (isPolygon(overlay) || isPolyline(overlay)) {
snapshot.path = overlay.getPath()?.getArray();
} else if (isRectangle(overlay)) {
snapshot.bounds = overlay.getBounds()?.toJSON();
}
return {
past: [...state.past, state.now],
now: [
...state.now,
{
type: action.payload.type,
geometry: action.payload.overlay,
snapshot,
},
],
future: [],
};
}
// This action is called when the undo button is clicked.
// Get the top item from the "past" stack and set it as the new "now".
// Add the old "now" to the "future" stack to enable redo functionality
case DrawingActionKind.UNDO: {
const last = state.past.slice(-1)[0];
if (!last) return state;
return {
past: [...state.past].slice(0, -1),
now: last,
future: state.now ? [...state.future, state.now] : state.future,
};
}
// This action is called when the redo button is clicked.
// Get the top item from the "future" stack and set it as the new "now".
// Add the old "now" to the "past" stack to enable undo functionality
case DrawingActionKind.REDO: {
const next = state.future.slice(-1)[0];
if (!next) return state;
return {
past: state.now ? [...state.past, state.now] : state.past,
now: next,
future: [...state.future].slice(0, -1),
};
}
}
}
// Handle drawing manager events
export function useDrawingManagerEvents(
drawingManager: google.maps.drawing.DrawingManager | null,
overlaysShouldUpdateRef: MutableRefObject<boolean>,
dispatch: Dispatch<Action>
) {
useEffect(() => {
if (!drawingManager) return;
const eventListeners: Array<google.maps.MapsEventListener> = [];
const addUpdateListener = (eventName: string, drawResult: DrawResult) => {
const updateListener = google.maps.event.addListener(drawResult.overlay, eventName, () => {
if (eventName === "dragstart") {
overlaysShouldUpdateRef.current = false;
}
if (eventName === "dragend") {
overlaysShouldUpdateRef.current = true;
}
if (overlaysShouldUpdateRef.current) {
dispatch({ type: DrawingActionKind.UPDATE_OVERLAYS });
}
});
eventListeners.push(updateListener);
};
const overlayCompleteListener = google.maps.event.addListener(
drawingManager,
"overlaycomplete",
(drawResult: DrawResult) => {
switch (drawResult.type) {
case google.maps.drawing.OverlayType.CIRCLE:
["center_changed", "radius_changed"].forEach((eventName) => addUpdateListener(eventName, drawResult));
break;
case google.maps.drawing.OverlayType.MARKER:
["dragend"].forEach((eventName) => addUpdateListener(eventName, drawResult));
break;
case google.maps.drawing.OverlayType.POLYGON:
case google.maps.drawing.OverlayType.POLYLINE:
["mouseup"].forEach((eventName) => addUpdateListener(eventName, drawResult));
case google.maps.drawing.OverlayType.RECTANGLE:
["bounds_changed", "dragstart", "dragend"].forEach((eventName) => addUpdateListener(eventName, drawResult));
break;
}
dispatch({ type: DrawingActionKind.SET_OVERLAY, payload: drawResult });
}
);
eventListeners.push(overlayCompleteListener);
return () => {
eventListeners.forEach((listener) => google.maps.event.removeListener(listener));
};
}, [dispatch, drawingManager, overlaysShouldUpdateRef]);
}
// Update overlays with the current "snapshot" when the "now" state changes
export function useOverlaySnapshots(
map: google.maps.Map | null,
state: State,
overlaysShouldUpdateRef: MutableRefObject<boolean>
) {
useEffect(() => {
if (!map || !state.now) return;
for (const overlay of state.now) {
overlaysShouldUpdateRef.current = false;
overlay.geometry.setMap(map);
const { radius, center, position, path, bounds } = overlay.snapshot;
if (isCircle(overlay.geometry)) {
overlay.geometry.setRadius(radius ?? 0);
overlay.geometry.setCenter(center ?? null);
} else if (isMarker(overlay.geometry)) {
overlay.geometry.setPosition(position);
} else if (isPolygon(overlay.geometry) || isPolyline(overlay.geometry)) {
overlay.geometry.setPath(path ?? []);
} else if (isRectangle(overlay.geometry)) {
overlay.geometry.setBounds(bounds ?? null);
}
overlaysShouldUpdateRef.current = true;
}
return () => {
for (const overlay of state.now) {
overlay.geometry.setMap(null);
}
};
}, [map, overlaysShouldUpdateRef, state.now]);
}