diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx
index f8317cc..1d200bc 100644
--- a/src/pages/Route/RouteCreatePage/index.tsx
+++ b/src/pages/Route/RouteCreatePage/index.tsx
@@ -420,7 +420,15 @@ export const RouteCreatePage = observer(() => {
type="number"
value={scaleMin}
onChange={(e) => {
- const value = e.target.value;
+ let value = e.target.value;
+ if (Number(value) > 297) {
+ value = "297";
+ }
+
+ if (Number(value) < 10) {
+ value = "10";
+ }
+
setScaleMin(value);
if (value && scaleMax && Number(value) > Number(scaleMax)) {
setScaleMax(value);
@@ -447,6 +455,10 @@ export const RouteCreatePage = observer(() => {
value={scaleMax}
required
onChange={(e) => {
+ if (Number(e.target.value) > 300) {
+ e.target.value = "300";
+ }
+
const value = e.target.value;
setScaleMax(value);
}}
diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx
index 77866f3..5a03801 100644
--- a/src/pages/Route/RouteEditPage/index.tsx
+++ b/src/pages/Route/RouteEditPage/index.tsx
@@ -393,20 +393,29 @@ export const RouteEditPage = observer(() => {
type="number"
value={editRouteData.scale_min ?? ""}
onChange={(e) => {
- const value =
- e.target.value === "" ? null : parseFloat(e.target.value);
+ let value = e.target.value === "" ? null : e.target.value;
+
+ if (value && Number(value) > 297) {
+ value = "297";
+ }
+
+ if (value && Number(value) < 10) {
+ value = "10";
+ }
+
routeStore.setEditRouteData({
- scale_min: value,
+ scale_min: value ? Number(value) : null,
});
// Если максимальный масштаб стал меньше минимального, обновляем его
if (
value !== null &&
editRouteData.scale_max !== null &&
editRouteData.scale_max !== undefined &&
- value > editRouteData.scale_max
+ value &&
+ Number(value) > (editRouteData.scale_max ?? 0)
) {
routeStore.setEditRouteData({
- scale_max: value,
+ scale_max: value ? Number(value) : null,
});
}
}}
@@ -418,12 +427,17 @@ export const RouteEditPage = observer(() => {
label="Масштаб (макс)"
type="number"
value={editRouteData.scale_max ?? ""}
- onChange={(e) =>
+ onChange={(e) => {
+ let value = e.target.value;
+
+ if (Number(value) > 300) {
+ value = "300";
+ }
+
routeStore.setEditRouteData({
- scale_max:
- e.target.value === "" ? null : parseFloat(e.target.value),
- })
- }
+ scale_max: value === "" ? null : parseFloat(value),
+ });
+ }}
error={
editRouteData.scale_min !== null &&
editRouteData.scale_min !== undefined &&
diff --git a/src/pages/Route/route-preview/RightSidebar.tsx b/src/pages/Route/route-preview/RightSidebar.tsx
index 428e6bf..ceabec4 100644
--- a/src/pages/Route/route-preview/RightSidebar.tsx
+++ b/src/pages/Route/route-preview/RightSidebar.tsx
@@ -145,8 +145,8 @@ export function RightSidebar() {
onChange={(e) => {
let newMinScale = Number(e.target.value);
- if (newMinScale < 1) {
- newMinScale = 1;
+ if (newMinScale < 10) {
+ newMinScale = 10;
}
setMinScale(newMinScale);
@@ -189,8 +189,8 @@ export function RightSidebar() {
onChange={(e) => {
let newMaxScale = Number(e.target.value);
- if (newMaxScale < 3) {
- newMaxScale = 3;
+ if (newMaxScale < 13) {
+ newMaxScale = 13;
}
if (newMaxScale > 300) {
diff --git a/src/pages/Route/route-preview/index.tsx b/src/pages/Route/route-preview/index.tsx
index adfc1e8..1775545 100644
--- a/src/pages/Route/route-preview/index.tsx
+++ b/src/pages/Route/route-preview/index.tsx
@@ -1,6 +1,6 @@
import { useRef, useEffect, useState } from "react";
import { Widgets } from "./Widgets";
-import { Application, extend } from "@pixi/react";
+import { extend } from "@pixi/react";
import {
Container,
Graphics,
@@ -12,22 +12,15 @@ import {
import { Box, Stack } from "@mui/material";
import { MapDataProvider, useMapData } from "./MapDataContext";
import { TransformProvider, useTransform } from "./TransformContext";
-import { InfiniteCanvas } from "./InfiniteCanvas";
-import { TravelPath } from "./TravelPath";
import { LeftSidebar } from "./LeftSidebar";
import { RightSidebar } from "./RightSidebar";
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";
import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype";
-import CircularProgress from "@mui/material/CircularProgress";
+import { CircularProgress } from "@mui/material";
extend({
Container,
@@ -43,7 +36,7 @@ const Loading = () => {
if (isRouteLoading || isStationLoading || isSightLoading) {
return (
-
+
);
@@ -91,15 +84,8 @@ export const RoutePreview = () => {
};
export const RouteMap = observer(() => {
- const { language } = languageStore;
const { setPosition, setTransform, screenCenter } = useTransform();
- const {
- routeData,
- stationData,
- sightData,
- originalRouteData,
- originalSightData,
- } = useMapData();
+ const { routeData, stationData, sightData, originalRouteData } = useMapData();
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
const [isSetup, setIsSetup] = useState(false);
diff --git a/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx b/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
index ef92193..2105738 100644
--- a/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
+++ b/src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState, type ReactElement } from "react";
+import { useEffect, useRef, useState, type ReactElement } from "react";
import { observer } from "mobx-react-lite";
import { languageStore } from "@shared";
@@ -114,7 +114,7 @@ const ArrowIcon = ({ rotation }: { rotation: number }) => (
const LanguageSelector = observer(
({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => {
- const { language, setLanguage } = languageStore;
+ const { setLanguage } = languageStore;
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef
(null);
@@ -145,20 +145,6 @@ const LanguageSelector = observer(
const toggle = () => setIsOpen((prev) => !prev);
- const containerWidth = useMemo(() => {
- const BUTTON_SIZE = 56;
- const GAP = 8;
- const backWidth = onBack ? BUTTON_SIZE + GAP : 0;
- const toggleWidth = BUTTON_SIZE;
- const collapsedWidth = backWidth + toggleWidth + BUTTON_SIZE;
- const expandedWidth =
- backWidth +
- toggleWidth +
- LANGUAGES.length * BUTTON_SIZE +
- (LANGUAGES.length - 1) * GAP;
- return isOpen ? expandedWidth : collapsedWidth;
- }, [isOpen, onBack]);
-
return (
{
- const canvasRef = useRef
(null);
- const glRef = useRef(null);
- const programRef = useRef(null);
- const bufferRef = useRef(null);
- const pointProgramRef = useRef(null);
- const pointBufferRef = useRef(null);
- const screenLineProgramRef = useRef(null);
- const screenLineBufferRef = useRef(null);
- const attribsRef = useRef<{ a_pos: number } | null>(null);
- const uniformsRef = useRef<{
- u_cameraPos: WebGLUniformLocation | null;
- u_scale: WebGLUniformLocation | null;
- u_resolution: WebGLUniformLocation | null;
- u_color: WebGLUniformLocation | null;
- } | null>(null);
-
- const { routeData, stationData, stationDataEn, stationDataZh, sightData } =
- useMapData() as any;
- const {
- position,
- scale,
- setPosition,
- setScale,
- isAutoMode,
- setIsAutoMode,
- screenCenter,
- setScreenCenter,
- userActivityTimestamp,
- updateUserActivity,
- } = useTransform();
-
- const cameraAnimationStore = useCameraAnimationStore();
-
- // Ref для хранения ограничений масштаба
- const scaleLimitsRef = useRef({
- min: null as number | null,
- max: null as number | null,
- });
-
- // Обновляем ограничения масштаба при изменении routeData
- useEffect(() => {
- if (
- routeData?.scale_min !== undefined &&
- routeData?.scale_max !== undefined
- ) {
- scaleLimitsRef.current = {
- min: routeData.scale_min / 10,
- max: routeData.scale_max / 10,
- };
- }
- }, [routeData?.scale_min, routeData?.scale_max]);
-
- // Функция для ограничения масштаба значениями с бекенда
- const clampScale = useCallback((value: number) => {
- const { min, max } = scaleLimitsRef.current;
-
- if (min === null || max === null) {
- return value;
- }
-
- const clampedValue = Math.max(min, Math.min(max, value));
-
- return clampedValue;
- }, []);
- const { selectedLanguage } = useGeolocationStore();
- const positionRef = useRef(position);
- const scaleRef = useRef(scale);
- const setPositionRef = useRef(setPosition);
- const setScaleRef = useRef(setScale);
-
- // Обновляем refs при изменении функций
- useEffect(() => {
- setPositionRef.current = setPosition;
- }, [setPosition]);
-
- useEffect(() => {
- setScaleRef.current = setScale;
- }, [setScale]);
-
- // Логирование данных маршрута для отладки
- useEffect(() => {
- if (routeData) {
- }
- }, [routeData]);
-
- useEffect(() => {
- positionRef.current = position;
- }, [position]);
-
- useEffect(() => {
- scaleRef.current = scale;
- }, [scale]);
-
- const rotationAngle = useMemo(() => {
- const deg = (routeData as any)?.rotate ?? 0;
- return (deg * Math.PI) / 180;
- }, [routeData]);
-
- const {
- position: animatedYellowDotPosition,
- animateTo: animateYellowDotTo,
- setPositionImmediate: setYellowDotPositionImmediate,
- } = useAnimatedPolarPosition(0, 0, 800);
-
- // Build transformed route path (map coords)
- const routePath = useMemo(() => {
- if (!routeData?.path || routeData?.path.length === 0)
- return new Float32Array();
- const centerLat = routeData.center_latitude;
- const centerLon = routeData.center_longitude;
- if (centerLat === undefined || centerLon === undefined)
- return new Float32Array();
-
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
-
- const verts: number[] = [];
- for (const [lat, lon] of routeData.path) {
- const local = coordinatesToLocal(lat - centerLat, lon - centerLon);
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
- verts.push(rx, ry);
- }
- return new Float32Array(verts);
- }, [
- routeData?.path,
- routeData?.center_latitude,
- routeData?.center_longitude,
- rotationAngle,
- ]);
-
- const transformedTramCoords = useMemo(() => {
- const centerLat = routeData?.center_latitude;
- const centerLon = routeData?.center_longitude;
- if (centerLat === undefined || centerLon === undefined) return null;
-
- const coords: any = apiStore?.context?.currentCoordinates;
- if (!coords) return null;
-
- const local = coordinatesToLocal(
- coords.latitude - centerLat,
- coords.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
-
- return { x: rx, y: ry };
- }, [
- routeData?.center_latitude,
- routeData?.center_longitude,
- apiStore?.context?.currentCoordinates,
- rotationAngle,
- ]);
-
- // Настройка CameraAnimationStore callback - только один раз при монтировании
- useEffect(() => {
- const callback = (newPos: { x: number; y: number }, newZoom: number) => {
- setPosition(newPos);
- setScale(newZoom);
- };
-
- cameraAnimationStore.setUpdateCallback(callback);
-
- // Синхронизируем начальное состояние только один раз
- cameraAnimationStore.syncState(position, scale);
-
- return () => {
- cameraAnimationStore.setUpdateCallback(null);
- };
- }, []); // Пустой массив - выполняется только при монтировании
-
- // Установка границ зума
- useEffect(() => {
- if (
- routeData?.scale_min !== undefined &&
- routeData?.scale_max !== undefined
- ) {
- cameraAnimationStore.setMaxZoom(routeData.scale_max / SCALE_FACTOR);
- cameraAnimationStore.setMinZoom(routeData.scale_min / SCALE_FACTOR);
- }
- }, [routeData?.scale_min, routeData?.scale_max, cameraAnimationStore]);
-
- // Автоматический режим - таймер для включения через 5 секунд бездействия
- useEffect(() => {
- const interval = setInterval(() => {
- const timeSinceActivity = Date.now() - userActivityTimestamp;
- if (timeSinceActivity >= 5000 && !isAutoMode) {
- // 5 секунд бездействия - включаем авто режим
- setIsAutoMode(true);
- }
- }, 1000); // Проверяем каждую секунду
-
- return () => clearInterval(interval);
- }, [userActivityTimestamp, isAutoMode, setIsAutoMode]);
-
- // Следование за желтой точкой с зумом при включенном авто режиме
- useEffect(() => {
- // Пропускаем обновление если анимация уже идет
- if (cameraAnimationStore.isActivelyAnimating) {
- return;
- }
-
- if (isAutoMode && transformedTramCoords && screenCenter) {
- // Преобразуем станции в формат для CameraAnimationStore
- const transformedStations = stationData
- ? stationData
- .map((station: any) => {
- const centerLat = routeData?.center_latitude;
- const centerLon = routeData?.center_longitude;
- if (centerLat === undefined || centerLon === undefined)
- return null;
-
- const local = coordinatesToLocal(
- station.latitude - centerLat,
- station.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
-
- return {
- longitude: rx,
- latitude: ry,
- id: station.id,
- };
- })
- .filter(Boolean)
- : [];
-
- if (
- transformedTramCoords &&
- screenCenter &&
- transformedStations &&
- scaleLimitsRef.current !== null &&
- scaleLimitsRef.current.max !== null &&
- scaleLimitsRef.current.min &&
- scaleLimitsRef.current.min !== null
- ) {
- cameraAnimationStore.setMaxZoom(scaleLimitsRef.current!.max);
- cameraAnimationStore.setMinZoom(scaleLimitsRef.current!.min);
-
- // Синхронизируем текущее состояние камеры перед запуском анимации
- cameraAnimationStore.syncState(positionRef.current, scaleRef.current);
-
- // Запускаем анимацию к желтой точке
- cameraAnimationStore.followTram(
- transformedTramCoords,
- screenCenter,
- transformedStations
- );
- }
- } else if (!isAutoMode) {
- cameraAnimationStore.stopAnimation();
- }
- }, [
- isAutoMode,
- transformedTramCoords,
- screenCenter,
- cameraAnimationStore,
- stationData,
- routeData,
- rotationAngle,
- ]);
-
- // Station label overlay positions (DOM overlay)
- const stationLabels = useMemo(() => {
- if (!stationData || !routeData)
- return [] as Array<{ x: number; y: number; name: string; sub?: string }>;
- const centerLat = routeData.center_latitude;
- const centerLon = routeData.center_longitude;
- if (centerLat === undefined || centerLon === undefined) return [];
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
- const result: Array<{ x: number; y: number; name: string; sub?: string }> =
- [];
- for (let i = 0; i < stationData.length; i++) {
- const st = stationData[i];
- const local = coordinatesToLocal(
- st.latitude - centerLat,
- st.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
- const DEFAULT_LABEL_OFFSET_X = 25;
- const DEFAULT_LABEL_OFFSET_Y = 0;
- const labelOffsetX =
- st.offset_x === 0 && st.offset_y === 0
- ? DEFAULT_LABEL_OFFSET_X
- : st.offset_x;
- const labelOffsetY =
- st.offset_x === 0 && st.offset_y === 0
- ? DEFAULT_LABEL_OFFSET_Y
- : st.offset_y;
- const textBlockPositionX = rx + labelOffsetX;
- const textBlockPositionY = ry + labelOffsetY;
- const dpr = Math.max(
- 1,
- (typeof window !== "undefined" && window.devicePixelRatio) || 1
- );
- const sx = (textBlockPositionX * scale + position.x) / dpr;
- const sy = (textBlockPositionY * scale + position.y) / dpr;
- let sub: string | undefined;
- if ((selectedLanguage as any) === "zh")
- sub = (stationDataZh as any)?.[i]?.name;
- else if (
- (selectedLanguage as any) === "en" ||
- (selectedLanguage as any) === "ru"
- )
- sub = (stationDataEn as any)?.[i]?.name;
- result.push({ x: sx, y: sy, name: st.name, sub });
- }
- return result;
- }, [
- stationData,
- stationDataEn as any,
- stationDataZh as any,
- position.x,
- position.y,
- scale,
- routeData?.center_latitude,
- routeData?.center_longitude,
- rotationAngle,
- selectedLanguage as any,
- ]);
-
- // Build transformed stations (map coords)
- const stationPoints = useMemo(() => {
- if (!stationData || !routeData) return new Float32Array();
- const centerLat = routeData.center_latitude;
- const centerLon = routeData.center_longitude;
- if (centerLat === undefined || centerLon === undefined)
- return new Float32Array();
-
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
- const verts: number[] = [];
- for (const s of stationData) {
- const local = coordinatesToLocal(
- s.latitude - centerLat,
- s.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
- verts.push(rx, ry);
- }
- return new Float32Array(verts);
- }, [
- stationData,
- routeData?.center_latitude,
- routeData?.center_longitude,
- rotationAngle,
- ]);
-
- // Build transformed sights (map coords)
- const sightPoints = useMemo(() => {
- if (!sightData || !routeData) return new Float32Array();
- const centerLat = routeData.center_latitude;
- const centerLon = routeData.center_longitude;
- if (centerLat === undefined || centerLon === undefined)
- return new Float32Array();
-
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
- const verts: number[] = [];
- for (const s of sightData) {
- const local = coordinatesToLocal(
- s.latitude - centerLat,
- s.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
- verts.push(rx, ry);
- }
- return new Float32Array(verts);
- }, [
- sightData,
- routeData?.center_latitude,
- routeData?.center_longitude,
- rotationAngle,
- ]);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- const gl = initWebGLContext(canvas);
- glRef.current = gl;
- if (!gl) return;
-
- const vertSrc = `
- attribute vec2 a_pos;
- uniform vec2 u_cameraPos;
- uniform float u_scale;
- uniform vec2 u_resolution;
- void main() {
- vec2 screen = a_pos * u_scale + u_cameraPos;
- vec2 zeroToOne = screen / u_resolution;
- vec2 zeroToTwo = zeroToOne * 2.0;
- vec2 clip = zeroToTwo - 1.0;
- gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
- }
- `;
- const fragSrc = `
- precision mediump float;
- uniform vec4 u_color;
- void main() {
- gl_FragColor = u_color;
- }
- `;
-
- const compile = (type: number, src: string) => {
- const s = gl.createShader(type)!;
- gl.shaderSource(s, src);
- gl.compileShader(s);
- return s;
- };
- const vs = compile(gl.VERTEX_SHADER, vertSrc);
- const fs = compile(gl.FRAGMENT_SHADER, fragSrc);
- const prog = gl.createProgram()!;
- gl.attachShader(prog, vs);
- gl.attachShader(prog, fs);
- gl.linkProgram(prog);
- programRef.current = prog;
- gl.useProgram(prog);
-
- const a_pos = gl.getAttribLocation(prog, "a_pos");
- const u_cameraPos = gl.getUniformLocation(prog, "u_cameraPos");
- const u_scale = gl.getUniformLocation(prog, "u_scale");
- const u_resolution = gl.getUniformLocation(prog, "u_resolution");
- const u_color = gl.getUniformLocation(prog, "u_color");
- attribsRef.current = { a_pos };
- uniformsRef.current = { u_cameraPos, u_scale, u_resolution, u_color };
-
- const buffer = gl.createBuffer();
- bufferRef.current = buffer;
-
- const pointVert = `
- attribute vec2 a_pos;
- uniform vec2 u_cameraPos;
- uniform float u_scale;
- uniform vec2 u_resolution;
- uniform float u_pointSize;
- void main() {
- vec2 screen = a_pos * u_scale + u_cameraPos;
- vec2 zeroToOne = screen / u_resolution;
- vec2 zeroToTwo = zeroToOne * 2.0;
- vec2 clip = zeroToTwo - 1.0;
- gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
- gl_PointSize = u_pointSize;
- }
- `;
- const pointFrag = `
- precision mediump float;
- uniform vec4 u_color;
- void main() {
- vec2 c = gl_PointCoord * 2.0 - 1.0;
- float d = dot(c, c);
- if (d > 1.0) discard;
- gl_FragColor = u_color;
- }
- `;
- const vs2 = compile(gl.VERTEX_SHADER, pointVert);
- const fs2 = compile(gl.FRAGMENT_SHADER, pointFrag);
- const pprog = gl.createProgram()!;
- gl.attachShader(pprog, vs2);
- gl.attachShader(pprog, fs2);
- gl.linkProgram(pprog);
- pointProgramRef.current = pprog;
- pointBufferRef.current = gl.createBuffer();
-
- const lineVert = `
- attribute vec2 a_screen;
- uniform vec2 u_resolution;
- void main(){
- vec2 zeroToOne = a_screen / u_resolution;
- vec2 clip = zeroToOne * 2.0 - 1.0;
- gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
- }
- `;
- const lineFrag = `
- precision mediump float;
- uniform vec4 u_color;
- void main(){ gl_FragColor = u_color; }
- `;
- const lv = compile(gl.VERTEX_SHADER, lineVert);
- const lf = compile(gl.FRAGMENT_SHADER, lineFrag);
- const lprog = gl.createProgram()!;
- gl.attachShader(lprog, lv);
- gl.attachShader(lprog, lf);
- gl.linkProgram(lprog);
- screenLineProgramRef.current = lprog;
- screenLineBufferRef.current = gl.createBuffer();
-
- const handleResize = () => {
- const changed = resizeCanvasToDisplaySize(canvas);
- if (!gl) return;
- // Update screen center when canvas size changes
- // Use physical pixels (canvas.width) instead of CSS pixels
- setScreenCenter({
- x: canvas.width / 2,
- y: canvas.height / 2,
- });
- if (changed) {
- gl.viewport(0, 0, canvas.width, canvas.height);
- }
- gl.clearColor(0, 0, 0, 1);
- gl.clear(gl.COLOR_BUFFER_BIT);
- };
-
- handleResize();
- window.addEventListener("resize", handleResize);
- return () => {
- window.removeEventListener("resize", handleResize);
- };
- }, []);
-
- useEffect(() => {
- const centerLat = routeData?.center_latitude;
- const centerLon = routeData?.center_longitude;
- if (centerLat !== undefined && centerLon !== undefined) {
- const coords: any = apiStore?.context?.currentCoordinates;
- if (coords) {
- const local = coordinatesToLocal(
- coords.latitude - centerLat,
- coords.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const cos = Math.cos(rotationAngle),
- sin = Math.sin(rotationAngle);
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
-
- // В авторежиме используем анимацию, иначе мгновенное обновление
- if (isAutoMode) {
- animateYellowDotTo(rx, ry);
- } else {
- setYellowDotPositionImmediate(rx, ry);
- }
- }
- }
- }, [
- routeData?.center_latitude,
- routeData?.center_longitude,
- rotationAngle,
- apiStore?.context?.currentCoordinates?.latitude,
- apiStore?.context?.currentCoordinates?.longitude,
- isAutoMode,
- animateYellowDotTo,
- setYellowDotPositionImmediate,
- ]);
-
- useEffect(() => {
- const gl = glRef.current;
- const canvas = canvasRef.current;
- const prog = programRef.current;
- const buffer = bufferRef.current;
- const attribs = attribsRef.current;
- const uniforms = uniformsRef.current;
- const pprog = pointProgramRef.current;
- const pbuffer = pointBufferRef.current;
- if (
- !gl ||
- !canvas ||
- !prog ||
- !buffer ||
- !attribs ||
- !uniforms ||
- !pprog ||
- !pbuffer
- )
- return;
-
- gl.viewport(0, 0, canvas.width, canvas.height);
- gl.clearColor(0, 0, 0, 1);
- gl.clear(gl.COLOR_BUFFER_BIT);
-
- gl.useProgram(prog);
- gl.uniform2f(uniforms.u_cameraPos, position.x, position.y);
- gl.uniform1f(uniforms.u_scale, scale);
- gl.uniform2f(uniforms.u_resolution, canvas.width, canvas.height);
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
- gl.bufferData(gl.ARRAY_BUFFER, routePath, gl.STATIC_DRAW);
- gl.enableVertexAttribArray(attribs.a_pos);
- gl.vertexAttribPointer(attribs.a_pos, 2, gl.FLOAT, false, 0, 0);
-
- const vcount = routePath.length / 2;
- let tramSegIndex = -1;
- {
- const centerLatTmp = routeData?.center_latitude;
- const centerLonTmp = routeData?.center_longitude;
- if (centerLatTmp !== undefined && centerLonTmp !== undefined) {
- const coordsAny: any = apiStore?.context?.currentCoordinates;
- if (coordsAny) {
- const loc = coordinatesToLocal(
- coordsAny.latitude - centerLatTmp,
- coordsAny.longitude - centerLonTmp
- );
- const wx = loc.x * UP_SCALE;
- const wy = loc.y * UP_SCALE;
- const cosR = Math.cos(rotationAngle),
- sinR = Math.sin(rotationAngle);
- const tX = wx * cosR - wy * sinR;
- const tY = wx * sinR + wy * cosR;
- let best = -1,
- bestD = Infinity;
- for (let i = 0; i < vcount - 1; i++) {
- const p1x = routePath[i * 2],
- p1y = routePath[i * 2 + 1];
- const p2x = routePath[(i + 1) * 2],
- p2y = routePath[(i + 1) * 2 + 1];
- const dx = p2x - p1x,
- dy = p2y - p1y;
- const len2 = dx * dx + dy * dy;
- if (!len2) continue;
- const t = ((tX - p1x) * dx + (tY - p1y) * dy) / len2;
- const cl = Math.max(0, Math.min(1, t));
- const px = p1x + cl * dx,
- py = p1y + cl * dy;
- const d = Math.hypot(tX - px, tY - py);
- if (d < bestD) {
- bestD = d;
- best = i;
- }
- }
- tramSegIndex = best;
- }
- }
- }
-
- const vertexCount = routePath.length / 2;
- if (vertexCount > 1) {
- // Generate thick line geometry using triangles with proper joins
- const generateThickLine = (points: Float32Array, width: number) => {
- const vertices: number[] = [];
- const halfWidth = width / 2;
-
- if (points.length < 4) return new Float32Array();
-
- // Process each segment
- for (let i = 0; i < points.length - 2; i += 2) {
- const x1 = points[i];
- const y1 = points[i + 1];
- const x2 = points[i + 2];
- const y2 = points[i + 3];
-
- // Calculate perpendicular vector
- const dx = x2 - x1;
- const dy = y2 - y1;
- const length = Math.sqrt(dx * dx + dy * dy);
- if (length === 0) continue;
-
- const perpX = (-dy / length) * halfWidth;
- const perpY = (dx / length) * halfWidth;
-
- // Create quad (two triangles) for this line segment
- // Triangle 1
- vertices.push(x1 + perpX, y1 + perpY);
- vertices.push(x1 - perpX, y1 - perpY);
- vertices.push(x2 + perpX, y2 + perpY);
-
- // Triangle 2
- vertices.push(x1 - perpX, y1 - perpY);
- vertices.push(x2 - perpX, y2 - perpY);
- vertices.push(x2 + perpX, y2 + perpY);
-
- // Add simple join triangles to fill gaps
- if (i < points.length - 4) {
- const x3 = points[i + 4];
- const y3 = points[i + 5];
-
- const dx2 = x3 - x2;
- const dy2 = y3 - y2;
- const length2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
- if (length2 > 0) {
- const perpX2 = (-dy2 / length2) * halfWidth;
- const perpY2 = (dx2 / length2) * halfWidth;
-
- // Simple join - just connect the endpoints
- vertices.push(x2 + perpX, y2 + perpY);
- vertices.push(x2 - perpX, y2 - perpY);
- vertices.push(x2 + perpX2, y2 + perpY2);
-
- vertices.push(x2 - perpX, y2 - perpY);
- vertices.push(x2 - perpX2, y2 - perpY2);
- vertices.push(x2 + perpX2, y2 + perpY2);
- }
- }
- }
-
- return new Float32Array(vertices);
- };
-
- const lineWidth = Math.min(6);
- const r1 = ((PATH_COLOR >> 16) & 0xff) / 255;
- const g1 = ((PATH_COLOR >> 8) & 0xff) / 255;
- const b1 = (PATH_COLOR & 0xff) / 255;
- gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
-
- if (tramSegIndex >= 0) {
- // Используем точную позицию желтой точки для определения конца красной линии
- const animatedPos = animatedYellowDotPosition;
- if (
- animatedPos &&
- animatedPos.x !== undefined &&
- animatedPos.y !== undefined
- ) {
- // Создаем массив точек от начала маршрута до позиции желтой точки
- const passedPoints: number[] = [];
-
- // Добавляем все точки до текущего сегмента
- for (let i = 0; i <= tramSegIndex; i++) {
- passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
- }
-
- // Добавляем точную позицию желтой точки как конечную точку
- passedPoints.push(animatedPos.x, animatedPos.y);
-
- if (passedPoints.length >= 4) {
- const thickLineVertices = generateThickLine(
- new Float32Array(passedPoints),
- lineWidth
- );
- gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
- gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
- }
- }
- }
-
- const r2 = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
- const g2 = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
- const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255;
- gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
-
- // Серая линия начинается точно от позиции желтой точки
- const animatedPos = animatedYellowDotPosition;
- if (
- animatedPos &&
- animatedPos.x !== undefined &&
- animatedPos.y !== undefined
- ) {
- const unpassedPoints: number[] = [];
-
- // Добавляем позицию желтой точки как начальную точку серой линии
- unpassedPoints.push(animatedPos.x, animatedPos.y);
-
- // Добавляем все точки после текущего сегмента
- for (let i = tramSegIndex + 1; i < vertexCount; i++) {
- unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
- }
-
- if (unpassedPoints.length >= 4) {
- const thickLineVertices = generateThickLine(
- new Float32Array(unpassedPoints),
- lineWidth
- );
- gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
- gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
- }
- }
- }
-
- // Draw stations
- if (stationPoints.length > 0) {
- gl.useProgram(pprog);
- const a_pos_pts = gl.getAttribLocation(pprog, "a_pos");
- const u_cameraPos_pts = gl.getUniformLocation(pprog, "u_cameraPos");
- const u_scale_pts = gl.getUniformLocation(pprog, "u_scale");
- const u_resolution_pts = gl.getUniformLocation(pprog, "u_resolution");
- const u_pointSize = gl.getUniformLocation(pprog, "u_pointSize");
- const u_color_pts = gl.getUniformLocation(pprog, "u_color");
-
- gl.uniform2f(u_cameraPos_pts, position.x, position.y);
- gl.uniform1f(u_scale_pts, scale);
- gl.uniform2f(u_resolution_pts, canvas.width, canvas.height);
- gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
- gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
- gl.enableVertexAttribArray(a_pos_pts);
- gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
-
- // Draw station outlines (black background)
- gl.uniform1f(u_pointSize, 10 * scale * 1.5);
- const r_outline = ((BACKGROUND_COLOR >> 16) & 0xff) / 255;
- const g_outline = ((BACKGROUND_COLOR >> 8) & 0xff) / 255;
- const b_outline = (BACKGROUND_COLOR & 0xff) / 255;
- gl.uniform4f(u_color_pts, r_outline, g_outline, b_outline, 1);
- gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
-
- // Draw station cores (colored based on passed/unpassed)
- gl.uniform1f(u_pointSize, 8.0 * scale * 1.5);
-
- // Draw passed stations (red)
- if (tramSegIndex >= 0) {
- const passedStations = [];
- for (let i = 0; i < stationData.length; i++) {
- if (i <= tramSegIndex) {
- // @ts-ignore
- passedStations.push(stationPoints[i * 2], stationPoints[i * 2 + 1]);
- }
- }
- if (passedStations.length > 0) {
- const r_passed = ((PATH_COLOR >> 16) & 0xff) / 255;
- const g_passed = ((PATH_COLOR >> 8) & 0xff) / 255;
- const b_passed = (PATH_COLOR & 0xff) / 255;
- gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- new Float32Array(passedStations),
- gl.STATIC_DRAW
- );
- gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
- }
- }
-
- // Draw unpassed stations (gray)
- if (tramSegIndex >= 0) {
- const unpassedStations = [];
- for (let i = 0; i < stationData.length; i++) {
- if (i > tramSegIndex) {
- unpassedStations.push(
- // @ts-ignore
- stationPoints[i * 2],
- stationPoints[i * 2 + 1]
- );
- }
- }
- if (unpassedStations.length > 0) {
- const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
- const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
- const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
- gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- new Float32Array(unpassedStations),
- gl.STATIC_DRAW
- );
- gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
- }
- } else {
- // If no tram position, draw all stations as unpassed
- const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
- const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
- const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
- gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1);
- gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
- gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
- }
- }
-
- const a_pos_pts = gl.getAttribLocation(pprog, "a_pos");
- const u_cameraPos_pts = gl.getUniformLocation(pprog, "u_cameraPos");
- const u_scale_pts = gl.getUniformLocation(pprog, "u_scale");
- const u_resolution_pts = gl.getUniformLocation(pprog, "u_resolution");
- const u_pointSize = gl.getUniformLocation(pprog, "u_pointSize");
- const u_color_pts = gl.getUniformLocation(pprog, "u_color");
-
- gl.uniform2f(u_cameraPos_pts, position.x, position.y);
- gl.uniform1f(u_scale_pts, scale);
- gl.uniform2f(u_resolution_pts, canvas.width, canvas.height);
-
- const toPointsArray = (arr: number[]) => new Float32Array(arr);
-
- const pathPts: { x: number; y: number }[] = [];
- for (let i = 0; i < routePath.length; i += 2)
- pathPts.push({ x: routePath[i], y: routePath[i + 1] });
- const getSeg = (px: number, py: number) => {
- if (pathPts.length < 2) return -1;
- let best = -1,
- bestD = Infinity;
- for (let i = 0; i < pathPts.length - 1; i++) {
- const p1 = pathPts[i],
- p2 = pathPts[i + 1];
- const dx = p2.x - p1.x,
- dy = p2.y - p1.y;
- const len2 = dx * dx + dy * dy;
- if (!len2) continue;
- const t = ((px - p1.x) * dx + (py - p1.y) * dy) / len2;
- const tt = Math.max(0, Math.min(1, t));
- const cx = p1.x + tt * dx,
- cy = p1.y + tt * dy;
- const d = Math.hypot(px - cx, py - cy);
- if (d < bestD) {
- bestD = d;
- best = i;
- }
- }
- return best;
- };
-
- let tramSegForStations = -1;
- {
- const cLat = routeData?.center_latitude,
- cLon = routeData?.center_longitude;
- const tram = apiStore?.context?.currentCoordinates as any;
- if (tram && cLat !== undefined && cLon !== undefined) {
- const loc = coordinatesToLocal(
- tram.latitude - cLat,
- tram.longitude - cLon
- );
- const wx = loc.x * UP_SCALE,
- wy = loc.y * UP_SCALE;
- const cosR = Math.cos(rotationAngle),
- sinR = Math.sin(rotationAngle);
- const tx = wx * cosR - wy * sinR,
- ty = wx * sinR + wy * cosR;
- tramSegForStations = getSeg(tx, ty);
- }
- }
-
- const passedStations: number[] = [];
- const unpassedStations: number[] = [];
- for (let i = 0; i < stationPoints.length; i += 2) {
- const sx = stationPoints[i],
- sy = stationPoints[i + 1];
- const seg = getSeg(sx, sy);
- if (tramSegForStations !== -1 && seg !== -1 && seg < tramSegForStations)
- passedStations.push(sx, sy);
- else unpassedStations.push(sx, sy);
- }
-
- const outlineSize = 10.0 * scale * 2,
- coreSize = 8.0 * scale * 2;
-
- gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- toPointsArray(unpassedStations),
- gl.STREAM_DRAW
- );
- gl.enableVertexAttribArray(a_pos_pts);
- gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
- gl.uniform1f(u_pointSize, outlineSize);
- gl.uniform4f(
- u_color_pts,
- ((BACKGROUND_COLOR >> 16) & 255) / 255,
- ((BACKGROUND_COLOR >> 8) & 255) / 255,
- (BACKGROUND_COLOR & 255) / 255,
- 1
- );
- if (unpassedStations.length)
- gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
- gl.uniform1f(u_pointSize, coreSize);
- gl.uniform4f(
- u_color_pts,
- ((UNPASSED_STATION_COLOR >> 16) & 255) / 255,
- ((UNPASSED_STATION_COLOR >> 8) & 255) / 255,
- (UNPASSED_STATION_COLOR & 255) / 255,
- 1
- );
- if (unpassedStations.length)
- gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
-
- gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- toPointsArray(passedStations),
- gl.STREAM_DRAW
- );
- gl.enableVertexAttribArray(a_pos_pts);
- gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
- gl.uniform1f(u_pointSize, outlineSize);
- gl.uniform4f(
- u_color_pts,
- ((BACKGROUND_COLOR >> 16) & 255) / 255,
- ((BACKGROUND_COLOR >> 8) & 255) / 255,
- (BACKGROUND_COLOR & 255) / 255,
- 1
- );
- if (passedStations.length)
- gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
- gl.uniform1f(u_pointSize, coreSize);
- gl.uniform4f(
- u_color_pts,
- ((PATH_COLOR >> 16) & 255) / 255,
- ((PATH_COLOR >> 8) & 255) / 255,
- (PATH_COLOR & 255) / 255,
- 1
- );
- if (passedStations.length)
- gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
-
- // Draw black dots with white outline for terminal stations (startStopId and endStopId) - 5x larger
- if (
- stationData &&
- stationData.length > 0 &&
- routeData &&
- apiStore?.context
- ) {
- const centerLat = routeData.center_latitude;
- const centerLon = routeData.center_longitude;
- if (centerLat !== undefined && centerLon !== undefined) {
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
-
- // Find terminal stations using startStopId and endStopId from context
- const startStationData = stationData.find(
- (station) => station.id.toString() === apiStore.context?.startStopId
- );
- const endStationData = stationData.find(
- (station) => station.id.toString() === apiStore.context?.endStopId
- );
-
- const terminalStations: number[] = [];
-
- // Transform start station coordinates if found
- if (startStationData) {
- const startLocal = coordinatesToLocal(
- startStationData.latitude - centerLat,
- startStationData.longitude - centerLon
- );
- const startX = startLocal.x * UP_SCALE;
- const startY = startLocal.y * UP_SCALE;
- const startRx = startX * cos - startY * sin;
- const startRy = startX * sin + startY * cos;
- terminalStations.push(startRx, startRy);
- }
-
- // Transform end station coordinates if found
- if (endStationData) {
- const endLocal = coordinatesToLocal(
- endStationData.latitude - centerLat,
- endStationData.longitude - centerLon
- );
- const endX = endLocal.x * UP_SCALE;
- const endY = endLocal.y * UP_SCALE;
- const endRx = endX * cos - endY * sin;
- const endRy = endX * sin + endY * cos;
- terminalStations.push(endRx, endRy);
- }
-
- if (terminalStations.length > 0) {
- // Determine if each terminal station is passed
- const terminalStationData: any[] = [];
- if (startStationData) terminalStationData.push(startStationData);
- if (endStationData) terminalStationData.push(endStationData);
-
- // Get tram segment index for comparison
- let tramSegIndex = -1;
- const coords: any = apiStore?.context?.currentCoordinates;
- if (coords && centerLat !== undefined && centerLon !== undefined) {
- const local = coordinatesToLocal(
- coords.latitude - centerLat,
- coords.longitude - centerLon
- );
- const wx = local.x * UP_SCALE;
- const wy = local.y * UP_SCALE;
- const cosR = Math.cos(rotationAngle);
- const sinR = Math.sin(rotationAngle);
- const tx = wx * cosR - wy * sinR;
- const ty = wx * sinR + wy * cosR;
-
- // Find closest segment to tram position
- let best = -1;
- let bestD = Infinity;
- for (let i = 0; i < routePath.length - 2; i += 2) {
- const p1x = routePath[i];
- const p1y = routePath[i + 1];
- const p2x = routePath[i + 2];
- const p2y = routePath[i + 3];
- const dx = p2x - p1x;
- const dy = p2y - p1y;
- const len2 = dx * dx + dy * dy;
- if (!len2) continue;
- const t = ((tx - p1x) * dx + (ty - p1y) * dy) / len2;
- const cl = Math.max(0, Math.min(1, t));
- const px = p1x + cl * dx;
- const py = p1y + cl * dy;
- const d = Math.hypot(tx - px, ty - py);
- if (d < bestD) {
- bestD = d;
- best = i / 2;
- }
- }
- tramSegIndex = best;
- }
-
- // Check if each terminal station is passed
- const isStartPassed = startStationData
- ? (() => {
- const sx = terminalStations[0];
- const sy = terminalStations[1];
- const seg = (() => {
- if (routePath.length < 4) return -1;
- let best = -1;
- let bestD = Infinity;
- for (let i = 0; i < routePath.length - 2; i += 2) {
- const p1x = routePath[i];
- const p1y = routePath[i + 1];
- const p2x = routePath[i + 2];
- const p2y = routePath[i + 3];
- const dx = p2x - p1x;
- const dy = p2y - p1y;
- const len2 = dx * dx + dy * dy;
- if (!len2) continue;
- const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2;
- const cl = Math.max(0, Math.min(1, t));
- const px = p1x + cl * dx;
- const py = p1y + cl * dy;
- const d = Math.hypot(sx - px, sy - py);
- if (d < bestD) {
- bestD = d;
- best = i / 2;
- }
- }
- return best;
- })();
- return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex;
- })()
- : false;
-
- const isEndPassed = endStationData
- ? (() => {
- const ex = terminalStations[terminalStations.length - 2];
- const ey = terminalStations[terminalStations.length - 1];
- const seg = (() => {
- if (routePath.length < 4) return -1;
- let best = -1;
- let bestD = Infinity;
- for (let i = 0; i < routePath.length - 2; i += 2) {
- const p1x = routePath[i];
- const p1y = routePath[i + 1];
- const p2x = routePath[i + 2];
- const p2y = routePath[i + 3];
- const dx = p2x - p1x;
- const dy = p2y - p1y;
- const len2 = dx * dx + dy * dy;
- if (!len2) continue;
- const t = ((ex - p1x) * dx + (ey - p1y) * dy) / len2;
- const cl = Math.max(0, Math.min(1, t));
- const px = p1x + cl * dx;
- const py = p1y + cl * dy;
- const d = Math.hypot(ex - px, ey - py);
- if (d < bestD) {
- bestD = d;
- best = i / 2;
- }
- }
- return best;
- })();
- return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex;
- })()
- : false;
-
- gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- new Float32Array(terminalStations),
- gl.STREAM_DRAW
- );
- gl.enableVertexAttribArray(a_pos_pts);
- gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
-
- // Draw colored outline based on passed status - 24 pixels (x2)
- gl.uniform1f(u_pointSize, 18.0 * scale);
- if (startStationData && endStationData) {
- // Both stations - draw each with its own color
- if (isStartPassed) {
- gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных
- } else {
- gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных
- }
- gl.drawArrays(gl.POINTS, 0, 1); // Draw start station
-
- if (isEndPassed) {
- gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных
- } else {
- gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных
- }
- gl.drawArrays(gl.POINTS, 1, 1); // Draw end station
- } else {
- // Single station - use appropriate color
- const isPassed = startStationData ? isStartPassed : isEndPassed;
- if (isPassed) {
- gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных
- } else {
- gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных
- }
- gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
- }
-
- // Draw dark center - 12 pixels (x2)
- gl.uniform1f(u_pointSize, 11.0 * scale);
- const r_center = ((BACKGROUND_COLOR >> 16) & 0xff) / 255;
- const g_center = ((BACKGROUND_COLOR >> 8) & 0xff) / 255;
- const b_center = (BACKGROUND_COLOR & 0xff) / 255;
- gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); // Dark color
- gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
- }
- }
- }
-
- // Draw yellow dot for tram position
- if (animatedYellowDotPosition) {
- const rx = animatedYellowDotPosition.x;
- const ry = animatedYellowDotPosition.y;
-
- gl.uniform1f(u_pointSize, 13.3333 * scale);
- gl.uniform4f(u_color_pts, 1.0, 1.0, 0.0, 1.0);
- const tmp = new Float32Array([rx, ry]);
- gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
- gl.bufferData(gl.ARRAY_BUFFER, tmp, gl.STREAM_DRAW);
- gl.enableVertexAttribArray(a_pos_pts);
- gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
- gl.drawArrays(gl.POINTS, 0, 1);
- }
- }, [
- routePath,
- stationPoints,
- sightPoints,
- position.x,
- position.y,
- scale,
- animatedYellowDotPosition?.x,
- animatedYellowDotPosition?.y,
- ]);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- if (!routePath || routePath.length < 4) return;
-
- let minX = Infinity,
- minY = Infinity,
- maxX = -Infinity,
- maxY = -Infinity;
- for (let i = 0; i < routePath.length; i += 2) {
- const x = routePath[i];
- const y = routePath[i + 1];
- if (x < minX) minX = x;
- if (y < minY) minY = y;
- if (x > maxX) maxX = x;
- if (y > maxY) maxY = y;
- }
- if (
- !isFinite(minX) ||
- !isFinite(minY) ||
- !isFinite(maxX) ||
- !isFinite(maxY)
- )
- return;
-
- const worldWidth = Math.max(1, maxX - minX);
- const worldHeight = Math.max(1, maxY - minY);
-
- const margin = 0.1;
- const targetScale = Math.min(
- (canvas.width * (1 - margin)) / worldWidth,
- (canvas.height * (1 - margin)) / worldHeight
- );
-
- const centerX = (minX + maxX) / 2;
- const centerY = (minY + maxY) / 2;
-
- const clampedScale = clampScale(targetScale);
-
- setScaleRef.current(clampedScale);
- setPositionRef.current({
- x: canvas.width / 2 - centerX * clampedScale,
- y: canvas.height / 2 - centerY * clampedScale,
- });
- }, [routePath]);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- let isDragging = false;
- let startMouse = { x: 0, y: 0 };
- let startPos = { x: 0, y: 0 };
-
- const activePointers = new Map();
- let isPinching = false;
- let pinchStart: {
- distance: number;
- midpoint: { x: number; y: number };
- scale: number;
- position: { x: number; y: number };
- } | null = null;
-
- const getDistance = (
- p1: { x: number; y: number },
- p2: { x: number; y: number }
- ) => Math.hypot(p2.x - p1.x, p2.y - p1.y);
-
- const getMidpoint = (
- p1: { x: number; y: number },
- p2: { x: number; y: number }
- ) => ({
- x: (p1.x + p2.x) / 2,
- y: (p1.y + p2.y) / 2,
- });
-
- const onPointerDown = (e: PointerEvent) => {
- // Отслеживаем активность пользователя
- updateUserActivity();
- if (isAutoMode) {
- setIsAutoMode(false);
- }
- cameraAnimationStore.stopAnimation();
-
- canvas.setPointerCapture(e.pointerId);
- const rect = canvas.getBoundingClientRect();
- activePointers.set(e.pointerId, {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top,
- });
- if (activePointers.size === 1) {
- isDragging = true;
- startMouse = { x: e.clientX - rect.left, y: e.clientY - rect.top };
- startPos = { x: positionRef.current.x, y: positionRef.current.y };
- } else if (activePointers.size === 2) {
- isDragging = false;
- const [p1, p2] = Array.from(activePointers.values());
- pinchStart = {
- distance: getDistance(p1, p2),
- midpoint: getMidpoint(p1, p2),
- scale: scaleRef.current,
- position: { x: positionRef.current.x, y: positionRef.current.y },
- };
- isPinching = true;
- }
- };
-
- const onPointerMove = (e: PointerEvent) => {
- if (!activePointers.has(e.pointerId)) return;
-
- // Отслеживаем активность пользователя
- updateUserActivity();
-
- const rect = canvas.getBoundingClientRect();
- activePointers.set(e.pointerId, {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top,
- });
-
- if (activePointers.size === 2) {
- isDragging = false;
-
- const pointersArray = Array.from(activePointers.values());
- if (pointersArray.length === 2) {
- const [p1, p2] = pointersArray;
-
- if (!isPinching || pinchStart === null) {
- isPinching = true;
- pinchStart = {
- distance: getDistance(p1, p2),
- midpoint: getMidpoint(p1, p2),
- scale: scaleRef.current,
- position: { x: positionRef.current.x, y: positionRef.current.y },
- };
- }
-
- // Process the pinch gesture
- if (pinchStart) {
- const currentDistance = getDistance(p1, p2);
- const zoomFactor = currentDistance / pinchStart.distance;
- const unclampedScale = pinchStart.scale * zoomFactor;
- const newScale = clampScale(Math.max(0.1, unclampedScale));
-
- const k = newScale / pinchStart.scale;
- const newPosition = {
- x: pinchStart.position.x * k + pinchStart.midpoint.x * (1 - k),
- y: pinchStart.position.y * k + pinchStart.midpoint.y * (1 - k),
- };
- setPositionRef.current(newPosition);
- setScaleRef.current(newScale);
- }
- }
- } else if (isDragging && activePointers.size === 1) {
- const p = Array.from(activePointers.values())[0];
-
- // Проверяем валидность значений
- if (
- !startMouse ||
- !startPos ||
- typeof startMouse.x !== "number" ||
- typeof startMouse.y !== "number" ||
- typeof startPos.x !== "number" ||
- typeof startPos.y !== "number"
- ) {
- console.warn(
- "WebGLMap: Некорректные значения startMouse или startPos:",
- {
- startMouse,
- startPos,
- p,
- }
- );
- return;
- }
-
- const dx = p.x - startMouse.x;
- const dy = p.y - startMouse.y;
-
- setPositionRef.current({ x: startPos.x + dx, y: startPos.y + dy });
- }
- };
-
- const onPointerUp = (e: PointerEvent) => {
- // Отслеживаем активность пользователя
- updateUserActivity();
-
- canvas.releasePointerCapture(e.pointerId);
- activePointers.delete(e.pointerId);
- if (activePointers.size < 2) {
- isPinching = false;
- pinchStart = null;
- }
- if (activePointers.size === 0) {
- isDragging = false;
- } else if (activePointers.size === 1) {
- const p = Array.from(activePointers.values())[0];
- startPos = { x: positionRef.current.x, y: positionRef.current.y };
- startMouse = { x: p.x, y: p.y };
- isDragging = true;
- }
- };
-
- const onPointerCancel = (e: PointerEvent) => {
- // Handle pointer cancellation (e.g., when touch is interrupted)
- updateUserActivity();
- canvas.releasePointerCapture(e.pointerId);
- activePointers.delete(e.pointerId);
- isPinching = false;
- pinchStart = null;
- if (activePointers.size === 0) {
- isDragging = false;
- }
- };
-
- const onWheel = (e: WheelEvent) => {
- e.preventDefault();
-
- // Отслеживаем активность пользователя
- updateUserActivity();
- if (isAutoMode) {
- setIsAutoMode(false);
- }
- cameraAnimationStore.stopAnimation();
-
- const rect = canvas.getBoundingClientRect();
- // Convert mouse coordinates from CSS pixels to physical canvas pixels
- const mouseX =
- (e.clientX - rect.left) * (canvas.width / canvas.clientWidth);
- const mouseY =
- (e.clientY - rect.top) * (canvas.height / canvas.clientHeight);
- const delta = e.deltaY > 0 ? 0.9 : 1.1;
- const unclampedScale = scaleRef.current * delta;
- const newScale = clampScale(Math.max(0.1, unclampedScale));
-
- const k = newScale / scaleRef.current;
- const newPosition = {
- x: positionRef.current.x * k + mouseX * (1 - k),
- y: positionRef.current.y * k + mouseY * (1 - k),
- };
- setScaleRef.current(newScale);
- setPositionRef.current(newPosition);
- };
-
- canvas.addEventListener("pointerdown", onPointerDown);
- canvas.addEventListener("pointermove", onPointerMove);
- canvas.addEventListener("pointerup", onPointerUp);
- canvas.addEventListener("pointercancel", onPointerCancel);
- canvas.addEventListener("pointerleave", onPointerUp);
- canvas.addEventListener("wheel", onWheel, { passive: false });
-
- return () => {
- canvas.removeEventListener("pointerdown", onPointerDown);
- canvas.removeEventListener("pointermove", onPointerMove);
- canvas.removeEventListener("pointerup", onPointerUp);
- canvas.removeEventListener("pointercancel", onPointerCancel);
- canvas.removeEventListener("pointerleave", onPointerUp);
- canvas.removeEventListener("wheel", onWheel as any);
- };
- }, [
- updateUserActivity,
- setIsAutoMode,
- cameraAnimationStore,
- isAutoMode,
- clampScale,
- ]);
-
- return (
-
-
-
- {stationLabels.map((l, idx) => (
-
-
{l.name}
- {l.sub ? (
-
- {l.sub}
-
- ) : null}
-
- ))}
- {sightData?.map((s: any, i: number) => {
- const centerLat = routeData?.center_latitude;
- const centerLon = routeData?.center_longitude;
- if (centerLat === undefined || centerLon === undefined) return null;
- const cos = Math.cos(rotationAngle);
- const sin = Math.sin(rotationAngle);
- const local = coordinatesToLocal(
- s.latitude - centerLat,
- s.longitude - centerLon
- );
- const x = local.x * UP_SCALE;
- const y = local.y * UP_SCALE;
- const rx = x * cos - y * sin;
- const ry = x * sin + y * cos;
- const dpr = Math.max(
- 1,
- (typeof window !== "undefined" && window.devicePixelRatio) || 1
- );
- const sx = (rx * scale + position.x) / dpr;
- const sy = (ry * scale + position.y) / dpr;
- const size = 30;
-
- // Обработчик клика для выбора достопримечательности
- const handleSightClick = () => {
- const {
- setSelectedSightId,
- setIsManualSelection,
- setIsRightWidgetSelectorOpen,
- closeGovernorModal,
- } = useGeolocationStore();
- setSelectedSightId(String(s.id));
- setIsManualSelection(true);
- setIsRightWidgetSelectorOpen(false);
- closeGovernorModal();
- };
-
- return (
-

- );
- })}
-
- {(() => {
- if (!routeData) return null;
- const centerLat = routeData.center_latitude;
- const centerLon = routeData.center_longitude;
- if (centerLat === undefined || centerLon === undefined) return null;
-
- const coords: any = apiStore?.context?.currentCoordinates;
- if (!coords) return null;
-
- const local = coordinatesToLocal(
- coords.latitude - centerLat,
- coords.longitude - centerLon
- );
- const wx = local.x * UP_SCALE;
- const wy = local.y * UP_SCALE;
- const cosR = Math.cos(rotationAngle);
- const sinR = Math.sin(rotationAngle);
- const rx = wx * cosR - wy * sinR;
- const ry = wx * sinR + wy * cosR;
- const dpr2 = Math.max(
- 1,
- (typeof window !== "undefined" && window.devicePixelRatio) || 1
- );
- const screenX = (rx * scale + position.x) / dpr2;
- const screenY = (ry * scale + position.y) / dpr2;
-
- const pathPts: { x: number; y: number }[] = [];
- for (let i = 0; i < routePath.length; i += 2)
- pathPts.push({ x: routePath[i], y: routePath[i + 1] });
- const stationsForAngle = (stationData || []).map((st: any) => {
- const loc = coordinatesToLocal(
- st.latitude - centerLat,
- st.longitude - centerLon
- );
- const x = loc.x * UP_SCALE,
- y = loc.y * UP_SCALE;
- const rx2 = x * cosR - y * sinR,
- ry2 = x * sinR + y * cosR;
- return {
- longitude: rx2,
- latitude: ry2,
- offset_x: st.offset_x,
- offset_y: st.offset_y,
- };
- });
- let tramSegIndex = -1;
- if (routePath.length >= 4) {
- let best = -1,
- bestD = Infinity;
- for (let i = 0; i < routePath.length - 2; i += 2) {
- const p1x = routePath[i],
- p1y = routePath[i + 1];
- const p2x = routePath[i + 2],
- p2y = routePath[i + 3];
- const dx = p2x - p1x,
- dy = p2y - p1y;
- const len2 = dx * dx + dy * dy;
- if (!len2) continue;
- const t = ((rx - p1x) * dx + (ry - p1y) * dy) / len2;
- const cl = Math.max(0, Math.min(1, t));
- const px = p1x + cl * dx,
- py = p1y + cl * dy;
- const d = Math.hypot(rx - px, ry - py);
- if (d < bestD) {
- bestD = d;
- best = i / 2;
- }
- }
- tramSegIndex = best;
- }
- const optimalAngle = (() => {
- const testRadiusInMap = 100 / scale;
- const minPath = 60,
- minPassed = 60,
- minStation = 50;
- let bestAng = 0,
- bestScore = Infinity;
- for (let i = 0; i < 12; i++) {
- const ang = (i * Math.PI * 2) / 12;
- const tx = rx + Math.cos(ang) * testRadiusInMap;
- const ty = ry + Math.sin(ang) * testRadiusInMap;
- const distPath = (function () {
- if (pathPts.length < 2) return Infinity;
- let md = Infinity;
- for (let k = 0; k < pathPts.length - 1; k++) {
- const p1 = pathPts[k],
- p2 = pathPts[k + 1];
- const L2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
- if (!L2) continue;
- const tt =
- ((tx - p1.x) * (p2.x - p1.x) +
- (ty - p1.y) * (p2.y - p1.y)) /
- L2;
- const cl = Math.max(0, Math.min(1, tt));
- const px = p1.x + cl * (p2.x - p1.x),
- py = p1.y + cl * (p2.y - p1.y);
- const d = Math.hypot(tx - px, ty - py);
- if (d < md) md = d;
- }
- return md * scale;
- })();
- const distPassed = (function () {
- if (pathPts.length < 2 || tramSegIndex < 0) return Infinity;
- let md = Infinity;
- for (
- let k = 0;
- k <= Math.min(tramSegIndex, pathPts.length - 2);
- k++
- ) {
- const p1 = pathPts[k],
- p2 = pathPts[k + 1];
- const L2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
- if (!L2) continue;
- const tt =
- ((tx - p1.x) * (p2.x - p1.x) +
- (ty - p1.y) * (p2.y - p1.y)) /
- L2;
- const cl = Math.max(0, Math.min(1, tt));
- const px = p1.x + cl * (p2.x - p1.x),
- py = p1.y + cl * (p2.y - p1.y);
- const d = Math.hypot(tx - px, ty - py);
- if (d < md) md = d;
- }
- return md * scale;
- })();
- const distStation = (function () {
- if (!stationsForAngle.length) return Infinity;
- const DEFAULT_LABEL_OFFSET_X = 25,
- DEFAULT_LABEL_OFFSET_Y = 0;
- let md = Infinity;
- for (const st of stationsForAngle) {
- const offsetX =
- st.offset_x === 0 && st.offset_y === 0
- ? DEFAULT_LABEL_OFFSET_X
- : st.offset_x || 0 * 3;
- const offsetY =
- st.offset_x === 0 && st.offset_y === 0
- ? DEFAULT_LABEL_OFFSET_Y
- : st.offset_y || 0 * 3;
- const lx = st.longitude + offsetX,
- ly = st.latitude + offsetY;
- const d = Math.hypot(tx - lx, ty - ly);
- if (d < md) md = d;
- }
- return md * scale;
- })();
- let weight = 0;
- if (distPath < minPath) weight += 100 * (1 - distPath / minPath);
- if (distPassed < minPassed)
- weight += 10 * (1 - distPassed / minPassed);
- if (distStation < minStation)
- weight += 1000 * (1 - distStation / minStation);
- if (weight < bestScore) {
- bestScore = weight;
- bestAng = ang;
- }
- }
- return bestAng;
- })();
-
- return (
-
- );
- })()}
-
-
- );
-});
-
-export default WebGLMap;
diff --git a/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx b/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx
index b203455..18778b1 100644
--- a/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx
+++ b/src/pages/Route/route-preview/webgl-prototype/WebGLRouteMapPrototype.tsx
@@ -370,6 +370,7 @@ export const WebGLRouteMapPrototype = observer(() => {
setSelectedSight,
setStationOffset,
setSightCoordinates,
+ setMapCenter,
} = useMapData();
const { language } = languageStore;
const { setScale: setSharedScale, scale: sharedScale } = useTransform();
@@ -446,6 +447,12 @@ export const WebGLRouteMapPrototype = observer(() => {
latitude: number | null;
longitude: number | null;
}>({ latitude: null, longitude: null });
+ const pendingCenterRef = useRef<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
+ const isUserInteractingRef = useRef(false);
+ const commitCenterTimeoutRef = useRef(null);
const getRelativePointerPosition = useCallback(
(clientX: number, clientY: number) => {
@@ -532,6 +539,58 @@ export const WebGLRouteMapPrototype = observer(() => {
[rotationAngle]
);
+ const cancelScheduledCenterCommit = useCallback(() => {
+ if (commitCenterTimeoutRef.current !== null) {
+ window.clearTimeout(commitCenterTimeoutRef.current);
+ commitCenterTimeoutRef.current = null;
+ }
+ }, []);
+
+ const commitCenter = useCallback(() => {
+ const center = lastCenterRef.current;
+ if (
+ !center ||
+ center.latitude == null ||
+ center.longitude == null ||
+ !Number.isFinite(center.latitude) ||
+ !Number.isFinite(center.longitude)
+ ) {
+ return;
+ }
+
+ const epsilon = 1e-7;
+ const prev = lastAppliedCenterRef.current;
+ if (
+ prev.latitude != null &&
+ prev.longitude != null &&
+ Math.abs(prev.latitude - center.latitude) < epsilon &&
+ Math.abs(prev.longitude - center.longitude) < epsilon
+ ) {
+ return;
+ }
+
+ lastAppliedCenterRef.current = {
+ latitude: center.latitude,
+ longitude: center.longitude,
+ };
+ setMapCenter(center.latitude, center.longitude);
+ }, [setMapCenter]);
+
+ const scheduleCenterCommit = useCallback(() => {
+ cancelScheduledCenterCommit();
+ commitCenterTimeoutRef.current = window.setTimeout(() => {
+ commitCenterTimeoutRef.current = null;
+ isUserInteractingRef.current = false;
+ commitCenter();
+ }, 120);
+ }, [cancelScheduledCenterCommit, commitCenter]);
+
+ useEffect(() => {
+ return () => {
+ cancelScheduledCenterCommit();
+ };
+ }, [cancelScheduledCenterCommit]);
+
const updateTransform = useCallback(
(next: Transform) => {
const adjusted = clampTransformScale(next);
@@ -1026,6 +1085,39 @@ export const WebGLRouteMapPrototype = observer(() => {
max: baseScale * 16,
};
}
+ const centerLat =
+ routeData?.center_latitude ?? originalRouteData?.center_latitude;
+ const centerLon =
+ routeData?.center_longitude ?? originalRouteData?.center_longitude;
+ if (
+ Number.isFinite(centerLat) &&
+ Number.isFinite(centerLon) &&
+ canvas.width > 0 &&
+ canvas.height > 0
+ ) {
+ const local = coordinatesToLocal(
+ centerLat as number,
+ centerLon as number
+ );
+ const baseX = local.x * UP_SCALE;
+ const baseY = local.y * UP_SCALE;
+ const cos = Math.cos(rotationAngle);
+ const sin = Math.sin(rotationAngle);
+ const rotatedX = baseX * cos - baseY * sin;
+ const rotatedY = baseX * sin + baseY * cos;
+ const scale = transform.scale || 1;
+ transform = {
+ scale,
+ translation: {
+ x: canvas.width / 2 - rotatedX * scale,
+ y: canvas.height / 2 - rotatedY * scale,
+ },
+ };
+ lastAppliedCenterRef.current = {
+ latitude: centerLat as number,
+ longitude: centerLon as number,
+ };
+ }
transform = clampTransformScale(transform);
updateTransform(transform);
} else {
@@ -1260,8 +1352,75 @@ export const WebGLRouteMapPrototype = observer(() => {
latitude: roundedLat,
longitude: roundedLon,
};
+
+ if (isUserInteractingRef.current) {
+ pendingCenterRef.current = {
+ latitude: roundedLat,
+ longitude: roundedLon,
+ };
+ return;
+ }
+
+ const transform =
+ transformRef.current ?? lastTransformRef.current ?? transformState;
+ const canvas = canvasRef.current;
+
+ if (!canvas || !transform) {
+ pendingCenterRef.current = {
+ latitude: roundedLat,
+ longitude: roundedLon,
+ };
+ return;
+ }
+
+ const width = canvas.width || canvas.clientWidth;
+ const height = canvas.height || canvas.clientHeight;
+ if (!width || !height) {
+ pendingCenterRef.current = {
+ latitude: roundedLat,
+ longitude: roundedLon,
+ };
+ return;
+ }
+
+ const local = coordinatesToLocal(roundedLat, roundedLon);
+ const baseX = local.x * UP_SCALE;
+ const baseY = local.y * UP_SCALE;
+
+ const cos = Math.cos(rotationAngle);
+ const sin = Math.sin(rotationAngle);
+ const rotatedX = baseX * cos - baseY * sin;
+ const rotatedY = baseX * sin + baseY * cos;
+
+ const scale = transform.scale || 1;
+ const targetTranslation = {
+ x: width / 2 - rotatedX * scale,
+ y: height / 2 - rotatedY * scale,
+ };
+
+ const currentTranslation = transform.translation;
+ const distance = Math.hypot(
+ targetTranslation.x - currentTranslation.x,
+ targetTranslation.y - currentTranslation.y
+ );
+
+ if (distance < 0.5) {
+ pendingCenterRef.current = null;
+ return;
+ }
+
+ const nextTransform: Transform = {
+ scale,
+ translation: targetTranslation,
+ };
+
+ transformRef.current = nextTransform;
+ lastTransformRef.current = nextTransform;
+ setTransformState(nextTransform);
+ drawSceneRef.current();
+ pendingCenterRef.current = null;
},
- []
+ [rotationAngle, setTransformState, transformState]
);
useEffect(() => {
@@ -1290,6 +1449,18 @@ export const WebGLRouteMapPrototype = observer(() => {
applyCenterFromCoordinates,
]);
+ useEffect(() => {
+ if (!pendingCenterRef.current || !transformRef.current) {
+ return;
+ }
+
+ const { latitude, longitude } = pendingCenterRef.current;
+ pendingCenterRef.current = null;
+ window.requestAnimationFrame(() => {
+ applyCenterFromCoordinates(latitude, longitude);
+ });
+ }, [transformState, applyCenterFromCoordinates]);
+
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
@@ -1333,6 +1504,8 @@ export const WebGLRouteMapPrototype = observer(() => {
}
event.preventDefault();
+ isUserInteractingRef.current = true;
+ cancelScheduledCenterCommit();
const position = getEventPosition(event);
activePointersRef.current.set(event.pointerId, position);
canvas.setPointerCapture(event.pointerId);
@@ -1420,6 +1593,7 @@ export const WebGLRouteMapPrototype = observer(() => {
dragStateRef.current = null;
pinchStateRef.current = null;
canvas.style.cursor = "grab";
+ scheduleCenterCommit();
} else if (activePointersRef.current.size === 1) {
const remaining = Array.from(activePointersRef.current.values())[0];
dragStateRef.current = { lastPos: remaining };
@@ -1432,6 +1606,8 @@ export const WebGLRouteMapPrototype = observer(() => {
event.preventDefault();
const transform = transformRef.current;
if (!transform) return;
+ isUserInteractingRef.current = true;
+ cancelScheduledCenterCommit();
const position = getEventPosition(event);
const delta = event.deltaY > 0 ? 0.9 : 1.1;
@@ -1459,6 +1635,7 @@ export const WebGLRouteMapPrototype = observer(() => {
},
});
drawSceneRef.current();
+ scheduleCenterCommit();
};
canvas.addEventListener("pointerdown", handlePointerDown);
@@ -1476,7 +1653,7 @@ export const WebGLRouteMapPrototype = observer(() => {
canvas.removeEventListener("pointerleave", handlePointerUp);
canvas.removeEventListener("wheel", handleWheel as EventListener);
};
- }, []);
+ }, [cancelScheduledCenterCommit, scheduleCenterCommit, updateTransform]);
return (
{
+ const handleBulkLink = async () => {
if (selectedItems.size === 0) return;
setError(null);
setIsLinkingBulk(true);
- Promise.all(
- Array.from(selectedItems).map((id) =>
- authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
+ const idsToLink = Array.from(selectedItems);
+ const linkedIds: number[] = [];
+ const failedIds: number[] = [];
+
+ for (const id of idsToLink) {
+ try {
+ await authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
station_id: id,
- })
- )
- )
- .then(() => {
- const newItems = allItems.filter((item) =>
- selectedItems.has(item.id)
- );
- setLinkedItems([...linkedItems, ...newItems]);
- setSelectedItems(new Set());
- onUpdate?.();
- })
- .catch((error) => {
- console.error("Error bulk linking stations:", error);
- setError("Failed to link stations");
- })
- .finally(() => {
- setIsLinkingBulk(false);
+ });
+ linkedIds.push(id);
+ } catch (error) {
+ console.error("Error linking station:", error);
+ failedIds.push(id);
+ }
+ }
+
+ if (linkedIds.length > 0) {
+ const newItems = allItems.filter((item) => linkedIds.includes(item.id));
+ setLinkedItems((prev) => {
+ const existingIds = new Set(prev.map((item) => item.id));
+ const additions = newItems.filter((item) => !existingIds.has(item.id));
+ return [...prev, ...additions];
});
+ onUpdate?.();
+ }
+
+ setSelectedItems((prev) => {
+ if (linkedIds.length === 0) {
+ return prev;
+ }
+ const remaining = new Set(prev);
+ linkedIds.forEach((id) => remaining.delete(id));
+ return failedIds.length > 0 ? remaining : new Set();
+ });
+
+ if (failedIds.length > 0) {
+ setError(
+ failedIds.length === idsToLink.length
+ ? "Failed to link stations"
+ : "Some stations failed to link"
+ );
+ }
+
+ setIsLinkingBulk(false);
};
const toggleDetachSelection = (itemId: number) => {
@@ -269,7 +291,7 @@ const LinkedStationsContentsInner = <
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
};
- const handleBulkDetach = () => {
+ const handleBulkDetach = async () => {
const idsToDetach = Array.from(selectedToDetach);
if (idsToDetach.length === 0) return;
setError(null);
@@ -281,32 +303,47 @@ const LinkedStationsContentsInner = <
return next;
});
- Promise.all(
- idsToDetach.map((itemId) =>
- authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
+ const detachedIds: number[] = [];
+ const failedIds: number[] = [];
+
+ for (const itemId of idsToDetach) {
+ try {
+ await authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
data: { [`${childResource}_id`]: itemId },
- })
- )
- )
- .then(() => {
- setLinkedItems(
- linkedItems.filter((item) => !idsToDetach.includes(item.id))
- );
- setSelectedToDetach(new Set());
- onUpdate?.();
- })
- .catch((error) => {
- console.error("Error bulk deleting stations:", error);
- setError("Failed to delete stations");
- })
- .finally(() => {
- setDetachingIds((prev) => {
- const next = new Set(prev);
- idsToDetach.forEach((id) => next.delete(id));
- return next;
});
- setIsBulkDetaching(false);
+ detachedIds.push(itemId);
+ } catch (error) {
+ console.error("Error deleting station:", error);
+ failedIds.push(itemId);
+ }
+ }
+
+ if (detachedIds.length > 0) {
+ setLinkedItems((prev) =>
+ prev.filter((item) => !detachedIds.includes(item.id))
+ );
+ setSelectedToDetach((prev) => {
+ const remaining = new Set(prev);
+ detachedIds.forEach((id) => remaining.delete(id));
+ return failedIds.length > 0 ? remaining : new Set();
});
+ onUpdate?.();
+ }
+
+ if (failedIds.length > 0) {
+ setError(
+ failedIds.length === idsToDetach.length
+ ? "Failed to delete stations"
+ : "Some stations failed to delete"
+ );
+ }
+
+ setDetachingIds((prev) => {
+ const next = new Set(prev);
+ idsToDetach.forEach((id) => next.delete(id));
+ return next;
+ });
+ setIsBulkDetaching(false);
};
const allSelectedForDetach =
diff --git a/src/pages/Station/LinkedSights.tsx b/src/pages/Station/LinkedSights.tsx
index a5b610c..07e0e4b 100644
--- a/src/pages/Station/LinkedSights.tsx
+++ b/src/pages/Station/LinkedSights.tsx
@@ -223,33 +223,58 @@ const LinkedSightsContentsInner = <
setSelectedItems(updated);
};
- const handleBulkLink = () => {
+ const handleBulkLink = async () => {
if (selectedItems.size === 0) return;
setError(null);
setIsLinkingBulk(true);
- Promise.all(
- Array.from(selectedItems).map((id) =>
- authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
- sight_id: id,
- })
- )
- )
- .then(() => {
- const newItems = allItems.filter((item) =>
- selectedItems.has(item.id)
+ const idsToLink = Array.from(selectedItems);
+ const linkedIds: number[] = [];
+ const failedIds: number[] = [];
+
+ for (const id of idsToLink) {
+ try {
+ await authInstance.post(
+ `/${parentResource}/${parentId}/${childResource}`,
+ {
+ sight_id: id,
+ }
);
- setLinkedItems([...linkedItems, ...newItems]);
- setSelectedItems(new Set());
- onUpdate?.();
- })
- .catch((error) => {
- console.error("Error bulk linking sights:", error);
- setError("Failed to link sights");
- })
- .finally(() => {
- setIsLinkingBulk(false);
+ linkedIds.push(id);
+ } catch (error) {
+ console.error("Error linking sight:", error);
+ failedIds.push(id);
+ }
+ }
+
+ if (linkedIds.length > 0) {
+ const newItems = allItems.filter((item) => linkedIds.includes(item.id));
+ setLinkedItems((prev) => {
+ const existingIds = new Set(prev.map((item) => item.id));
+ const additions = newItems.filter((item) => !existingIds.has(item.id));
+ return [...prev, ...additions];
});
+ onUpdate?.();
+ }
+
+ setSelectedItems((prev) => {
+ if (linkedIds.length === 0) {
+ return prev;
+ }
+ const remaining = new Set(prev);
+ linkedIds.forEach((id) => remaining.delete(id));
+ return failedIds.length > 0 ? remaining : new Set();
+ });
+
+ if (failedIds.length > 0) {
+ setError(
+ failedIds.length === idsToLink.length
+ ? "Failed to link sights"
+ : "Some sights failed to link"
+ );
+ }
+
+ setIsLinkingBulk(false);
};
const toggleDetachSelection = (itemId: number) => {
@@ -270,7 +295,7 @@ const LinkedSightsContentsInner = <
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
};
- const handleBulkDetach = () => {
+ const handleBulkDetach = async () => {
const idsToDetach = Array.from(selectedToDetach);
if (idsToDetach.length === 0) return;
setError(null);
@@ -282,32 +307,50 @@ const LinkedSightsContentsInner = <
return next;
});
- Promise.all(
- idsToDetach.map((itemId) =>
- authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
- data: { [`${childResource}_id`]: itemId },
- })
- )
- )
- .then(() => {
- setLinkedItems(
- linkedItems.filter((item) => !idsToDetach.includes(item.id))
+ const detachedIds: number[] = [];
+ const failedIds: number[] = [];
+
+ for (const itemId of idsToDetach) {
+ try {
+ await authInstance.delete(
+ `/${parentResource}/${parentId}/${childResource}`,
+ {
+ data: { [`${childResource}_id`]: itemId },
+ }
);
- setSelectedToDetach(new Set());
- onUpdate?.();
- })
- .catch((error) => {
- console.error("Error bulk deleting sights:", error);
- setError("Failed to delete sights");
- })
- .finally(() => {
- setDetachingIds((prev) => {
- const next = new Set(prev);
- idsToDetach.forEach((id) => next.delete(id));
- return next;
- });
- setIsBulkDetaching(false);
+ detachedIds.push(itemId);
+ } catch (error) {
+ console.error("Error deleting sight:", error);
+ failedIds.push(itemId);
+ }
+ }
+
+ if (detachedIds.length > 0) {
+ setLinkedItems((prev) =>
+ prev.filter((item) => !detachedIds.includes(item.id))
+ );
+ setSelectedToDetach((prev) => {
+ const remaining = new Set(prev);
+ detachedIds.forEach((id) => remaining.delete(id));
+ return failedIds.length > 0 ? remaining : new Set();
});
+ onUpdate?.();
+ }
+
+ if (failedIds.length > 0) {
+ setError(
+ failedIds.length === idsToDetach.length
+ ? "Failed to delete sights"
+ : "Some sights failed to delete"
+ );
+ }
+
+ setDetachingIds((prev) => {
+ const next = new Set(prev);
+ idsToDetach.forEach((id) => next.delete(id));
+ return next;
+ });
+ setIsBulkDetaching(false);
};
const allSelectedForDetach =
@@ -465,8 +508,9 @@ const LinkedSightsContentsInner = <
item.id === selectedItemId) ||
- null
+ availableItems?.find(
+ (item) => item.id === selectedItemId
+ ) || null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
diff --git a/src/shared/store/SightsStore/index.tsx b/src/shared/store/SightsStore/index.tsx
index d4dc4f0..4198c1f 100644
--- a/src/shared/store/SightsStore/index.tsx
+++ b/src/shared/store/SightsStore/index.tsx
@@ -132,12 +132,16 @@ class SightsStore {
common: boolean
) => {
if (common) {
+ // @ts-ignore
this.sight!.common = {
+ // @ts-ignore
...this.sight!.common,
...content,
};
} else {
+ // @ts-ignore
this.sight![language] = {
+ // @ts-ignore
...this.sight![language],
...content,
};
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
index d174581..ab33043 100644
--- a/tsconfig.tsbuildinfo
+++ b/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/webglmap.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/languageselector.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
\ No newline at end of file