feat: Add language switcher
for preview route
This commit is contained in:
parent
ede6681c28
commit
b837aae711
@ -121,7 +121,7 @@ export const StationEditModal = observer(() => {
|
||||
>
|
||||
<Box sx={style}>
|
||||
<Edit
|
||||
title={<Typography variant="h5">Редактирование станции</Typography>}
|
||||
title={<Typography variant="h5">Редактирование остановки</Typography>}
|
||||
saveButtonProps={saveButtonProps}
|
||||
>
|
||||
<Box
|
||||
|
@ -94,9 +94,9 @@
|
||||
},
|
||||
"station": {
|
||||
"titles": {
|
||||
"create": "Создать станцию",
|
||||
"edit": "Редактировать станцию",
|
||||
"show": "Показать станцию"
|
||||
"create": "Создать остановку",
|
||||
"edit": "Редактировать остановку",
|
||||
"show": "Показать остановку"
|
||||
}
|
||||
},
|
||||
"snapshots": {
|
||||
|
@ -1,179 +1,231 @@
|
||||
import { FederatedMouseEvent, FederatedWheelEvent } from "pixi.js";
|
||||
import { Component, ReactNode, useEffect, useState } from "react";
|
||||
import { Component, ReactNode, useEffect, useState, useRef } from "react";
|
||||
import { useTransform } from "./TransformContext";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { SCALE_FACTOR } from "./Constants";
|
||||
import { useApplication } from "@pixi/react";
|
||||
import { languageStore } from "@/store/LanguageStore";
|
||||
|
||||
class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
|
||||
state = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error("Error caught:", error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.hasError ? <p>Whoopsie Daisy!</p> : this.props.children;
|
||||
}
|
||||
class ErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
state = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error("Error caught:", error, info);
|
||||
}
|
||||
|
||||
export function InfiniteCanvas({children} : Readonly<{children?: ReactNode}>) {
|
||||
const { position, setPosition, scale, setScale, rotation, setRotation, setScreenCenter, screenCenter } = useTransform();
|
||||
const { routeData, originalRouteData } = useMapData();
|
||||
render() {
|
||||
return this.state.hasError ? <p>Whoopsie Daisy!</p> : this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const applicationRef = useApplication();
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [startRotation, setStartRotation] = useState(0);
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||
export function InfiniteCanvas({
|
||||
children,
|
||||
}: Readonly<{ children?: ReactNode }>) {
|
||||
const {
|
||||
position,
|
||||
setPosition,
|
||||
scale,
|
||||
setScale,
|
||||
rotation,
|
||||
setRotation,
|
||||
setScreenCenter,
|
||||
screenCenter,
|
||||
} = useTransform();
|
||||
const { routeData, originalRouteData } = useMapData();
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = applicationRef?.app.canvas;
|
||||
if (!canvas) return;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const canvasLeft = canvasRect?.left ?? 0;
|
||||
const canvasTop = canvasRect?.top ?? 0;
|
||||
const centerX = window.innerWidth / 2 - canvasLeft;
|
||||
const centerY = window.innerHeight / 2 - canvasTop;
|
||||
setScreenCenter({x: centerX, y: centerY});
|
||||
}, [applicationRef?.app.canvas, window.innerWidth, window.innerHeight]);
|
||||
const applicationRef = useApplication();
|
||||
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(true);
|
||||
setStartPosition({
|
||||
x: position.x,
|
||||
y: position.y
|
||||
});
|
||||
setStartMousePosition({
|
||||
x: e.globalX,
|
||||
y: e.globalY
|
||||
});
|
||||
setStartRotation(rotation);
|
||||
e.stopPropagation();
|
||||
};
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [startRotation, setStartRotation] = useState(0);
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRotation((originalRouteData?.rotate ?? 0) * Math.PI / 180);
|
||||
}, [originalRouteData?.rotate]);
|
||||
// Get canvas element and its dimensions/position
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
// Реф для отслеживания последнего значения originalRouteData?.rotate
|
||||
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
||||
|
||||
if (e.shiftKey) {
|
||||
const center = screenCenter ?? {x: 0, y: 0};
|
||||
const startAngle = Math.atan2(startMousePosition.y - center.y, startMousePosition.x - center.x);
|
||||
const currentAngle = Math.atan2(e.globalY - center.y, e.globalX - center.x);
|
||||
useEffect(() => {
|
||||
const canvas = applicationRef?.app.canvas;
|
||||
if (!canvas) return;
|
||||
|
||||
// Calculate rotation difference in radians
|
||||
const rotationDiff = currentAngle - startAngle;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const canvasLeft = canvasRect.left;
|
||||
const canvasTop = canvasRect.top;
|
||||
const centerX = window.innerWidth / 2 - canvasLeft;
|
||||
const centerY = window.innerHeight / 2 - canvasTop;
|
||||
setScreenCenter({ x: centerX, y: centerY });
|
||||
}, [applicationRef?.app.canvas, setScreenCenter]);
|
||||
|
||||
// Update rotation
|
||||
setRotation(startRotation + rotationDiff);
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(true);
|
||||
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
||||
setStartPosition({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
});
|
||||
setStartMousePosition({
|
||||
x: e.globalX,
|
||||
y: e.globalY,
|
||||
});
|
||||
setStartRotation(rotation);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const cosDelta = Math.cos(rotationDiff);
|
||||
const sinDelta = Math.sin(rotationDiff);
|
||||
|
||||
setPosition({
|
||||
x: center.x * (1 - cosDelta) + startPosition.x * cosDelta + (center.y - startPosition.y) * sinDelta,
|
||||
y: center.y * (1 - cosDelta) + startPosition.y * cosDelta + (startPosition.x - center.x) * sinDelta
|
||||
});
|
||||
|
||||
} else {
|
||||
setRotation(startRotation);
|
||||
setPosition({
|
||||
x: startPosition.x - startMousePosition.x + e.globalX,
|
||||
y: startPosition.y - startMousePosition.y + e.globalY
|
||||
});
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
// Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя
|
||||
useEffect(() => {
|
||||
const newRotation = originalRouteData?.rotate ?? 0;
|
||||
|
||||
// Handle mouse up
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(false);
|
||||
e.stopPropagation();
|
||||
};
|
||||
// Handle mouse wheel for zooming
|
||||
const handleWheel = (e: FederatedWheelEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Get mouse position relative to canvas
|
||||
const mouseX = e.globalX - position.x;
|
||||
const mouseY = e.globalY - position.y;
|
||||
// Обновляем rotation только если:
|
||||
// 1. Пользователь не взаимодействует с канвасом
|
||||
// 2. Значение действительно изменилось
|
||||
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
||||
setRotation((newRotation * Math.PI) / 180);
|
||||
lastOriginalRotation.current = newRotation;
|
||||
}
|
||||
}, [originalRouteData?.rotate, isUserInteracting, setRotation]);
|
||||
|
||||
// Calculate new scale
|
||||
const scaleMin = (routeData?.scale_min ?? 10)/SCALE_FACTOR;
|
||||
const scaleMax = (routeData?.scale_max ?? 20)/SCALE_FACTOR;
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
let zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
||||
//const newScale = scale * zoomFactor;
|
||||
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||
zoomFactor = newScale / scale;
|
||||
if (e.shiftKey) {
|
||||
const center = screenCenter ?? { x: 0, y: 0 };
|
||||
const startAngle = Math.atan2(
|
||||
startMousePosition.y - center.y,
|
||||
startMousePosition.x - center.x
|
||||
);
|
||||
const currentAngle = Math.atan2(
|
||||
e.globalY - center.y,
|
||||
e.globalX - center.x
|
||||
);
|
||||
|
||||
if (scale === newScale) {
|
||||
return;
|
||||
}
|
||||
// Calculate rotation difference in radians
|
||||
const rotationDiff = currentAngle - startAngle;
|
||||
|
||||
// Update position to zoom towards mouse cursor
|
||||
setPosition({
|
||||
x: position.x + mouseX * (1 - zoomFactor),
|
||||
y: position.y + mouseY * (1 - zoomFactor)
|
||||
});
|
||||
// Update rotation
|
||||
setRotation(startRotation + rotationDiff);
|
||||
|
||||
setScale(newScale);
|
||||
};
|
||||
const cosDelta = Math.cos(rotationDiff);
|
||||
const sinDelta = Math.sin(rotationDiff);
|
||||
|
||||
useEffect(() => {
|
||||
applicationRef?.app.render();
|
||||
console.log(position, scale, rotation);
|
||||
}, [position, scale, rotation]);
|
||||
setPosition({
|
||||
x:
|
||||
center.x * (1 - cosDelta) +
|
||||
startPosition.x * cosDelta +
|
||||
(center.y - startPosition.y) * sinDelta,
|
||||
y:
|
||||
center.y * (1 - cosDelta) +
|
||||
startPosition.y * cosDelta +
|
||||
(startPosition.x - center.x) * sinDelta,
|
||||
});
|
||||
} else {
|
||||
setRotation(startRotation);
|
||||
setPosition({
|
||||
x: startPosition.x - startMousePosition.x + e.globalX,
|
||||
y: startPosition.y - startMousePosition.y + e.globalY,
|
||||
});
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{applicationRef?.app && (
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
const canvas = applicationRef.app.canvas;
|
||||
g.clear();
|
||||
g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
|
||||
g.fill("#111");
|
||||
}}
|
||||
eventMode={'static'}
|
||||
interactive
|
||||
onPointerDown={handlePointerDown}
|
||||
onGlobalPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerUpOutside={handlePointerUp}
|
||||
onWheel={handleWheel}
|
||||
/>
|
||||
)}
|
||||
<pixiContainer
|
||||
x={position.x}
|
||||
y={position.y}
|
||||
scale={scale}
|
||||
rotation={rotation}
|
||||
>
|
||||
{children}
|
||||
</pixiContainer>
|
||||
{/* Show center of the screen.
|
||||
<pixiGraphics
|
||||
eventMode="none"
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(false);
|
||||
// Сбрасываем флаг взаимодействия через небольшую задержку
|
||||
// чтобы избежать немедленного срабатывания useEffect
|
||||
setTimeout(() => {
|
||||
setIsUserInteracting(false);
|
||||
}, 100);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
const center = screenCenter ?? {x: 0, y: 0};
|
||||
g.circle(center.x, center.y, 1);
|
||||
g.fill("#fff");
|
||||
}}
|
||||
/> */}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
const handleWheel = (e: FederatedWheelEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsUserInteracting(true); // Устанавливаем флаг при зуме
|
||||
|
||||
// Get mouse position relative to canvas
|
||||
const mouseX = e.globalX - position.x;
|
||||
const mouseY = e.globalY - position.y;
|
||||
|
||||
// Calculate new scale
|
||||
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
||||
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
||||
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
||||
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||
const actualZoomFactor = newScale / scale;
|
||||
|
||||
if (scale === newScale) {
|
||||
// Сбрасываем флаг, если зум не изменился
|
||||
setTimeout(() => {
|
||||
setIsUserInteracting(false);
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update position to zoom towards mouse cursor
|
||||
setPosition({
|
||||
x: position.x + mouseX * (1 - actualZoomFactor),
|
||||
y: position.y + mouseY * (1 - actualZoomFactor),
|
||||
});
|
||||
|
||||
setScale(newScale);
|
||||
|
||||
// Сбрасываем флаг взаимодействия через задержку
|
||||
setTimeout(() => {
|
||||
setIsUserInteracting(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
applicationRef?.app.render();
|
||||
console.log(position, scale, rotation);
|
||||
}, [position, scale, rotation]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{applicationRef?.app && (
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
const canvas = applicationRef.app.canvas;
|
||||
g.clear();
|
||||
g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
|
||||
g.fill("#111");
|
||||
}}
|
||||
eventMode={"static"}
|
||||
interactive
|
||||
onPointerDown={handlePointerDown}
|
||||
onGlobalPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerUpOutside={handlePointerUp}
|
||||
onWheel={handleWheel}
|
||||
/>
|
||||
)}
|
||||
<pixiContainer
|
||||
x={position.x}
|
||||
y={position.y}
|
||||
scale={scale}
|
||||
rotation={rotation}
|
||||
>
|
||||
{children}
|
||||
</pixiContainer>
|
||||
{/* Show center of the screen.
|
||||
<pixiGraphics
|
||||
eventMode="none"
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
const center = screenCenter ?? {x: 0, y: 0};
|
||||
g.circle(center.x, center.y, 1);
|
||||
g.fill("#fff");
|
||||
}}
|
||||
/> */}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
@ -1,33 +1,89 @@
|
||||
import { Stack, Typography, Button } from "@mui/material";
|
||||
|
||||
import { useNavigate, useNavigationType } from "react-router";
|
||||
|
||||
export function LeftSidebar() {
|
||||
return (
|
||||
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
||||
<Stack direction="column" alignItems="center" justifyContent="center" my={10}>
|
||||
<img src={"/Emblem.svg"} alt="logo" width={100} height={100} />
|
||||
<Typography sx={{ mb: 2 }} textAlign="center">
|
||||
При поддержке Правительства
|
||||
Санкт-Петербурга
|
||||
</Typography>
|
||||
</Stack>
|
||||
const navigate = useNavigate();
|
||||
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||
|
||||
|
||||
<Stack direction="column" alignItems="center" justifyContent="center" my={10} spacing={2}>
|
||||
<Button variant="outlined" color="warning" fullWidth>
|
||||
Достопримечательности
|
||||
</Button>
|
||||
<Button variant="outlined" color="warning" fullWidth>
|
||||
Остановки
|
||||
</Button>
|
||||
</Stack>
|
||||
const handleBack = () => {
|
||||
if (navigationType === "PUSH") {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate("/route");
|
||||
}
|
||||
};
|
||||
|
||||
<Stack direction="column" alignItems="center" justifyContent="center" my={10}>
|
||||
<img src={"/GET.png"} alt="logo" width="80%" style={{margin: "0 auto"}}/>
|
||||
</Stack>
|
||||
|
||||
<Typography variant="h6" textAlign="center" mt="auto">#ВсемПоПути</Typography>
|
||||
return (
|
||||
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
type="button"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
color: "#fff",
|
||||
backgroundColor: "#222",
|
||||
borderRadius: 10,
|
||||
width: "100%",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<p>Назад</p>
|
||||
</button>
|
||||
|
||||
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
>
|
||||
<img src={"/Emblem.svg"} alt="logo" width={100} height={100} />
|
||||
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||
При поддержке Правительства Санкт-Петербурга
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
spacing={2}
|
||||
>
|
||||
<Button variant="outlined" color="warning" fullWidth>
|
||||
Достопримечательности
|
||||
</Button>
|
||||
<Button variant="outlined" color="warning" fullWidth>
|
||||
Остановки
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
>
|
||||
<img
|
||||
src={"/GET.png"}
|
||||
alt="logo"
|
||||
width="80%"
|
||||
style={{ margin: "0 auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign="center"
|
||||
mt="auto"
|
||||
sx={{ color: "#fff" }}
|
||||
>
|
||||
#ВсемПоПути
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCustom, useApiUrl } from "@refinedev/core";
|
||||
import { useApiUrl } from "@refinedev/core";
|
||||
import { useParams } from "react-router";
|
||||
import {
|
||||
createContext,
|
||||
@ -16,6 +16,9 @@ import {
|
||||
StationPatchData,
|
||||
} from "./types";
|
||||
import { axiosInstance } from "../../providers/data";
|
||||
import { languageStore, META_LANGUAGE } from "@/store/LanguageStore";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { axiosInstanceForGet } from "@/providers/data";
|
||||
|
||||
const MapDataContext = createContext<{
|
||||
originalRouteData?: RouteData;
|
||||
@ -57,210 +60,230 @@ const MapDataContext = createContext<{
|
||||
saveChanges: () => {},
|
||||
});
|
||||
|
||||
export function MapDataProvider({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>) {
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
const apiUrl = useApiUrl();
|
||||
export const MapDataProvider = observer(
|
||||
({ children }: Readonly<{ children: ReactNode }>) => {
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
const apiUrl = useApiUrl();
|
||||
|
||||
const [originalRouteData, setOriginalRouteData] = useState<RouteData>();
|
||||
const [originalStationData, setOriginalStationData] =
|
||||
useState<StationData[]>();
|
||||
const [originalSightData, setOriginalSightData] = useState<SightData[]>();
|
||||
const [originalRouteData, setOriginalRouteData] = useState<RouteData>();
|
||||
const [originalStationData, setOriginalStationData] =
|
||||
useState<StationData[]>();
|
||||
const [originalSightData, setOriginalSightData] = useState<SightData[]>();
|
||||
|
||||
const [routeData, setRouteData] = useState<RouteData>();
|
||||
const [stationData, setStationData] = useState<StationData[]>();
|
||||
const [sightData, setSightData] = useState<SightData[]>();
|
||||
const [routeData, setRouteData] = useState<RouteData>();
|
||||
const [stationData, setStationData] = useState<StationData[]>();
|
||||
const [sightData, setSightData] = useState<SightData[]>();
|
||||
|
||||
const [routeChanges, setRouteChanges] = useState<RouteData>({} as RouteData);
|
||||
const [stationChanges, setStationChanges] = useState<StationPatchData[]>([]);
|
||||
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
|
||||
const [routeChanges, setRouteChanges] = useState<Partial<RouteData>>({});
|
||||
const [stationChanges, setStationChanges] = useState<StationPatchData[]>(
|
||||
[]
|
||||
);
|
||||
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
|
||||
const { language } = languageStore;
|
||||
|
||||
const { data: routeQuery, isLoading: isRouteLoading } = useCustom({
|
||||
url: `${apiUrl}/route/${routeId}`,
|
||||
method: "get",
|
||||
});
|
||||
const { data: stationQuery, isLoading: isStationLoading } = useCustom({
|
||||
url: `${apiUrl}/route/${routeId}/station`,
|
||||
method: "get",
|
||||
});
|
||||
const { data: sightQuery, isLoading: isSightLoading } = useCustom({
|
||||
url: `${apiUrl}/route/${routeId}/sight`,
|
||||
method: "get",
|
||||
});
|
||||
useEffect(() => {
|
||||
// if not undefined, set original data
|
||||
if (routeQuery?.data) setOriginalRouteData(routeQuery.data as RouteData);
|
||||
if (stationQuery?.data)
|
||||
setOriginalStationData(stationQuery.data as StationData[]);
|
||||
if (sightQuery?.data) setOriginalSightData(sightQuery.data as SightData[]);
|
||||
console.log("queries", routeQuery, stationQuery, sightQuery);
|
||||
}, [routeQuery, stationQuery, sightQuery]);
|
||||
const [isRouteLoading, setIsRouteLoading] = useState(true);
|
||||
const [isStationLoading, setIsStationLoading] = useState(true);
|
||||
const [isSightLoading, setIsSightLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// combine changes with original data
|
||||
if (originalRouteData)
|
||||
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||
if (originalStationData) setStationData(originalStationData);
|
||||
if (originalSightData) setSightData(originalSightData);
|
||||
}, [
|
||||
originalRouteData,
|
||||
originalStationData,
|
||||
originalSightData,
|
||||
routeChanges,
|
||||
stationChanges,
|
||||
sightChanges,
|
||||
]);
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsRouteLoading(true);
|
||||
setIsStationLoading(true);
|
||||
setIsSightLoading(true);
|
||||
|
||||
function setScaleRange(min: number, max: number) {
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, scale_min: min, scale_max: max };
|
||||
});
|
||||
}
|
||||
const [routeResponse, stationResponse, sightResponse] =
|
||||
await Promise.all([
|
||||
axiosInstanceForGet.get(`/route/${routeId}`),
|
||||
axiosInstanceForGet.get(`/route/${routeId}/station`),
|
||||
axiosInstanceForGet.get(`/route/${routeId}/sight`),
|
||||
]);
|
||||
|
||||
function setMapRotation(rotation: number) {
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, rotate: rotation };
|
||||
});
|
||||
}
|
||||
setOriginalRouteData(routeResponse.data as RouteData);
|
||||
setOriginalStationData(stationResponse.data as StationData[]);
|
||||
setOriginalSightData(sightResponse.data as SightData[]);
|
||||
|
||||
function setMapCenter(x: number, y: number) {
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, center_latitude: x, center_longitude: y };
|
||||
});
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
await axiosInstance.patch(`/route/${routeId}`, routeData);
|
||||
await saveStationChanges();
|
||||
await saveSightChanges();
|
||||
}
|
||||
|
||||
async function saveStationChanges() {
|
||||
for (const station of stationChanges) {
|
||||
const response = await axiosInstance.patch(
|
||||
`/route/${routeId}/station`,
|
||||
station
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSightChanges() {
|
||||
console.log("sightChanges", sightChanges);
|
||||
for (const sight of sightChanges) {
|
||||
const response = await axiosInstance.patch(
|
||||
`/route/${routeId}/sight`,
|
||||
sight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setStationOffset(stationId: number, x: number, y: number) {
|
||||
setStationChanges((prev) => {
|
||||
let found = prev.find((station) => station.station_id === stationId);
|
||||
if (found) {
|
||||
found.offset_x = x;
|
||||
found.offset_y = y;
|
||||
|
||||
return prev.map((station) => {
|
||||
if (station.station_id === stationId) {
|
||||
return found;
|
||||
}
|
||||
return station;
|
||||
});
|
||||
} else {
|
||||
const foundStation = stationData?.find(
|
||||
(station) => station.id === stationId
|
||||
);
|
||||
if (foundStation) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
station_id: stationId,
|
||||
offset_x: x,
|
||||
offset_y: y,
|
||||
transfers: foundStation.transfers,
|
||||
},
|
||||
];
|
||||
setIsRouteLoading(false);
|
||||
setIsStationLoading(false);
|
||||
setIsSightLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setIsRouteLoading(false);
|
||||
setIsStationLoading(false);
|
||||
setIsSightLoading(false);
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function setSightCoordinates(
|
||||
sightId: number,
|
||||
latitude: number,
|
||||
longitude: number
|
||||
) {
|
||||
setSightChanges((prev) => {
|
||||
let found = prev.find((sight) => sight.sight_id === sightId);
|
||||
if (found) {
|
||||
found.latitude = latitude;
|
||||
found.longitude = longitude;
|
||||
fetchData();
|
||||
}, [routeId, language]);
|
||||
|
||||
return prev.map((sight) => {
|
||||
if (sight.sight_id === sightId) {
|
||||
return found;
|
||||
}
|
||||
return sight;
|
||||
});
|
||||
} else {
|
||||
const foundSight = sightData?.find((sight) => sight.id === sightId);
|
||||
if (foundSight) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
sight_id: sightId,
|
||||
latitude,
|
||||
longitude,
|
||||
},
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("sightChanges", sightChanges);
|
||||
}, [sightChanges]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
originalRouteData: originalRouteData,
|
||||
originalStationData: originalStationData,
|
||||
originalSightData: originalSightData,
|
||||
routeData: routeData,
|
||||
stationData: stationData,
|
||||
sightData: sightData,
|
||||
isRouteLoading,
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
setScaleRange,
|
||||
setMapRotation,
|
||||
setMapCenter,
|
||||
saveChanges,
|
||||
setStationOffset,
|
||||
setSightCoordinates,
|
||||
}),
|
||||
[
|
||||
useEffect(() => {
|
||||
// combine changes with original data
|
||||
if (originalRouteData)
|
||||
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||
if (originalStationData) setStationData(originalStationData);
|
||||
if (originalSightData) setSightData(originalSightData);
|
||||
}, [
|
||||
originalRouteData,
|
||||
originalStationData,
|
||||
originalSightData,
|
||||
routeData,
|
||||
stationData,
|
||||
sightData,
|
||||
isRouteLoading,
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
]
|
||||
);
|
||||
routeChanges,
|
||||
stationChanges,
|
||||
sightChanges,
|
||||
]);
|
||||
|
||||
return (
|
||||
<MapDataContext.Provider value={value}>{children}</MapDataContext.Provider>
|
||||
);
|
||||
}
|
||||
function setScaleRange(min: number, max: number) {
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, scale_min: min, scale_max: max };
|
||||
});
|
||||
}
|
||||
|
||||
function setMapRotation(rotation: number) {
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, rotate: rotation };
|
||||
});
|
||||
}
|
||||
|
||||
function setMapCenter(x: number, y: number) {
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, center_latitude: x, center_longitude: y };
|
||||
});
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
await axiosInstance.patch(`/route/${routeId}`, routeData);
|
||||
await saveStationChanges();
|
||||
await saveSightChanges();
|
||||
}
|
||||
|
||||
async function saveStationChanges() {
|
||||
for (const station of stationChanges) {
|
||||
const response = await axiosInstance.patch(
|
||||
`/route/${routeId}/station`,
|
||||
station
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSightChanges() {
|
||||
console.log("sightChanges", sightChanges);
|
||||
for (const sight of sightChanges) {
|
||||
const response = await axiosInstance.patch(
|
||||
`/route/${routeId}/sight`,
|
||||
sight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setStationOffset(stationId: number, x: number, y: number) {
|
||||
setStationChanges((prev) => {
|
||||
let found = prev.find((station) => station.station_id === stationId);
|
||||
if (found) {
|
||||
found.offset_x = x;
|
||||
found.offset_y = y;
|
||||
|
||||
return prev.map((station) => {
|
||||
if (station.station_id === stationId) {
|
||||
return found;
|
||||
}
|
||||
return station;
|
||||
});
|
||||
} else {
|
||||
const foundStation = stationData?.find(
|
||||
(station) => station.id === stationId
|
||||
);
|
||||
if (foundStation) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
station_id: stationId,
|
||||
offset_x: x,
|
||||
offset_y: y,
|
||||
transfers: foundStation.transfers,
|
||||
},
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setSightCoordinates(
|
||||
sightId: number,
|
||||
latitude: number,
|
||||
longitude: number
|
||||
) {
|
||||
setSightChanges((prev) => {
|
||||
let found = prev.find((sight) => sight.sight_id === sightId);
|
||||
if (found) {
|
||||
found.latitude = latitude;
|
||||
found.longitude = longitude;
|
||||
|
||||
return prev.map((sight) => {
|
||||
if (sight.sight_id === sightId) {
|
||||
return found;
|
||||
}
|
||||
return sight;
|
||||
});
|
||||
} else {
|
||||
const foundSight = sightData?.find((sight) => sight.id === sightId);
|
||||
if (foundSight) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
sight_id: sightId,
|
||||
latitude,
|
||||
longitude,
|
||||
},
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("sightChanges", sightChanges);
|
||||
}, [sightChanges]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
originalRouteData: originalRouteData,
|
||||
originalStationData: originalStationData,
|
||||
originalSightData: originalSightData,
|
||||
routeData: routeData,
|
||||
stationData: stationData,
|
||||
sightData: sightData,
|
||||
isRouteLoading,
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
setScaleRange,
|
||||
setMapRotation,
|
||||
setMapCenter,
|
||||
saveChanges,
|
||||
setStationOffset,
|
||||
setSightCoordinates,
|
||||
}),
|
||||
[
|
||||
originalRouteData,
|
||||
originalStationData,
|
||||
originalSightData,
|
||||
routeData,
|
||||
stationData,
|
||||
sightData,
|
||||
isRouteLoading,
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<MapDataContext.Provider value={value}>
|
||||
{children}
|
||||
</MapDataContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const useMapData = () => {
|
||||
const context = useContext(MapDataContext);
|
||||
|
@ -5,187 +5,228 @@ import { useTransform } from "./TransformContext";
|
||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||
|
||||
export function RightSidebar() {
|
||||
const { routeData, setScaleRange, saveChanges, originalRouteData, setMapRotation, setMapCenter } = useMapData();
|
||||
const { rotation, position, screenToLocal, screenCenter, rotateToAngle, setTransform } = useTransform();
|
||||
const [minScale, setMinScale] = useState<number>(1);
|
||||
const [maxScale, setMaxScale] = useState<number>(10);
|
||||
const [localCenter, setLocalCenter] = useState<{x: number, y: number}>({x: 0, y: 0});
|
||||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||
const {
|
||||
routeData,
|
||||
setScaleRange,
|
||||
saveChanges,
|
||||
originalRouteData,
|
||||
setMapRotation,
|
||||
setMapCenter,
|
||||
} = useMapData();
|
||||
const {
|
||||
rotation,
|
||||
position,
|
||||
screenToLocal,
|
||||
screenCenter,
|
||||
rotateToAngle,
|
||||
setTransform,
|
||||
} = useTransform();
|
||||
const [minScale, setMinScale] = useState<number>(1);
|
||||
const [maxScale, setMaxScale] = useState<number>(10);
|
||||
const [localCenter, setLocalCenter] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if(originalRouteData) {
|
||||
setMinScale(originalRouteData.scale_min ?? 1);
|
||||
setMaxScale(originalRouteData.scale_max ?? 10);
|
||||
setRotationDegrees(originalRouteData.rotate ?? 0);
|
||||
setLocalCenter({x: originalRouteData.center_latitude ?? 0, y: originalRouteData.center_longitude ?? 0});
|
||||
}
|
||||
}, [originalRouteData]);
|
||||
useEffect(() => {
|
||||
if (originalRouteData) {
|
||||
setMinScale(originalRouteData.scale_min ?? 1);
|
||||
setMaxScale(originalRouteData.scale_max ?? 10);
|
||||
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(() => {
|
||||
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(() => {
|
||||
setRotationDegrees((Math.round(rotation * 180 / Math.PI) % 360 + 360) % 360);
|
||||
}, [rotation]);
|
||||
useEffect(() => {
|
||||
setMapRotation(rotationDegrees);
|
||||
}, [rotationDegrees]);
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
useEffect(() => {
|
||||
setMapCenter(localCenter.x, localCenter.y);
|
||||
}, [localCenter]);
|
||||
|
||||
function setRotationFromDegrees(degrees: number) {
|
||||
rotateToAngle((degrees * Math.PI) / 180);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMapCenter(localCenter.x, localCenter.y);
|
||||
}, [localCenter]);
|
||||
function pan({ x, y }: { x: number; y: number }) {
|
||||
const coordinates = coordinatesToLocal(x, y);
|
||||
setTransform(coordinates.x, coordinates.y);
|
||||
}
|
||||
|
||||
function setRotationFromDegrees(degrees: number) {
|
||||
rotateToAngle(degrees * Math.PI / 180);
|
||||
}
|
||||
if (!routeData) {
|
||||
console.error("routeData is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
function pan({x, y}: {x: number, y: number}) {
|
||||
const coordinates = coordinatesToLocal(x,y);
|
||||
setTransform(coordinates.x, coordinates.y);
|
||||
}
|
||||
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>
|
||||
|
||||
if(!routeData) {
|
||||
console.error("routeData is null");
|
||||
return null;
|
||||
}
|
||||
<Stack spacing={2} direction="row" alignItems="center">
|
||||
<TextField
|
||||
type="number"
|
||||
label="Минимальный масштаб"
|
||||
variant="filled"
|
||||
value={minScale}
|
||||
onChange={(e) => setMinScale(Number(e.target.value))}
|
||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
color: "#fff",
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0.1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Максимальный масштаб"
|
||||
variant="filled"
|
||||
value={maxScale}
|
||||
onChange={(e) => setMaxScale(Number(e.target.value))}
|
||||
style={{ backgroundColor: "#222", borderRadius: 4, color: "#fff" }}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
color: "#fff",
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0.1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
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 }} textAlign="center">
|
||||
Детали о достопримечательностях
|
||||
</Typography>
|
||||
<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 spacing={2} direction="row" alignItems="center">
|
||||
<TextField
|
||||
type="number"
|
||||
label="Минимальный масштаб"
|
||||
variant="filled"
|
||||
value={minScale}
|
||||
onChange={(e) => setMinScale(Number(e.target.value))}
|
||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
||||
sx={{
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: "#fff"
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0.1
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Максимальный масштаб"
|
||||
variant="filled"
|
||||
value={maxScale}
|
||||
onChange={(e) => setMaxScale(Number(e.target.value))}
|
||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
||||
sx={{
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: "#fff"
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0.1
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Центр карты, широта"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.x * 100000) / 100000}
|
||||
onChange={(e) => {
|
||||
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
||||
pan({ x: Number(e.target.value), y: localCenter.y });
|
||||
}}
|
||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
color: "#fff",
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Центр карты, высота"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.y * 100000) / 100000}
|
||||
onChange={(e) => {
|
||||
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
||||
pan({ x: localCenter.x, y: Number(e.target.value) });
|
||||
}}
|
||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
color: "#fff",
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<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.Mui-focused': {
|
||||
color: "#fff"
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0,
|
||||
max: 360
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Центр карты, широта"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.x*100000)/100000}
|
||||
onChange={(e) => {
|
||||
setLocalCenter(prev => ({...prev, x: Number(e.target.value)}))
|
||||
pan({x: Number(e.target.value), y: localCenter.y});
|
||||
}}
|
||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
||||
sx={{
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: "#fff"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Центр карты, высота"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.y*100000)/100000}
|
||||
onChange={(e) => {
|
||||
setLocalCenter(prev => ({...prev, y: Number(e.target.value)}))
|
||||
pan({x: localCenter.x, y: Number(e.target.value)});
|
||||
}}
|
||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
||||
sx={{
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: "#fff"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={() => {
|
||||
saveChanges();
|
||||
}}
|
||||
>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={() => {
|
||||
saveChanges();
|
||||
}}
|
||||
>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,150 +1,204 @@
|
||||
import { createContext, ReactNode, useContext, useMemo, useState } from "react";
|
||||
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 },
|
||||
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>>
|
||||
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: () => {}
|
||||
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}>();
|
||||
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;
|
||||
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;
|
||||
// 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
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: rotatedX / UP_SCALE,
|
||||
y: rotatedY / UP_SCALE,
|
||||
};
|
||||
},
|
||||
[position.x, position.y, scale, rotation]
|
||||
);
|
||||
|
||||
// Inverse of screenToLocal
|
||||
function localToScreen(localX: number, localY: number) {
|
||||
// Inverse of screenToLocal
|
||||
const localToScreen = useCallback(
|
||||
(localX: number, localY: number) => {
|
||||
const upscaledX = localX * UP_SCALE;
|
||||
const upscaledY = localY * UP_SCALE;
|
||||
|
||||
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 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;
|
||||
|
||||
const translatedX = rotatedX*scale + position.x;
|
||||
const translatedY = rotatedY*scale + position.y;
|
||||
return {
|
||||
x: translatedX,
|
||||
y: translatedY,
|
||||
};
|
||||
},
|
||||
[position.x, position.y, scale, rotation]
|
||||
);
|
||||
|
||||
return {
|
||||
x: translatedX,
|
||||
y: translatedY
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
const currentFromPosition = fromPosition ?? position;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
// Update both rotation and position in a single batch to avoid stale closure
|
||||
setRotation(to);
|
||||
setPosition(newPosition);
|
||||
},
|
||||
[rotation, position, screenCenter]
|
||||
);
|
||||
|
||||
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 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 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;
|
||||
console.log("center", center.x, center.y);
|
||||
|
||||
|
||||
setPosition(newPosition);
|
||||
setRotation(selectedRotation);
|
||||
setScale(selectedScale);
|
||||
}
|
||||
const newPosition = {
|
||||
x: -latitude * UP_SCALE * selectedScale,
|
||||
y: -longitude * UP_SCALE * selectedScale,
|
||||
};
|
||||
|
||||
const value = useMemo(() => ({
|
||||
position,
|
||||
scale,
|
||||
rotation,
|
||||
screenCenter,
|
||||
setPosition,
|
||||
setScale,
|
||||
setRotation,
|
||||
rotateToAngle,
|
||||
screenToLocal,
|
||||
localToScreen,
|
||||
setTransform,
|
||||
setScreenCenter
|
||||
}), [position, scale, rotation, screenCenter]);
|
||||
const cosRot = Math.cos(selectedRotation);
|
||||
const sinRot = Math.sin(selectedRotation);
|
||||
|
||||
return (
|
||||
<TransformContext.Provider value={value}>
|
||||
{children}
|
||||
</TransformContext.Provider>
|
||||
);
|
||||
// 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 value = useMemo(
|
||||
() => ({
|
||||
position,
|
||||
scale,
|
||||
rotation,
|
||||
screenCenter,
|
||||
setPosition,
|
||||
setScale,
|
||||
setRotation,
|
||||
rotateToAngle,
|
||||
screenToLocal,
|
||||
localToScreen,
|
||||
setTransform,
|
||||
setScreenCenter,
|
||||
}),
|
||||
[
|
||||
position,
|
||||
scale,
|
||||
rotation,
|
||||
screenCenter,
|
||||
rotateToAngle,
|
||||
screenToLocal,
|
||||
localToScreen,
|
||||
setTransform,
|
||||
]
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
const context = useContext(TransformContext);
|
||||
if (!context) {
|
||||
throw new Error("useTransform must be used within a TransformProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
@ -3,37 +3,32 @@ import { useCallback } from "react";
|
||||
import { PATH_COLOR, PATH_WIDTH } from "./Constants";
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
|
||||
|
||||
interface TravelPathProps {
|
||||
points: {x: number, y: number}[];
|
||||
points: { x: number; y: number }[];
|
||||
}
|
||||
|
||||
export function TravelPath({
|
||||
points
|
||||
}: Readonly<TravelPathProps>) {
|
||||
export function TravelPath({ points }: Readonly<TravelPathProps>) {
|
||||
const draw = useCallback(
|
||||
(g: Graphics) => {
|
||||
g.clear();
|
||||
const coordStart = coordinatesToLocal(points[0].x, points[0].y);
|
||||
g.moveTo(coordStart.x, coordStart.y);
|
||||
for (let i = 1; i <= points.length - 1; i++) {
|
||||
const coordinates = coordinatesToLocal(points[i].x, points[i].y);
|
||||
g.lineTo(coordinates.x, coordinates.y);
|
||||
}
|
||||
g.stroke({
|
||||
color: PATH_COLOR,
|
||||
width: PATH_WIDTH,
|
||||
});
|
||||
},
|
||||
[points]
|
||||
);
|
||||
|
||||
const draw = useCallback((g: Graphics) => {
|
||||
g.clear();
|
||||
const coordStart = coordinatesToLocal(points[0].x, points[0].y);
|
||||
g.moveTo(coordStart.x, coordStart.y);
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const coordinates = coordinatesToLocal(points[i].x, points[i].y);
|
||||
g.lineTo(coordinates.x, coordinates.y);
|
||||
}
|
||||
g.stroke({
|
||||
color: PATH_COLOR,
|
||||
width: PATH_WIDTH
|
||||
});
|
||||
}, [points]);
|
||||
if (points.length === 0) {
|
||||
console.error("points is empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(points.length === 0) {
|
||||
console.error("points is empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<pixiGraphics
|
||||
draw={draw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <pixiGraphics draw={draw} />;
|
||||
}
|
||||
|
@ -1,31 +1,43 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
|
||||
export function Widgets() {
|
||||
return (
|
||||
<Stack
|
||||
direction="column" spacing={2}
|
||||
position="absolute"
|
||||
top={32} left={32}
|
||||
sx={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<Stack bgcolor="primary.main"
|
||||
width={361} height={96}
|
||||
p={2} m={2}
|
||||
borderRadius={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Typography variant="h6">Станция</Typography>
|
||||
</Stack>
|
||||
<Stack bgcolor="primary.main"
|
||||
width={223} height={262}
|
||||
p={2} m={2}
|
||||
borderRadius={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Typography variant="h6">Погода</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={2}
|
||||
position="absolute"
|
||||
top={32}
|
||||
left={32}
|
||||
sx={{ pointerEvents: "none" }}
|
||||
>
|
||||
<Stack
|
||||
bgcolor="primary.main"
|
||||
width={361}
|
||||
height={96}
|
||||
p={2}
|
||||
m={2}
|
||||
borderRadius={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||
Станция
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
bgcolor="primary.main"
|
||||
width={223}
|
||||
height={262}
|
||||
p={2}
|
||||
m={2}
|
||||
borderRadius={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||
Погода
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +1,14 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
|
||||
import { Application, ApplicationRef, extend } from "@pixi/react";
|
||||
import {
|
||||
Application,
|
||||
ApplicationRef,
|
||||
extend
|
||||
} from '@pixi/react';
|
||||
import {
|
||||
Container,
|
||||
Graphics,
|
||||
Sprite,
|
||||
Texture,
|
||||
TilingSprite,
|
||||
Text
|
||||
} from 'pixi.js';
|
||||
Container,
|
||||
Graphics,
|
||||
Sprite,
|
||||
Texture,
|
||||
TilingSprite,
|
||||
Text,
|
||||
} from "pixi.js";
|
||||
import { Stack } from "@mui/material";
|
||||
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||
import { TransformProvider, useTransform } from "./TransformContext";
|
||||
@ -25,128 +21,147 @@ import { LeftSidebar } from "./LeftSidebar";
|
||||
import { RightSidebar } from "./RightSidebar";
|
||||
import { Widgets } from "./Widgets";
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { LanguageSwitch } from "@/components/LanguageSwitch";
|
||||
|
||||
extend({
|
||||
Container,
|
||||
Graphics,
|
||||
Sprite,
|
||||
Texture,
|
||||
TilingSprite,
|
||||
Text
|
||||
Container,
|
||||
Graphics,
|
||||
Sprite,
|
||||
Texture,
|
||||
TilingSprite,
|
||||
Text,
|
||||
});
|
||||
|
||||
export const RoutePreview = () => {
|
||||
return (
|
||||
<MapDataProvider>
|
||||
<TransformProvider>
|
||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||
<LeftSidebar />
|
||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||
<Widgets />
|
||||
<RouteMap />
|
||||
<RightSidebar />
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</TransformProvider>
|
||||
</MapDataProvider>
|
||||
);
|
||||
return (
|
||||
<MapDataProvider>
|
||||
<TransformProvider>
|
||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<LanguageSwitch />
|
||||
</div>
|
||||
<LeftSidebar />
|
||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||
<Widgets />
|
||||
<RouteMap />
|
||||
<RightSidebar />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TransformProvider>
|
||||
</MapDataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export function RouteMap() {
|
||||
const { setPosition, screenToLocal, setTransform, screenCenter } = useTransform();
|
||||
const {
|
||||
routeData, stationData, sightData, originalRouteData
|
||||
} = useMapData();
|
||||
const [points, setPoints] = useState<{x: number, y: number}[]>([]);
|
||||
const [isSetup, setIsSetup] = useState(false);
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (originalRouteData) {
|
||||
const path = originalRouteData?.path;
|
||||
const points = path?.map(([x, y]: [number, number]) => ({x: x * UP_SCALE, y: y * UP_SCALE})) ?? [];
|
||||
setPoints(points);
|
||||
}
|
||||
}, [originalRouteData]);
|
||||
const { setPosition, screenToLocal, setTransform, screenCenter } =
|
||||
useTransform();
|
||||
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||
const [isSetup, setIsSetup] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if(isSetup || !screenCenter) {
|
||||
return;
|
||||
}
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (
|
||||
originalRouteData?.center_latitude === originalRouteData?.center_longitude &&
|
||||
originalRouteData?.center_latitude === 0
|
||||
) {
|
||||
if (points.length > 0) {
|
||||
let boundingBox = {
|
||||
from: {x: Infinity, y: Infinity},
|
||||
to: {x: -Infinity, y: -Infinity}
|
||||
};
|
||||
for (const point of points) {
|
||||
boundingBox.from.x = Math.min(boundingBox.from.x, point.x);
|
||||
boundingBox.from.y = Math.min(boundingBox.from.y, point.y);
|
||||
boundingBox.to.x = Math.max(boundingBox.to.x, point.x);
|
||||
boundingBox.to.y = Math.max(boundingBox.to.y, point.y);
|
||||
}
|
||||
const newCenter = {
|
||||
x: -(boundingBox.from.x + boundingBox.to.x) / 2,
|
||||
y: -(boundingBox.from.y + boundingBox.to.y) / 2
|
||||
};
|
||||
setPosition(newCenter);
|
||||
setIsSetup(true);
|
||||
}
|
||||
} else if (
|
||||
originalRouteData?.center_latitude &&
|
||||
originalRouteData?.center_longitude
|
||||
) {
|
||||
const coordinates = coordinatesToLocal(originalRouteData?.center_latitude, originalRouteData?.center_longitude);
|
||||
|
||||
setTransform(
|
||||
coordinates.x,
|
||||
coordinates.y,
|
||||
originalRouteData?.rotate,
|
||||
originalRouteData?.scale_min
|
||||
);
|
||||
setIsSetup(true);
|
||||
}
|
||||
}, [points, originalRouteData?.center_latitude, originalRouteData?.center_longitude, originalRouteData?.rotate, isSetup, screenCenter]);
|
||||
useEffect(() => {
|
||||
if (originalRouteData) {
|
||||
const path = originalRouteData?.path;
|
||||
const points =
|
||||
path?.map(([x, y]: [number, number]) => ({
|
||||
x: x * UP_SCALE,
|
||||
y: y * UP_SCALE,
|
||||
})) ?? [];
|
||||
setPoints(points);
|
||||
}
|
||||
}, [originalRouteData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSetup || !screenCenter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!routeData || !stationData || !sightData) {
|
||||
console.error("routeData, stationData or sightData is null");
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
if (
|
||||
originalRouteData?.center_latitude ===
|
||||
originalRouteData?.center_longitude &&
|
||||
originalRouteData?.center_latitude === 0
|
||||
) {
|
||||
if (points.length > 0) {
|
||||
let boundingBox = {
|
||||
from: { x: Infinity, y: Infinity },
|
||||
to: { x: -Infinity, y: -Infinity },
|
||||
};
|
||||
for (const point of points) {
|
||||
boundingBox.from.x = Math.min(boundingBox.from.x, point.x);
|
||||
boundingBox.from.y = Math.min(boundingBox.from.y, point.y);
|
||||
boundingBox.to.x = Math.max(boundingBox.to.x, point.x);
|
||||
boundingBox.to.y = Math.max(boundingBox.to.y, point.y);
|
||||
}
|
||||
const newCenter = {
|
||||
x: -(boundingBox.from.x + boundingBox.to.x) / 2,
|
||||
y: -(boundingBox.from.y + boundingBox.to.y) / 2,
|
||||
};
|
||||
setPosition(newCenter);
|
||||
setIsSetup(true);
|
||||
}
|
||||
} else if (
|
||||
originalRouteData?.center_latitude &&
|
||||
originalRouteData?.center_longitude
|
||||
) {
|
||||
const coordinates = coordinatesToLocal(
|
||||
originalRouteData?.center_latitude,
|
||||
originalRouteData?.center_longitude
|
||||
);
|
||||
|
||||
setTransform(
|
||||
coordinates.x,
|
||||
coordinates.y,
|
||||
originalRouteData?.rotate,
|
||||
originalRouteData?.scale_min
|
||||
);
|
||||
setIsSetup(true);
|
||||
}
|
||||
}, [
|
||||
points,
|
||||
originalRouteData?.center_latitude,
|
||||
originalRouteData?.center_longitude,
|
||||
originalRouteData?.rotate,
|
||||
isSetup,
|
||||
screenCenter,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", height:"100%"}} ref={parentRef}>
|
||||
<Application
|
||||
resizeTo={parentRef}
|
||||
background="#fff"
|
||||
>
|
||||
<InfiniteCanvas>
|
||||
<TravelPath points={points}/>
|
||||
{stationData?.map((obj) => (
|
||||
<Station station={obj} key={obj.id}/>
|
||||
))}
|
||||
{sightData?.map((obj, index) => (
|
||||
<Sight sight={obj} id={index} key={obj.id}/>
|
||||
))}
|
||||
if (!routeData || !stationData || !sightData) {
|
||||
console.error("routeData, stationData or sightData is null");
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
const localCenter = screenToLocal(0,0);
|
||||
g.circle(localCenter.x, localCenter.y, 10);
|
||||
g.fill("#fff");
|
||||
}}
|
||||
/>
|
||||
</InfiniteCanvas>
|
||||
</Application>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||
<Application resizeTo={parentRef} background="#fff">
|
||||
<InfiniteCanvas>
|
||||
<TravelPath points={points} />
|
||||
{stationData?.map((obj) => (
|
||||
<Station station={obj} key={obj.id} />
|
||||
))}
|
||||
{sightData?.map((obj, index) => (
|
||||
<Sight sight={obj} id={index} key={obj.id} />
|
||||
))}
|
||||
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
const localCenter = screenToLocal(0, 0);
|
||||
g.circle(localCenter.x, localCenter.y, 10);
|
||||
g.fill("#fff");
|
||||
}}
|
||||
/>
|
||||
</InfiniteCanvas>
|
||||
</Application>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -393,18 +393,18 @@ export const RouteEdit = observer(() => {
|
||||
parentResource="route"
|
||||
childResource="station"
|
||||
fields={stationFields}
|
||||
title="станции"
|
||||
title="остановки"
|
||||
dragAllowed={true}
|
||||
/>
|
||||
|
||||
<LinkedItems<VehicleItem>
|
||||
{/* <LinkedItems<VehicleItem>
|
||||
type="edit"
|
||||
parentId={routeId}
|
||||
parentResource="route"
|
||||
childResource="vehicle"
|
||||
fields={vehicleFields}
|
||||
title="транспортные средства"
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -80,17 +80,17 @@ export const RouteShow = observer(() => {
|
||||
parentResource="route"
|
||||
childResource="station"
|
||||
fields={stationFields}
|
||||
title="станции"
|
||||
title="остановки"
|
||||
/>
|
||||
|
||||
<LinkedItems<VehicleItem>
|
||||
{/* <LinkedItems<VehicleItem>
|
||||
type="show"
|
||||
parentId={record.id}
|
||||
parentResource="route"
|
||||
childResource="vehicle"
|
||||
fields={vehicleFields}
|
||||
title="транспортные средства"
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<LinkedItems<SightItem>
|
||||
type="show"
|
||||
@ -103,10 +103,9 @@ export const RouteShow = observer(() => {
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-start" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`/route-preview/${id}`)}
|
||||
>
|
||||
|
@ -4,6 +4,7 @@ import { TOKEN_KEY } from "@providers";
|
||||
|
||||
import axios from "axios";
|
||||
import { languageStore } from "@stores";
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_KRBL_API,
|
||||
});
|
||||
@ -22,6 +23,19 @@ axiosInstance.interceptors.request.use((config) => {
|
||||
return config;
|
||||
});
|
||||
|
||||
export const axiosInstanceForGet = axios.create({
|
||||
baseURL: import.meta.env.VITE_KRBL_API,
|
||||
});
|
||||
|
||||
axiosInstanceForGet.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
config.headers["X-Language"] = languageStore.language;
|
||||
return config;
|
||||
});
|
||||
const apiUrl = import.meta.env.VITE_KRBL_API;
|
||||
|
||||
export const customDataProvider = dataProvider(apiUrl, axiosInstance);
|
||||
|
Loading…
Reference in New Issue
Block a user