822 lines
28 KiB
TypeScript
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,
|
|
};
|
|
}
|