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>; setScale: React.Dispatch>; setRotation: React.Dispatch>; 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 ( {children} ); }; // 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; };