From 43ce58f820e61c70406304cf8d5547facefc62d9 Mon Sep 17 00:00:00 2001 From: Stefan Forsgren <33832077+steff-o@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:51:38 +0100 Subject: [PATCH] Added trace to draw interaction (#2087) Co-authored-by: Stefan Forsgren --- src/controls/editor/edithandler.js | 155 +++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 7 deletions(-) diff --git a/src/controls/editor/edithandler.js b/src/controls/editor/edithandler.js index 9a22d3f12..987aa01d9 100644 --- a/src/controls/editor/edithandler.js +++ b/src/controls/editor/edithandler.js @@ -4,8 +4,11 @@ import Modify from 'ol/interaction/Modify'; import Snap from 'ol/interaction/Snap'; import Collection from 'ol/Collection'; import Feature from 'ol/Feature'; -import { LineString, MultiPolygon, MultiLineString, MultiPoint } from 'ol/geom'; +import { LineString, MultiPolygon, MultiLineString, MultiPoint, Polygon } from 'ol/geom'; import { noModifierKeys } from 'ol/events/condition'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import { squaredDistance, toFixed } from 'ol/math'; import { Button, Element as El, Modal } from '../../ui'; import Infowindow from '../../components/infowindow'; import store from './editsstore'; @@ -23,6 +26,7 @@ import topology from '../../utils/topology'; import attachmentsform from './attachmentsform'; import relatedTablesForm from './relatedtablesform'; import relatedtables from '../../utils/relatedtables'; +import Style from '../../style'; const editsStore = store(); let editLayers = {}; @@ -57,6 +61,11 @@ let breadcrumbs = []; let autoCreatedFeature = false; let infowindowCmp = false; let preselectedFeature; +let traceHighligtLayer; +let snapTolerance; +let snapSources; +let traceSource; +let useTrace; function isActive() { // FIXME: this only happens at startup as they are set to null on closing. If checking for null/falsley/not truely it could work as isVisible with @@ -310,8 +319,17 @@ function ensureCorrectGeometryType(f) { return featureGeometryType === layerGeometryType; } +/** + * Clears all traces of an active trace + */ +function clearTrace() { + traceHighligtLayer.getSource().clear(); + traceSource.clear(); +} + // Handler for OL Draw interaction function onDrawEnd(evt) { + clearTrace(); const f = evt.feature; // Reset pointer to drawFeature, OL resuses the same feature @@ -349,10 +367,11 @@ function onDrawStart(evt) { } /** - * Called when draw is aborted from OL + * Called when draw is aborted from OL, typically by shift-click * */ function onDrawAbort() { drawFeature = null; + clearTrace(); } /** * Called by OL on mouse down to check if a vertex should be added @@ -476,7 +495,8 @@ function addSnapInteraction(sources) { const snapInteractions = []; sources.forEach((source) => { const interaction = new Snap({ - source + source, + pixelTolerance: snapTolerance }); snapInteractions.push(interaction); map.addInteraction(interaction); @@ -518,14 +538,115 @@ function setAllowedOperations() { } } +/** + * Helper that adds candidate linear strings as features to be displayed as trace possibilities if they + * are clicked on. Mimics what OL does in Draw interaction, but in less code as it uses higher level functions + * @param {any} coordinate + * @param {any} coordinates + */ +function appendTraceTarget(coordinate, coordinates) { + const x = coordinate[0]; + const y = coordinate[1]; + const geom = new LineString(coordinates); + const nearestPoint = geom.getClosestPoint(coordinate); + // Round distance so we can determine if point is on line. It still is pretty small so + // snapping must be activated in order to have any chance at actually hitting a line. + const squaredD = toFixed(squaredDistance(x, y, nearestPoint[0], nearestPoint[1]), 10); + if (squaredD === 0) { + traceHighligtLayer.getSource().addFeature(new Feature(geom)); + } +} + +/** + * Breaks down a complex geometry to linearstrings that can be traced. + * Mimics the implementation in OL + * @param {any} coordinate + * @param {any} geometry + * @returns + */ +function appendGeometryTraceTargets(coordinate, geometry) { + if (geometry instanceof LineString) { + appendTraceTarget(coordinate, geometry.getCoordinates()); + return; + } + if (geometry instanceof MultiLineString || geometry instanceof Polygon) { + const coordinates = geometry.getCoordinates(); + coordinates.forEach(currRing => { + appendTraceTarget(coordinate, currRing); + }); + return; + } + if (geometry instanceof MultiPolygon) { + const polys = geometry.getCoordinates(); + polys.forEach(currPoly => { + currPoly.forEach(currRing => { + appendTraceTarget(coordinate, currRing); + }); + }); + } + // other types cannot be traced +} + +/** + * Callback that OL draw calls before a trace is started or ended. Too bad we don't get to know why it is called + * Sets up features for tracing, as trace can only handle one vector source, so we have do load candidates into one source + * to support tracing from all layers that we can snap to. + * @param {any} evt + * @returns {boolean} If false operation will be aborted + */ +function traceCallback(evt) { + // Try to figure out if we're about to start or end a trace. As OL won't let us know which it is we have to guess, + // so don't base any crucial logic around it. + const traceActive = traceHighligtLayer.getSource().getFeatures().length > 0; + + // Get som candidates for snapping where we clicked. It will contain a lot of false positives. + // This is roughly the same algorithm as OL uses, which means layer does not have to be visible! + // Actual snap tolerance value is actually not important, mouse has already snapped and OL uses another value, + // but it is a nice value to use. + const lowerLeft = map.getCoordinateFromPixel([ + evt.pixel[0] - snapTolerance, + evt.pixel[1] + snapTolerance + ]); + const upperRight = map.getCoordinateFromPixel([ + evt.pixel[0] + snapTolerance, + evt.pixel[1] - snapTolerance + ]); + const extent = [lowerLeft[0], lowerLeft[1], upperRight[0], upperRight[1]]; + const candidateFeatures = []; + snapSources.forEach(currSource => { + candidateFeatures.push(...currSource.getFeaturesInExtent(extent)); + }); + clearTrace(); + // This is what Draw interaction gets, it will narrow it down itself + traceSource.addFeatures(candidateFeatures); + // Try to figure out which segments OL will use for tracing by mimicing their method and + // add all segments as features to visualize where it is possible to trace. + // It would have been a lot easier if OL just had exposed getTraceTargets() + // As we don't know for sure if we're actually starting or ending trace, we just toggle visibility of + // possible trace linestrings. OL will hopefully always do the right thing, but if we don't mimic OL exactly + // the visualization may be out of sync until drawing is finished or aborted. + if (!traceActive) { + candidateFeatures.forEach(currCandidate => { + appendGeometryTraceTargets(evt.coordinate, currCandidate.getGeometry()); + }); + } + + // Return true to allow the trace start/stop. + return true; +} function setInteractions(drawType) { const editLayer = editLayers[currentLayer]; attributes = editLayer.get('attributes'); title = editLayer.get('title') || 'Information'; + hasSnap = editLayer.get('snap'); const drawOptions = { type: editLayer.get('geometryType'), - geometryName: editLayer.get('geometryName') + geometryName: editLayer.get('geometryName'), + traceSource }; + if (hasSnap && useTrace) { + drawOptions.trace = traceCallback; + } if (drawType) { Object.assign(drawOptions, shapes(drawType)); } @@ -632,11 +753,10 @@ function setInteractions(drawType) { setActive(); // If snap should be active then add snap internactions for all snap layers - hasSnap = editLayer.get('snap'); if (hasSnap) { // FIXME: selection will almost certainly be empty as featureInfo is cleared const selectionSource = featureInfo.getSelectionLayer().getSource(); - const snapSources = editLayer.get('snapLayers') ? getSnapSources(editLayer.get('snapLayers')) : [editLayer.get('source')]; + snapSources = editLayer.get('snapLayers') ? getSnapSources(editLayer.get('snapLayers')) : [editLayer.get('source')]; snapSources.push(selectionSource); snap = addSnapInteraction(snapSources); } @@ -1609,12 +1729,30 @@ function preselectFeature(feature) { */ export default function editHandler(options, v) { viewer = v; + map = viewer.getMap(); + + // Set up a layer for displaying trace possibilities. Do it up front as it may become possible to turn it on later + traceHighligtLayer = new VectorLayer({ + source: new VectorSource(), + style: { + 'stroke-color': 'rgba(100, 255, 0, 1)', + 'stroke-width': 3 + }, + visible: true + }); + if (options.traceStyle) { + const s = Style.createStyle({ style: options.traceStyle, viewer }); + traceHighligtLayer.setStyle(s); + } + map.addLayer(traceHighligtLayer); + traceSource = new VectorSource(); + useTrace = options.trace; + featureInfo = viewer.getControlByName('featureInfo'); if (options.featureList) { infowindowCmp = Infowindow({ viewer, type: 'floating', title: 'Välj objekt' }); infowindowCmp.render(); } - map = viewer.getMap(); currentLayer = options.currentLayer; editableLayers = options.editableLayers; @@ -1631,6 +1769,9 @@ export default function editHandler(options, v) { autoSave = options.autoSave; autoForm = options.autoForm; validateOnDraw = options.validateOnDraw; + // We set tolerace as we can't read default from OL, but we can set it + // Cant' set 0, but that case you can disable snap + snapTolerance = options.snapTolerance || 10; // Listen to DOM events from menus and forms document.addEventListener('toggleEdit', onToggleEdit);