preview map editor 60%
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Basic GeoJSON geometry union (no GeometryCollection).
|
||||
*/
|
||||
// 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][] }
|
||||
@@ -13,10 +11,11 @@ export type Geometry =
|
||||
|
||||
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[];
|
||||
line_mode?: string | null;
|
||||
entity_id?: string | null;
|
||||
entity_ids?: string[];
|
||||
entity_name?: string | null;
|
||||
@@ -35,29 +34,28 @@ export type FeatureCollection = {
|
||||
features: Feature[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Change map entry for saving.
|
||||
*/
|
||||
// Kiểu thay đổi dùng để gửi payload lưu dữ liệu.
|
||||
export type Change =
|
||||
| { type: "create"; feature: Feature }
|
||||
| { type: "update"; id: FeatureProperties["id"]; geometry: Geometry }
|
||||
| { type: "delete"; id: FeatureProperties["id"] };
|
||||
| { action: "create"; feature: Feature }
|
||||
| { action: "update"; id: FeatureProperties["id"]; geometry: Geometry }
|
||||
| { action: "delete"; id: FeatureProperties["id"] };
|
||||
|
||||
/**
|
||||
* Minimal undo record.
|
||||
*/
|
||||
// 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 = <T,>(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);
|
||||
}
|
||||
|
||||
// Tạo baseline map id -> geometry từ dữ liệu đã lưu.
|
||||
function buildInitialMap(fc: FeatureCollection) {
|
||||
const map = new Map<FeatureProperties["id"], Geometry>();
|
||||
for (const f of fc.features) {
|
||||
@@ -66,6 +64,7 @@ function buildInitialMap(fc: FeatureCollection) {
|
||||
return map;
|
||||
}
|
||||
|
||||
// Tính diff giữa draft hiện tại và baseline để sinh payload thay đổi.
|
||||
function diffDraftToInitial(
|
||||
draft: FeatureCollection,
|
||||
initialMap: Map<FeatureProperties["id"], Geometry>
|
||||
@@ -81,22 +80,23 @@ function diffDraftToInitial(
|
||||
seen.add(id);
|
||||
const initialGeom = initialMap.get(id);
|
||||
if (!initialGeom) {
|
||||
next.set(id, { type: "create", feature: deepClone(f) });
|
||||
next.set(id, { action: "create", feature: deepClone(f) });
|
||||
} else if (!geometryEquals(initialGeom, f.geometry)) {
|
||||
next.set(id, { type: "update", id, geometry: deepClone(f.geometry) });
|
||||
next.set(id, { action: "update", id, geometry: deepClone(f.geometry) });
|
||||
}
|
||||
}
|
||||
|
||||
// deletions
|
||||
for (const [id] of initialMap.entries()) {
|
||||
if (!seen.has(id)) {
|
||||
next.set(id, { type: "delete", 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;
|
||||
@@ -124,12 +124,10 @@ function isSameUndo(a: UndoAction | undefined, b: UndoAction) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Central state for the editor.
|
||||
* - draft: source of truth for UI rendering
|
||||
* - changes: map of pending changes for save
|
||||
* - undoStack: minimal actions to revert last step
|
||||
*/
|
||||
// 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<FeatureCollection>(() => deepClone(initialData));
|
||||
const [undoStack, setUndoStack] = useState<UndoAction[]>([]);
|
||||
@@ -168,6 +166,7 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
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];
|
||||
@@ -176,9 +175,7 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new feature to draft and record "create".
|
||||
*/
|
||||
// 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({
|
||||
@@ -188,9 +185,7 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
pushUndo({ type: "create", id: featureClone.properties.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch non-geometry properties on a feature (used for entity/time metadata).
|
||||
*/
|
||||
// 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<FeatureProperties>
|
||||
@@ -209,9 +204,7 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
commitDraft({ ...draftRef.current, features: nextFeatures });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update geometry of an existing feature and record change.
|
||||
*/
|
||||
// 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
|
||||
@@ -231,9 +224,7 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
commitDraft({ ...draftRef.current, features: nextFeatures });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a feature from draft and record delete.
|
||||
*/
|
||||
// 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;
|
||||
@@ -247,9 +238,7 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
commitDraft({ ...draftRef.current, features: nextFeatures });
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo last action, reverting both draft and change map.
|
||||
*/
|
||||
// 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) => {
|
||||
@@ -294,22 +283,19 @@ export function useEditorState(initialData: FeatureCollection) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build payload array for API save.
|
||||
*/
|
||||
// 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pending changes after successful save.
|
||||
*/
|
||||
// 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user