fix: Fix webp + delete ctrl z + filter by city

This commit is contained in:
2025-10-06 10:03:41 +03:00
parent 26e4d70b95
commit 4bcc2e2cca
7 changed files with 65 additions and 309 deletions

View File

@@ -595,10 +595,6 @@ interface MapServiceConfig {
zoom: number; zoom: number;
} }
interface HistoryState {
state: string;
}
type FeatureType = "station" | "route" | "sight"; type FeatureType = "station" | "route" | "sight";
class MapService { class MapService {
@@ -620,9 +616,6 @@ class MapService {
private modifyInteraction: Modify; private modifyInteraction: Modify;
private selectInteraction: Select; private selectInteraction: Select;
private hoveredFeatureId: string | number | null; private hoveredFeatureId: string | number | null;
private history: HistoryState[];
private historyIndex: number;
private beforeActionState: string | null = null;
private boundHandlePointerMove: ( private boundHandlePointerMove: (
event: MapBrowserEvent<PointerEvent> event: MapBrowserEvent<PointerEvent>
) => void; ) => void;
@@ -674,8 +667,6 @@ class MapService {
this.currentDrawingFeatureType = null; this.currentDrawingFeatureType = null;
this.currentInteraction = null; this.currentInteraction = null;
this.hoveredFeatureId = null; this.hoveredFeatureId = null;
this.history = [];
this.historyIndex = -1;
this.clusterStyleCache = {}; this.clusterStyleCache = {};
this.setLoading = setLoading; this.setLoading = setLoading;
@@ -1037,21 +1028,10 @@ class MapService {
}, },
}); });
// @ts-ignore
this.modifyInteraction.on("modifystart", () => {
if (!this.beforeActionState) {
this.beforeActionState = this.getCurrentStateAsGeoJSON();
}
});
this.modifyInteraction.on("modifyend", (event) => { this.modifyInteraction.on("modifyend", (event) => {
if (this.beforeActionState) {
this.addStateToHistory(this.beforeActionState);
}
event.features.getArray().forEach((feature) => { event.features.getArray().forEach((feature) => {
this.saveModifiedFeature(feature as Feature<Geometry>); this.saveModifiedFeature(feature as Feature<Geometry>);
}); });
this.beforeActionState = null;
}); });
if (this.map) { if (this.map) {
@@ -1100,11 +1080,6 @@ class MapService {
return; return;
} }
const beforeState = this.getCurrentStateAsGeoJSON();
if (beforeState) {
this.addStateToHistory(beforeState);
}
const newCoordinates = coordinates.filter( const newCoordinates = coordinates.filter(
(_, index) => index !== closestIndex (_, index) => index !== closestIndex
); );
@@ -1330,197 +1305,6 @@ class MapService {
this.lineSource.addFeatures(lineFeatures); this.lineSource.addFeatures(lineFeatures);
this.updateFeaturesInReact(); this.updateFeaturesInReact();
const initialState = this.getCurrentStateAsGeoJSON();
if (initialState) {
this.addStateToHistory(initialState);
}
}
private addStateToHistory(stateToSave: string): void {
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push({ state: stateToSave });
this.historyIndex = this.history.length - 1;
}
private getCurrentStateAsGeoJSON(): string | null {
if (!this.map) return null;
const geoJSONFormat = new GeoJSON();
const allFeatures = [
...this.pointSource.getFeatures(),
...this.lineSource.getFeatures(),
];
return geoJSONFormat.writeFeatures(allFeatures, {
dataProjection: "EPSG:4326",
featureProjection: this.map.getView().getProjection().getCode(),
});
}
private applyHistoryState(geoJSONState: string) {
if (!this.map) return;
const projection = this.map.getView().getProjection();
const geoJSONFormat = new GeoJSON({
dataProjection: "EPSG:4326",
featureProjection: projection.getCode(),
});
const features = geoJSONFormat.readFeatures(
geoJSONState
) as Feature<Geometry>[];
this.unselect();
this.pointSource.clear();
this.lineSource.clear();
const pointFeatures: Feature<Point>[] = [];
const lineFeatures: Feature<LineString>[] = [];
features.forEach((feature) => {
const featureType = feature.get("featureType");
const isProxy = feature.get("isProxy");
if (featureType === "route" && !isProxy) {
lineFeatures.push(feature as Feature<LineString>);
} else {
pointFeatures.push(feature as Feature<Point>);
}
});
this.pointSource.addFeatures(pointFeatures);
this.lineSource.addFeatures(lineFeatures);
this.updateFeaturesInReact();
const newStations: ApiStation[] = [];
const newRoutes: ApiRoute[] = [];
const newSights: ApiSight[] = [];
features.forEach((feature) => {
const id = feature.getId();
if (!id || feature.get("isProxy")) return;
const [featureType, numericIdStr] = String(id).split("-");
const numericId = parseInt(numericIdStr, 10);
if (isNaN(numericId)) return;
const geometry = feature.getGeometry();
if (!geometry) return;
const properties = feature.getProperties();
if (featureType === "station") {
const coords = (geometry as Point).getCoordinates();
const [lon, lat] = toLonLat(coords, projection);
newStations.push({
id: numericId,
name: properties.name,
latitude: lat,
longitude: lon,
city_id: properties.city_id || 1, // Default city_id if not available
});
} else if (featureType === "sight") {
const coords = (geometry as Point).getCoordinates();
const [lon, lat] = toLonLat(coords, projection);
newSights.push({
id: numericId,
name: properties.name,
description: properties.description,
latitude: lat,
longitude: lon,
city_id: properties.city_id || 1, // Default city_id if not available
});
} else if (featureType === "route") {
const coords = (geometry as LineString).getCoordinates();
const path = coords.map((c) => {
const [lon, lat] = toLonLat(c, projection);
return [lat, lon] as [number, number];
});
const centerCoords = getCenter(geometry.getExtent());
const [center_longitude, center_latitude] = toLonLat(
centerCoords,
projection
);
newRoutes.push({
id: numericId,
route_number: properties.name,
path: path,
center_latitude,
center_longitude,
});
}
});
mapStore.stations = newStations;
mapStore.routes = newRoutes.sort((a, b) =>
a.route_number.localeCompare(b.route_number)
);
mapStore.sights = newSights;
}
public undo(): void {
if (this.historyIndex > 0) {
this.historyIndex--;
const stateToRestore = this.history[this.historyIndex].state;
this.applyHistoryState(stateToRestore);
const features = [
...this.pointSource.getFeatures().filter((f) => !f.get("isProxy")),
...this.lineSource.getFeatures(),
];
const updatePromises = features.map((feature) => {
const featureType = feature.get("featureType");
const geoJSONFormat = new GeoJSON({
dataProjection: "EPSG:4326",
featureProjection: this.map?.getView().getProjection().getCode(),
});
const featureGeoJSON = geoJSONFormat.writeFeatureObject(feature);
return mapStore.updateFeature(featureType, featureGeoJSON);
});
Promise.all(updatePromises)
.then(() => {})
.catch((error) => {
console.error("Failed to update backend after undo:", error);
this.historyIndex++;
const previousState = this.history[this.historyIndex].state;
this.applyHistoryState(previousState);
});
} else {
toast.info("Больше отменять нечего");
}
}
public redo(): void {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
const stateToRestore = this.history[this.historyIndex].state;
this.applyHistoryState(stateToRestore);
const features = [
...this.pointSource.getFeatures().filter((f) => !f.get("isProxy")),
...this.lineSource.getFeatures(),
];
const updatePromises = features.map((feature) => {
const featureType = feature.get("featureType");
const geoJSONFormat = new GeoJSON({
dataProjection: "EPSG:4326",
featureProjection: this.map?.getView().getProjection().getCode(),
});
const featureGeoJSON = geoJSONFormat.writeFeatureObject(feature);
return mapStore.updateFeature(featureType, featureGeoJSON);
});
Promise.all(updatePromises)
.then(() => {
toast.info("Действие повторено");
})
.catch((error) => {
console.error("Failed to update backend after redo:", error);
toast.error("Не удалось обновить данные на сервере");
this.historyIndex--;
const previousState = this.history[this.historyIndex].state;
this.applyHistoryState(previousState);
});
} else {
toast.info("Больше повторять нечего");
}
} }
private updateFeaturesInReact(): void { private updateFeaturesInReact(): void {
@@ -1543,16 +1327,6 @@ class MapService {
} }
private handleKeyDown(event: KeyboardEvent): void { private handleKeyDown(event: KeyboardEvent): void {
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
event.preventDefault();
this.undo();
return;
}
if ((event.ctrlKey || event.metaKey) && event.key === "y") {
event.preventDefault();
this.redo();
return;
}
if (event.key === "Escape") { if (event.key === "Escape") {
this.unselect(); this.unselect();
} }
@@ -1619,36 +1393,20 @@ class MapService {
style: styleForDrawing, style: styleForDrawing,
}); });
this.currentInteraction.on("drawstart", () => {
this.beforeActionState = this.getCurrentStateAsGeoJSON();
});
this.currentInteraction.on("drawend", async (event: DrawEvent) => { this.currentInteraction.on("drawend", async (event: DrawEvent) => {
if (this.beforeActionState) {
this.addStateToHistory(this.beforeActionState);
}
this.beforeActionState = null;
const feature = event.feature as Feature<Geometry>; const feature = event.feature as Feature<Geometry>;
const fType = this.currentDrawingFeatureType; const fType = this.currentDrawingFeatureType;
if (!fType) return; if (!fType) return;
feature.set("featureType", fType); feature.set("featureType", fType);
let resourceName: string; let resourceName: string;
const allFeatures = [
...this.pointSource.getFeatures(),
...this.lineSource.getFeatures(),
];
switch (fType) { switch (fType) {
case "station": case "station":
const existingStations = allFeatures.filter( // Используем полный список из mapStore, а не отфильтрованный
(f) => f.get("featureType") === "station" const stationNumbers = mapStore.stations
); .map((station) => {
const stationNumbers = existingStations const match = station.name?.match(/^Остановка (\d+)$/);
.map((f) => {
const name = f.get("name") as string;
const match = name?.match(/^Остановка (\d+)$/);
return match ? parseInt(match[1], 10) : 0; return match ? parseInt(match[1], 10) : 0;
}) })
.filter((num) => num > 0); .filter((num) => num > 0);
@@ -1657,13 +1415,10 @@ class MapService {
resourceName = `Остановка ${nextStationNumber}`; resourceName = `Остановка ${nextStationNumber}`;
break; break;
case "sight": case "sight":
const existingSights = allFeatures.filter( // Используем полный список из mapStore, а не отфильтрованный
(f) => f.get("featureType") === "sight" const sightNumbers = mapStore.sights
); .map((sight) => {
const sightNumbers = existingSights const match = sight.name?.match(/^Достопримечательность (\d+)$/);
.map((f) => {
const name = f.get("name") as string;
const match = name?.match(/^Достопримечательность (\d+)$/);
return match ? parseInt(match[1], 10) : 0; return match ? parseInt(match[1], 10) : 0;
}) })
.filter((num) => num > 0); .filter((num) => num > 0);
@@ -1672,13 +1427,10 @@ class MapService {
resourceName = `Достопримечательность ${nextSightNumber}`; resourceName = `Достопримечательность ${nextSightNumber}`;
break; break;
case "route": case "route":
const existingRoutes = allFeatures.filter( // Используем полный список из mapStore, а не отфильтрованный
(f) => f.get("featureType") === "route" && !f.get("isProxy") const routeNumbers = mapStore.routes
); .map((route) => {
const routeNumbers = existingRoutes const match = route.route_number?.match(/^Маршрут (\d+)$/);
.map((f) => {
const name = f.get("name") as string;
const match = name?.match(/^Маршрут (\d+)$/);
return match ? parseInt(match[1], 10) : 0; return match ? parseInt(match[1], 10) : 0;
}) })
.filter((num) => num > 0); .filter((num) => num > 0);
@@ -1841,18 +1593,12 @@ class MapService {
): void { ): void {
if (featureId === undefined) return; if (featureId === undefined) return;
this.beforeActionState = this.getCurrentStateAsGeoJSON();
const numericId = parseInt(String(featureId).split("-")[1], 10); const numericId = parseInt(String(featureId).split("-")[1], 10);
if (!recourse || isNaN(numericId)) return; if (!recourse || isNaN(numericId)) return;
mapStore mapStore
.deleteFeature(recourse, numericId) .deleteFeature(recourse, numericId)
.then(() => { .then(() => {
if (this.beforeActionState)
this.addStateToHistory(this.beforeActionState);
this.beforeActionState = null;
if (recourse === "route") { if (recourse === "route") {
const lineFeature = this.lineSource.getFeatureById(featureId); const lineFeature = this.lineSource.getFeatureById(featureId);
if (lineFeature) if (lineFeature)
@@ -1877,8 +1623,6 @@ class MapService {
public deleteMultipleFeatures(featureIds: (string | number)[]): void { public deleteMultipleFeatures(featureIds: (string | number)[]): void {
if (!featureIds || featureIds.length === 0) return; if (!featureIds || featureIds.length === 0) return;
this.beforeActionState = this.getCurrentStateAsGeoJSON();
const deletePromises = Array.from(featureIds).map((id) => { const deletePromises = Array.from(featureIds).map((id) => {
const recourse = String(id).split("-")[0]; const recourse = String(id).split("-")[0];
const numericId = parseInt(String(id).split("-")[1], 10); const numericId = parseInt(String(id).split("-")[1], 10);
@@ -1895,10 +1639,6 @@ class MapService {
| number | number
)[]; )[];
if (successfulDeletes.length > 0) { if (successfulDeletes.length > 0) {
if (this.beforeActionState)
this.addStateToHistory(this.beforeActionState);
this.beforeActionState = null;
successfulDeletes.forEach((id) => { successfulDeletes.forEach((id) => {
const recourse = String(id).split("-")[0]; const recourse = String(id).split("-")[0];
if (recourse === "route") { if (recourse === "route") {
@@ -2029,9 +1769,6 @@ class MapService {
// Метод для сброса кешей карты // Метод для сброса кешей карты
public clearCaches() { public clearCaches() {
this.clusterStyleCache = {}; this.clusterStyleCache = {};
this.history = [];
this.historyIndex = -1;
this.beforeActionState = null;
this.hoveredFeatureId = null; this.hoveredFeatureId = null;
this.selectedIds.clear(); this.selectedIds.clear();
@@ -2086,9 +1823,6 @@ class MapService {
} catch (error) { } catch (error) {
console.error("Failed to update feature:", error); console.error("Failed to update feature:", error);
toast.error(`Не удалось обновить: ${error}`); toast.error(`Не удалось обновить: ${error}`);
if (this.beforeActionState) {
this.applyHistoryState(this.beforeActionState);
}
} }
} }
@@ -2156,10 +1890,6 @@ class MapService {
if (this.pointSource.hasFeature(feature as Feature<Point>)) if (this.pointSource.hasFeature(feature as Feature<Point>))
this.pointSource.removeFeature(feature as Feature<Point>); this.pointSource.removeFeature(feature as Feature<Point>);
} }
if (this.beforeActionState) {
this.applyHistoryState(this.beforeActionState);
}
this.beforeActionState = null;
} }
} }
} }
@@ -2994,18 +2724,6 @@ export const MapPage: React.FC = observer(() => {
<span className="font-mono bg-gray-100 px-1 rounded">Esc</span>{" "} <span className="font-mono bg-gray-100 px-1 rounded">Esc</span>{" "}
- Отменить выделение - Отменить выделение
</li> </li>
<li>
<span className="font-mono bg-gray-100 px-1 rounded">
Ctrl+Z
</span>{" "}
- Отменить действие
</li>
<li>
<span className="font-mono bg-gray-100 px-1 rounded">
Ctrl+Y
</span>{" "}
- Повторить действие
</li>
</ul> </ul>
<button <button
onClick={() => setShowHelp(false)} onClick={() => setShowHelp(false)}

View File

@@ -60,7 +60,11 @@ export const MediaEditPage = observer(() => {
if (extension) { if (extension) {
if (["glb", "gltf"].includes(extension)) { if (["glb", "gltf"].includes(extension)) {
setAvailableMediaTypes([6]); // 3D model setAvailableMediaTypes([6]); // 3D model
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) { } else if (
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
extension
)
) {
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
} else if (["mp4", "webm", "mov"].includes(extension)) { } else if (["mp4", "webm", "mov"].includes(extension)) {
setAvailableMediaTypes([2]); // Video setAvailableMediaTypes([2]); // Video
@@ -109,7 +113,11 @@ export const MediaEditPage = observer(() => {
if (["glb", "gltf"].includes(extension)) { if (["glb", "gltf"].includes(extension)) {
setAvailableMediaTypes([6]); // 3D model setAvailableMediaTypes([6]); // 3D model
setMediaType(6); setMediaType(6);
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) { } else if (
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
extension
)
) {
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
setMediaType(1); // Default to Photo setMediaType(1); // Default to Photo
} else if (["mp4", "webm", "mov"].includes(extension)) { } else if (["mp4", "webm", "mov"].includes(extension)) {

View File

@@ -1,7 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { cityStore, languageStore, sightsStore } from "@shared"; import {
import { useEffect, useState } from "react"; cityStore,
languageStore,
sightsStore,
selectedCityStore,
} from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react"; import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -92,7 +97,16 @@ export const SightListPage = observer(() => {
}, },
]; ];
const rows = sights.map((sight) => ({ // Фильтрация достопримечательностей по выбранному городу
const filteredSights = useMemo(() => {
const { selectedCityId } = selectedCityStore;
if (!selectedCityId) {
return sights;
}
return sights.filter((sight: any) => sight.city_id === selectedCityId);
}, [sights, selectedCityStore.selectedCityId]);
const rows = filteredSights.map((sight) => ({
id: sight.id, id: sight.id,
name: sight.name, name: sight.name,
city_id: sight.city_id, city_id: sight.city_id,

View File

@@ -1,7 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { languageStore, stationsStore } from "@shared"; import {
import { useEffect, useState } from "react"; languageStore,
stationsStore,
selectedCityStore,
cityStore,
} from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react"; import { Eye, Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -21,6 +26,7 @@ export const StationListPage = observer(() => {
useEffect(() => { useEffect(() => {
const fetchStations = async () => { const fetchStations = async () => {
setIsLoading(true); setIsLoading(true);
await cityStore.getCities(language);
await getStationList(); await getStationList();
setIsLoading(false); setIsLoading(false);
}; };
@@ -109,7 +115,18 @@ export const StationListPage = observer(() => {
}, },
]; ];
const rows = stationLists[language].data.map((station: any) => ({ // Фильтрация станций по выбранному городу
const filteredStations = useMemo(() => {
const { selectedCityId } = selectedCityStore;
if (!selectedCityId) {
return stationLists[language].data;
}
return stationLists[language].data.filter(
(station: any) => station.city_id === selectedCityId
);
}, [stationLists, language, selectedCityStore.selectedCityId]);
const rows = filteredStations.map((station: any) => ({
id: station.id, id: station.id,
name: station.name, name: station.name,
system_name: station.system_name, system_name: station.system_name,

View File

@@ -105,7 +105,11 @@ export const UploadMediaDialog = observer(
setAvailableMediaTypes([6]); setAvailableMediaTypes([6]);
setMediaType(6); setMediaType(6);
} }
if (["jpg", "jpeg", "png", "gif", "svg"].includes(extension)) { if (
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
extension
)
) {
// Для изображений доступны все типы кроме видео // Для изображений доступны все типы кроме видео
setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель
setMediaType(1); // По умолчанию Фото setMediaType(1); // По умолчанию Фото

View File

@@ -67,11 +67,6 @@ export function MediaViewer({
style={{ style={{
height: fullHeight ? "100%" : height ? height : "auto", height: fullHeight ? "100%" : height ? height : "auto",
width: fullWidth ? "100%" : width ? width : "auto", width: fullWidth ? "100%" : width ? width : "auto",
...(media?.filename?.toLowerCase().endsWith(".webp") && {
maxWidth: "300px",
maxHeight: "300px",
objectFit: "contain",
}),
}} }}
/> />
)} )}

File diff suppressed because one or more lines are too long