378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
import { Button, Stack, TextField, Typography, Slider } from "@mui/material";
|
||
import { useMapData } from "./MapDataContext";
|
||
import { useEffect, useState } from "react";
|
||
import { useTransform } from "./TransformContext";
|
||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||
import { SCALE_FACTOR } from "./Constants";
|
||
import { toast } from "react-toastify";
|
||
|
||
export function RightSidebar() {
|
||
const {
|
||
routeData,
|
||
setScaleRange,
|
||
saveChanges,
|
||
originalRouteData,
|
||
setMapRotation,
|
||
setMapCenter,
|
||
} = useMapData();
|
||
const {
|
||
rotation,
|
||
position,
|
||
screenToLocal,
|
||
screenCenter,
|
||
rotateToAngle,
|
||
setTransform,
|
||
scale,
|
||
setScaleAtCenter,
|
||
} = useTransform();
|
||
|
||
const [minScale, setMinScale] = useState<number>(1);
|
||
const [maxScale, setMaxScale] = useState<number>(5);
|
||
const [localCenter, setLocalCenter] = useState<{ x: number; y: number }>({
|
||
x: 0,
|
||
y: 0,
|
||
});
|
||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||
const [isUserEditing, setIsUserEditing] = useState<boolean>(false);
|
||
|
||
useEffect(() => {
|
||
if (originalRouteData) {
|
||
// Проверяем и сбрасываем минимальный масштаб если нужно
|
||
const originalMinScale = originalRouteData.scale_min ?? 1;
|
||
const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
|
||
|
||
// Проверяем и сбрасываем максимальный масштаб если нужно
|
||
const originalMaxScale = originalRouteData.scale_max ?? 5;
|
||
const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
|
||
|
||
setMinScale(resetMinScale);
|
||
setMaxScale(resetMaxScale);
|
||
setRotationDegrees(originalRouteData.rotate ?? 0);
|
||
setLocalCenter({
|
||
x: originalRouteData.center_latitude ?? 0,
|
||
y: originalRouteData.center_longitude ?? 0,
|
||
});
|
||
}
|
||
}, [originalRouteData]);
|
||
|
||
useEffect(() => {
|
||
if (minScale && maxScale) {
|
||
setScaleRange(minScale, maxScale);
|
||
}
|
||
}, [minScale, maxScale]);
|
||
|
||
useEffect(() => {
|
||
setRotationDegrees(
|
||
((Math.round((rotation * 180) / Math.PI) % 360) + 360) % 360
|
||
);
|
||
}, [rotation]);
|
||
|
||
useEffect(() => {
|
||
setMapRotation(rotationDegrees);
|
||
}, [rotationDegrees]);
|
||
|
||
useEffect(() => {
|
||
if (!isUserEditing) {
|
||
const center = screenCenter ?? { x: 0, y: 0 };
|
||
const localCenter = screenToLocal(center.x, center.y);
|
||
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
||
setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
||
}
|
||
}, [
|
||
position,
|
||
screenCenter,
|
||
screenToLocal,
|
||
localToCoordinates,
|
||
setLocalCenter,
|
||
isUserEditing,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
setMapCenter(localCenter.x, localCenter.y);
|
||
}, [localCenter]);
|
||
|
||
function setRotationFromDegrees(degrees: number) {
|
||
rotateToAngle((degrees * Math.PI) / 180);
|
||
}
|
||
|
||
function pan({ x, y }: { x: number; y: number }) {
|
||
const coordinates = coordinatesToLocal(x, y);
|
||
setTransform(coordinates.x, coordinates.y);
|
||
}
|
||
|
||
if (!routeData) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<Stack
|
||
position="absolute"
|
||
right={8}
|
||
top={8}
|
||
bottom={8}
|
||
p={2}
|
||
gap={1}
|
||
minWidth="400px"
|
||
bgcolor="primary.main"
|
||
border="1px solid #e0e0e0"
|
||
borderRadius={2}
|
||
>
|
||
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||
Детали о достопримечательностях
|
||
</Typography>
|
||
|
||
<Stack spacing={2} direction="row" alignItems="center">
|
||
<TextField
|
||
type="number"
|
||
label="Минимальный масштаб"
|
||
variant="filled"
|
||
value={minScale}
|
||
onChange={(e) => {
|
||
let newMinScale = Number(e.target.value);
|
||
|
||
// Сбрасываем к 1 если меньше
|
||
if (newMinScale < 1) {
|
||
newMinScale = 1;
|
||
}
|
||
|
||
setMinScale(newMinScale);
|
||
|
||
if (maxScale - newMinScale < 2) {
|
||
let newMaxScale = newMinScale + 2;
|
||
// Сбрасываем максимальный к 3 если меньше минимального
|
||
if (newMaxScale < 3) {
|
||
newMaxScale = 3;
|
||
setMinScale(1); // Сбрасываем минимальный к 1
|
||
}
|
||
setMaxScale(newMaxScale);
|
||
}
|
||
|
||
if (newMinScale > scale * SCALE_FACTOR) {
|
||
setScaleAtCenter(newMinScale / SCALE_FACTOR);
|
||
}
|
||
}}
|
||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||
sx={{
|
||
"& .MuiInputLabel-root": {
|
||
color: "#fff",
|
||
},
|
||
"& .MuiInputBase-input": {
|
||
color: "#fff",
|
||
},
|
||
}}
|
||
slotProps={{
|
||
input: {
|
||
min: 1,
|
||
max: 10,
|
||
},
|
||
}}
|
||
/>
|
||
<TextField
|
||
type="number"
|
||
label="Максимальный масштаб"
|
||
variant="filled"
|
||
value={maxScale}
|
||
onChange={(e) => {
|
||
let newMaxScale = Number(e.target.value);
|
||
|
||
// Сбрасываем к 3 если меньше минимального
|
||
if (newMaxScale < 3) {
|
||
newMaxScale = 3;
|
||
}
|
||
|
||
setMaxScale(newMaxScale);
|
||
|
||
if (newMaxScale - minScale < 2) {
|
||
let newMinScale = newMaxScale - 2;
|
||
// Сбрасываем минимальный к 1 если меньше
|
||
if (newMinScale < 1) {
|
||
newMinScale = 1;
|
||
setMaxScale(3); // Сбрасываем максимальный к минимальному значению
|
||
}
|
||
setMinScale(newMinScale);
|
||
}
|
||
|
||
if (newMaxScale < scale * SCALE_FACTOR) {
|
||
setScaleAtCenter(newMaxScale / SCALE_FACTOR);
|
||
}
|
||
}}
|
||
style={{ backgroundColor: "#222", borderRadius: 4, color: "#fff" }}
|
||
sx={{
|
||
"& .MuiInputLabel-root": {
|
||
color: "#fff",
|
||
},
|
||
"& .MuiInputBase-input": {
|
||
color: "#fff",
|
||
},
|
||
}}
|
||
slotProps={{
|
||
input: {
|
||
min: 3,
|
||
max: 10,
|
||
},
|
||
}}
|
||
/>
|
||
</Stack>
|
||
|
||
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||
Текущий масштаб: {Math.round(scale * SCALE_FACTOR * 100) / 100}
|
||
</Typography>
|
||
|
||
<Slider
|
||
value={scale * SCALE_FACTOR}
|
||
onChange={(_, newValue) => {
|
||
if (typeof newValue === "number") {
|
||
setScaleAtCenter(newValue / SCALE_FACTOR);
|
||
}
|
||
}}
|
||
min={minScale}
|
||
max={maxScale}
|
||
step={0.1}
|
||
sx={{
|
||
color: "#fff",
|
||
"& .MuiSlider-thumb": {
|
||
backgroundColor: "#fff",
|
||
},
|
||
"& .MuiSlider-track": {
|
||
backgroundColor: "#fff",
|
||
},
|
||
"& .MuiSlider-rail": {
|
||
backgroundColor: "#666",
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<TextField
|
||
type="number"
|
||
label="Текущий масштаб"
|
||
variant="filled"
|
||
value={Math.round(scale * SCALE_FACTOR * 100) / 100}
|
||
onChange={(e) => {
|
||
const newScale = Number(e.target.value);
|
||
if (
|
||
!isNaN(newScale) &&
|
||
newScale >= minScale &&
|
||
newScale <= maxScale
|
||
) {
|
||
setScaleAtCenter(newScale / SCALE_FACTOR);
|
||
}
|
||
}}
|
||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||
sx={{
|
||
"& .MuiInputLabel-root": {
|
||
color: "#fff",
|
||
},
|
||
"& .MuiInputBase-input": {
|
||
color: "#fff",
|
||
},
|
||
}}
|
||
inputProps={{
|
||
min: minScale,
|
||
max: maxScale,
|
||
}}
|
||
/>
|
||
|
||
<TextField
|
||
type="number"
|
||
label="Поворот (в градусах)"
|
||
variant="filled"
|
||
value={rotationDegrees}
|
||
onChange={(e) => {
|
||
const value = Number(e.target.value);
|
||
if (!isNaN(value)) {
|
||
setRotationFromDegrees(value);
|
||
}
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
e.currentTarget.blur();
|
||
}
|
||
}}
|
||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||
sx={{
|
||
"& .MuiInputLabel-root": {
|
||
color: "#fff",
|
||
},
|
||
"& .MuiInputBase-input": {
|
||
color: "#fff",
|
||
},
|
||
}}
|
||
slotProps={{
|
||
input: {
|
||
min: 0,
|
||
max: 360,
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Stack direction="row" spacing={2}>
|
||
<TextField
|
||
type="number"
|
||
label="Центр карты, широта"
|
||
variant="filled"
|
||
value={Math.round(localCenter.x * 1000) / 1000}
|
||
onChange={(e) => {
|
||
setIsUserEditing(true);
|
||
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
||
pan({ x: Number(e.target.value), y: localCenter.y });
|
||
}}
|
||
onBlur={() => setIsUserEditing(false)}
|
||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||
sx={{
|
||
"& .MuiInputLabel-root": {
|
||
color: "#fff",
|
||
},
|
||
"& .MuiInputBase-input": {
|
||
color: "#fff",
|
||
},
|
||
}}
|
||
inputProps={{
|
||
step: 0.001,
|
||
}}
|
||
/>
|
||
<TextField
|
||
type="number"
|
||
label="Центр карты, высота"
|
||
variant="filled"
|
||
value={Math.round(localCenter.y * 1000) / 1000}
|
||
onChange={(e) => {
|
||
setIsUserEditing(true);
|
||
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
||
pan({ x: localCenter.x, y: Number(e.target.value) });
|
||
}}
|
||
onBlur={() => setIsUserEditing(false)}
|
||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||
sx={{
|
||
"& .MuiInputLabel-root": {
|
||
color: "#fff",
|
||
},
|
||
"& .MuiInputBase-input": {
|
||
color: "#fff",
|
||
},
|
||
}}
|
||
inputProps={{
|
||
step: 0.001,
|
||
}}
|
||
/>
|
||
</Stack>
|
||
|
||
<Button
|
||
variant="contained"
|
||
color="secondary"
|
||
sx={{ mt: 2 }}
|
||
onClick={async () => {
|
||
try {
|
||
await saveChanges();
|
||
toast.success("Изменения сохранены");
|
||
} catch (error) {
|
||
console.error(error);
|
||
toast.error("Ошибка при сохранении изменений");
|
||
}
|
||
}}
|
||
>
|
||
Сохранить изменения
|
||
</Button>
|
||
</Stack>
|
||
);
|
||
}
|