Files
WhiteNightsAdminPanel/src/pages/Route/route-preview/TransformContext.tsx

248 lines
7.0 KiB
TypeScript

import {
createContext,
ReactNode,
useContext,
useMemo,
useState,
useCallback,
} from "react";
import { SCALE_FACTOR, UP_SCALE } from "./Constants";
const TransformContext = createContext<{
position: { x: number; y: number };
scale: number;
rotation: number;
screenCenter?: { x: number; y: number };
setPosition: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
setScale: React.Dispatch<React.SetStateAction<number>>;
setRotation: React.Dispatch<React.SetStateAction<number>>;
screenToLocal: (x: number, y: number) => { x: number; y: number };
localToScreen: (x: number, y: number) => { x: number; y: number };
rotateToAngle: (to: number, fromPosition?: { x: number; y: number }) => void;
setTransform: (
latitude: number,
longitude: number,
rotationDegrees?: number,
scale?: number
) => void;
setScaleOnly: (newScale: number) => void;
setScaleWithoutMovingCenter: (newScale: number) => void;
setScreenCenter: React.Dispatch<
React.SetStateAction<{ x: number; y: number } | undefined>
>;
setScaleAtCenter: (newScale: number) => void;
}>({
position: { x: 0, y: 0 },
scale: 1,
rotation: 0,
screenCenter: undefined,
setPosition: () => {},
setScale: () => {},
setRotation: () => {},
screenToLocal: () => ({ x: 0, y: 0 }),
localToScreen: () => ({ x: 0, y: 0 }),
rotateToAngle: () => {},
setTransform: () => {},
setScaleOnly: () => {},
setScaleWithoutMovingCenter: () => {},
setScreenCenter: () => {},
setScaleAtCenter: () => {},
});
// Provider component
export const TransformProvider = ({ children }: { children: ReactNode }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [scale, setScale] = useState(1);
const [rotation, setRotation] = useState(0);
const [screenCenter, setScreenCenter] = useState<{ x: number; y: number }>();
const screenToLocal = useCallback(
(screenX: number, screenY: number) => {
// Translate point relative to current pan position
const translatedX = (screenX - position.x) / scale;
const translatedY = (screenY - position.y) / scale;
// Rotate point around center
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
const sinRotation = Math.sin(-rotation);
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
return {
x: rotatedX / UP_SCALE,
y: rotatedY / UP_SCALE,
};
},
[position.x, position.y, scale, rotation]
);
// Inverse of screenToLocal
const localToScreen = useCallback(
(localX: number, localY: number) => {
const upscaledX = localX * UP_SCALE;
const upscaledY = localY * UP_SCALE;
const cosRotation = Math.cos(rotation);
const sinRotation = Math.sin(rotation);
const rotatedX = upscaledX * cosRotation - upscaledY * sinRotation;
const rotatedY = upscaledX * sinRotation + upscaledY * cosRotation;
const translatedX = rotatedX * scale + position.x;
const translatedY = rotatedY * scale + position.y;
return {
x: translatedX,
y: translatedY,
};
},
[position.x, position.y, scale, rotation]
);
const rotateToAngle = useCallback(
(to: number, fromPosition?: { x: number; y: number }) => {
const rotationDiff = to - rotation;
const center = screenCenter ?? { x: 0, y: 0 };
const cosDelta = Math.cos(rotationDiff);
const sinDelta = Math.sin(rotationDiff);
const currentFromPosition = fromPosition ?? position;
const newPosition = {
x:
center.x * (1 - cosDelta) +
currentFromPosition.x * cosDelta +
(center.y - currentFromPosition.y) * sinDelta,
y:
center.y * (1 - cosDelta) +
currentFromPosition.y * cosDelta +
(currentFromPosition.x - center.x) * sinDelta,
};
// Update both rotation and position in a single batch to avoid stale closure
setRotation(to);
setPosition(newPosition);
},
[rotation, position, screenCenter]
);
const setTransform = useCallback(
(
latitude: number,
longitude: number,
rotationDegrees?: number,
useScale?: number
) => {
const selectedRotation =
rotationDegrees !== undefined
? (rotationDegrees * Math.PI) / 180
: rotation;
const selectedScale =
useScale !== undefined ? useScale / SCALE_FACTOR : scale;
const center = screenCenter ?? { x: 0, y: 0 };
const newPosition = {
x: -latitude * UP_SCALE * selectedScale,
y: -longitude * UP_SCALE * selectedScale,
};
const cosRot = Math.cos(selectedRotation);
const sinRot = Math.sin(selectedRotation);
// Translate point relative to center, rotate, then translate back
const dx = newPosition.x;
const dy = newPosition.y;
newPosition.x = dx * cosRot - dy * sinRot + center.x;
newPosition.y = dx * sinRot + dy * cosRot + center.y;
// Batch state updates to avoid intermediate renders
setPosition(newPosition);
setRotation(selectedRotation);
setScale(selectedScale);
},
[rotation, scale, screenCenter]
);
const setScaleAtCenter = useCallback(
(newScale: number) => {
if (scale === newScale) return;
const center = screenCenter ?? { x: 0, y: 0 };
const actualZoomFactor = newScale / scale;
const newPosition = {
x: position.x + (center.x - position.x) * (1 - actualZoomFactor),
y: position.y + (center.y - position.y) * (1 - actualZoomFactor),
};
setPosition(newPosition);
setScale(newScale);
},
[position, scale, screenCenter]
);
const setScaleOnly = useCallback((newScale: number) => {
// Изменяем только масштаб, не трогая позицию и поворот
setScale(newScale);
}, []);
const setScaleWithoutMovingCenter = useCallback(
(newScale: number) => {
setScale(newScale);
},
[setScale]
);
const value = useMemo(
() => ({
position,
scale,
rotation,
screenCenter,
setPosition,
setScale,
setRotation,
rotateToAngle,
screenToLocal,
localToScreen,
setTransform,
setScaleOnly,
setScaleWithoutMovingCenter,
setScreenCenter,
setScaleAtCenter,
}),
[
position,
scale,
rotation,
screenCenter,
setScale,
rotateToAngle,
screenToLocal,
localToScreen,
setTransform,
setScaleOnly,
setScaleWithoutMovingCenter,
setScreenCenter,
setScaleAtCenter,
]
);
return (
<TransformContext.Provider value={value}>
{children}
</TransformContext.Provider>
);
};
// Custom hook for easy access to transform values
export const useTransform = () => {
const context = useContext(TransformContext);
if (!context) {
throw new Error("useTransform must be used within a TransformProvider");
}
return context;
};