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;
}
interface HistoryState {
state: string;
}
type FeatureType = "station" | "route" | "sight";
class MapService {
@@ -620,9 +616,6 @@ class MapService {
private modifyInteraction: Modify;
private selectInteraction: Select;
private hoveredFeatureId: string | number | null;
private history: HistoryState[];
private historyIndex: number;
private beforeActionState: string | null = null;
private boundHandlePointerMove: (
event: MapBrowserEvent<PointerEvent>
) => void;
@@ -674,8 +667,6 @@ class MapService {
this.currentDrawingFeatureType = null;
this.currentInteraction = null;
this.hoveredFeatureId = null;
this.history = [];
this.historyIndex = -1;
this.clusterStyleCache = {};
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) => {
if (this.beforeActionState) {
this.addStateToHistory(this.beforeActionState);
}
event.features.getArray().forEach((feature) => {
this.saveModifiedFeature(feature as Feature<Geometry>);
});
this.beforeActionState = null;
});
if (this.map) {
@@ -1100,11 +1080,6 @@ class MapService {
return;
}
const beforeState = this.getCurrentStateAsGeoJSON();
if (beforeState) {
this.addStateToHistory(beforeState);
}
const newCoordinates = coordinates.filter(
(_, index) => index !== closestIndex
);
@@ -1330,197 +1305,6 @@ class MapService {
this.lineSource.addFeatures(lineFeatures);
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 {
@@ -1543,16 +1327,6 @@ class MapService {
}
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") {
this.unselect();
}
@@ -1619,36 +1393,20 @@ class MapService {
style: styleForDrawing,
});
this.currentInteraction.on("drawstart", () => {
this.beforeActionState = this.getCurrentStateAsGeoJSON();
});
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 fType = this.currentDrawingFeatureType;
if (!fType) return;
feature.set("featureType", fType);
let resourceName: string;
const allFeatures = [
...this.pointSource.getFeatures(),
...this.lineSource.getFeatures(),
];
switch (fType) {
case "station":
const existingStations = allFeatures.filter(
(f) => f.get("featureType") === "station"
);
const stationNumbers = existingStations
.map((f) => {
const name = f.get("name") as string;
const match = name?.match(/^Остановка (\d+)$/);
// Используем полный список из mapStore, а не отфильтрованный
const stationNumbers = mapStore.stations
.map((station) => {
const match = station.name?.match(/^Остановка (\d+)$/);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => num > 0);
@@ -1657,13 +1415,10 @@ class MapService {
resourceName = `Остановка ${nextStationNumber}`;
break;
case "sight":
const existingSights = allFeatures.filter(
(f) => f.get("featureType") === "sight"
);
const sightNumbers = existingSights
.map((f) => {
const name = f.get("name") as string;
const match = name?.match(/^Достопримечательность (\d+)$/);
// Используем полный список из mapStore, а не отфильтрованный
const sightNumbers = mapStore.sights
.map((sight) => {
const match = sight.name?.match(/^Достопримечательность (\d+)$/);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => num > 0);
@@ -1672,13 +1427,10 @@ class MapService {
resourceName = `Достопримечательность ${nextSightNumber}`;
break;
case "route":
const existingRoutes = allFeatures.filter(
(f) => f.get("featureType") === "route" && !f.get("isProxy")
);
const routeNumbers = existingRoutes
.map((f) => {
const name = f.get("name") as string;
const match = name?.match(/^Маршрут (\d+)$/);
// Используем полный список из mapStore, а не отфильтрованный
const routeNumbers = mapStore.routes
.map((route) => {
const match = route.route_number?.match(/^Маршрут (\d+)$/);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => num > 0);
@@ -1841,18 +1593,12 @@ class MapService {
): void {
if (featureId === undefined) return;
this.beforeActionState = this.getCurrentStateAsGeoJSON();
const numericId = parseInt(String(featureId).split("-")[1], 10);
if (!recourse || isNaN(numericId)) return;
mapStore
.deleteFeature(recourse, numericId)
.then(() => {
if (this.beforeActionState)
this.addStateToHistory(this.beforeActionState);
this.beforeActionState = null;
if (recourse === "route") {
const lineFeature = this.lineSource.getFeatureById(featureId);
if (lineFeature)
@@ -1877,8 +1623,6 @@ class MapService {
public deleteMultipleFeatures(featureIds: (string | number)[]): void {
if (!featureIds || featureIds.length === 0) return;
this.beforeActionState = this.getCurrentStateAsGeoJSON();
const deletePromises = Array.from(featureIds).map((id) => {
const recourse = String(id).split("-")[0];
const numericId = parseInt(String(id).split("-")[1], 10);
@@ -1895,10 +1639,6 @@ class MapService {
| number
)[];
if (successfulDeletes.length > 0) {
if (this.beforeActionState)
this.addStateToHistory(this.beforeActionState);
this.beforeActionState = null;
successfulDeletes.forEach((id) => {
const recourse = String(id).split("-")[0];
if (recourse === "route") {
@@ -2029,9 +1769,6 @@ class MapService {
// Метод для сброса кешей карты
public clearCaches() {
this.clusterStyleCache = {};
this.history = [];
this.historyIndex = -1;
this.beforeActionState = null;
this.hoveredFeatureId = null;
this.selectedIds.clear();
@@ -2086,9 +1823,6 @@ class MapService {
} catch (error) {
console.error("Failed to update feature:", 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>))
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>{" "}
- Отменить выделение
</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>
<button
onClick={() => setShowHelp(false)}

View File

@@ -60,7 +60,11 @@ export const MediaEditPage = observer(() => {
if (extension) {
if (["glb", "gltf"].includes(extension)) {
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
} else if (["mp4", "webm", "mov"].includes(extension)) {
setAvailableMediaTypes([2]); // Video
@@ -109,7 +113,11 @@ export const MediaEditPage = observer(() => {
if (["glb", "gltf"].includes(extension)) {
setAvailableMediaTypes([6]); // 3D model
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
setMediaType(1); // Default to Photo
} else if (["mp4", "webm", "mov"].includes(extension)) {

View File

@@ -1,7 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import { cityStore, languageStore, sightsStore } from "@shared";
import { useEffect, useState } from "react";
import {
cityStore,
languageStore,
sightsStore,
selectedCityStore,
} from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
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,
name: sight.name,
city_id: sight.city_id,

View File

@@ -1,7 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales";
import { languageStore, stationsStore } from "@shared";
import { useEffect, useState } from "react";
import {
languageStore,
stationsStore,
selectedCityStore,
cityStore,
} from "@shared";
import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
@@ -21,6 +26,7 @@ export const StationListPage = observer(() => {
useEffect(() => {
const fetchStations = async () => {
setIsLoading(true);
await cityStore.getCities(language);
await getStationList();
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,
name: station.name,
system_name: station.system_name,

View File

@@ -105,7 +105,11 @@ export const UploadMediaDialog = observer(
setAvailableMediaTypes([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Д-модель
setMediaType(1); // По умолчанию Фото

View File

@@ -67,11 +67,6 @@ export function MediaViewer({
style={{
height: fullHeight ? "100%" : height ? height : "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