preview map editor 60%

This commit is contained in:
taDuc
2026-04-13 21:47:28 +07:00
parent 3023fa947c
commit 458de8dadc
16 changed files with 1664 additions and 1149 deletions

View File

@@ -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);
}