fix: Update map with tables fixes
This commit is contained in:
@ -2,8 +2,8 @@ export const UP_SCALE = 30000;
|
||||
export const PATH_WIDTH = 15;
|
||||
export const STATION_RADIUS = 20;
|
||||
export const STATION_OUTLINE_WIDTH = 10;
|
||||
export const SIGHT_SIZE = 60;
|
||||
export const SIGHT_SIZE = 40;
|
||||
export const SCALE_FACTOR = 50;
|
||||
|
||||
export const BACKGROUND_COLOR = 0x111111;
|
||||
export const PATH_COLOR = 0xff4d4d;
|
||||
export const PATH_COLOR = 0xff4d4d;
|
||||
|
@ -37,7 +37,7 @@ export function InfiniteCanvas({
|
||||
setScreenCenter,
|
||||
screenCenter,
|
||||
} = useTransform();
|
||||
const { routeData, originalRouteData } = useMapData();
|
||||
const { routeData, originalRouteData, setSelectedSight } = useMapData();
|
||||
|
||||
const applicationRef = useApplication();
|
||||
|
||||
@ -45,6 +45,7 @@ export function InfiniteCanvas({
|
||||
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [startRotation, setStartRotation] = useState(0);
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||
const [isPointerDown, setIsPointerDown] = useState(false);
|
||||
|
||||
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||
@ -65,7 +66,8 @@ export function InfiniteCanvas({
|
||||
}, [applicationRef?.app.canvas, setScreenCenter]);
|
||||
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(true);
|
||||
setIsPointerDown(true);
|
||||
setIsDragging(false);
|
||||
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
||||
setStartPosition({
|
||||
x: position.x,
|
||||
@ -93,7 +95,18 @@ export function InfiniteCanvas({
|
||||
}, [originalRouteData?.rotate, isUserInteracting, setRotation]);
|
||||
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
if (!isPointerDown) return;
|
||||
|
||||
// Проверяем, началось ли перетаскивание
|
||||
if (!isDragging) {
|
||||
const dx = e.globalX - startMousePosition.x;
|
||||
const dy = e.globalY - startMousePosition.y;
|
||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||
setIsDragging(true);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
const center = screenCenter ?? { x: 0, y: 0 };
|
||||
@ -136,6 +149,12 @@ export function InfiniteCanvas({
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
// Если не было перетаскивания, то это простой клик - закрываем виджет
|
||||
if (!isDragging) {
|
||||
setSelectedSight(undefined);
|
||||
}
|
||||
|
||||
setIsPointerDown(false);
|
||||
setIsDragging(false);
|
||||
// Сбрасываем флаг взаимодействия через небольшую задержку
|
||||
// чтобы избежать немедленного срабатывания useEffect
|
||||
@ -185,7 +204,6 @@ export function InfiniteCanvas({
|
||||
|
||||
useEffect(() => {
|
||||
applicationRef?.app.render();
|
||||
console.log(position, scale, rotation);
|
||||
}, [position, scale, rotation]);
|
||||
|
||||
return (
|
||||
|
@ -1,10 +1,30 @@
|
||||
import { Stack, Typography, Button } from "@mui/material";
|
||||
|
||||
import { useNavigate, useNavigationType } from "react-router";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
export function LeftSidebar() {
|
||||
export const LeftSidebar = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||
const { routeData } = useMapData();
|
||||
const [carrierThumbnail, setCarrierThumbnail] = useState<string | null>(null);
|
||||
const [carrierLogo, setCarrierLogo] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
async function fetchCarrierThumbnail() {
|
||||
if (routeData?.carrier_id) {
|
||||
const { city_id, logo } = (
|
||||
await authInstance.get(`/carrier/${routeData.carrier_id}`)
|
||||
).data;
|
||||
const { arms } = (await authInstance.get(`/city/${city_id}`)).data;
|
||||
setCarrierThumbnail(arms);
|
||||
setCarrierLogo(logo);
|
||||
}
|
||||
}
|
||||
fetchCarrierThumbnail();
|
||||
}, [routeData?.carrier_id]);
|
||||
|
||||
const handleBack = () => {
|
||||
if (navigationType === "PUSH") {
|
||||
@ -27,6 +47,7 @@ export function LeftSidebar() {
|
||||
color: "#fff",
|
||||
backgroundColor: "#222",
|
||||
borderRadius: 10,
|
||||
height: 40,
|
||||
width: "100%",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
@ -41,10 +62,30 @@ export function LeftSidebar() {
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
>
|
||||
<img src={"/Emblem.svg"} alt="logo" width={100} height={100} />
|
||||
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||
При поддержке Правительства Санкт-Петербурга
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{carrierThumbnail && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierThumbnail,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||
При поддержке Правительства
|
||||
</Typography>{" "}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
@ -65,15 +106,20 @@ export function LeftSidebar() {
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
maxHeight={150}
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
>
|
||||
<img
|
||||
src={"/GET.png"}
|
||||
alt="logo"
|
||||
width="80%"
|
||||
style={{ margin: "0 auto" }}
|
||||
/>
|
||||
{carrierLogo && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierLogo,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail_logo",
|
||||
}}
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
@ -86,4 +132,4 @@ export function LeftSidebar() {
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -29,10 +29,13 @@ const MapDataContext = createContext<{
|
||||
isRouteLoading: boolean;
|
||||
isStationLoading: boolean;
|
||||
isSightLoading: boolean;
|
||||
selectedSight?: SightData;
|
||||
setSelectedSight: (sight?: SightData) => void;
|
||||
setScaleRange: (min: number, max: number) => void;
|
||||
setMapRotation: (rotation: number) => void;
|
||||
setMapCenter: (x: number, y: number) => void;
|
||||
setStationOffset: (stationId: number, x: number, y: number) => void;
|
||||
setStationAlign: (stationId: number, align: number) => void;
|
||||
setSightCoordinates: (
|
||||
sightId: number,
|
||||
latitude: number,
|
||||
@ -50,10 +53,13 @@ const MapDataContext = createContext<{
|
||||
isRouteLoading: true,
|
||||
isStationLoading: true,
|
||||
isSightLoading: true,
|
||||
selectedSight: undefined,
|
||||
setSelectedSight: () => {},
|
||||
setScaleRange: () => {},
|
||||
setMapRotation: () => {},
|
||||
setMapCenter: () => {},
|
||||
setStationOffset: () => {},
|
||||
setStationAlign: () => {},
|
||||
setSightCoordinates: () => {},
|
||||
saveChanges: () => {},
|
||||
});
|
||||
@ -87,6 +93,7 @@ export const MapDataProvider = observer(
|
||||
const [isRouteLoading, setIsRouteLoading] = useState(true);
|
||||
const [isStationLoading, setIsStationLoading] = useState(true);
|
||||
const [isSightLoading, setIsSightLoading] = useState(true);
|
||||
const [selectedSight, setSelectedSight] = useState<SightData>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@ -106,17 +113,18 @@ export const MapDataProvider = observer(
|
||||
languageInstance("ru").get(`/route/${routeId}/station`),
|
||||
languageInstance("en").get(`/route/${routeId}/station`),
|
||||
languageInstance("zh").get(`/route/${routeId}/station`),
|
||||
authInstance.get(`/route/${routeId}/sight`),
|
||||
languageInstance("ru").get(`/route/${routeId}/sight`),
|
||||
]);
|
||||
|
||||
setOriginalRouteData(routeResponse.data as RouteData);
|
||||
const routeData = routeResponse.data as RouteData;
|
||||
setOriginalRouteData(routeData);
|
||||
setOriginalStationData(ruStationResponse.data as StationData[]);
|
||||
setStationData({
|
||||
ru: ruStationResponse.data as StationData[],
|
||||
en: enStationResponse.data as StationData[],
|
||||
zh: zhStationResponse.data as StationData[],
|
||||
});
|
||||
setOriginalSightData(sightResponse as unknown as SightData[]);
|
||||
setOriginalSightData(sightResponse.data as SightData[]);
|
||||
|
||||
setIsRouteLoading(false);
|
||||
setIsStationLoading(false);
|
||||
@ -176,43 +184,136 @@ export const MapDataProvider = observer(
|
||||
}
|
||||
|
||||
async function saveSightChanges() {
|
||||
console.log("sightChanges", sightChanges);
|
||||
for (const sight of sightChanges) {
|
||||
await authInstance.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;
|
||||
const currentStation = stationData.ru?.find(
|
||||
(station) => station.id === stationId
|
||||
);
|
||||
if (
|
||||
currentStation &&
|
||||
Math.abs(currentStation.offset_x - x) < 0.01 &&
|
||||
Math.abs(currentStation.offset_y - y) < 0.01
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return prev.map((station) => {
|
||||
if (station.station_id === stationId) {
|
||||
return found;
|
||||
setStationChanges((prev) => {
|
||||
const existingIndex = prev.findIndex(
|
||||
(station) => station.station_id === stationId
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const newChanges = [...prev];
|
||||
newChanges[existingIndex] = {
|
||||
...newChanges[existingIndex],
|
||||
offset_x: x,
|
||||
offset_y: y,
|
||||
};
|
||||
return newChanges;
|
||||
} else {
|
||||
const originalStation = originalStationData?.find(
|
||||
(s) => s.id === stationId
|
||||
);
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
station_id: stationId,
|
||||
offset_x: x,
|
||||
offset_y: y,
|
||||
align: originalStation?.align ?? 1,
|
||||
transfers: originalStation?.transfers ?? {
|
||||
bus: "",
|
||||
metro_blue: "",
|
||||
metro_green: "",
|
||||
metro_orange: "",
|
||||
metro_purple: "",
|
||||
metro_red: "",
|
||||
train: "",
|
||||
tram: "",
|
||||
trolleybus: "",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
setStationData((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((lang) => {
|
||||
updated[lang] = updated[lang].map((station) => {
|
||||
if (station.id === stationId) {
|
||||
return { ...station, offset_x: x, offset_y: y };
|
||||
}
|
||||
return station;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
function setStationAlign(stationId: number, align: number) {
|
||||
const currentStation = stationData.ru?.find(
|
||||
(station) => station.id === stationId
|
||||
);
|
||||
if (currentStation && currentStation.align === align) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStationChanges((prev) => {
|
||||
const existingIndex = prev.findIndex(
|
||||
(station) => station.station_id === stationId
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const newChanges = [...prev];
|
||||
newChanges[existingIndex] = {
|
||||
...newChanges[existingIndex],
|
||||
align: align,
|
||||
};
|
||||
return newChanges;
|
||||
} else {
|
||||
const foundStation = stationData.ru?.find(
|
||||
(station) => station.id === stationId
|
||||
const originalStation = originalStationData?.find(
|
||||
(s) => s.id === stationId
|
||||
);
|
||||
if (foundStation) {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
station_id: stationId,
|
||||
offset_x: x,
|
||||
offset_y: y,
|
||||
transfers: foundStation.transfers,
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
station_id: stationId,
|
||||
align: align,
|
||||
offset_x: originalStation?.offset_x ?? 0,
|
||||
offset_y: originalStation?.offset_y ?? 0,
|
||||
transfers: originalStation?.transfers ?? {
|
||||
bus: "",
|
||||
metro_blue: "",
|
||||
metro_green: "",
|
||||
metro_orange: "",
|
||||
metro_purple: "",
|
||||
metro_red: "",
|
||||
train: "",
|
||||
tram: "",
|
||||
trolleybus: "",
|
||||
},
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
setStationData((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((lang) => {
|
||||
updated[lang] = updated[lang].map((station) => {
|
||||
if (station.id === stationId) {
|
||||
return { ...station, align: align };
|
||||
}
|
||||
return station;
|
||||
});
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
function setSightCoordinates(
|
||||
@ -221,14 +322,18 @@ export const MapDataProvider = observer(
|
||||
longitude: number
|
||||
) {
|
||||
setSightChanges((prev) => {
|
||||
let found = prev.find((sight) => sight.sight_id === sightId);
|
||||
if (found) {
|
||||
found.latitude = latitude;
|
||||
found.longitude = longitude;
|
||||
const existingIndex = prev.findIndex(
|
||||
(sight) => sight.sight_id === sightId
|
||||
);
|
||||
|
||||
return prev.map((sight) => {
|
||||
if (sight.sight_id === sightId) {
|
||||
return found;
|
||||
if (existingIndex !== -1) {
|
||||
return prev.map((sight, index) => {
|
||||
if (index === existingIndex) {
|
||||
return {
|
||||
...sight,
|
||||
latitude,
|
||||
longitude,
|
||||
};
|
||||
}
|
||||
return sight;
|
||||
});
|
||||
@ -249,9 +354,7 @@ export const MapDataProvider = observer(
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("sightChanges", sightChanges);
|
||||
}, [sightChanges]);
|
||||
useEffect(() => {}, [sightChanges]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
@ -264,11 +367,14 @@ export const MapDataProvider = observer(
|
||||
isRouteLoading,
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
selectedSight,
|
||||
setSelectedSight,
|
||||
setScaleRange,
|
||||
setMapRotation,
|
||||
setMapCenter,
|
||||
saveChanges,
|
||||
setStationOffset,
|
||||
setStationAlign,
|
||||
setSightCoordinates,
|
||||
}),
|
||||
[
|
||||
@ -281,6 +387,7 @@ export const MapDataProvider = observer(
|
||||
isRouteLoading,
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
selectedSight,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Button, Stack, TextField, Typography } from "@mui/material";
|
||||
import { Button, Stack, TextField, Typography, Slider } from "@mui/material";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTransform } from "./TransformContext";
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||
import { SCALE_FACTOR } from "./Constants";
|
||||
|
||||
export function RightSidebar() {
|
||||
const {
|
||||
@ -15,24 +16,36 @@ export function RightSidebar() {
|
||||
} = useMapData();
|
||||
const {
|
||||
rotation,
|
||||
// position,
|
||||
// screenToLocal,
|
||||
// screenCenter,
|
||||
position,
|
||||
screenToLocal,
|
||||
screenCenter,
|
||||
rotateToAngle,
|
||||
setTransform,
|
||||
scale,
|
||||
setScaleAtCenter,
|
||||
} = useTransform();
|
||||
|
||||
const [minScale, setMinScale] = useState<number>(1);
|
||||
const [maxScale, setMaxScale] = useState<number>(10);
|
||||
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) {
|
||||
setMinScale(originalRouteData.scale_min ?? 1);
|
||||
setMaxScale(originalRouteData.scale_max ?? 10);
|
||||
// Проверяем и сбрасываем минимальный масштаб если нужно
|
||||
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,
|
||||
@ -52,16 +65,26 @@ export function RightSidebar() {
|
||||
((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(() => {
|
||||
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);
|
||||
@ -104,7 +127,30 @@ export function RightSidebar() {
|
||||
label="Минимальный масштаб"
|
||||
variant="filled"
|
||||
value={minScale}
|
||||
onChange={(e) => setMinScale(Number(e.target.value))}
|
||||
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": {
|
||||
@ -116,7 +162,8 @@ export function RightSidebar() {
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0.1,
|
||||
min: 1,
|
||||
max: 10,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@ -125,7 +172,30 @@ export function RightSidebar() {
|
||||
label="Максимальный масштаб"
|
||||
variant="filled"
|
||||
value={maxScale}
|
||||
onChange={(e) => setMaxScale(Number(e.target.value))}
|
||||
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": {
|
||||
@ -137,12 +207,71 @@ export function RightSidebar() {
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0.1,
|
||||
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="Поворот (в градусах)"
|
||||
@ -181,11 +310,13 @@ export function RightSidebar() {
|
||||
type="number"
|
||||
label="Центр карты, широта"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.x * 100000) / 100000}
|
||||
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": {
|
||||
@ -195,16 +326,21 @@ export function RightSidebar() {
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
step: 0.001,
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Центр карты, высота"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.y * 100000) / 100000}
|
||||
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": {
|
||||
@ -214,6 +350,9 @@ export function RightSidebar() {
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
step: 0.001,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTransform } from "./TransformContext";
|
||||
import { SightData } from "./types";
|
||||
import { Assets, FederatedMouseEvent, Graphics, Texture } from "pixi.js";
|
||||
import { Assets, FederatedMouseEvent, Texture } from "pixi.js";
|
||||
|
||||
import { SIGHT_SIZE, UP_SCALE } from "./Constants";
|
||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||
@ -12,19 +12,21 @@ interface SightProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function Sight({ sight, id }: Readonly<SightProps>) {
|
||||
export const Sight = ({ sight, id }: Readonly<SightProps>) => {
|
||||
const { rotation, scale } = useTransform();
|
||||
const { setSightCoordinates } = useMapData();
|
||||
const { setSightCoordinates, setSelectedSight } = useMapData();
|
||||
|
||||
const [position, setPosition] = useState(
|
||||
coordinatesToLocal(sight.latitude, sight.longitude)
|
||||
);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isPointerDown, setIsPointerDown] = useState(false);
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(true);
|
||||
setIsPointerDown(true);
|
||||
setIsDragging(false);
|
||||
setStartPosition({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
@ -37,7 +39,18 @@ export function Sight({ sight, id }: Readonly<SightProps>) {
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
if (!isPointerDown) return;
|
||||
|
||||
if (!isDragging) {
|
||||
const dx = e.globalX - startMousePosition.x;
|
||||
const dy = e.globalY - startMousePosition.y;
|
||||
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
|
||||
setIsDragging(true);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE;
|
||||
const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE;
|
||||
const cos = Math.cos(rotation);
|
||||
@ -53,30 +66,37 @@ export function Sight({ sight, id }: Readonly<SightProps>) {
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
setIsPointerDown(false);
|
||||
|
||||
// Если не было перетаскивания, то это клик
|
||||
if (!isDragging) {
|
||||
setSelectedSight(sight);
|
||||
}
|
||||
|
||||
setIsDragging(false);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const [texture, setTexture] = useState(Texture.EMPTY);
|
||||
useEffect(() => {
|
||||
if (texture === Texture.EMPTY) {
|
||||
Assets.load("/SightIcon.png").then((result) => {
|
||||
setTexture(result);
|
||||
});
|
||||
}
|
||||
}, [texture]);
|
||||
Assets.load("/SightIcon.png").then(setTexture);
|
||||
}, []);
|
||||
|
||||
function draw(g: Graphics) {
|
||||
g.clear();
|
||||
g.circle(0, 0, 20);
|
||||
g.fill({ color: "#000" }); // Fill circle with primary color
|
||||
}
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
`Rendering Sight ${id + 1} at [${sight.latitude}, ${sight.longitude}]`
|
||||
);
|
||||
}, [id, sight.latitude, sight.longitude]);
|
||||
|
||||
if (!sight) {
|
||||
console.error("sight is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Компенсируем масштаб для сохранения постоянного размера
|
||||
const compensatedSize = SIGHT_SIZE / scale;
|
||||
const compensatedFontSize = 24 / scale;
|
||||
|
||||
return (
|
||||
<pixiContainer
|
||||
rotation={-rotation}
|
||||
@ -86,22 +106,34 @@ export function Sight({ sight, id }: Readonly<SightProps>) {
|
||||
onGlobalPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerUpOutside={handlePointerUp}
|
||||
x={position.x * UP_SCALE - SIGHT_SIZE / 2} // Offset by half width to center
|
||||
y={position.y * UP_SCALE - SIGHT_SIZE / 2} // Offset by half height to center
|
||||
x={position.x * UP_SCALE - SIGHT_SIZE / 2}
|
||||
y={position.y * UP_SCALE - SIGHT_SIZE / 2}
|
||||
>
|
||||
<pixiSprite texture={texture} width={SIGHT_SIZE} height={SIGHT_SIZE} />
|
||||
<pixiGraphics draw={draw} x={SIGHT_SIZE} y={0} />
|
||||
<pixiSprite
|
||||
texture={texture}
|
||||
width={compensatedSize}
|
||||
height={compensatedSize}
|
||||
/>
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.circle(0, 0, 20 / scale);
|
||||
g.fill({ color: "#000" });
|
||||
}}
|
||||
x={compensatedSize}
|
||||
y={0}
|
||||
/>
|
||||
<pixiText
|
||||
text={`${id + 1}`}
|
||||
x={SIGHT_SIZE + 1}
|
||||
x={compensatedSize + 1 / scale}
|
||||
y={0}
|
||||
anchor={0.5}
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontSize: compensatedFontSize,
|
||||
fontWeight: "bold",
|
||||
fill: "#ffffff",
|
||||
}}
|
||||
/>
|
||||
</pixiContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
60
src/pages/Route/route-preview/SightInfoWidget.tsx
Normal file
60
src/pages/Route/route-preview/SightInfoWidget.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Box, Typography, IconButton } from "@mui/material";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
|
||||
export function SightInfoWidget() {
|
||||
const { selectedSight, setSelectedSight } = useMapData();
|
||||
|
||||
if (!selectedSight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 16,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.9)",
|
||||
color: "white",
|
||||
padding: "12px 16px",
|
||||
borderRadius: "4px",
|
||||
minWidth: 250,
|
||||
maxWidth: 400,
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.2)",
|
||||
zIndex: 1000,
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold", color: "#fff" }}>
|
||||
{selectedSight.name}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSelectedSight(undefined)}
|
||||
sx={{ color: "#fff", p: 0, minWidth: 24, width: 24, height: 24 }}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ color: "#ccc", mb: 1 }}>
|
||||
{selectedSight.address}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" sx={{ color: "#999" }}>
|
||||
Город: {selectedSight.city}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
import { FederatedMouseEvent, Graphics } from "pixi.js";
|
||||
import { useCallback, useState, useEffect, useRef, FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// --- Заглушки для зависимостей (замените на ваши реальные импорты) ---
|
||||
import {
|
||||
BACKGROUND_COLOR,
|
||||
PATH_COLOR,
|
||||
@ -7,140 +11,545 @@ import {
|
||||
UP_SCALE,
|
||||
} from "./Constants";
|
||||
import { useTransform } from "./TransformContext";
|
||||
import { useCallback, useState } from "react";
|
||||
import { StationData } from "./types";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { languageStore } from "@shared";
|
||||
// --- Конец заглушек ---
|
||||
|
||||
// --- Декларации для react-pixi ---
|
||||
// (В реальном проекте типы должны быть предоставлены библиотекой @pixi/react-pixi)
|
||||
declare const pixiContainer: any;
|
||||
declare const pixiGraphics: any;
|
||||
declare const pixiText: any;
|
||||
|
||||
// --- Типы ---
|
||||
type HorizontalAlign = "left" | "center" | "right";
|
||||
type VerticalAlign = "top" | "center" | "bottom";
|
||||
type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
|
||||
type LabelAlign = "left" | "center" | "right";
|
||||
|
||||
// --- Утилиты ---
|
||||
|
||||
/**
|
||||
* Преобразует текстовое позиционирование в anchor координаты.
|
||||
*/
|
||||
const getAnchorFromTextAlign = (align: TextAlign): { x: number; y: number } => {
|
||||
const parts = align.split(" ");
|
||||
const horizontal = parts[0] as HorizontalAlign;
|
||||
const vertical = (parts[1] as VerticalAlign) || "center";
|
||||
const horizontalMap: Record<HorizontalAlign, number> = {
|
||||
left: 0,
|
||||
center: 0.5,
|
||||
right: 1,
|
||||
};
|
||||
const verticalMap: Record<VerticalAlign, number> = {
|
||||
top: 0,
|
||||
center: 0.5,
|
||||
bottom: 1,
|
||||
};
|
||||
return { x: horizontalMap[horizontal], y: verticalMap[vertical] };
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает координату anchor.x из типа выравнивания.
|
||||
*/
|
||||
|
||||
// --- Интерфейсы пропсов ---
|
||||
|
||||
interface StationProps {
|
||||
station: StationData;
|
||||
ruLabel: string | null;
|
||||
anchorPoint?: { x: number; y: number };
|
||||
/** Anchor для всего блока с текстом. По умолчанию: `"right center"` */
|
||||
labelBlockAnchor?: TextAlign | { x: number; y: number };
|
||||
/** Внутреннее выравнивание текста в блоке. По умолчанию: `"left"` */
|
||||
labelAlign?: LabelAlign;
|
||||
/** Callback для изменения внутреннего выравнивания */
|
||||
onLabelAlignChange?: (align: LabelAlign) => void;
|
||||
}
|
||||
|
||||
export const Station = observer(
|
||||
({ station, ruLabel }: Readonly<StationProps>) => {
|
||||
const draw = useCallback((g: Graphics) => {
|
||||
interface LabelAlignmentControlProps {
|
||||
scale: number;
|
||||
currentAlign: LabelAlign;
|
||||
onAlignChange: (align: LabelAlign) => void;
|
||||
onPointerOver: () => void;
|
||||
onPointerOut: () => void;
|
||||
onControlPointerEnter: () => void;
|
||||
onControlPointerLeave: () => void;
|
||||
}
|
||||
|
||||
interface StationLabelProps
|
||||
extends Omit<StationProps, "ruLabelAnchor" | "nameLabelAnchor"> {}
|
||||
|
||||
// =========================================================================
|
||||
// Компонент: Панель управления выравниванием в стиле УрФУ
|
||||
// =========================================================================
|
||||
|
||||
const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
scale,
|
||||
currentAlign,
|
||||
onAlignChange,
|
||||
|
||||
onControlPointerEnter,
|
||||
onControlPointerLeave,
|
||||
}) => {
|
||||
const controlHeight = 50 / scale;
|
||||
const controlWidth = 200 / scale;
|
||||
const fontSize = 18 / scale;
|
||||
const borderRadius = 8 / scale;
|
||||
const compensatedRuFontSize = (26 * 0.75) / scale;
|
||||
const buttonWidth = controlWidth / 3;
|
||||
const strokeWidth = 2 / scale;
|
||||
|
||||
const drawBg = useCallback(
|
||||
(g: Graphics) => {
|
||||
g.clear();
|
||||
|
||||
// Основной фон с градиентом
|
||||
g.roundRect(
|
||||
-controlWidth / 2,
|
||||
0,
|
||||
controlWidth,
|
||||
controlHeight,
|
||||
borderRadius
|
||||
);
|
||||
g.fill({ color: "#1a1a1a" }); // Темный фон как у УрФУ
|
||||
|
||||
// Тонкая рамка
|
||||
g.roundRect(
|
||||
-controlWidth / 2,
|
||||
0,
|
||||
controlWidth,
|
||||
controlHeight,
|
||||
borderRadius
|
||||
);
|
||||
g.stroke({ color: "#333333", width: strokeWidth });
|
||||
|
||||
// Разделители между кнопками
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const x = -controlWidth / 2 + buttonWidth * i;
|
||||
g.moveTo(x, strokeWidth);
|
||||
g.lineTo(x, controlHeight - strokeWidth);
|
||||
g.stroke({ color: "#333333", width: strokeWidth });
|
||||
}
|
||||
},
|
||||
[controlWidth, controlHeight, borderRadius, buttonWidth, strokeWidth]
|
||||
);
|
||||
|
||||
const drawButtonHighlight = useCallback(
|
||||
(g: Graphics, index: number, isActive: boolean) => {
|
||||
g.clear();
|
||||
|
||||
if (isActive) {
|
||||
const x = -controlWidth / 2 + buttonWidth * index;
|
||||
g.roundRect(
|
||||
x + strokeWidth,
|
||||
strokeWidth,
|
||||
buttonWidth - strokeWidth * 2,
|
||||
controlHeight - strokeWidth * 2,
|
||||
borderRadius / 2
|
||||
);
|
||||
g.fill({ color: "#0066cc", alpha: 0.8 }); // Синий акцент УрФУ
|
||||
}
|
||||
},
|
||||
[controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
|
||||
);
|
||||
|
||||
const getTextStyle = (isActive: boolean) => ({
|
||||
fontSize,
|
||||
fontWeight: isActive ? ("bold" as const) : ("normal" as const),
|
||||
fill: isActive ? "#ffffff" : "#cccccc",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
});
|
||||
|
||||
const alignOptions = [
|
||||
{ key: "left" as const, label: "Left" },
|
||||
{ key: "center" as const, label: "Center" },
|
||||
{ key: "right" as const, label: "Right" },
|
||||
];
|
||||
|
||||
return (
|
||||
<pixiContainer
|
||||
position={{ x: 0, y: compensatedRuFontSize * 1.1 + 15 / scale }}
|
||||
eventMode="static"
|
||||
onPointerOver={(e: FederatedMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onControlPointerEnter();
|
||||
}}
|
||||
onPointerOut={(e: FederatedMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onControlPointerLeave();
|
||||
}}
|
||||
onPointerDown={(e: FederatedMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{/* Основной фон */}
|
||||
<pixiGraphics draw={drawBg} />
|
||||
|
||||
{/* Кнопки с подсветкой */}
|
||||
{alignOptions.map((option, index) => (
|
||||
<pixiContainer key={option.key}>
|
||||
{/* Подсветка активной кнопки */}
|
||||
<pixiGraphics
|
||||
draw={(g: Graphics) =>
|
||||
drawButtonHighlight(g, index, option.key === currentAlign)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Текст кнопки */}
|
||||
<pixiText
|
||||
text={option.label}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
position={{
|
||||
x: -controlWidth / 2 + buttonWidth * (index + 0.5),
|
||||
y: controlHeight / 2,
|
||||
}}
|
||||
style={getTextStyle(option.key === currentAlign)}
|
||||
eventMode="static"
|
||||
cursor="pointer"
|
||||
onClick={(e: FederatedMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onAlignChange(option.key);
|
||||
}}
|
||||
onPointerDown={(e: FederatedMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onAlignChange(option.key);
|
||||
}}
|
||||
onPointerOver={(e: FederatedMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onControlPointerEnter();
|
||||
}}
|
||||
/>
|
||||
</pixiContainer>
|
||||
))}
|
||||
</pixiContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Компонент: Метка Станции (с логикой)
|
||||
// =========================================================================
|
||||
|
||||
const StationLabel = observer(
|
||||
({
|
||||
station,
|
||||
ruLabel,
|
||||
anchorPoint,
|
||||
labelBlockAnchor: labelBlockAnchorProp,
|
||||
labelAlign: labelAlignProp = "center",
|
||||
onLabelAlignChange,
|
||||
}: Readonly<StationLabelProps>) => {
|
||||
const { language } = languageStore;
|
||||
const { rotation, scale } = useTransform();
|
||||
const { setStationOffset, setStationAlign } = useMapData();
|
||||
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isPointerDown, setIsPointerDown] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isControlHovered, setIsControlHovered] = useState(false);
|
||||
const [currentLabelAlign, setCurrentLabelAlign] = useState(labelAlignProp);
|
||||
const [ruLabelWidth, setRuLabelWidth] = useState(0);
|
||||
|
||||
const dragStartPos = useRef({ x: 0, y: 0 });
|
||||
const mouseStartPos = useRef({ x: 0, y: 0 });
|
||||
const hideTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
const ruLabelRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimer.current) {
|
||||
clearTimeout(hideTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePointerEnter = () => {
|
||||
if (hideTimer.current) {
|
||||
clearTimeout(hideTimer.current);
|
||||
hideTimer.current = null;
|
||||
}
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleControlPointerEnter = () => {
|
||||
// Дополнительная обработка для панели управления
|
||||
if (hideTimer.current) {
|
||||
clearTimeout(hideTimer.current);
|
||||
hideTimer.current = null;
|
||||
}
|
||||
setIsControlHovered(true);
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleControlPointerLeave = () => {
|
||||
setIsControlHovered(false);
|
||||
// Если курсор не над основным контейнером, скрываем панель через некоторое время
|
||||
if (!isHovered) {
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerLeave = () => {
|
||||
// Увеличиваем время до скрытия панели и добавляем проверку
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
// Если курсор не над панелью управления, скрываем и её
|
||||
if (!isControlHovered) {
|
||||
setIsControlHovered(false);
|
||||
}
|
||||
}, 100); // Увеличиваем время до скрытия панели
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
|
||||
}, [station.offset_x, station.offset_y, station.id]);
|
||||
|
||||
// Функция для конвертации числового align в строковый
|
||||
const convertNumericAlign = (align: number): LabelAlign => {
|
||||
switch (align) {
|
||||
case 0:
|
||||
return "left";
|
||||
case 1:
|
||||
return "center";
|
||||
case 2:
|
||||
return "right";
|
||||
default:
|
||||
return "center";
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для конвертации строкового align в числовой
|
||||
const convertStringAlign = (align: LabelAlign): number => {
|
||||
switch (align) {
|
||||
case "left":
|
||||
return 0;
|
||||
case "center":
|
||||
return 1;
|
||||
case "right":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLabelAlign(convertNumericAlign(station.align ?? 1));
|
||||
}, [station.align]);
|
||||
|
||||
if (!station) return null;
|
||||
|
||||
const coordinates = coordinatesToLocal(station.latitude, station.longitude);
|
||||
const compensatedRuFontSize = (26 * 0.75) / scale;
|
||||
const compensatedNameFontSize = (16 * 0.75) / scale;
|
||||
const minDistance = 30;
|
||||
const compensatedOffset = Math.max(minDistance / scale, 24 / scale);
|
||||
const textBlockPosition = anchorPoint || position;
|
||||
const finalLabelBlockAnchor = labelBlockAnchorProp || "right center";
|
||||
const labelBlockAnchor =
|
||||
typeof finalLabelBlockAnchor === "string"
|
||||
? getAnchorFromTextAlign(finalLabelBlockAnchor)
|
||||
: finalLabelBlockAnchor;
|
||||
|
||||
// Измеряем ширину верхнего лейбла
|
||||
useEffect(() => {
|
||||
if (ruLabelRef.current && ruLabel) {
|
||||
setRuLabelWidth(ruLabelRef.current.width);
|
||||
}
|
||||
}, [ruLabel, compensatedRuFontSize]);
|
||||
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsPointerDown(true);
|
||||
setIsDragging(false);
|
||||
dragStartPos.current = { ...position };
|
||||
mouseStartPos.current = { x: e.global.x, y: e.global.y };
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isPointerDown) return;
|
||||
if (!isDragging) {
|
||||
const dx = e.global.x - mouseStartPos.current.x;
|
||||
const dy = e.global.y - mouseStartPos.current.y;
|
||||
if (Math.hypot(dx, dy) > 3) setIsDragging(true);
|
||||
else return;
|
||||
}
|
||||
const dx = (e.global.x - mouseStartPos.current.x) / scale;
|
||||
const dy = (e.global.y - mouseStartPos.current.y) / scale;
|
||||
const newPosition = {
|
||||
x: dragStartPos.current.x + dx,
|
||||
y: dragStartPos.current.y + dy,
|
||||
};
|
||||
|
||||
// Проверяем, изменилась ли позиция
|
||||
if (
|
||||
Math.abs(newPosition.x - position.x) > 0.01 ||
|
||||
Math.abs(newPosition.y - position.y) > 0.01
|
||||
) {
|
||||
setPosition(newPosition);
|
||||
setStationOffset(station.id, newPosition.x, newPosition.y);
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
setIsPointerDown(false);
|
||||
setTimeout(() => setIsDragging(false), 50);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleAlignChange = async (align: LabelAlign) => {
|
||||
setCurrentLabelAlign(align);
|
||||
onLabelAlignChange?.(align);
|
||||
// Сохраняем в стор
|
||||
const numericAlign = convertStringAlign(align);
|
||||
setStationAlign(station.id, numericAlign);
|
||||
};
|
||||
|
||||
// Функция для расчета позиции нижнего лейбла относительно ширины верхнего
|
||||
const getSecondLabelPosition = (): number => {
|
||||
if (!ruLabelWidth) return 0;
|
||||
|
||||
switch (currentLabelAlign) {
|
||||
case "left":
|
||||
// Позиционируем относительно левого края верхнего текста
|
||||
return -ruLabelWidth / 2;
|
||||
case "center":
|
||||
// Центрируем относительно центра верхнего текста
|
||||
return 0;
|
||||
case "right":
|
||||
// Позиционируем относительно правого края верхнего текста
|
||||
return ruLabelWidth / 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для расчета anchor нижнего лейбла
|
||||
const getSecondLabelAnchor = (): number => {
|
||||
switch (currentLabelAlign) {
|
||||
case "left":
|
||||
return 0; // anchor.x = 0 (левый край)
|
||||
case "center":
|
||||
return 0.5; // anchor.x = 0.5 (центр)
|
||||
case "right":
|
||||
return 1; // anchor.x = 1 (правый край)
|
||||
default:
|
||||
return 0.5;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<pixiContainer
|
||||
x={coordinates.x * UP_SCALE}
|
||||
y={coordinates.y * UP_SCALE}
|
||||
rotation={-rotation}
|
||||
eventMode="static"
|
||||
interactive
|
||||
cursor={isDragging ? "grabbing" : "grab"}
|
||||
onPointerOver={handlePointerEnter}
|
||||
onPointerOut={handlePointerLeave}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerUpOutside={handlePointerUp}
|
||||
onGlobalPointerMove={handlePointerMove}
|
||||
>
|
||||
<pixiContainer
|
||||
position={{
|
||||
x:
|
||||
textBlockPosition.x +
|
||||
compensatedOffset * (labelBlockAnchor.x === 1 ? -1 : 1),
|
||||
y: textBlockPosition.y,
|
||||
}}
|
||||
anchor={labelBlockAnchor}
|
||||
>
|
||||
{ruLabel && (
|
||||
<pixiText
|
||||
ref={ruLabelRef}
|
||||
text={ruLabel}
|
||||
position={{ x: 0, y: 0 }}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
style={{
|
||||
fontSize: compensatedRuFontSize,
|
||||
fontWeight: "bold",
|
||||
fill: "#ffffff",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{station.name && language !== "ru" && ruLabel && (
|
||||
<pixiText
|
||||
text={station.name}
|
||||
position={{
|
||||
x: getSecondLabelPosition(),
|
||||
y: compensatedRuFontSize * 1.1,
|
||||
}}
|
||||
anchor={{ x: getSecondLabelAnchor(), y: 0.5 }}
|
||||
style={{
|
||||
fontSize: compensatedNameFontSize,
|
||||
fontWeight: "bold",
|
||||
fill: "#CCCCCC",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(isHovered || isControlHovered) && !isDragging && (
|
||||
<LabelAlignmentControl
|
||||
scale={scale}
|
||||
currentAlign={currentLabelAlign}
|
||||
onAlignChange={handleAlignChange}
|
||||
onPointerOver={handlePointerEnter}
|
||||
onPointerOut={handlePointerLeave}
|
||||
onControlPointerEnter={handleControlPointerEnter}
|
||||
onControlPointerLeave={handleControlPointerLeave}
|
||||
/>
|
||||
)}
|
||||
</pixiContainer>
|
||||
</pixiContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// =========================================================================
|
||||
// Главный экспортируемый компонент: Станция
|
||||
// =========================================================================
|
||||
|
||||
export const Station = ({
|
||||
station,
|
||||
ruLabel,
|
||||
anchorPoint,
|
||||
labelBlockAnchor,
|
||||
labelAlign,
|
||||
onLabelAlignChange,
|
||||
}: Readonly<StationProps>) => {
|
||||
const draw = useCallback(
|
||||
(g: Graphics) => {
|
||||
g.clear();
|
||||
const coordinates = coordinatesToLocal(
|
||||
station.latitude,
|
||||
station.longitude
|
||||
);
|
||||
g.circle(
|
||||
coordinates.x * UP_SCALE,
|
||||
coordinates.y * UP_SCALE,
|
||||
STATION_RADIUS
|
||||
);
|
||||
const radius = STATION_RADIUS;
|
||||
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
||||
g.fill({ color: PATH_COLOR });
|
||||
g.stroke({ color: BACKGROUND_COLOR, width: STATION_OUTLINE_WIDTH });
|
||||
}, []);
|
||||
},
|
||||
[station.latitude, station.longitude]
|
||||
);
|
||||
|
||||
return (
|
||||
<pixiContainer>
|
||||
<pixiGraphics draw={draw} />
|
||||
<StationLabel station={station} ruLabel={ruLabel} />
|
||||
</pixiContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const StationLabel = observer(
|
||||
({ station, ruLabel }: Readonly<StationProps>) => {
|
||||
const { rotation, scale } = useTransform();
|
||||
const { setStationOffset } = useMapData();
|
||||
|
||||
const [position, setPosition] = useState({
|
||||
x: station.offset_x,
|
||||
y: station.offset_y,
|
||||
});
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||
const [startMousePosition, setStartMousePosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
if (!station) {
|
||||
console.error("station is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(true);
|
||||
setStartPosition({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
});
|
||||
setStartMousePosition({
|
||||
x: e.globalX,
|
||||
y: e.globalY,
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
const dx = e.globalX - startMousePosition.x;
|
||||
const dy = e.globalY - startMousePosition.y;
|
||||
const newPosition = {
|
||||
x: startPosition.x + dx,
|
||||
y: startPosition.y + dy,
|
||||
};
|
||||
setPosition(newPosition);
|
||||
setStationOffset(station.id, newPosition.x, newPosition.y);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
setIsDragging(false);
|
||||
e.stopPropagation();
|
||||
};
|
||||
const coordinates = coordinatesToLocal(station.latitude, station.longitude);
|
||||
|
||||
return (
|
||||
<pixiContainer
|
||||
eventMode="static"
|
||||
interactive
|
||||
onPointerDown={handlePointerDown}
|
||||
onGlobalPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerUpOutside={handlePointerUp}
|
||||
width={48}
|
||||
height={48}
|
||||
x={coordinates.x * UP_SCALE}
|
||||
y={coordinates.y * UP_SCALE}
|
||||
rotation={-rotation}
|
||||
>
|
||||
<pixiText
|
||||
anchor={{ x: 1, y: 0.5 }}
|
||||
text={station.name}
|
||||
position={{
|
||||
x: position.x / scale + 24,
|
||||
y: position.y / scale,
|
||||
}}
|
||||
style={{
|
||||
fontSize: 26,
|
||||
fontWeight: "bold",
|
||||
fill: "#ffffff",
|
||||
}}
|
||||
/>
|
||||
|
||||
{ruLabel && (
|
||||
<pixiText
|
||||
anchor={{ x: 1, y: -1 }}
|
||||
text={ruLabel}
|
||||
position={{
|
||||
x: position.x / scale + 24,
|
||||
y: position.y / scale,
|
||||
}}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
fill: "#CCCCCC",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</pixiContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<pixiContainer>
|
||||
<pixiGraphics draw={draw} />
|
||||
<StationLabel
|
||||
station={station}
|
||||
ruLabel={ruLabel}
|
||||
anchorPoint={anchorPoint}
|
||||
labelBlockAnchor={labelBlockAnchor}
|
||||
labelAlign={labelAlign}
|
||||
onLabelAlignChange={onLabelAlignChange}
|
||||
/>
|
||||
</pixiContainer>
|
||||
);
|
||||
};
|
||||
|
@ -26,9 +26,12 @@ const TransformContext = createContext<{
|
||||
rotationDegrees?: number,
|
||||
scale?: number
|
||||
) => void;
|
||||
setScaleOnly: (newScale: number) => void;
|
||||
setScaleWithoutMovingCenter: (newScale: number) => void;
|
||||
setScreenCenter: React.Dispatch<
|
||||
React.SetStateAction<{ x: number; y: number } | undefined>
|
||||
>;
|
||||
setScaleAtCenter: (newScale: number) => void;
|
||||
}>({
|
||||
position: { x: 0, y: 0 },
|
||||
scale: 1,
|
||||
@ -41,7 +44,10 @@ const TransformContext = createContext<{
|
||||
localToScreen: () => ({ x: 0, y: 0 }),
|
||||
rotateToAngle: () => {},
|
||||
setTransform: () => {},
|
||||
setScaleOnly: () => {},
|
||||
setScaleWithoutMovingCenter: () => {},
|
||||
setScreenCenter: () => {},
|
||||
setScaleAtCenter: () => {},
|
||||
});
|
||||
|
||||
// Provider component
|
||||
@ -136,8 +142,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
useScale !== undefined ? 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,
|
||||
@ -160,6 +164,37 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
[rotation, scale, screenCenter]
|
||||
);
|
||||
|
||||
const setScaleAtCenter = useCallback(
|
||||
(newScale: number) => {
|
||||
if (scale === newScale) return;
|
||||
|
||||
const center = screenCenter ?? { x: 0, y: 0 };
|
||||
|
||||
const actualZoomFactor = newScale / scale;
|
||||
|
||||
const newPosition = {
|
||||
x: position.x + (center.x - position.x) * (1 - actualZoomFactor),
|
||||
y: position.y + (center.y - position.y) * (1 - actualZoomFactor),
|
||||
};
|
||||
|
||||
setPosition(newPosition);
|
||||
setScale(newScale);
|
||||
},
|
||||
[position, scale, screenCenter]
|
||||
);
|
||||
|
||||
const setScaleOnly = useCallback((newScale: number) => {
|
||||
// Изменяем только масштаб, не трогая позицию и поворот
|
||||
setScale(newScale);
|
||||
}, []);
|
||||
|
||||
const setScaleWithoutMovingCenter = useCallback(
|
||||
(newScale: number) => {
|
||||
setScale(newScale);
|
||||
},
|
||||
[setScale]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
position,
|
||||
@ -173,17 +208,25 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
screenToLocal,
|
||||
localToScreen,
|
||||
setTransform,
|
||||
setScaleOnly,
|
||||
setScaleWithoutMovingCenter,
|
||||
setScreenCenter,
|
||||
setScaleAtCenter,
|
||||
}),
|
||||
[
|
||||
position,
|
||||
scale,
|
||||
rotation,
|
||||
screenCenter,
|
||||
setScale,
|
||||
rotateToAngle,
|
||||
screenToLocal,
|
||||
localToScreen,
|
||||
setTransform,
|
||||
setScaleOnly,
|
||||
setScaleWithoutMovingCenter,
|
||||
setScreenCenter,
|
||||
setScaleAtCenter,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Stack, Typography, Box, IconButton } from "@mui/material";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { Landmark } from "lucide-react";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
|
||||
export function Widgets() {
|
||||
const { selectedSight, setSelectedSight } = useMapData();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
@ -24,6 +29,8 @@ export function Widgets() {
|
||||
Станция
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{/* Виджет выбранной достопримечательности (заменяет виджет погоды) */}
|
||||
<Stack
|
||||
bgcolor="primary.main"
|
||||
width={223}
|
||||
@ -31,12 +38,102 @@ export function Widgets() {
|
||||
p={2}
|
||||
m={2}
|
||||
borderRadius={2}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
pointerEvents: "auto",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||
Погода
|
||||
</Typography>
|
||||
{selectedSight ? (
|
||||
<Box
|
||||
sx={{ height: "100%", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
{/* Заголовок с кнопкой закрытия */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<Landmark size={16} />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: "#fff", fontWeight: "bold" }}
|
||||
>
|
||||
{selectedSight.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSelectedSight(undefined)}
|
||||
sx={{
|
||||
color: "#fff",
|
||||
p: 0,
|
||||
minWidth: 20,
|
||||
width: 20,
|
||||
height: 20,
|
||||
"&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
|
||||
}}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Описание достопримечательности */}
|
||||
{selectedSight.address && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: "#fff",
|
||||
mb: 1,
|
||||
opacity: 0.9,
|
||||
lineHeight: 1.3,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
}}
|
||||
>
|
||||
{selectedSight.address}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Город */}
|
||||
{selectedSight.city && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: "#fff",
|
||||
opacity: 0.7,
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
Город: {selectedSight.city}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Landmark size={32} />
|
||||
<Typography variant="body2" sx={{ color: "#fff", opacity: 0.8 }}>
|
||||
Выберите достопримечательность
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
|
||||
import { Widgets } from "./Widgets";
|
||||
import { Application, extend } from "@pixi/react";
|
||||
import {
|
||||
Container,
|
||||
@ -14,16 +14,18 @@ import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||
import { TransformProvider, useTransform } from "./TransformContext";
|
||||
import { InfiniteCanvas } from "./InfiniteCanvas";
|
||||
|
||||
import { UP_SCALE } from "./Constants";
|
||||
import { Station } from "./Station";
|
||||
import { TravelPath } from "./TravelPath";
|
||||
import { LeftSidebar } from "./LeftSidebar";
|
||||
import { RightSidebar } from "./RightSidebar";
|
||||
import { Widgets } from "./Widgets";
|
||||
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { languageStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Sight } from "./Sight";
|
||||
import { SightData } from "./types";
|
||||
import { Station } from "./Station";
|
||||
import { UP_SCALE } from "./Constants";
|
||||
|
||||
extend({
|
||||
Container,
|
||||
@ -43,8 +45,8 @@ export const RoutePreview = () => {
|
||||
|
||||
<LeftSidebar />
|
||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||
<Widgets />
|
||||
<RouteMap />
|
||||
<Widgets />
|
||||
<RightSidebar />
|
||||
</Stack>
|
||||
</Stack>
|
||||
@ -55,15 +57,27 @@ export const RoutePreview = () => {
|
||||
|
||||
export const RouteMap = observer(() => {
|
||||
const { language } = languageStore;
|
||||
const { setPosition, screenToLocal, setTransform, screenCenter } =
|
||||
useTransform();
|
||||
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||
console.log(stationData);
|
||||
const { setPosition, setTransform, screenCenter } = useTransform();
|
||||
const {
|
||||
routeData,
|
||||
stationData,
|
||||
sightData,
|
||||
originalRouteData,
|
||||
originalSightData,
|
||||
} = useMapData();
|
||||
|
||||
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||
const [isSetup, setIsSetup] = useState(false);
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (originalRouteData) {
|
||||
const path = originalRouteData?.path;
|
||||
@ -146,20 +160,14 @@ export const RouteMap = observer(() => {
|
||||
key={obj.id}
|
||||
ruLabel={
|
||||
language === "ru"
|
||||
? stationData.en[index].name
|
||||
? stationData.ru[index].name
|
||||
: stationData.ru[index].name
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
const localCenter = screenToLocal(0, 0);
|
||||
g.circle(localCenter.x, localCenter.y, 10);
|
||||
g.fill("#fff");
|
||||
}}
|
||||
/>
|
||||
{originalSightData?.map((sight: SightData, index: number) => {
|
||||
return <Sight sight={sight} id={index} key={sight.id} />;
|
||||
})}
|
||||
</InfiniteCanvas>
|
||||
</Application>
|
||||
</div>
|
||||
|
@ -1,69 +1,72 @@
|
||||
export interface RouteData {
|
||||
carrier: string;
|
||||
carrier_id: number;
|
||||
center_latitude: number;
|
||||
center_longitude: number;
|
||||
governor_appeal: number;
|
||||
id: number;
|
||||
path: [number, number][];
|
||||
rotate: number;
|
||||
route_direction: boolean;
|
||||
route_number: string;
|
||||
route_sys_number: string;
|
||||
scale_max: number;
|
||||
scale_min: number;
|
||||
carrier: string;
|
||||
carrier_id: number;
|
||||
center_latitude: number;
|
||||
center_longitude: number;
|
||||
governor_appeal: number;
|
||||
id: number;
|
||||
path: [number, number][];
|
||||
rotate: number;
|
||||
route_direction: boolean;
|
||||
route_number: string;
|
||||
route_sys_number: string;
|
||||
scale_max: number;
|
||||
scale_min: number;
|
||||
thumbnail?: string; // uuid логотипа маршрута
|
||||
}
|
||||
|
||||
export interface StationTransferData {
|
||||
bus: string;
|
||||
metro_blue: string;
|
||||
metro_green: string;
|
||||
metro_orange: string;
|
||||
metro_purple: string;
|
||||
metro_red: string;
|
||||
train: string;
|
||||
tram: string;
|
||||
trolleybus: string;
|
||||
bus: string;
|
||||
metro_blue: string;
|
||||
metro_green: string;
|
||||
metro_orange: string;
|
||||
metro_purple: string;
|
||||
metro_red: string;
|
||||
train: string;
|
||||
tram: string;
|
||||
trolleybus: string;
|
||||
}
|
||||
|
||||
export interface StationData {
|
||||
address: string;
|
||||
city_id?: number;
|
||||
description: string;
|
||||
id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
offset_x: number;
|
||||
offset_y: number;
|
||||
system_name: string;
|
||||
transfers: StationTransferData;
|
||||
address: string;
|
||||
city_id?: number;
|
||||
description: string;
|
||||
id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
offset_x: number;
|
||||
offset_y: number;
|
||||
system_name: string;
|
||||
transfers: StationTransferData;
|
||||
align: number;
|
||||
}
|
||||
|
||||
export interface StationPatchData {
|
||||
station_id: number;
|
||||
offset_x: number;
|
||||
offset_y: number;
|
||||
transfers: StationTransferData;
|
||||
station_id: number;
|
||||
offset_x: number;
|
||||
offset_y: number;
|
||||
align: number;
|
||||
transfers: StationTransferData;
|
||||
}
|
||||
|
||||
export interface SightPatchData {
|
||||
sight_id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
sight_id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface SightData {
|
||||
address: string;
|
||||
city: string;
|
||||
city_id: number;
|
||||
id: number;
|
||||
latitude: number;
|
||||
left_article: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
preview_media: number;
|
||||
thumbnail: string; // uuid
|
||||
watermark_lu: string; // uuid
|
||||
watermark_rd: string; // uuid
|
||||
}
|
||||
address: string;
|
||||
city: string;
|
||||
city_id: number;
|
||||
id: number;
|
||||
latitude: number;
|
||||
left_article: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
preview_media: number;
|
||||
thumbnail: string; // uuid
|
||||
watermark_lu: string; // uuid
|
||||
watermark_rd: string; // uuid
|
||||
}
|
||||
|
Reference in New Issue
Block a user