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

453 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
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);
setLocalCenter((prev) => ({ ...prev, x: 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,
}}
/>
<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) }));
}}
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>
);
}