212 lines
7.4 KiB
TypeScript
212 lines
7.4 KiB
TypeScript
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<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 } };
|
|
|
|
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<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);
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|