map undo redo

This commit is contained in:
2025-12-23 16:06:55 +09:00
parent 1bb9ca0108
commit f7acfec80e
5 changed files with 241 additions and 22 deletions

View File

@ -17,7 +17,7 @@ import { IconBoxPadding, IconChevronLeft, } from '@tabler/icons-react'
import axios from 'axios' import axios from 'axios'
import MapToolbar from './MapToolbar/MapToolbar' import MapToolbar from './MapToolbar/MapToolbar'
import MapStatusbar from './MapStatusbar/MapStatusbar' import MapStatusbar from './MapStatusbar/MapStatusbar'
import { setAlignMode, setTypeRoles, useMapStore, setMapLabel, } from '../../store/map' import { setAlignMode, setTypeRoles, useMapStore, setMapLabel, getChanges, } from '../../store/map'
import ObjectTree from '../Tree/ObjectTree' import ObjectTree from '../Tree/ObjectTree'
import { setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects' import { setSelectedDistrict, setSelectedRegion, setSelectedYear, useObjectsStore } from '../../store/objects'
import ObjectParameters from './ObjectParameters/ObjectParameters' import ObjectParameters from './ObjectParameters/ObjectParameters'
@ -53,6 +53,7 @@ const MapComponent = ({
// Store // Store
const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore().id[id] const { selectedYear, currentObjectId, selectedRegion, selectedDistrict } = useObjectsStore().id[id]
const { const {
changes, currentChange,
mode, map, currentTool, alignMode, satMapsProvider, mode, map, currentTool, alignMode, satMapsProvider,
selectedObjectType, measureDraw, draw, snap, translate, selectedObjectType, measureDraw, draw, snap, translate,
drawingLayerSource, drawingLayerSource,
@ -478,6 +479,34 @@ const MapComponent = ({
} }
}, [selectedYear]) }, [selectedYear])
useEffect(() => {
if (changes) {
console.log(changes)
}
}, [changes])
useEffect(() => {
if (currentChange) {
const figures = figuresLayer.getSource()?.getFeatures()
const lines = linesLayer.getSource()?.getFeatures()
const changes = getChanges(id).get(currentChange)
if (figures && lines) {
changes.map((f: { object_id: string, year: number, type: 'figure' | 'line', feature: Feature }) => {
const geometry = new GeoJSON()
const feature = geometry.readFeature(f.feature) as Feature<Geometry>
if (f.type === 'figure') {
figures.find(fig => fig.getProperties()['object_id'] === f.object_id && fig.getProperties()['year'] === f.year)?.setGeometry(feature.getGeometry())
} else if (f.type === 'line') {
lines.find(l => l.getProperties()['object_id'] === f.object_id && l.getProperties()['year'] === f.year)?.setGeometry(feature.getGeometry())
}
})
}
}
}, [currentChange])
return ( return (
<div style={{ display: 'grid', gridTemplateRows: 'auto min-content', position: 'relative', width: '100%', height: '100%' }}> <div style={{ display: 'grid', gridTemplateRows: 'auto min-content', position: 'relative', width: '100%', height: '100%' }}>

View File

@ -1,13 +1,14 @@
import { Mode, setMode, useMapStore } from '../../store/map' import { cleanChanges, getChanges, Mode, setMode, useMapStore } from '../../store/map'
import { IconCropLandscape, IconCropPortrait, IconEdit, IconEye, IconPrinter } from '@tabler/icons-react' import { IconCropLandscape, IconCropPortrait, IconEdit, IconEye, IconPrinter } from '@tabler/icons-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { PrintOrientation, setPrintOrientation, usePrintStore } from '../../store/print' import { PrintOrientation, setPrintOrientation, usePrintStore } from '../../store/print'
import { Button, Menu, MenuItemRadio, MenuList, MenuPopover, MenuProps, MenuTrigger, SplitButton } from '@fluentui/react-components' import { Button, Menu, MenuItemRadio, MenuList, MenuPopover, MenuProps, MenuTrigger, SplitButton } from '@fluentui/react-components'
import axiosInstance from '../../http/axiosInstanceNest'
const MapMode = ({ const MapMode = ({
map_id map_id
}: { map_id: string }) => { }: { map_id: string }) => {
const { mode } = useMapStore().id[map_id] const { mode, changes, currentChange } = useMapStore().id[map_id]
const { printOrientation } = usePrintStore() const { printOrientation } = usePrintStore()
@ -85,6 +86,62 @@ const MapMode = ({
</MenuList> </MenuList>
</MenuPopover> </MenuPopover>
</Menu> </Menu>
{changes && currentChange && mode === 'edit' && <Button
appearance={mode === 'edit' ? 'primary' : 'subtle'}
key={'save'}
// onClick={async () => {
// setMode(map_id, 'edit' as Mode)
// const changes = getChanges(map_id).get(currentChange)
// if (changes) {
// await axiosInstance.post(`/gis/features/update`, {
// features: changes
// },
// {
// baseURL: import.meta.env.VITE_API_NEST_URL,
// }).then(() => {
// cleanChanges(map_id)
// })
// }
// }}
onClick={async () => {
setMode(map_id, 'edit' as Mode);
const changes = getChanges(map_id);
const latestChanges = new Map();
// Process ALL changes in the map
for (const change of changes.values()) {
if (Array.isArray(change)) {
change.forEach(c => {
if (c.object_id) {
latestChanges.set(c.object_id, c);
}
});
} else if (change.object_id) {
latestChanges.set(change.object_id, change);
}
}
const featuresToUpdate = Array.from(latestChanges.values());
if (featuresToUpdate.length > 0) {
await axiosInstance.post(`/gis/features/update`, {
features: featuresToUpdate
}, {
baseURL: import.meta.env.VITE_API_NEST_URL,
}).then(() => {
cleanChanges(map_id); // This clears the entire map
});
}
}}
icon={<IconEdit size={16} />}
//mod={{ active: mode === 'edit' as Mode }}
>
Сохранить
</Button>}
</div > </div >
) )
} }

View File

@ -1,5 +1,5 @@
import { IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon } from '@tabler/icons-react' import { IconArrowBackUp, IconArrowForwardUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPoint, IconPolygon } from '@tabler/icons-react'
import { getDraw, setCurrentTool, useMapStore } from '../../../store/map'; import { getChanges, getCurrentChange, setCurrentChange, setCurrentTool, useMapStore } from '../../../store/map';
import { saveFeatures } from '../mapUtils'; import { saveFeatures } from '../mapUtils';
import { Button, Tooltip } from '@fluentui/react-components'; import { Button, Tooltip } from '@fluentui/react-components';
import { useAppStore } from '../../../store/app'; import { useAppStore } from '../../../store/app';
@ -13,6 +13,24 @@ const MapToolbar = ({
const { selectedRegion, selectedDistrict, selectedYear } = useObjectsStore().id[map_id] const { selectedRegion, selectedDistrict, selectedYear } = useObjectsStore().id[map_id]
const { colorScheme } = useAppStore(); const { colorScheme } = useAppStore();
const getEntries = () => {
const entries = Array.from(getChanges(map_id).entries())
const currentIndex = entries.findIndex(([key]) => key === getCurrentChange(map_id))
let prevEntry = undefined
let currentEntry = undefined
let nextEntry = undefined
if (currentIndex !== -1) {
prevEntry = currentIndex > 0 ? entries[currentIndex - 1] : undefined;
nextEntry = currentIndex < entries.length - 1 ? entries[currentIndex + 1] : undefined;
currentEntry = entries[currentIndex]
}
return { prevEntry: prevEntry, currentEntry: currentEntry, nextEntry: nextEntry }
}
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}> <div style={{ display: 'flex', flexDirection: 'column', zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
@ -21,7 +39,27 @@ const MapToolbar = ({
<Button icon={<IconExclamationCircle />} appearance='transparent' onClick={() => saveFeatures(map_id)} /> <Button icon={<IconExclamationCircle />} appearance='transparent' onClick={() => saveFeatures(map_id)} />
<Tooltip content={"Отмена"} relationship='label' hideDelay={0} showDelay={0} withArrow> <Tooltip content={"Отмена"} relationship='label' hideDelay={0} showDelay={0} withArrow>
<Button icon={<IconArrowBackUp />} appearance='transparent' onClick={() => getDraw(map_id)?.removeLastPoint()} /> <Button disabled={getEntries().prevEntry ? false : true} icon={<IconArrowBackUp />} appearance='transparent' onClick={() => {
//getDraw(map_id)?.removeLastPoint()
const { prevEntry } = getEntries()
if (prevEntry) {
console.log(prevEntry)
setCurrentChange(map_id, prevEntry[0])
}
}
} />
</Tooltip>
<Tooltip content={"Повтор"} relationship='label' hideDelay={0} showDelay={0} withArrow>
<Button disabled={getEntries().nextEntry ? false : true} icon={<IconArrowForwardUp />} appearance='transparent' onClick={() => {
//getDraw(map_id)?.removeLastPoint()
const { nextEntry } = getEntries()
if (nextEntry) {
console.log(nextEntry)
setCurrentChange(map_id, nextEntry[0])
}
}
} />
</Tooltip> </Tooltip>
<Tooltip content={"Редактировать"} relationship='label' hideDelay={0} showDelay={0} withArrow> <Tooltip content={"Редактировать"} relationship='label' hideDelay={0} showDelay={0} withArrow>

View File

@ -15,14 +15,13 @@ import { ImageStatic } from "ol/source";
import { ICitySettings, IFigure, ILine } from "../../interfaces/gis"; import { ICitySettings, IFigure, ILine } from "../../interfaces/gis";
import { fromCircle, fromExtent } from "ol/geom/Polygon"; import { fromCircle, fromExtent } from "ol/geom/Polygon";
import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles"; import { measureStyleFunction, modifyStyle } from "./Measure/MeasureStyles";
import { getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSelectedFeatures, getSnap, getTipPoint, getTranslate, PrintOrientation, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate, useMapStore } from "../../store/map"; import { addChanges, getChanges, getCurrentTool, getDraw, getDrawingLayerSource, getImageLayer, getMap, getMeasureClearPrevious, getMeasureDraw, getMeasureModify, getMeasureSource, getMeasureType, getOverlayLayerSource, getSelectedFeatures, getSnap, getTipPoint, getTranslate, PrintOrientation, setDraw, setFile, setMeasureDraw, setPolygonExtent, setRectCoords, setSnap, setTranslate, useMapStore } from "../../store/map";
import Collection from "ol/Collection"; import Collection from "ol/Collection";
import { SketchCoordType } from "ol/interaction/Draw"; import { SketchCoordType } from "ol/interaction/Draw";
import VectorImageLayer from "ol/layer/VectorImage"; import VectorImageLayer from "ol/layer/VectorImage";
import VectorSource from "ol/source/Vector"; import VectorSource from "ol/source/Vector";
import Map from "ol/Map"; import Map from "ol/Map";
import { Icon, Style } from "ol/style"; import { Icon, Style } from "ol/style";
import axiosInstance from "../../http/axiosInstanceNest";
export function getCitySettings() { export function getCitySettings() {
return { return {
@ -97,7 +96,6 @@ export const addFigures = (
type: "FeatureCollection", type: "FeatureCollection",
features: figuresData.map((figure: IFigure) => { features: figuresData.map((figure: IFigure) => {
if (figure.modified) { if (figure.modified) {
console.log("found modified", JSON.parse(figure.modified))
return { return {
...JSON.parse(figure.modified), properties: { ...JSON.parse(figure.modified), properties: {
year: figure.year, year: figure.year,
@ -817,6 +815,43 @@ export const addInteractions = (
features: new Collection(getSelectedFeatures(map_id)) features: new Collection(getSelectedFeatures(map_id))
}) })
translateMode.on('translatestart', async (e) => {
const features = e.features.getArray();
const existingChanges = getChanges(map_id);
const newChanges: any = [];
features.forEach((f: Feature<Geometry>) => {
const objectId = f.get('object_id');
const year = f.get('year');
// Check if this specific feature already has pending changes
const hasExistingChange = Array.from(existingChanges.values()).some(
change => change.object_id === objectId && change.year === year
);
// Only add if no existing change
if (!hasExistingChange) {
const json = new GeoJSON();
newChanges.push({
object_id: objectId,
year: year,
type: f.get('geometry_type') === 'line' ? 'line' : 'figure',
feature: JSON.parse(json.writeFeature(f, {
featureProjection: 'EPSG:3857',
dataProjection: 'EPSG:3857'
}))
});
}
});
// Add only new changes
if (newChanges.length > 0) {
addChanges(map_id, newChanges);
}
});
translateMode.on('translateend', async (e) => { translateMode.on('translateend', async (e) => {
const features = e.features.getArray() const features = e.features.getArray()
@ -850,14 +885,7 @@ export const addInteractions = (
} }
}) })
console.log(changesJSON) addChanges(map_id, changesJSON)
await axiosInstance.post(`/gis/features/update`, {
features: changesJSON
},
{
baseURL: import.meta.env.VITE_API_NEST_URL,
})
}) })
setTranslate(map_id, translateMode) setTranslate(map_id, translateMode)

View File

@ -1,7 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { ToolType } from '../types/tools'; import { ToolType } from '../types/tools';
import { Geometry, Point } from 'ol/geom'; import { Geometry, Point } from 'ol/geom';
import Map from 'ol/Map'; import OLMap from 'ol/Map';
import { Coordinate } from 'ol/coordinate'; import { Coordinate } from 'ol/coordinate';
import { IRectCoords, SatelliteMapsProvider } from '../interfaces/map'; import { IRectCoords, SatelliteMapsProvider } from '../interfaces/map';
import { TypeRole } from '../interfaces/gis'; import { TypeRole } from '../interfaces/gis';
@ -41,7 +41,7 @@ interface MapState {
measureShowSegments: boolean; measureShowSegments: boolean;
measureClearPrevious: boolean; measureClearPrevious: boolean;
tipPoint: Point | null; tipPoint: Point | null;
map: Map | null; map: OLMap | null;
currentZ: number | undefined; currentZ: number | undefined;
currentX: number | undefined; currentX: number | undefined;
currentY: number | undefined; currentY: number | undefined;
@ -97,6 +97,8 @@ interface MapState {
selectionDragBox: DragBox; selectionDragBox: DragBox;
capitalsLayer: VectorLayer capitalsLayer: VectorLayer
selectedFeatures: Feature<Geometry>[] selectedFeatures: Feature<Geometry>[]
changes: Map<string, any>
currentChange: string | undefined
}>; }>;
} }
@ -306,7 +308,7 @@ export const initializeMapState = (
} }
}) })
const map = new Map({ const map = new OLMap({
controls: [], controls: [],
layers: [ layers: [
baseLayer, baseLayer,
@ -523,13 +525,78 @@ export const initializeMapState = (
printScaleLine: true, printScaleLine: true,
selectionDragBox: selectionDragBox, selectionDragBox: selectionDragBox,
capitalsLayer: capitalsLayer, capitalsLayer: capitalsLayer,
selectedFeatures: [] selectedFeatures: [],
changes: new Map(),
currentChange: undefined
} }
} }
} }
}) })
} }
export const getCurrentChange = (id: string) => useMapStore.getState().id[id].currentChange
export const setCurrentChange = (id: string, currentChange: string | undefined) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], currentChange: currentChange }
}
}
})
export const cleanChanges = (id: string) => useMapStore.setState((state) => {
return {
id: {
...state.id,
[id]: { ...state.id[id], changes: new Map(), currentChange: undefined }
}
}
})
export const getChanges = (id: string) => useMapStore.getState().id[id].changes
export const addChanges = (id: string, changes: any) => useMapStore.setState((state) => {
const uid = uuidv4()
const date = new Date().getTime()
const key = `${date}-${uid}`
const mapState = state.id[id]
const currentChanges = new Map(mapState.changes)
const currentChangeKey = mapState.currentChange
// If we're not at the latest change (we've undone),
// delete all changes after currentChange
if (currentChangeKey) {
let shouldDelete = false
const entries = Array.from(currentChanges.entries())
// Find entries after currentChangeKey
for (let i = 0; i < entries.length; i++) {
const [entryKey] = entries[i]
if (entryKey === currentChangeKey) {
// From next entry onward, mark for deletion
shouldDelete = true
} else if (shouldDelete) {
currentChanges.delete(entryKey)
}
}
}
// Add the new change
currentChanges.set(key, changes)
return {
id: {
...state.id,
[id]: {
...mapState,
changes: currentChanges,
currentChange: key
}
}
}
})
export const getSelectionDragBox = (id: string) => useMapStore.getState().id[id].selectionDragBox export const getSelectionDragBox = (id: string) => useMapStore.getState().id[id].selectionDragBox
export const setPrintOrientation = (id: string, orientation: PrintOrientation) => useMapStore.setState((state) => { export const setPrintOrientation = (id: string, orientation: PrintOrientation) => useMapStore.setState((state) => {
@ -772,7 +839,7 @@ export const setSelectedObjectType = (id: string, t: number | null) => useMapSto
} }
}) })
export const setMap = (id: string, m: Map | null) => useMapStore.setState((state) => { export const setMap = (id: string, m: OLMap | null) => useMapStore.setState((state) => {
return { return {
id: { id: {
...state.id, ...state.id,