150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
import { createContext, ReactNode, useContext, useMemo, useState } 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,
|
|
setScreenCenter: React.Dispatch<React.SetStateAction<{ x: number, y: number } | undefined>>
|
|
}>({
|
|
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: () => {},
|
|
setScreenCenter: () => {}
|
|
});
|
|
|
|
// 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}>();
|
|
|
|
function screenToLocal(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
|
|
};
|
|
}
|
|
|
|
// Inverse of screenToLocal
|
|
function localToScreen(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
|
|
};
|
|
}
|
|
|
|
|
|
|
|
function rotateToAngle(to: number, fromPosition?: {x: number, y: number}) {
|
|
setRotation(to);
|
|
const rotationDiff = to - rotation;
|
|
|
|
const center = screenCenter ?? {x: 0, y: 0};
|
|
const cosDelta = Math.cos(rotationDiff);
|
|
const sinDelta = Math.sin(rotationDiff);
|
|
|
|
fromPosition ??= position;
|
|
|
|
setPosition({
|
|
x: center.x * (1 - cosDelta) + fromPosition.x * cosDelta + (center.y - fromPosition.y) * sinDelta,
|
|
y: center.y * (1 - cosDelta) + fromPosition.y * cosDelta + (fromPosition.x - center.x) * sinDelta
|
|
});
|
|
}
|
|
|
|
function setTransform(latitude: number, longitude: number, rotationDegrees?: number, useScale ?: number) {
|
|
const selectedRotation = rotationDegrees ? (rotationDegrees * Math.PI / 180) : rotation;
|
|
const selectedScale = useScale ? useScale/SCALE_FACTOR : scale;
|
|
const center = screenCenter ?? {x: 0, y: 0};
|
|
console.log("center", center.x, center.y);
|
|
const newPosition = {
|
|
x: -latitude * UP_SCALE * selectedScale,
|
|
y: -longitude * UP_SCALE * selectedScale
|
|
};
|
|
|
|
const cos = Math.cos(selectedRotation);
|
|
const sin = Math.sin(selectedRotation);
|
|
|
|
// Translate point relative to center, rotate, then translate back
|
|
const dx = newPosition.x;
|
|
const dy = newPosition.y;
|
|
newPosition.x = (dx * cos - dy * sin) + center.x;
|
|
newPosition.y = (dx * sin + dy * cos) + center.y;
|
|
|
|
|
|
setPosition(newPosition);
|
|
setRotation(selectedRotation);
|
|
setScale(selectedScale);
|
|
}
|
|
|
|
const value = useMemo(() => ({
|
|
position,
|
|
scale,
|
|
rotation,
|
|
screenCenter,
|
|
setPosition,
|
|
setScale,
|
|
setRotation,
|
|
rotateToAngle,
|
|
screenToLocal,
|
|
localToScreen,
|
|
setTransform,
|
|
setScreenCenter
|
|
}), [position, scale, rotation, screenCenter]);
|
|
|
|
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;
|
|
}; |