Skip to content

Commit

Permalink
Added trace to draw interaction (#2087)
Browse files Browse the repository at this point in the history
Co-authored-by: Stefan Forsgren <[email protected]>
  • Loading branch information
steff-o and Stefan Forsgren authored Dec 19, 2024
1 parent e0b79d6 commit 43ce58f
Showing 1 changed file with 148 additions and 7 deletions.
155 changes: 148 additions & 7 deletions src/controls/editor/edithandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;

Expand All @@ -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);
Expand Down

0 comments on commit 43ce58f

Please sign in to comment.