import { useEffect, useMemo, useRef, useState } from "react"; // Kiểu union các GeoJSON geometry cơ bản (không gồm GeometryCollection). export type Geometry = | { type: "Point"; coordinates: [number, number] } | { type: "MultiPoint"; coordinates: [number, number][] } | { type: "LineString"; coordinates: [number, number][] } | { type: "MultiLineString"; coordinates: [number, number][][] } | { type: "Polygon"; coordinates: [number, number][][] } | { type: "MultiPolygon"; coordinates: [number, number][][][] }; export type FeatureProperties = { id: string | number; type?: string | null; geometry_preset?: "point" | "line" | "polygon" | "circle-area" | null; time_start?: number | null; time_end?: number | null; binding?: string[]; entity_id?: string | null; entity_ids?: string[]; entity_name?: string | null; entity_names?: string[]; entity_type_id?: string | null; }; export type Feature = { type: "Feature"; properties: FeatureProperties; geometry: Geometry; }; export type FeatureCollection = { type: "FeatureCollection"; features: Feature[]; }; // Kiểu thay đổi dùng để gửi payload lưu dữ liệu. export type Change = | { action: "create"; feature: Feature } | { action: "update"; id: FeatureProperties["id"]; geometry: Geometry } | { action: "delete"; id: FeatureProperties["id"] }; // Kiểu bản ghi undo tối thiểu. export type UndoAction = | { type: "update"; id: FeatureProperties["id"]; prevGeometry: Geometry } | { type: "delete"; feature: Feature } | { type: "create"; id: FeatureProperties["id"] }; // Deep clone dữ liệu JSON-serializable để tránh mutate tham chiếu cũ. const deepClone = (obj: T): T => JSON.parse(JSON.stringify(obj)); // So sánh hai geometry theo nội dung tuần tự JSON. function geometryEquals(a: Geometry | undefined, b: Geometry | undefined): boolean { if (!a || !b) return false; return JSON.stringify(a) === JSON.stringify(b); } 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); } // Tạo baseline map id -> geometry từ dữ liệu đã lưu. function buildInitialMap(fc: FeatureCollection) { const map = new Map(); for (const f of fc.features) { map.set(f.properties.id, deepClone(f)); } return map; } // Tính diff giữa draft hiện tại và baseline để sinh payload thay đổi. function diffDraftToInitial( draft: FeatureCollection, initialMap: Map ) { const next = new Map(); // track which initial ids are still present const seen = new Set(); // additions & updates for (const f of draft.features) { const id = f.properties.id; seen.add(id); const initialFeature = initialMap.get(id); if (!initialFeature) { next.set(id, { action: "create", feature: deepClone(f) }); } else if (!featureEquals(initialFeature, f)) { next.set(id, { action: "update", id, geometry: deepClone(f.geometry) }); } } // deletions for (const [id] of initialMap.entries()) { if (!seen.has(id)) { next.set(id, { action: "delete", id }); } } return next; } // Kiểm tra 2 undo action có cùng nội dung hay không (để tránh push trùng). 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; return a.id === next.id; } case "delete": { const next = b as Extract; return ( a.feature.properties.id === next.feature.properties.id && geometryEquals(a.feature.geometry, next.feature.geometry) ); } case "update": { const next = b as Extract; return ( a.id === next.id && geometryEquals(a.prevGeometry, next.prevGeometry) ); } default: return false; } } // State trung tâm của editor: // - draft: dữ liệu nguồn để render UI // - changes: map các thay đổi chờ lưu // - undoStack: lịch sử thao tác tối thiểu để hoàn tác export function useEditorState(initialData: FeatureCollection) { const [draft, setDraft] = useState(() => deepClone(initialData)); const [undoStack, setUndoStack] = useState([]); // baseline to know what is "saved" state const initialMapRef = useRef>( buildInitialMap(initialData) ); const draftRef = useRef(deepClone(initialData)); // central entrypoint: keep draftRef + React state in sync const commitDraft = (nextDraft: FeatureCollection) => { const cloned = deepClone(nextDraft); draftRef.current = cloned; setDraft(cloned); }; // reset when initialData changes (e.g., after first load or after refresh) useEffect(() => { commitDraft(deepClone(initialData)); setUndoStack([]); initialMapRef.current = buildInitialMap(initialData); }, [initialData]); // derive pending changes on every render: source of truth for save + changeCount. // read baseline from state captured in closure to avoid ref access during render. const [baselineVersion, setBaselineVersion] = useState(0); const changes = useMemo(() => { const baseline = initialMapRef.current; return diffDraftToInitial(draft, baseline); // eslint-disable-next-line react-hooks/exhaustive-deps }, [draft, baselineVersion]); const changeCount = useMemo(() => changes.size, [changes]); useEffect(() => { draftRef.current = draft; }, [draft]); // Đẩy undo action mới vào stack nếu khác action gần nhất. function pushUndo(action: UndoAction) { setUndoStack((prev) => { const last = prev[prev.length - 1]; if (isSameUndo(last, action)) return prev; // tránh trùng lặp liên tiếp return [...prev, action]; }); } // Thêm feature mới vào draft và ghi nhận thao tác "create". function createFeature(feature: Feature) { const featureClone = deepClone(feature); commitDraft({ ...draftRef.current, features: [...draftRef.current.features, featureClone], }); pushUndo({ type: "create", id: featureClone.properties.id }); } // Cập nhật các thuộc tính không phải geometry của feature (entity/time metadata). function patchFeatureProperties( id: FeatureProperties["id"], patch: Partial ) { const idx = draftRef.current.features.findIndex((f) => f.properties.id === id); if (idx === -1) return; const nextFeatures = [...draftRef.current.features]; nextFeatures[idx] = { ...nextFeatures[idx], properties: { ...nextFeatures[idx].properties, ...deepClone(patch), }, }; commitDraft({ ...draftRef.current, features: nextFeatures }); } // Cập nhật geometry của feature hiện có và ghi nhận thay đổi. function updateFeature(id: FeatureProperties["id"], newGeometry: Geometry) { const idx = draftRef.current.features.findIndex((f) => f.properties.id === id); if (idx === -1) return; // nothing to update const prevFeature = draftRef.current.features[idx]; const prevGeometry = prevFeature.geometry; const updatedFeature = { ...prevFeature, geometry: deepClone(newGeometry), }; pushUndo({ type: "update", id, prevGeometry: deepClone(prevGeometry) }); const nextFeatures = [...draftRef.current.features]; nextFeatures[idx] = updatedFeature; commitDraft({ ...draftRef.current, features: nextFeatures }); } // Xóa feature khỏi draft và ghi nhận thao tác delete. function deleteFeature(id: FeatureProperties["id"]) { const idx = draftRef.current.features.findIndex((f) => f.properties.id === id); if (idx === -1) return; const feature = draftRef.current.features[idx]; // store undo pushUndo({ type: "delete", feature: deepClone(feature) }); const nextFeatures = [...draftRef.current.features]; nextFeatures.splice(idx, 1); commitDraft({ ...draftRef.current, features: nextFeatures }); } // Hoàn tác thao tác gần nhất, đồng bộ lại cả draft và danh sách thay đổi. function undo() { let applied = false; // guards against React StrictMode double invoke of setState updater setUndoStack((prev) => { if (applied) return prev; if (!prev.length) return prev; applied = true; const last = prev[prev.length - 1]; const remaining = prev.slice(0, -1); switch (last.type) { case "create": { commitDraft({ ...draftRef.current, features: draftRef.current.features.filter((f) => f.properties.id !== last.id), }); break; } case "delete": { const feature = deepClone(last.feature); commitDraft({ ...draftRef.current, features: [...draftRef.current.features, feature], }); break; } case "update": { const { id, prevGeometry } = last; const idx = draftRef.current.features.findIndex((f) => f.properties.id === id); if (idx === -1) return remaining; const updated = { ...draftRef.current.features[idx], geometry: deepClone(prevGeometry), }; const nextFeatures = [...draftRef.current.features]; nextFeatures[idx] = updated; commitDraft({ ...draftRef.current, features: nextFeatures }); break; } } return remaining; }); } // Dựng mảng payload gửi API save từ map changes hiện tại. function buildPayload(): Change[] { return Array.from(changes.values()).map((c) => deepClone(c)); } // Xóa thay đổi đang chờ sau khi lưu thành công. function clearChanges() { setUndoStack([]); initialMapRef.current = buildInitialMap(draftRef.current); setBaselineVersion((v) => v + 1); } // Kiểm tra feature id đã tồn tại ở baseline đã lưu hay chưa. function hasPersistedFeature(id: FeatureProperties["id"]) { return initialMapRef.current.has(id); } return { draft, changes, undoStack, changeCount, createFeature, patchFeatureProperties, updateFeature, deleteFeature, undo, buildPayload, clearChanges, hasPersistedFeature, }; }