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; }; export function createEditingEngine(options: { mapRef: React.MutableRefObject; 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 } }; 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); }; 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 = { type: "FeatureCollection", features: [ { type: "Feature", geometry: { type: "Polygon", coordinates: [closedRing] }, properties: {}, }, ], }; const handles: GeoJSON.FeatureCollection = { 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); }; 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(); }; const cancelEditing = () => { clearEditing(); }; 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(); }; 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 ); }; const bindEditEvents = (map: maplibregl.Map) => { 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(); }; 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(); }; const stopDragging = () => { dragStateRef.current = null; map.getCanvas().style.cursor = ""; map.dragPan.enable(); }; 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(); } }; const onKeyUp = (e: KeyboardEvent) => { if (e.key === "Control") { modifierRef.current.ctrl = false; } else if (e.key === "Meta") { modifierRef.current.meta = false; } }; 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(); }; 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, }; }