477 lines
14 KiB
TypeScript
477 lines
14 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 { SCALE_FACTOR } from "./Constants";
|
||
import { toast } from "react-toastify";
|
||
|
||
export function RightSidebar() {
|
||
const {
|
||
routeData,
|
||
setScaleRange,
|
||
saveChanges,
|
||
originalRouteData,
|
||
setMapRotation,
|
||
setMapCenter,
|
||
setIconSize: updateIconSize,
|
||
setFontSize: updateFontSize,
|
||
} = useMapData();
|
||
const { rotation, rotateToAngle, 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);
|
||
const [iconSize, setIconSize] = useState<number>(100);
|
||
const [fontSize, setFontSize] = useState<number>(100);
|
||
|
||
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,
|
||
});
|
||
setIconSize(originalRouteData.icon_size ?? 100);
|
||
setFontSize(originalRouteData.font_size ?? 100);
|
||
}
|
||
}, [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) {
|
||
return;
|
||
}
|
||
|
||
const latitude = routeData?.center_latitude ?? 0;
|
||
const longitude = routeData?.center_longitude ?? 0;
|
||
|
||
setLocalCenter((prev) => {
|
||
if (
|
||
Math.abs(prev.x - latitude) < 1e-6 &&
|
||
Math.abs(prev.y - longitude) < 1e-6
|
||
) {
|
||
return prev;
|
||
}
|
||
return { x: latitude, y: longitude };
|
||
});
|
||
}, [isUserEditing, routeData?.center_latitude, routeData?.center_longitude]);
|
||
|
||
function setRotationFromDegrees(degrees: number) {
|
||
rotateToAngle((degrees * Math.PI) / 180);
|
||
}
|
||
|
||
const handleIconSizeChange = (value: number) => {
|
||
if (!Number.isFinite(value)) {
|
||
return;
|
||
}
|
||
const clamped = Math.max(50, Math.min(300, Math.round(value)));
|
||
setIconSize(clamped);
|
||
updateIconSize(clamped);
|
||
};
|
||
|
||
const handleFontSizeChange = (value: number) => {
|
||
if (!Number.isFinite(value)) {
|
||
return;
|
||
}
|
||
const clamped = Math.max(50, Math.min(300, Math.round(value)));
|
||
setFontSize(clamped);
|
||
updateFontSize(clamped);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const next = routeData?.icon_size ?? originalRouteData?.icon_size ?? 100;
|
||
setIconSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||
}, [routeData?.icon_size, originalRouteData?.icon_size]);
|
||
|
||
useEffect(() => {
|
||
const next = routeData?.font_size ?? originalRouteData?.font_size ?? 100;
|
||
setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||
}, [routeData?.font_size, originalRouteData?.font_size]);
|
||
|
||
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);
|
||
|
||
if (newMinScale < 10) {
|
||
newMinScale = 10;
|
||
}
|
||
|
||
setMinScale(newMinScale);
|
||
|
||
if (maxScale - newMinScale < 2) {
|
||
let newMaxScale = newMinScale + 2;
|
||
|
||
if (newMaxScale < 3) {
|
||
newMaxScale = 3;
|
||
setMinScale(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);
|
||
|
||
if (newMaxScale < 13) {
|
||
newMaxScale = 13;
|
||
}
|
||
|
||
if (newMaxScale > 300) {
|
||
newMaxScale = 300;
|
||
}
|
||
|
||
setMaxScale(newMaxScale);
|
||
|
||
if (newMaxScale - minScale < 2) {
|
||
let newMinScale = newMaxScale - 2;
|
||
|
||
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: 300,
|
||
},
|
||
}}
|
||
/>
|
||
</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,
|
||
}}
|
||
/>
|
||
|
||
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||
Размер иконок: {iconSize}%
|
||
</Typography>
|
||
|
||
<Slider
|
||
value={iconSize}
|
||
onChange={(_, value) => {
|
||
if (typeof value === "number") {
|
||
handleIconSizeChange(value);
|
||
}
|
||
}}
|
||
min={50}
|
||
max={300}
|
||
step={1}
|
||
sx={{
|
||
color: "#fff",
|
||
"& .MuiSlider-thumb": {
|
||
backgroundColor: "#fff",
|
||
},
|
||
"& .MuiSlider-track": {
|
||
backgroundColor: "#fff",
|
||
},
|
||
"& .MuiSlider-rail": {
|
||
backgroundColor: "#666",
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||
Размер шрифта: {fontSize}%
|
||
</Typography>
|
||
|
||
<Slider
|
||
value={fontSize}
|
||
onChange={(_, value) => {
|
||
if (typeof value === "number") {
|
||
handleFontSizeChange(value);
|
||
}
|
||
}}
|
||
min={50}
|
||
max={300}
|
||
step={1}
|
||
sx={{
|
||
color: "#fff",
|
||
"& .MuiSlider-thumb": {
|
||
backgroundColor: "#fff",
|
||
},
|
||
"& .MuiSlider-track": {
|
||
backgroundColor: "#fff",
|
||
},
|
||
"& .MuiSlider-rail": {
|
||
backgroundColor: "#666",
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<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);
|
||
const newValue = Number(e.target.value);
|
||
setLocalCenter((prev) => ({ ...prev, x: newValue }));
|
||
if (!isNaN(newValue) && localCenter.y !== undefined) {
|
||
setMapCenter(newValue, 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);
|
||
const newValue = Number(e.target.value);
|
||
setLocalCenter((prev) => ({ ...prev, y: newValue }));
|
||
if (!isNaN(newValue) && localCenter.x !== undefined) {
|
||
setMapCenter(localCenter.x, newValue);
|
||
}
|
||
}}
|
||
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>
|
||
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="48"
|
||
height="48"
|
||
viewBox="0 0 48 48"
|
||
fill="none"
|
||
style={{ cursor: "pointer" }}
|
||
className="absolute bottom-5 left-[-68px] z-100"
|
||
>
|
||
<path
|
||
d="M24.0013 0C23.3513 0 22.7013 0.03 22.0413 0.08C10.4213 1 1.01127 10.41 0.0812683 22.03C-0.428732 28.39 1.55127 34.28 5.14127 38.83C5.60127 39.42 5.76127 40.21 5.46127 40.89C4.76127 42.43 3.63127 43.64 3.05127 44.23C2.50127 44.78 1.97127 45.27 1.38127 45.7C0.791268 46.13 0.841268 47.03 1.45127 47.42C2.08127 47.82 3.01127 47.99 4.12127 47.99C6.84127 47.99 10.5813 46.99 13.3013 46.06C13.5013 45.99 13.7013 45.96 13.9113 45.96C14.1813 45.96 14.4613 46.02 14.7113 46.13C17.5713 47.33 20.7113 48 24.0013 48C24.6513 48 25.3213 47.97 25.9813 47.92C37.6313 46.98 47.0613 37.51 47.9313 25.85C48.9913 11.76 37.8713 0 24.0013 0ZM29.5113 37.71C29.4813 37.82 29.3413 37.94 29.2313 37.98C27.7413 38.48 26.2713 39.12 24.7313 39.42C22.9513 39.77 21.1413 39.68 19.5513 38.58C18.2213 37.66 17.7313 36.36 17.8113 34.8C17.9013 32.91 18.5113 31.13 19.0013 29.33C19.5213 27.42 20.1113 25.53 20.4613 23.59C20.9413 20.94 19.7813 20.48 17.3913 20.74C16.8013 20.8 16.2313 21.04 15.5813 21.22C15.7213 20.62 15.8313 20.08 15.9913 19.55C16.0213 19.45 19.6313 17.94 21.4413 17.78C23.3513 17.61 25.2013 17.8 26.6013 19.32C27.3913 20.17 27.6113 21.21 27.5913 22.33C27.5413 24.8 26.5813 27.07 25.9813 29.42C25.6113 30.86 25.2513 32.3 24.9313 33.75C24.8413 34.15 24.8413 34.59 24.8813 35C24.9613 35.97 25.4413 36.39 26.4313 36.57C27.6213 36.78 28.7213 36.45 29.9313 36.04C29.7813 36.66 29.6613 37.19 29.5213 37.7L29.5113 37.71ZM26.8513 15.21C26.6513 15.23 26.4613 15.23 26.2013 15.25C24.4013 15.27 22.7313 14.15 22.2013 12.52C21.5913 10.65 22.5613 8.71 24.5313 7.86C26.7913 6.88 29.5813 8.07 30.2713 10.33C31.0513 12.87 29.0613 14.95 26.8613 15.21H26.8513Z"
|
||
fill="white"
|
||
/>
|
||
</svg>
|
||
</Stack>
|
||
);
|
||
}
|