fix: Fix problems and bugs

This commit is contained in:
2025-07-28 08:18:21 +03:00
parent 470a58a3fa
commit 4f038551a2
7 changed files with 381 additions and 148 deletions

View File

@ -75,6 +75,19 @@ import { makeAutoObservable } from "mobx";
import { stationsStore, routeStore, sightsStore } from "@shared";
// Функция для сброса кешей карты
export const clearMapCaches = () => {
// Сброс кешей маршрутов
mapStore.routes = [];
mapStore.stations = [];
mapStore.sights = [];
// Сброс кешей MapService если он доступен
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
(window as any).mapServiceInstance.clearCaches();
}
};
interface ApiRoute {
id: number;
route_number: string;
@ -328,6 +341,11 @@ class MapStore {
const mapStore = new MapStore();
// Делаем mapStore доступным глобально для сброса кешей
if (typeof window !== "undefined") {
(window as any).mapStore = mapStore;
}
// --- CONFIGURATION ---
export const mapConfig = {
center: [30.311, 59.94] as [number, number],
@ -453,7 +471,7 @@ class MapService {
public routeLayer: VectorLayer<VectorSource<Feature<LineString>>>; // Public for deselect
private clusterSource: Cluster;
private clusterStyleCache: { [key: number]: Style };
private unclusteredRouteIds: Set<string | number> = new Set();
private tooltipElement: HTMLElement;
private tooltipOverlay: Overlay | null;
private mode: string | null;
@ -488,8 +506,7 @@ class MapService {
private sightIconStyle: Style;
private selectedSightIconStyle: Style;
private drawSightIconStyle: Style;
private routeIconStyle: Style;
private selectedRouteIconStyle: Style;
private universalHoverStylePoint: Style;
private hoverSightIconStyle: Style;
private universalHoverStyleLine: Style;
@ -574,21 +591,6 @@ class MapService {
}),
});
this.routeIconStyle = new Style({
image: new CircleStyle({
radius: 8,
fill: new Fill({ color: "rgba(34, 197, 94, 0.8)" }), // Green
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
}),
});
this.selectedRouteIconStyle = new Style({
image: new CircleStyle({
radius: 10,
fill: new Fill({ color: "rgba(221, 107, 32, 0.9)" }), // Orange on select
stroke: new Stroke({ color: "#ffffff", width: 2 }),
}),
});
this.sightIconStyle = new Style({
image: new RegularShape({
fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }),
@ -659,10 +661,7 @@ class MapService {
if (!feature) return this.defaultStyle;
const fId = feature.getId();
if (fId === undefined || !this.unclusteredRouteIds.has(fId)) {
return null;
}
// Все маршруты всегда отображаются, так как они не кластеризуются
const isSelected =
this.selectInteraction?.getFeatures().getArray().includes(feature) ||
(fId !== undefined && this.selectedIds.has(fId));
@ -705,8 +704,6 @@ class MapService {
const originalFeature = featuresInCluster[0];
const fId = originalFeature.getId();
const featureType = originalFeature.get("featureType");
const isProxy = originalFeature.get("isProxy");
if (isProxy) return new Style(); // Invisible empty style
const isSelected = fId !== undefined && this.selectedIds.has(fId);
const isHovered = this.hoveredFeatureId === fId;
@ -719,45 +716,20 @@ class MapService {
if (isSelected) {
if (featureType === "sight") return this.selectedSightIconStyle;
if (featureType === "route") return this.selectedRouteIconStyle;
return this.selectedBusIconStyle;
}
if (featureType === "sight") return this.sightIconStyle;
if (featureType === "route") return this.routeIconStyle;
return this.busIconStyle;
}
},
});
this.clusterSource.on("change", () => {
const newUnclusteredRouteIds = new Set<string | number>();
this.clusterSource
.getFeatures()
.forEach((clusterFeature: Feature<any>) => {
const originalFeatures = clusterFeature.get(
"features"
) as Feature<Point>[];
if (originalFeatures && originalFeatures.length === 1) {
const originalFeature = originalFeatures[0];
if (originalFeature.get("featureType") === "route") {
const featureId = originalFeature.getId();
if (featureId !== undefined) {
newUnclusteredRouteIds.add(featureId);
}
}
}
});
if (
newUnclusteredRouteIds.size !== this.unclusteredRouteIds.size ||
![...newUnclusteredRouteIds].every((id) =>
this.unclusteredRouteIds.has(id)
)
) {
this.unclusteredRouteIds = newUnclusteredRouteIds;
this.routeLayer.changed();
}
// Поскольку маршруты больше не добавляются как точки,
// нам не нужно отслеживать unclusteredRouteIds
// Все маршруты всегда отображаются как линии
this.routeLayer.changed();
});
this.boundHandlePointerMove = this.handlePointerMove.bind(this);
@ -1209,23 +1181,7 @@ class MapService {
lineFeature.set("featureType", "route");
lineFeatures.push(lineFeature);
if (route.center_longitude != null && route.center_latitude != null) {
const centerPoint = new Point(
transform(
[route.center_longitude, route.center_latitude],
"EPSG:4326",
projection
)
);
const proxyPointFeature = new Feature({
geometry: centerPoint,
name: route.route_number,
isProxy: true,
});
proxyPointFeature.setId(routeId);
proxyPointFeature.set("featureType", "route");
pointFeatures.push(proxyPointFeature);
}
// Не создаем прокси-точки для маршрутов - они должны оставаться только линиями
});
this.pointSource.addFeatures(pointFeatures);
@ -1926,6 +1882,32 @@ class MapService {
return this.map;
}
// Метод для сброса кешей карты
public clearCaches() {
this.clusterStyleCache = {};
this.history = [];
this.historyIndex = -1;
this.beforeActionState = null;
this.hoveredFeatureId = null;
this.selectedIds.clear();
// Очищаем источники данных
if (this.pointSource) {
this.pointSource.clear();
}
if (this.lineSource) {
this.lineSource.clear();
}
// Обновляем слои
if (this.clusterLayer) {
this.clusterLayer.changed();
}
if (this.routeLayer) {
this.routeLayer.changed();
}
}
public saveCurrentPosition(): void {
if (!this.map) return;
const center = this.map.getView().getCenter();
@ -1941,20 +1923,6 @@ class MapService {
const featureId = feature.getId();
if (!featureType || featureId === undefined || !this.map) return;
if (
featureType === "route" &&
feature.getGeometry()?.getType() === "LineString"
) {
const proxyPoint = this.pointSource.getFeatureById(
featureId
) as Feature<Point>;
if (proxyPoint) {
const lineGeom = feature.getGeometry() as LineString;
const newCenter = getCenter(lineGeom.getExtent());
proxyPoint.getGeometry()?.setCoordinates(newCenter);
}
}
if (typeof featureId === "number" || !String(featureId).includes("-")) {
console.warn(
"Skipping save for feature with non-standard ID:",
@ -2023,22 +1991,7 @@ class MapService {
);
feature.setGeometry(lineGeom);
// Create and add proxy point
const centerPointGeom = new Point(
transform(
[routeData.center_longitude, routeData.center_latitude],
"EPSG:4326",
projection
)
);
const proxyPointFeature = new Feature({
geometry: centerPointGeom,
name: displayName,
isProxy: true,
});
proxyPointFeature.setId(newFeatureId);
proxyPointFeature.set("featureType", "route");
this.pointSource.addFeature(proxyPointFeature);
// Не создаем прокси-точку для маршрута - только линия
} else {
// For points: update existing
feature.setId(newFeatureId);
@ -2603,6 +2556,12 @@ export const MapPage: React.FC = () => {
setSelectedIds
);
setMapServiceInstance(service);
// Делаем mapServiceInstance доступным глобально для сброса кешей
if (typeof window !== "undefined") {
(window as any).mapServiceInstance = service;
}
loadInitialData(service);
} catch (e: any) {
setError(
@ -2615,6 +2574,11 @@ export const MapPage: React.FC = () => {
return () => {
service?.destroy();
setMapServiceInstance(null);
// Удаляем глобальную ссылку
if (typeof window !== "undefined") {
delete (window as any).mapServiceInstance;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@ -225,7 +225,7 @@ export const LinkedItemsContents = <
if (type === "edit") {
setError(null);
authInstance
.get(`/${childResource}/`)
.get(`/${childResource}`)
.then((response) => {
setAllItems(response?.data || []);
})

View File

@ -1,4 +1,4 @@
import { Button, Paper, TextField } from "@mui/material";
import { Button, TextField } from "@mui/material";
import { snapshotStore } from "@shared";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
@ -20,50 +20,56 @@ export const SnapshotCreatePage = observer(() => {
}, [id]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex justify-between items-center">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<h1 className="text-2xl font-bold">Создание снапшота</h1>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
className="w-full"
label="Название"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
<div className="w-full h-[400px] flex justify-center items-center">
<div className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex justify-between items-center">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<h1 className="text-2xl font-bold">Создание снапшота</h1>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
className="w-full"
label="Название"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button
variant="contained"
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={async () => {
try {
setIsLoading(true);
await createSnapshot(name);
setIsLoading(false);
toast.success("Снапшот успешно создан");
} catch (error) {
console.error(error);
}
}}
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Сохранить"
)}
</Button>
<Button
variant="contained"
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={async () => {
try {
setIsLoading(true);
await createSnapshot(name);
setIsLoading(false);
toast.success("Снапшот успешно создан");
navigate(-1);
} catch (error) {
console.error(error);
toast.error("Ошибка при создании снапшота");
} finally {
setIsLoading(false);
}
}}
disabled={isLoading || !name.trim()}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Сохранить"
)}
</Button>
</div>
</div>
</Paper>
</div>
);
});

View File

@ -1,3 +1,2 @@
export * from "./SnapshotListPage";
export * from "./SnapshotCreatePage";

View File

@ -176,7 +176,7 @@ export const LinkedSightsContents = <
if (type === "edit") {
setError(null);
authInstance
.get(`/${childResource}/`)
.get(`/${childResource}`)
.then((response) => {
setAllItems(response?.data || []);
})