feat: Корректировки 07.11.25
This commit is contained in:
@@ -420,7 +420,15 @@ export const RouteCreatePage = observer(() => {
|
|||||||
type="number"
|
type="number"
|
||||||
value={scaleMin}
|
value={scaleMin}
|
||||||
onChange={(e) => {
|
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);
|
setScaleMin(value);
|
||||||
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
||||||
setScaleMax(value);
|
setScaleMax(value);
|
||||||
@@ -447,6 +455,10 @@ export const RouteCreatePage = observer(() => {
|
|||||||
value={scaleMax}
|
value={scaleMax}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
if (Number(e.target.value) > 300) {
|
||||||
|
e.target.value = "300";
|
||||||
|
}
|
||||||
|
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setScaleMax(value);
|
setScaleMax(value);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -393,20 +393,29 @@ export const RouteEditPage = observer(() => {
|
|||||||
type="number"
|
type="number"
|
||||||
value={editRouteData.scale_min ?? ""}
|
value={editRouteData.scale_min ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value =
|
let value = e.target.value === "" ? null : e.target.value;
|
||||||
e.target.value === "" ? null : parseFloat(e.target.value);
|
|
||||||
|
if (value && Number(value) > 297) {
|
||||||
|
value = "297";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && Number(value) < 10) {
|
||||||
|
value = "10";
|
||||||
|
}
|
||||||
|
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_min: value,
|
scale_min: value ? Number(value) : null,
|
||||||
});
|
});
|
||||||
// Если максимальный масштаб стал меньше минимального, обновляем его
|
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||||
if (
|
if (
|
||||||
value !== null &&
|
value !== null &&
|
||||||
editRouteData.scale_max !== null &&
|
editRouteData.scale_max !== null &&
|
||||||
editRouteData.scale_max !== undefined &&
|
editRouteData.scale_max !== undefined &&
|
||||||
value > editRouteData.scale_max
|
value &&
|
||||||
|
Number(value) > (editRouteData.scale_max ?? 0)
|
||||||
) {
|
) {
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_max: value,
|
scale_max: value ? Number(value) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -418,12 +427,17 @@ export const RouteEditPage = observer(() => {
|
|||||||
label="Масштаб (макс)"
|
label="Масштаб (макс)"
|
||||||
type="number"
|
type="number"
|
||||||
value={editRouteData.scale_max ?? ""}
|
value={editRouteData.scale_max ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
let value = e.target.value;
|
||||||
|
|
||||||
|
if (Number(value) > 300) {
|
||||||
|
value = "300";
|
||||||
|
}
|
||||||
|
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_max:
|
scale_max: value === "" ? null : parseFloat(value),
|
||||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
});
|
||||||
})
|
}}
|
||||||
}
|
|
||||||
error={
|
error={
|
||||||
editRouteData.scale_min !== null &&
|
editRouteData.scale_min !== null &&
|
||||||
editRouteData.scale_min !== undefined &&
|
editRouteData.scale_min !== undefined &&
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let newMinScale = Number(e.target.value);
|
let newMinScale = Number(e.target.value);
|
||||||
|
|
||||||
if (newMinScale < 1) {
|
if (newMinScale < 10) {
|
||||||
newMinScale = 1;
|
newMinScale = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMinScale(newMinScale);
|
setMinScale(newMinScale);
|
||||||
@@ -189,8 +189,8 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let newMaxScale = Number(e.target.value);
|
let newMaxScale = Number(e.target.value);
|
||||||
|
|
||||||
if (newMaxScale < 3) {
|
if (newMaxScale < 13) {
|
||||||
newMaxScale = 3;
|
newMaxScale = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newMaxScale > 300) {
|
if (newMaxScale > 300) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
import { Widgets } from "./Widgets";
|
import { Widgets } from "./Widgets";
|
||||||
import { Application, extend } from "@pixi/react";
|
import { extend } from "@pixi/react";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Graphics,
|
Graphics,
|
||||||
@@ -12,22 +12,15 @@ import {
|
|||||||
import { Box, Stack } from "@mui/material";
|
import { Box, Stack } from "@mui/material";
|
||||||
import { MapDataProvider, useMapData } from "./MapDataContext";
|
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||||
import { TransformProvider, useTransform } from "./TransformContext";
|
import { TransformProvider, useTransform } from "./TransformContext";
|
||||||
import { InfiniteCanvas } from "./InfiniteCanvas";
|
|
||||||
|
|
||||||
import { TravelPath } from "./TravelPath";
|
|
||||||
import { LeftSidebar } from "./LeftSidebar";
|
import { LeftSidebar } from "./LeftSidebar";
|
||||||
import { RightSidebar } from "./RightSidebar";
|
import { RightSidebar } from "./RightSidebar";
|
||||||
|
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
|
||||||
import { languageStore } from "@shared";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Sight } from "./Sight";
|
|
||||||
import { SightData } from "./types";
|
|
||||||
import { Station } from "./Station";
|
|
||||||
import { UP_SCALE } from "./Constants";
|
import { UP_SCALE } from "./Constants";
|
||||||
import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype";
|
import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
extend({
|
extend({
|
||||||
Container,
|
Container,
|
||||||
@@ -43,7 +36,7 @@ const Loading = () => {
|
|||||||
|
|
||||||
if (isRouteLoading || isStationLoading || isSightLoading) {
|
if (isRouteLoading || isStationLoading || isSightLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
|
<div className="fixed flex z-1000000000 items-center justify-center h-screen w-screen bg-[#111]">
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -91,15 +84,8 @@ export const RoutePreview = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RouteMap = observer(() => {
|
export const RouteMap = observer(() => {
|
||||||
const { language } = languageStore;
|
|
||||||
const { setPosition, setTransform, screenCenter } = useTransform();
|
const { setPosition, setTransform, screenCenter } = useTransform();
|
||||||
const {
|
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||||
routeData,
|
|
||||||
stationData,
|
|
||||||
sightData,
|
|
||||||
originalRouteData,
|
|
||||||
originalSightData,
|
|
||||||
} = useMapData();
|
|
||||||
|
|
||||||
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||||
const [isSetup, setIsSetup] = useState(false);
|
const [isSetup, setIsSetup] = useState(false);
|
||||||
|
|||||||
@@ -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 { observer } from "mobx-react-lite";
|
||||||
import { languageStore } from "@shared";
|
import { languageStore } from "@shared";
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ const ArrowIcon = ({ rotation }: { rotation: number }) => (
|
|||||||
|
|
||||||
const LanguageSelector = observer(
|
const LanguageSelector = observer(
|
||||||
({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => {
|
({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => {
|
||||||
const { language, setLanguage } = languageStore;
|
const { setLanguage } = languageStore;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -145,20 +145,6 @@ const LanguageSelector = observer(
|
|||||||
|
|
||||||
const toggle = () => setIsOpen((prev) => !prev);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -370,6 +370,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
setSelectedSight,
|
setSelectedSight,
|
||||||
setStationOffset,
|
setStationOffset,
|
||||||
setSightCoordinates,
|
setSightCoordinates,
|
||||||
|
setMapCenter,
|
||||||
} = useMapData();
|
} = useMapData();
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { setScale: setSharedScale, scale: sharedScale } = useTransform();
|
const { setScale: setSharedScale, scale: sharedScale } = useTransform();
|
||||||
@@ -446,6 +447,12 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
latitude: number | null;
|
latitude: number | null;
|
||||||
longitude: number | null;
|
longitude: number | null;
|
||||||
}>({ latitude: null, longitude: null });
|
}>({ latitude: null, longitude: null });
|
||||||
|
const pendingCenterRef = useRef<{
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
} | null>(null);
|
||||||
|
const isUserInteractingRef = useRef(false);
|
||||||
|
const commitCenterTimeoutRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const getRelativePointerPosition = useCallback(
|
const getRelativePointerPosition = useCallback(
|
||||||
(clientX: number, clientY: number) => {
|
(clientX: number, clientY: number) => {
|
||||||
@@ -532,6 +539,58 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
[rotationAngle]
|
[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(
|
const updateTransform = useCallback(
|
||||||
(next: Transform) => {
|
(next: Transform) => {
|
||||||
const adjusted = clampTransformScale(next);
|
const adjusted = clampTransformScale(next);
|
||||||
@@ -1026,6 +1085,39 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
max: baseScale * 16,
|
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);
|
transform = clampTransformScale(transform);
|
||||||
updateTransform(transform);
|
updateTransform(transform);
|
||||||
} else {
|
} else {
|
||||||
@@ -1260,8 +1352,75 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
latitude: roundedLat,
|
latitude: roundedLat,
|
||||||
longitude: roundedLon,
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -1290,6 +1449,18 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
applyCenterFromCoordinates,
|
applyCenterFromCoordinates,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingCenterRef.current || !transformRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { latitude, longitude } = pendingCenterRef.current;
|
||||||
|
pendingCenterRef.current = null;
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
applyCenterFromCoordinates(latitude, longitude);
|
||||||
|
});
|
||||||
|
}, [transformState, applyCenterFromCoordinates]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
@@ -1333,6 +1504,8 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
isUserInteractingRef.current = true;
|
||||||
|
cancelScheduledCenterCommit();
|
||||||
const position = getEventPosition(event);
|
const position = getEventPosition(event);
|
||||||
activePointersRef.current.set(event.pointerId, position);
|
activePointersRef.current.set(event.pointerId, position);
|
||||||
canvas.setPointerCapture(event.pointerId);
|
canvas.setPointerCapture(event.pointerId);
|
||||||
@@ -1420,6 +1593,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
dragStateRef.current = null;
|
dragStateRef.current = null;
|
||||||
pinchStateRef.current = null;
|
pinchStateRef.current = null;
|
||||||
canvas.style.cursor = "grab";
|
canvas.style.cursor = "grab";
|
||||||
|
scheduleCenterCommit();
|
||||||
} else if (activePointersRef.current.size === 1) {
|
} else if (activePointersRef.current.size === 1) {
|
||||||
const remaining = Array.from(activePointersRef.current.values())[0];
|
const remaining = Array.from(activePointersRef.current.values())[0];
|
||||||
dragStateRef.current = { lastPos: remaining };
|
dragStateRef.current = { lastPos: remaining };
|
||||||
@@ -1432,6 +1606,8 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const transform = transformRef.current;
|
const transform = transformRef.current;
|
||||||
if (!transform) return;
|
if (!transform) return;
|
||||||
|
isUserInteractingRef.current = true;
|
||||||
|
cancelScheduledCenterCommit();
|
||||||
|
|
||||||
const position = getEventPosition(event);
|
const position = getEventPosition(event);
|
||||||
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
const delta = event.deltaY > 0 ? 0.9 : 1.1;
|
||||||
@@ -1459,6 +1635,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
drawSceneRef.current();
|
drawSceneRef.current();
|
||||||
|
scheduleCenterCommit();
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.addEventListener("pointerdown", handlePointerDown);
|
canvas.addEventListener("pointerdown", handlePointerDown);
|
||||||
@@ -1476,7 +1653,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
canvas.removeEventListener("pointerleave", handlePointerUp);
|
canvas.removeEventListener("pointerleave", handlePointerUp);
|
||||||
canvas.removeEventListener("wheel", handleWheel as EventListener);
|
canvas.removeEventListener("wheel", handleWheel as EventListener);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [cancelScheduledCenterCommit, scheduleCenterCommit, updateTransform]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -222,33 +222,55 @@ const LinkedStationsContentsInner = <
|
|||||||
setSelectedItems(updated);
|
setSelectedItems(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkLink = () => {
|
const handleBulkLink = async () => {
|
||||||
if (selectedItems.size === 0) return;
|
if (selectedItems.size === 0) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
setIsLinkingBulk(true);
|
setIsLinkingBulk(true);
|
||||||
Promise.all(
|
const idsToLink = Array.from(selectedItems);
|
||||||
Array.from(selectedItems).map((id) =>
|
const linkedIds: number[] = [];
|
||||||
authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
const failedIds: number[] = [];
|
||||||
|
|
||||||
|
for (const id of idsToLink) {
|
||||||
|
try {
|
||||||
|
await authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
station_id: id,
|
station_id: id,
|
||||||
})
|
});
|
||||||
)
|
linkedIds.push(id);
|
||||||
)
|
} catch (error) {
|
||||||
.then(() => {
|
console.error("Error linking station:", error);
|
||||||
const newItems = allItems.filter((item) =>
|
failedIds.push(id);
|
||||||
selectedItems.has(item.id)
|
}
|
||||||
);
|
}
|
||||||
setLinkedItems([...linkedItems, ...newItems]);
|
|
||||||
setSelectedItems(new Set());
|
if (linkedIds.length > 0) {
|
||||||
onUpdate?.();
|
const newItems = allItems.filter((item) => linkedIds.includes(item.id));
|
||||||
})
|
setLinkedItems((prev) => {
|
||||||
.catch((error) => {
|
const existingIds = new Set(prev.map((item) => item.id));
|
||||||
console.error("Error bulk linking stations:", error);
|
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||||
setError("Failed to link stations");
|
return [...prev, ...additions];
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLinkingBulk(false);
|
|
||||||
});
|
});
|
||||||
|
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) => {
|
const toggleDetachSelection = (itemId: number) => {
|
||||||
@@ -269,7 +291,7 @@ const LinkedStationsContentsInner = <
|
|||||||
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkDetach = () => {
|
const handleBulkDetach = async () => {
|
||||||
const idsToDetach = Array.from(selectedToDetach);
|
const idsToDetach = Array.from(selectedToDetach);
|
||||||
if (idsToDetach.length === 0) return;
|
if (idsToDetach.length === 0) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -281,32 +303,47 @@ const LinkedStationsContentsInner = <
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(
|
const detachedIds: number[] = [];
|
||||||
idsToDetach.map((itemId) =>
|
const failedIds: number[] = [];
|
||||||
authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
|
||||||
|
for (const itemId of idsToDetach) {
|
||||||
|
try {
|
||||||
|
await authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||||
data: { [`${childResource}_id`]: itemId },
|
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 =
|
const allSelectedForDetach =
|
||||||
|
|||||||
@@ -223,33 +223,58 @@ const LinkedSightsContentsInner = <
|
|||||||
setSelectedItems(updated);
|
setSelectedItems(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkLink = () => {
|
const handleBulkLink = async () => {
|
||||||
if (selectedItems.size === 0) return;
|
if (selectedItems.size === 0) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
setIsLinkingBulk(true);
|
setIsLinkingBulk(true);
|
||||||
Promise.all(
|
const idsToLink = Array.from(selectedItems);
|
||||||
Array.from(selectedItems).map((id) =>
|
const linkedIds: number[] = [];
|
||||||
authInstance.post(`/${parentResource}/${parentId}/${childResource}`, {
|
const failedIds: number[] = [];
|
||||||
sight_id: id,
|
|
||||||
})
|
for (const id of idsToLink) {
|
||||||
)
|
try {
|
||||||
)
|
await authInstance.post(
|
||||||
.then(() => {
|
`/${parentResource}/${parentId}/${childResource}`,
|
||||||
const newItems = allItems.filter((item) =>
|
{
|
||||||
selectedItems.has(item.id)
|
sight_id: id,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
setLinkedItems([...linkedItems, ...newItems]);
|
linkedIds.push(id);
|
||||||
setSelectedItems(new Set());
|
} catch (error) {
|
||||||
onUpdate?.();
|
console.error("Error linking sight:", error);
|
||||||
})
|
failedIds.push(id);
|
||||||
.catch((error) => {
|
}
|
||||||
console.error("Error bulk linking sights:", error);
|
}
|
||||||
setError("Failed to link sights");
|
|
||||||
})
|
if (linkedIds.length > 0) {
|
||||||
.finally(() => {
|
const newItems = allItems.filter((item) => linkedIds.includes(item.id));
|
||||||
setIsLinkingBulk(false);
|
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) => {
|
const toggleDetachSelection = (itemId: number) => {
|
||||||
@@ -270,7 +295,7 @@ const LinkedSightsContentsInner = <
|
|||||||
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkDetach = () => {
|
const handleBulkDetach = async () => {
|
||||||
const idsToDetach = Array.from(selectedToDetach);
|
const idsToDetach = Array.from(selectedToDetach);
|
||||||
if (idsToDetach.length === 0) return;
|
if (idsToDetach.length === 0) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -282,32 +307,50 @@ const LinkedSightsContentsInner = <
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(
|
const detachedIds: number[] = [];
|
||||||
idsToDetach.map((itemId) =>
|
const failedIds: number[] = [];
|
||||||
authInstance.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
|
||||||
data: { [`${childResource}_id`]: itemId },
|
for (const itemId of idsToDetach) {
|
||||||
})
|
try {
|
||||||
)
|
await authInstance.delete(
|
||||||
)
|
`/${parentResource}/${parentId}/${childResource}`,
|
||||||
.then(() => {
|
{
|
||||||
setLinkedItems(
|
data: { [`${childResource}_id`]: itemId },
|
||||||
linkedItems.filter((item) => !idsToDetach.includes(item.id))
|
}
|
||||||
);
|
);
|
||||||
setSelectedToDetach(new Set());
|
detachedIds.push(itemId);
|
||||||
onUpdate?.();
|
} catch (error) {
|
||||||
})
|
console.error("Error deleting sight:", error);
|
||||||
.catch((error) => {
|
failedIds.push(itemId);
|
||||||
console.error("Error bulk deleting sights:", error);
|
}
|
||||||
setError("Failed to delete sights");
|
}
|
||||||
})
|
|
||||||
.finally(() => {
|
if (detachedIds.length > 0) {
|
||||||
setDetachingIds((prev) => {
|
setLinkedItems((prev) =>
|
||||||
const next = new Set(prev);
|
prev.filter((item) => !detachedIds.includes(item.id))
|
||||||
idsToDetach.forEach((id) => next.delete(id));
|
);
|
||||||
return next;
|
setSelectedToDetach((prev) => {
|
||||||
});
|
const remaining = new Set(prev);
|
||||||
setIsBulkDetaching(false);
|
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 =
|
const allSelectedForDetach =
|
||||||
@@ -465,8 +508,9 @@ const LinkedSightsContentsInner = <
|
|||||||
<Autocomplete
|
<Autocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
value={
|
value={
|
||||||
availableItems?.find((item) => item.id === selectedItemId) ||
|
availableItems?.find(
|
||||||
null
|
(item) => item.id === selectedItemId
|
||||||
|
) || null
|
||||||
}
|
}
|
||||||
onChange={(_, newValue) =>
|
onChange={(_, newValue) =>
|
||||||
setSelectedItemId(newValue?.id || null)
|
setSelectedItemId(newValue?.id || null)
|
||||||
|
|||||||
@@ -132,12 +132,16 @@ class SightsStore {
|
|||||||
common: boolean
|
common: boolean
|
||||||
) => {
|
) => {
|
||||||
if (common) {
|
if (common) {
|
||||||
|
// @ts-ignore
|
||||||
this.sight!.common = {
|
this.sight!.common = {
|
||||||
|
// @ts-ignore
|
||||||
...this.sight!.common,
|
...this.sight!.common,
|
||||||
...content,
|
...content,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
this.sight![language] = {
|
this.sight![language] = {
|
||||||
|
// @ts-ignore
|
||||||
...this.sight![language],
|
...this.sight![language],
|
||||||
...content,
|
...content,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user