refactor
This commit is contained in:
55
lib/editor/draft/draftDiff.ts
Normal file
55
lib/editor/draft/draftDiff.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
Feature,
|
||||
FeatureCollection,
|
||||
FeatureProperties,
|
||||
Geometry,
|
||||
} from "@/types/geo";
|
||||
import type { Change } from "@/lib/editor/draft/editorTypes";
|
||||
|
||||
export const deepClone = <T,>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export function geometryEquals(a: Geometry | undefined, b: Geometry | undefined): boolean {
|
||||
if (!a || !b) return false;
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
export function featureEquals(a: Feature | undefined, b: Feature | undefined): boolean {
|
||||
if (!a || !b) return false;
|
||||
return JSON.stringify(a.geometry) === JSON.stringify(b.geometry) &&
|
||||
JSON.stringify(a.properties) === JSON.stringify(b.properties);
|
||||
}
|
||||
|
||||
export function buildInitialMap(fc: FeatureCollection) {
|
||||
const map = new Map<FeatureProperties["id"], Feature>();
|
||||
for (const feature of fc.features) {
|
||||
map.set(feature.properties.id, deepClone(feature));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function diffDraftToInitial(
|
||||
draft: FeatureCollection,
|
||||
initialMap: Map<FeatureProperties["id"], Feature>
|
||||
) {
|
||||
const next = new Map<FeatureProperties["id"], Change>();
|
||||
const seen = new Set<FeatureProperties["id"]>();
|
||||
|
||||
for (const feature of draft.features) {
|
||||
const id = feature.properties.id;
|
||||
seen.add(id);
|
||||
const initialFeature = initialMap.get(id);
|
||||
if (!initialFeature) {
|
||||
next.set(id, { action: "create", feature: deepClone(feature) });
|
||||
} else if (!featureEquals(initialFeature, feature)) {
|
||||
next.set(id, { action: "update", id, geometry: deepClone(feature.geometry) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id] of initialMap.entries()) {
|
||||
if (!seen.has(id)) {
|
||||
next.set(id, { action: "delete", id });
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
15
lib/editor/draft/editorTypes.ts
Normal file
15
lib/editor/draft/editorTypes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
Feature,
|
||||
FeatureProperties,
|
||||
Geometry,
|
||||
GeometryChange,
|
||||
} from "@/types/geo";
|
||||
|
||||
export type Change = GeometryChange;
|
||||
|
||||
export type UndoAction =
|
||||
| { type: "update"; id: FeatureProperties["id"]; prevGeometry: Geometry }
|
||||
| { type: "properties"; id: FeatureProperties["id"]; prevProperties: FeatureProperties }
|
||||
| { type: "delete"; feature: Feature }
|
||||
| { type: "create"; id: FeatureProperties["id"] };
|
||||
|
||||
29
lib/editor/draft/useDraftState.ts
Normal file
29
lib/editor/draft/useDraftState.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { FeatureCollection } from "@/types/geo";
|
||||
import { deepClone } from "@/lib/editor/draft/draftDiff";
|
||||
|
||||
export function useDraftState(initialData: FeatureCollection) {
|
||||
const [draft, setDraft] = useState<FeatureCollection>(() => deepClone(initialData));
|
||||
const draftRef = useRef<FeatureCollection>(deepClone(initialData));
|
||||
|
||||
const commitDraft = useCallback((nextDraft: FeatureCollection) => {
|
||||
const cloned = deepClone(nextDraft);
|
||||
draftRef.current = cloned;
|
||||
setDraft(cloned);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
draftRef.current = draft;
|
||||
}, [draft]);
|
||||
|
||||
const resetDraft = useCallback((nextDraft: FeatureCollection) => {
|
||||
commitDraft(nextDraft);
|
||||
}, [commitDraft]);
|
||||
|
||||
return {
|
||||
draft,
|
||||
draftRef,
|
||||
commitDraft,
|
||||
resetDraft,
|
||||
};
|
||||
}
|
||||
80
lib/editor/draft/useUndoStack.ts
Normal file
80
lib/editor/draft/useUndoStack.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { UndoAction } from "@/lib/editor/draft/editorTypes";
|
||||
import { geometryEquals } from "@/lib/editor/draft/draftDiff";
|
||||
|
||||
type Options = {
|
||||
applyUndoAction: (action: UndoAction) => boolean;
|
||||
};
|
||||
|
||||
export function useUndoStack(options: Options) {
|
||||
const { applyUndoAction } = options;
|
||||
const [undoStack, setUndoStack] = useState<UndoAction[]>([]);
|
||||
|
||||
const pushUndo = useCallback((action: UndoAction) => {
|
||||
setUndoStack((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (isSameUndo(last, action)) return prev;
|
||||
return [...prev, action];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
let applied = false;
|
||||
setUndoStack((prev) => {
|
||||
if (applied) return prev;
|
||||
if (!prev.length) return prev;
|
||||
|
||||
const last = prev[prev.length - 1];
|
||||
const remaining = prev.slice(0, -1);
|
||||
applied = true;
|
||||
|
||||
const didApply = applyUndoAction(last);
|
||||
return didApply ? remaining : prev;
|
||||
});
|
||||
}, [applyUndoAction]);
|
||||
|
||||
const clearUndo = useCallback(() => {
|
||||
setUndoStack([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
undoStack,
|
||||
pushUndo,
|
||||
undo,
|
||||
clearUndo,
|
||||
};
|
||||
}
|
||||
|
||||
function isSameUndo(a: UndoAction | undefined, b: UndoAction) {
|
||||
if (!a) return false;
|
||||
if (a.type !== b.type) return false;
|
||||
switch (a.type) {
|
||||
case "create": {
|
||||
const next = b as Extract<UndoAction, { type: "create" }>;
|
||||
return a.id === next.id;
|
||||
}
|
||||
case "delete": {
|
||||
const next = b as Extract<UndoAction, { type: "delete" }>;
|
||||
return (
|
||||
a.feature.properties.id === next.feature.properties.id &&
|
||||
geometryEquals(a.feature.geometry, next.feature.geometry)
|
||||
);
|
||||
}
|
||||
case "update": {
|
||||
const next = b as Extract<UndoAction, { type: "update" }>;
|
||||
return (
|
||||
a.id === next.id &&
|
||||
geometryEquals(a.prevGeometry, next.prevGeometry)
|
||||
);
|
||||
}
|
||||
case "properties": {
|
||||
const next = b as Extract<UndoAction, { type: "properties" }>;
|
||||
return (
|
||||
a.id === next.id &&
|
||||
JSON.stringify(a.prevProperties) === JSON.stringify(next.prevProperties)
|
||||
);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user