refactor
This commit is contained in:
226
lib/engine/editingEngine.ts
Normal file
226
lib/engine/editingEngine.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/lib/useEditorState";
|
||||
|
||||
export type EditingHandle = {
|
||||
id: string | number;
|
||||
ring: [number, number][];
|
||||
original: Geometry;
|
||||
};
|
||||
|
||||
export type EditingAPI = {
|
||||
beginEditing: (feature: maplibregl.MapGeoJSONFeature) => void;
|
||||
clearEditing: () => void;
|
||||
bindEditEvents: (map: maplibregl.Map) => void;
|
||||
};
|
||||
|
||||
// Tạo engine chỉnh sửa polygon đã có (kéo đỉnh, thêm đỉnh, commit/cancel).
|
||||
export function createEditingEngine(options: {
|
||||
mapRef: React.MutableRefObject<maplibregl.Map | null>;
|
||||
onUpdate: (id: string | number, geometry: Geometry) => void;
|
||||
}) {
|
||||
const { mapRef, onUpdate } = options;
|
||||
const editingRef = { current: null as EditingHandle | null };
|
||||
const dragStateRef = { current: null as { idx: number } | null };
|
||||
const modifierRef = { current: { ctrl: false, meta: false } };
|
||||
|
||||
// Hủy trạng thái chỉnh sửa hiện tại và dọn hai source edit.
|
||||
const clearEditing = () => {
|
||||
editingRef.current = null;
|
||||
dragStateRef.current = null;
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const empty: GeoJSON.FeatureCollection = { type: "FeatureCollection", features: [] };
|
||||
(map.getSource("edit-shape") as maplibregl.GeoJSONSource | undefined)?.setData(empty);
|
||||
(map.getSource("edit-handles") as maplibregl.GeoJSONSource | undefined)?.setData(empty);
|
||||
};
|
||||
|
||||
// Đồng bộ polygon tạm và các handle point lên map source.
|
||||
const updateEditSources = () => {
|
||||
const editing = editingRef.current;
|
||||
const map = mapRef.current;
|
||||
if (!editing || !map) return;
|
||||
|
||||
const closedRing = [...editing.ring, editing.ring[0]];
|
||||
const shape: GeoJSON.FeatureCollection<GeoJSON.Polygon> = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{
|
||||
type: "Feature",
|
||||
geometry: { type: "Polygon", coordinates: [closedRing] },
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const handles: GeoJSON.FeatureCollection<GeoJSON.Point> = {
|
||||
type: "FeatureCollection",
|
||||
features: editing.ring.map((c, idx) => ({
|
||||
type: "Feature",
|
||||
geometry: { type: "Point", coordinates: c },
|
||||
properties: { idx },
|
||||
})),
|
||||
};
|
||||
|
||||
(map.getSource("edit-shape") as maplibregl.GeoJSONSource | undefined)?.setData(shape);
|
||||
(map.getSource("edit-handles") as maplibregl.GeoJSONSource | undefined)?.setData(handles);
|
||||
};
|
||||
|
||||
// Chốt chỉnh sửa và emit geometry mới cho caller.
|
||||
const finishEditing = () => {
|
||||
const editing = editingRef.current;
|
||||
if (!editing) return;
|
||||
const geometry: Geometry = {
|
||||
type: "Polygon",
|
||||
coordinates: [[...editing.ring, editing.ring[0]]],
|
||||
};
|
||||
onUpdate(editing.id, geometry);
|
||||
clearEditing();
|
||||
};
|
||||
|
||||
// Thoát chế độ chỉnh sửa mà không lưu thay đổi.
|
||||
const cancelEditing = () => {
|
||||
clearEditing();
|
||||
};
|
||||
|
||||
// Bắt đầu chỉnh sửa từ feature polygon được chọn.
|
||||
const beginEditing = (feature: maplibregl.MapGeoJSONFeature) => {
|
||||
if (feature.geometry.type !== "Polygon") return;
|
||||
const coords = (feature.geometry.coordinates?.[0] ?? []) as [number, number][];
|
||||
if (coords.length < 4) return;
|
||||
|
||||
// remove duplicated closing point
|
||||
const ring = coords.slice(0, -1).map((c) => [c[0], c[1]] as [number, number]);
|
||||
editingRef.current = {
|
||||
id: feature.id ?? feature.properties?.id,
|
||||
ring,
|
||||
original: feature.geometry as Geometry,
|
||||
};
|
||||
updateEditSources();
|
||||
};
|
||||
|
||||
// Kiểm tra trạng thái nhấn phím modifier để bật thao tác chèn đỉnh.
|
||||
const isModifierPressed = (e?: maplibregl.MapLayerMouseEvent | maplibregl.MapMouseEvent) => {
|
||||
const oe = e?.originalEvent as MouseEvent | undefined;
|
||||
return (
|
||||
modifierRef.current.ctrl ||
|
||||
modifierRef.current.meta ||
|
||||
!!oe?.ctrlKey ||
|
||||
!!oe?.metaKey
|
||||
);
|
||||
};
|
||||
|
||||
// Gắn toàn bộ sự kiện phục vụ chỉnh sửa hình.
|
||||
const bindEditEvents = (map: maplibregl.Map) => {
|
||||
// Bắt đầu kéo một handle point.
|
||||
const onHandleDown = (e: maplibregl.MapLayerMouseEvent) => {
|
||||
if (!editingRef.current) return;
|
||||
const feature = e.features?.[0];
|
||||
const idx = feature?.properties?.idx;
|
||||
if (idx === undefined) return;
|
||||
e.preventDefault();
|
||||
dragStateRef.current = { idx };
|
||||
map.getCanvas().style.cursor = "grabbing";
|
||||
map.dragPan.disable();
|
||||
};
|
||||
|
||||
// Cập nhật vị trí đỉnh trong lúc kéo chuột.
|
||||
const onHandleMove = (e: maplibregl.MapMouseEvent) => {
|
||||
const drag = dragStateRef.current;
|
||||
const editing = editingRef.current;
|
||||
if (!drag || !editing) return;
|
||||
|
||||
editing.ring[drag.idx] = [e.lngLat.lng, e.lngLat.lat];
|
||||
updateEditSources();
|
||||
};
|
||||
|
||||
// Kết thúc kéo đỉnh và khôi phục trạng thái tương tác map.
|
||||
const stopDragging = () => {
|
||||
dragStateRef.current = null;
|
||||
map.getCanvas().style.cursor = "";
|
||||
map.dragPan.enable();
|
||||
};
|
||||
|
||||
// Bắt phím điều khiển phiên chỉnh sửa (Enter/Escape + modifier flags).
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Control") {
|
||||
modifierRef.current.ctrl = true;
|
||||
} else if (e.key === "Meta") {
|
||||
modifierRef.current.meta = true;
|
||||
}
|
||||
if (!editingRef.current) return;
|
||||
if (e.key === "Enter") {
|
||||
finishEditing();
|
||||
} else if (e.key === "Escape") {
|
||||
cancelEditing();
|
||||
}
|
||||
};
|
||||
|
||||
// Hạ cờ modifier khi nhả phím.
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === "Control") {
|
||||
modifierRef.current.ctrl = false;
|
||||
} else if (e.key === "Meta") {
|
||||
modifierRef.current.meta = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Chèn thêm một đỉnh mới vào ring tại vị trí gần điểm click nhất.
|
||||
const onInsertHandle = (e: maplibregl.MapLayerMouseEvent) => {
|
||||
if (!editingRef.current) return;
|
||||
if (!isModifierPressed(e)) return;
|
||||
e.preventDefault();
|
||||
const editing = editingRef.current;
|
||||
const ring = editing.ring;
|
||||
const click = [e.lngLat.lng, e.lngLat.lat] as [number, number];
|
||||
let nearestIdx = 0;
|
||||
let bestDist = Number.POSITIVE_INFINITY;
|
||||
ring.forEach((pt, idx) => {
|
||||
const dx = pt[0] - click[0];
|
||||
const dy = pt[1] - click[1];
|
||||
const d = dx * dx + dy * dy; // Dùng khoảng cách Euclid bình phương để so sánh nhanh, không cần sqrt.
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
nearestIdx = idx;
|
||||
}
|
||||
});
|
||||
const insertIdx = nearestIdx + 1;
|
||||
ring.splice(insertIdx, 0, click);
|
||||
dragStateRef.current = { idx: insertIdx };
|
||||
map.getCanvas().style.cursor = "grabbing";
|
||||
map.dragPan.disable();
|
||||
updateEditSources();
|
||||
};
|
||||
|
||||
// Ngắt kéo nếu con trỏ rời canvas.
|
||||
const onCanvasLeave = () => {
|
||||
stopDragging();
|
||||
};
|
||||
|
||||
map.on("mousedown", "edit-handles-circle", onHandleDown);
|
||||
map.on("mousedown", "edit-shape-line", onInsertHandle);
|
||||
map.on("mousemove", onHandleMove);
|
||||
map.on("mouseup", stopDragging);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
map.getCanvas().addEventListener("mouseleave", onCanvasLeave);
|
||||
|
||||
map.on("remove", () => {
|
||||
map.off("mousedown", "edit-handles-circle", onHandleDown);
|
||||
map.off("mousedown", "edit-shape-line", onInsertHandle);
|
||||
map.off("mousemove", onHandleMove);
|
||||
map.off("mouseup", stopDragging);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("keyup", onKeyUp);
|
||||
map.getCanvas().removeEventListener("mouseleave", onCanvasLeave);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
beginEditing,
|
||||
clearEditing,
|
||||
bindEditEvents,
|
||||
updateEditSources,
|
||||
editingRef,
|
||||
dragStateRef,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user