Files
frontend/apps/sn/app/(map-layout)/map/lib/use-gis-edit-transform.ts

822 lines
28 KiB
TypeScript

"use client";
import type Geometry from "ol/geom/Geometry.js";
import type * as React from "react";
import {
type VertexHandleMeta,
type VertexHandleCandidate,
} from "@/features/gis-edit/model/vertex-handles";
import {
createGisEditPreviewStyle,
GIS_EDIT_VERTEX_DRAGGING_STYLE,
getGisEditVertexHandleStyle,
} from "@/features/gis-edit/lib/preview-styles";
import {
createVertexHandleFeature,
buildVertexHandleFeatures,
findHoveredMidpointCandidate,
} from "@/features/gis-edit/lib/vertex-handles";
import {
applyVertexHandleMove,
deleteSelectedVertexHandles,
applyGeometryMoveSnap,
getClosestGeometryVertexCoordinate,
getGeometryVertexCoordinateByMeta,
} from "@/features/gis-edit/lib/geometry-mutations";
import { useCallback, useEffect, useRef } from "react";
import Collection from "ol/Collection.js";
import Feature from "ol/Feature.js";
import GeoJSON from "ol/format/GeoJSON.js";
import Snap from "ol/interaction/Snap.js";
import Translate from "ol/interaction/Translate.js";
import VectorLayer from "ol/layer/Vector.js";
import { unByKey } from "ol/Observable.js";
import VectorSource from "ol/source/Vector.js";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
import {
FLOW_DIRECTION_SRS,
GIS_EDIT_VERTEX_LAYER_ID,
} from "../model/map-constants";
import { geometryObjectToKey, isTypingElement } from "./map-values";
import { normalizeGisEditFid, getGisEditFeatureFid } from "./gis-edit-features";
import { getSelectedFeatureLayerTypeName } from "./right-panel-features";
/** 이동·꼭지점 편집 interaction과 선택 핸들의 생성·정리를 소유한다. */
type Options = {
gisMaps: React.RefObject<any>;
gisEditPreviewSourceRef: React.RefObject<VectorSource<Geometry> | null>;
gisEdit: ReturnType<typeof useGisEdit>;
gisEditPreviewLayerRef: React.RefObject<any>;
setGisEditPreviewCursor: (cursor: string) => void;
mapReady: boolean;
ensureGisEditPreviewLayer: () => VectorSource<Geometry> | null;
shouldUsePipeSnapSource: boolean;
ensureGisEditPipeSnapSource: () => VectorSource<Geometry>;
gisEditMoveGripMetaRef: React.RefObject<{
changeId: string;
meta: VertexHandleMeta | null;
} | null>;
};
/** 이동·꼭지점 편집 interaction과 선택 핸들의 생성·정리를 소유한다. */
export function useGisEditTransform({
gisMaps,
gisEditPreviewSourceRef,
gisEdit,
gisEditPreviewLayerRef,
setGisEditPreviewCursor,
mapReady,
ensureGisEditPreviewLayer,
shouldUsePipeSnapSource,
ensureGisEditPipeSnapSource,
gisEditMoveGripMetaRef,
}: Options) {
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
const {
selectedLayer: editSelectedLayer,
changes: editChanges,
isEditMode: editIsEditMode,
pendingInteraction: editPendingInteraction,
clearPendingInteraction: editClearPendingInteraction,
isSnapEnabled: editIsSnapEnabled,
updateChangeGeometry: editUpdateChangeGeometry,
announceStatus: editAnnounceStatus,
activeTool: editActiveTool,
} = gisEdit;
// vertex Delete는 현재 선택된 주황색 실꼭지점 묶음을 ref로 기억해 키보드 이벤트에서 즉시 사용한다.
const gisEditSelectedVertexHandleKeyRef = useRef("");
const gisEditSelectedVertexMetaListRef = useRef<VertexHandleMeta[] | null>(
null,
);
const gisEditVertexLayerRef = useRef<VectorLayer<any> | null>(null);
const gisEditVertexSourceRef = useRef<VectorSource | null>(null);
const gisEditTranslateRef = useRef<any | null>(null);
const gisEditModifyRef = useRef<any | null>(null);
// 스냅 interaction은 MOVE/VERTEX_EDIT 진입마다 다시 붙이므로 별도 ref로 정리한다.
const gisEditSnapRef = useRef<any | null>(null);
const ensureGisEditVertexLayer = useCallback(() => {
const map = gisMaps.current;
if (!map) return null;
if (gisEditVertexLayerRef.current && gisEditVertexSourceRef.current) {
return gisEditVertexSourceRef.current;
}
const source = new VectorSource();
const layer = new VectorLayer({
source,
zIndex: 10005,
updateWhileAnimating: true,
updateWhileInteracting: true,
});
layer.set("id", GIS_EDIT_VERTEX_LAYER_ID);
layer.set("lyrNm", "GIS 편집 꼭지점");
layer.set("lyrTyNm", GIS_EDIT_VERTEX_LAYER_ID);
layer.set("clickPsblYn", "N");
const core = GisApp.Module.core as any;
core?.deleteLayer?.(GIS_EDIT_VERTEX_LAYER_ID);
map.addLayer(layer);
gisEditVertexLayerRef.current = layer;
gisEditVertexSourceRef.current = source;
return source;
}, [gisMaps]);
const clearGisEditVertexLayer = useCallback(() => {
gisEditVertexSourceRef.current?.clear();
gisEditSelectedVertexHandleKeyRef.current = "";
gisEditSelectedVertexMetaListRef.current = null;
}, []);
const applySelectedVertexHandleStyle = useCallback(() => {
const selectedHandleKey = gisEditSelectedVertexHandleKeyRef.current;
const vertexSource = gisEditVertexSourceRef.current;
if (!vertexSource) return;
vertexSource.getFeatures().forEach((feature: any) => {
const handleKind = String(
feature?.get?.("handleKind") ?? "",
) as VertexHandleMeta["kind"];
const handleSelectionKey = String(
feature?.get?.("handleSelectionKey") ?? "",
);
const isSelected =
Boolean(selectedHandleKey) && handleSelectionKey === selectedHandleKey;
feature.setStyle(getGisEditVertexHandleStyle(handleKind, isSelected));
});
vertexSource.changed?.();
}, []);
const clearSelectedVertexHandle = useCallback(() => {
gisEditSelectedVertexHandleKeyRef.current = "";
gisEditSelectedVertexMetaListRef.current = null;
applySelectedVertexHandleStyle();
}, [applySelectedVertexHandleStyle]);
const collectGisEditSnapFeatures = useCallback(
(targetFeature: any) => {
const map = gisMaps.current;
const previewSource = gisEditPreviewSourceRef.current;
const selectedLayerId = editSelectedLayer?.layerId;
if (!map || !previewSource || !selectedLayerId) return [] as any[];
const hiddenFidSet = new Set(
editChanges
.filter((change) => change.editType === "DELETE")
.map((change) =>
normalizeGisEditFid(
change.fid ?? change.featureRef?.fid ?? change.objectId,
),
)
.filter(Boolean),
);
const seen = new Set<string>();
const snapFeatures: any[] = [];
const targetChangeId = String(
targetFeature?.get?.("changeId") ?? "",
).trim();
const registerFeature = (feature: any) => {
if (!feature || feature === targetFeature) return;
if (!feature.getGeometry?.()) return;
if (getSelectedFeatureLayerTypeName(feature) !== selectedLayerId)
return;
const changeId = String(feature.get?.("changeId") ?? "").trim();
if (changeId && changeId === targetChangeId) return;
if (String(feature.get?.("editType") ?? "").trim() === "DELETE") return;
const fid = getGisEditFeatureFid(feature);
if (fid && hiddenFidSet.has(fid)) return;
const objectKey =
changeId ||
fid ||
String(feature.get?.("objectId") ?? feature.getId?.() ?? "");
const featureKey = `${selectedLayerId}:${objectKey}`;
if (!objectKey || seen.has(featureKey)) return;
seen.add(featureKey);
snapFeatures.push(feature);
};
previewSource.getFeatures().forEach(registerFeature);
map.getAllLayers?.().forEach((layer: any) => {
if (
layer === gisEditPreviewLayerRef.current ||
layer === gisEditVertexLayerRef.current
)
return;
const layerTypeName = String(layer?.get?.("lyrTyNm") ?? "").trim();
if (layerTypeName !== selectedLayerId) return;
layer.getSource?.()?.getFeatures?.().forEach(registerFeature);
});
return snapFeatures;
},
[
editChanges,
editSelectedLayer?.layerId,
gisEditPreviewLayerRef,
gisEditPreviewSourceRef,
gisMaps,
],
);
const removeGisEditInteractions = useCallback(() => {
const map = gisMaps.current;
if (!map) return;
if (gisEditSnapRef.current) {
map.removeInteraction(gisEditSnapRef.current);
gisEditSnapRef.current = null;
}
if (gisEditTranslateRef.current) {
map.removeInteraction(gisEditTranslateRef.current);
gisEditTranslateRef.current = null;
}
if (gisEditModifyRef.current) {
const modifyInteractions = Array.isArray(gisEditModifyRef.current)
? gisEditModifyRef.current
: [gisEditModifyRef.current];
modifyInteractions.forEach((interaction: any) => {
if (interaction) {
map.removeInteraction(interaction);
}
});
gisEditModifyRef.current = null;
}
clearGisEditVertexLayer();
setGisEditPreviewCursor("");
}, [clearGisEditVertexLayer, gisMaps, setGisEditPreviewCursor]);
useEffect(() => {
if (
!mapReady ||
!gisMaps.current ||
!editIsEditMode ||
!editPendingInteraction
)
return;
const source = ensureGisEditPreviewLayer();
const map = gisMaps.current;
if (!source || !map) return;
const targetFeature = source
.getFeatures()
.find(
(feature: any) =>
feature.get("changeId") === editPendingInteraction?.changeId,
);
if (!targetFeature) {
editClearPendingInteraction();
return;
}
removeGisEditInteractions();
const features = new Collection([targetFeature]);
let cleanupPointerMove: (() => void) | undefined;
const writeGeometryObject = () => {
const geometry = targetFeature.getGeometry();
if (!geometry) return null;
return new GeoJSON().writeGeometryObject(geometry, {
featureProjection: GisApp.TO_PROJ,
dataProjection: FLOW_DIRECTION_SRS,
}) as Record<string, unknown>;
};
const usePipeSnapSource =
shouldUsePipeSnapSource &&
(editPendingInteraction.editType === "MOVE" ||
editPendingInteraction.editType === "VERTEX_EDIT");
const pipeSnapSource = usePipeSnapSource
? ensureGisEditPipeSnapSource()
: null;
const pipeSnapSourceHasFeatures = Boolean(
pipeSnapSource?.getFeatures?.().length,
);
const snapFeatures =
(!usePipeSnapSource || !pipeSnapSourceHasFeatures) && editIsSnapEnabled
? collectGisEditSnapFeatures(targetFeature)
: [];
if (editPendingInteraction.editType === "MOVE") {
const translate = new Translate({ features, hitTolerance: 10 });
let lastTranslateCoordinate: number[] | null = null;
let translateStartCoordinate: number[] | null = null;
let originalMoveGeometrySnapshot =
targetFeature.getGeometry?.()?.clone?.() ?? null;
let moveGripCoordinate: number[] | null = null;
const moveGripMeta =
gisEditMoveGripMetaRef.current?.changeId ===
editPendingInteraction.changeId
? gisEditMoveGripMetaRef.current.meta
: null;
translate.on("translatestart", (event: any) => {
lastTranslateCoordinate = Array.isArray(event?.coordinate)
? event.coordinate
: null;
translateStartCoordinate = Array.isArray(event?.coordinate)
? event.coordinate
: null;
originalMoveGeometrySnapshot =
targetFeature.getGeometry?.()?.clone?.() ?? null;
moveGripCoordinate =
getGeometryVertexCoordinateByMeta(
originalMoveGeometrySnapshot,
moveGripMeta,
) ??
getClosestGeometryVertexCoordinate(
originalMoveGeometrySnapshot,
map,
translateStartCoordinate,
);
targetFeature.set("isDragging", true);
targetFeature.set("previewLabel", "이동 중");
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
setGisEditPreviewCursor("grabbing");
targetFeature.changed?.();
source.changed?.();
map.renderSync?.();
});
translate.on("translating", (event: any) => {
lastTranslateCoordinate = Array.isArray(event?.coordinate)
? event.coordinate
: lastTranslateCoordinate;
if (
originalMoveGeometrySnapshot?.clone &&
Array.isArray(lastTranslateCoordinate) &&
Array.isArray(translateStartCoordinate)
) {
const geometry = originalMoveGeometrySnapshot.clone();
const deltaX =
Number(lastTranslateCoordinate[0] ?? 0) -
Number(translateStartCoordinate[0] ?? 0);
const deltaY =
Number(lastTranslateCoordinate[1] ?? 0) -
Number(translateStartCoordinate[1] ?? 0);
geometry.translate(deltaX, deltaY);
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
const movedGripCoordinate = Array.isArray(moveGripCoordinate)
? [
Number(moveGripCoordinate[0] ?? 0) + deltaX,
Number(moveGripCoordinate[1] ?? 0) + deltaY,
]
: lastTranslateCoordinate;
applyGeometryMoveSnap(
geometry,
pipeSnapSource,
map,
movedGripCoordinate,
28,
);
}
targetFeature.setGeometry(geometry);
}
targetFeature.changed?.();
source.changed?.();
map.renderSync?.();
});
translate.on("translateend", (event: any) => {
const objectId = String(targetFeature.get("objectId") ?? "");
const layerName = editSelectedLayer?.layerName ?? "선택 레이어";
lastTranslateCoordinate = Array.isArray(event?.coordinate)
? event.coordinate
: lastTranslateCoordinate;
targetFeature.set("isDragging", false);
targetFeature.set("previewLabel", "드래그 이동");
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
setGisEditPreviewCursor("grab");
if (
originalMoveGeometrySnapshot?.clone &&
Array.isArray(lastTranslateCoordinate) &&
Array.isArray(translateStartCoordinate)
) {
const geometry = originalMoveGeometrySnapshot.clone();
const deltaX =
Number(lastTranslateCoordinate[0] ?? 0) -
Number(translateStartCoordinate[0] ?? 0);
const deltaY =
Number(lastTranslateCoordinate[1] ?? 0) -
Number(translateStartCoordinate[1] ?? 0);
geometry.translate(deltaX, deltaY);
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
const movedGripCoordinate = Array.isArray(moveGripCoordinate)
? [
Number(moveGripCoordinate[0] ?? 0) + deltaX,
Number(moveGripCoordinate[1] ?? 0) + deltaY,
]
: lastTranslateCoordinate;
applyGeometryMoveSnap(
geometry,
pipeSnapSource,
map,
movedGripCoordinate,
28,
);
}
targetFeature.setGeometry(geometry);
}
editUpdateChangeGeometry(
editPendingInteraction!.changeId,
writeGeometryObject(),
`${layerName} ${objectId} 객체 위치를 지도에서 이동했습니다.`,
{ keepMoveToolActive: true },
);
targetFeature.changed?.();
source.changed?.();
map.renderSync?.();
});
map.addInteraction(translate);
gisEditTranslateRef.current = translate;
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
const snap = new Snap({
source: pipeSnapSource,
edge: true,
vertex: true,
pixelTolerance: 14,
});
map.addInteraction(snap);
gisEditSnapRef.current = snap;
} else if (editIsSnapEnabled && snapFeatures.length > 0) {
const snap = new Snap({
features: new Collection(snapFeatures),
edge: true,
vertex: true,
pixelTolerance: 14,
});
map.addInteraction(snap);
gisEditSnapRef.current = snap;
}
setGisEditPreviewCursor("grab");
map.renderSync?.();
}
if (editPendingInteraction.editType === "VERTEX_EDIT") {
const geometry = targetFeature.getGeometry?.();
const geometryType = geometry?.getType?.();
const objectId = String(targetFeature.get("objectId") ?? "");
const layerName = editSelectedLayer?.layerName ?? "선택 레이어";
if (
!geometry ||
!geometryType ||
geometryType === "Point" ||
geometryType === "MultiPoint"
) {
editAnnounceStatus(
`${layerName} ${objectId} 객체는 꼭지점편집 대상 geometry가 아닙니다.`,
);
editClearPendingInteraction();
return;
}
const vertexSource = ensureGisEditVertexLayer();
const vertexLayer = gisEditVertexLayerRef.current;
if (!vertexSource || !vertexLayer) {
editAnnounceStatus("꼭지점 편집 레이어를 준비하지 못했습니다.");
editClearPendingInteraction();
return;
}
const renderVertexHandles = (
hoveredMidpoint: VertexHandleCandidate | null = null,
) => {
vertexSource.clear();
buildVertexHandleFeatures(targetFeature.getGeometry?.(), map).forEach(
(handleFeature: Feature) => {
vertexSource.addFeature(handleFeature);
},
);
if (hoveredMidpoint) {
vertexSource.addFeature(
createVertexHandleFeature(
hoveredMidpoint.coordinate,
hoveredMidpoint.meta,
),
);
}
vertexSource.changed?.();
applySelectedVertexHandleStyle();
};
let geometryBeforeModifyKey = geometryObjectToKey(writeGeometryObject());
let activeMidpointKey = "";
let isDraggingHandle = false;
let originalGeometrySnapshot =
targetFeature.getGeometry?.()?.clone?.() ?? null;
renderVertexHandles();
targetFeature.set("previewLabel", "꼭지점 편집");
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
setGisEditPreviewCursor("crosshair");
const pointerMoveKey = map.on("pointermove", (event: any) => {
if (isDraggingHandle || event?.dragging) return;
const hoveredMidpoint = findHoveredMidpointCandidate(
map,
targetFeature.getGeometry?.(),
event.coordinate,
);
const nextMidpointKey = hoveredMidpoint
? `${hoveredMidpoint.meta.geometryType}:${hoveredMidpoint.meta.partIndex ?? -1}:${hoveredMidpoint.meta.polygonIndex ?? -1}:${hoveredMidpoint.meta.ringIndex ?? -1}:${hoveredMidpoint.meta.coordIndex}`
: "";
if (nextMidpointKey === activeMidpointKey) return;
activeMidpointKey = nextMidpointKey;
renderVertexHandles(hoveredMidpoint);
setGisEditPreviewCursor(hoveredMidpoint ? "copy" : "crosshair");
map.renderSync?.();
});
const vertexTranslate = new Translate({
layers: [vertexLayer],
hitTolerance: 14,
});
vertexTranslate.on("translatestart", (event: any) => {
isDraggingHandle = true;
geometryBeforeModifyKey = geometryObjectToKey(writeGeometryObject());
originalGeometrySnapshot =
targetFeature.getGeometry?.()?.clone?.() ?? null;
clearSelectedVertexHandle();
const handleFeature = event.features?.item?.(0);
if (handleFeature) {
handleFeature.setStyle(GIS_EDIT_VERTEX_DRAGGING_STYLE);
}
targetFeature.set("previewLabel", "꼭지점 편집 중");
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
setGisEditPreviewCursor("grabbing");
map.renderSync?.();
});
vertexTranslate.on("translating", (event: any) => {
const handleFeature = event.features?.item?.(0);
const handleGeometry = handleFeature?.getGeometry?.();
const nextCoordinate = handleGeometry?.getCoordinates?.();
const handleMeta = handleFeature?.get?.("handleMeta") as
| VertexHandleMeta
| undefined;
const handleMetaList =
(handleFeature?.get?.("handleMetaList") as
| VertexHandleMeta[]
| undefined) ?? (handleMeta ? [handleMeta] : []);
if (
handleMetaList.length === 0 ||
!Array.isArray(nextCoordinate) ||
!originalGeometrySnapshot?.clone
) {
return;
}
targetFeature.setGeometry(originalGeometrySnapshot.clone());
handleMetaList.forEach((meta) => {
applyVertexHandleMove(
targetFeature.getGeometry?.(),
meta,
nextCoordinate,
);
});
targetFeature.changed?.();
source.changed?.();
map.renderSync?.();
});
vertexTranslate.on("translateend", (event: any) => {
isDraggingHandle = false;
const handleFeature = event.features?.item?.(0);
const handleGeometry = handleFeature?.getGeometry?.();
const nextCoordinate = handleGeometry?.getCoordinates?.();
const handleMeta = handleFeature?.get?.("handleMeta") as
| VertexHandleMeta
| undefined;
const handleMetaList =
(handleFeature?.get?.("handleMetaList") as
| VertexHandleMeta[]
| undefined) ?? (handleMeta ? [handleMeta] : []);
targetFeature.set("previewLabel", "꼭지점 편집");
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
setGisEditPreviewCursor("crosshair");
activeMidpointKey = "";
if (handleMetaList.length === 0 || !Array.isArray(nextCoordinate)) {
renderVertexHandles();
map.renderSync?.();
return;
}
handleMetaList.forEach((meta) => {
applyVertexHandleMove(
targetFeature.getGeometry?.(),
meta,
nextCoordinate,
);
});
const nextGeometry = writeGeometryObject();
const geometryChanged =
geometryBeforeModifyKey !== geometryObjectToKey(nextGeometry);
renderVertexHandles();
if (!geometryChanged) {
editAnnounceStatus(
`${layerName} ${objectId} 객체는 아직 형상이 바뀌지 않았습니다. 주황색 꼭지점 또는 가상 꼭지점을 직접 드래그해 주세요.`,
);
map.renderSync?.();
return;
}
editUpdateChangeGeometry(
editPendingInteraction!.changeId,
nextGeometry,
`${layerName} ${objectId} 객체 꼭지점을 지도에서 조정했습니다.`,
{ keepVertexEditActive: true },
);
targetFeature.changed?.();
source.changed?.();
map.renderSync?.();
});
map.addInteraction(vertexTranslate);
gisEditModifyRef.current = vertexTranslate;
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
const snap = new Snap({
source: pipeSnapSource,
edge: true,
vertex: true,
pixelTolerance: 14,
});
map.addInteraction(snap);
gisEditSnapRef.current = snap;
} else if (editIsSnapEnabled && snapFeatures.length > 0) {
const snap = new Snap({
features: new Collection(snapFeatures),
edge: true,
vertex: true,
pixelTolerance: 14,
});
map.addInteraction(snap);
gisEditSnapRef.current = snap;
}
gisMaps.current?.renderSync?.();
cleanupPointerMove = () => {
unByKey(pointerMoveKey);
};
}
return cleanupPointerMove;
}, [
applySelectedVertexHandleStyle,
clearGisEditVertexLayer,
clearSelectedVertexHandle,
collectGisEditSnapFeatures,
ensureGisEditPipeSnapSource,
ensureGisEditVertexLayer,
ensureGisEditPreviewLayer,
editIsSnapEnabled,
editPendingInteraction,
editSelectedLayer,
editUpdateChangeGeometry,
editAnnounceStatus,
editClearPendingInteraction,
editIsEditMode,
mapReady,
removeGisEditInteractions,
shouldUsePipeSnapSource,
gisMaps,
gisEditMoveGripMetaRef,
setGisEditPreviewCursor,
]);
useEffect(() => {
if (
!mapReady ||
!gisMaps.current ||
!editIsEditMode ||
editPendingInteraction?.editType !== "VERTEX_EDIT"
) {
clearSelectedVertexHandle();
return;
}
const pendingInteraction = editPendingInteraction;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Delete") return;
if (
isTypingElement(event.target) ||
isTypingElement(document.activeElement)
)
return;
const source = ensureGisEditPreviewLayer();
const map = gisMaps.current;
if (!source || !map) return;
const targetFeature = source
.getFeatures()
.find(
(feature: any) =>
feature.get("changeId") === editPendingInteraction?.changeId,
);
if (!targetFeature) return;
const selectedMetaList = gisEditSelectedVertexMetaListRef.current ?? [];
if (selectedMetaList.length === 0) {
editAnnounceStatus(
"Delete로 삭제할 주황색 실꼭지점을 먼저 클릭하세요.",
);
return;
}
const deleteResult = deleteSelectedVertexHandles(
targetFeature.getGeometry?.(),
selectedMetaList,
);
if (!deleteResult.ok) {
editAnnounceStatus(
deleteResult.message ?? "꼭지점을 삭제하지 못했습니다.",
);
return;
}
event.preventDefault();
clearSelectedVertexHandle();
const objectId = String(targetFeature.get("objectId") ?? "");
const layerName = editSelectedLayer?.layerName ?? "선택 레이어";
const geometry = targetFeature.getGeometry();
if (!geometry) {
editAnnounceStatus("꼭지점 삭제 후 geometry를 확인하지 못했습니다.");
return;
}
targetFeature.set("previewLabel", "꼭지점 편집");
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
editUpdateChangeGeometry(
pendingInteraction.changeId,
new GeoJSON().writeGeometryObject(geometry, {
featureProjection: GisApp.TO_PROJ,
dataProjection: FLOW_DIRECTION_SRS,
}) as Record<string, unknown>,
`${layerName} ${objectId} 객체 꼭지점을 삭제했습니다.`,
{ keepVertexEditActive: true },
);
targetFeature.changed?.();
source.changed?.();
map.renderSync?.();
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [
clearSelectedVertexHandle,
ensureGisEditPreviewLayer,
editIsEditMode,
editPendingInteraction,
editSelectedLayer,
editUpdateChangeGeometry,
editAnnounceStatus,
mapReady,
gisMaps,
]);
useEffect(() => {
if (
!editIsEditMode ||
(editActiveTool !== "MOVE" && editActiveTool !== "VERTEX_EDIT")
) {
removeGisEditInteractions();
}
}, [editActiveTool, editIsEditMode, removeGisEditInteractions]);
useEffect(
() => () => {
removeGisEditInteractions();
if (gisEditVertexLayerRef.current)
gisMaps.current?.removeLayer?.(gisEditVertexLayerRef.current);
gisEditVertexLayerRef.current = null;
gisEditVertexSourceRef.current = null;
},
[gisMaps, removeGisEditInteractions],
);
return {
gisEditSelectedVertexHandleKeyRef,
gisEditSelectedVertexMetaListRef,
gisEditVertexLayerRef,
clearGisEditVertexLayer,
applySelectedVertexHandleStyle,
clearSelectedVertexHandle,
removeGisEditInteractions,
};
}