Support tracing with the draw interaction

This commit is contained in:
Tim Schaub
2022-08-20 14:11:01 -06:00
parent 31a42949e6
commit f8ba7dec0c
4 changed files with 897 additions and 252 deletions

View File

@@ -1,15 +1,14 @@
---
layout: example.html
title: Tracing around a polygon
shortdesc: Example of setting up a draw interaction to easily snap to an existing feature.
shortdesc: Using the draw interaction to trace around features.
docs: >
This example showcases how the draw interaction API can be set up to make snapping along
an existing geometry easier while preserving topology, which is sometimes called "tracing".
When the user clicks on two different points on the Idaho state border,
the part of the border comprised between these two points is added to
the currently drawn feature.
This leverages the `appendCoordinates` method of the `ol/interaction/Draw` interaction.
tags: "draw, trace, snap, vector, topology"
The draw interaction has a <code>trace</code> option to enable tracing
around existing features. This example uses the <code>traceSource</code> option
to trace features from one source and add them to another source. The first click
on an edge of the Idaho feature will start tracing. The second click on the edge
will stop tracing.
tags: "draw, trace, vector, snap, topology"
---
<div id="map" class="map"></div>
<form>

View File

@@ -11,101 +11,6 @@ import View from '../src/ol/View.js';
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
// math utilities
// coordinates; will return the length of the [a, b] segment
function length(a, b) {
return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
);
}
// coordinates; will return true if c is on the [a, b] segment
function isOnSegment(c, a, b) {
const lengthAc = length(a, c);
const lengthAb = length(a, b);
const dot =
((c[0] - a[0]) * (b[0] - a[0]) + (c[1] - a[1]) * (b[1] - a[1])) / lengthAb;
return Math.abs(lengthAc - dot) < 1e-6 && lengthAc < lengthAb;
}
// modulo for negative values, eg: mod(-1, 4) returns 3
function mod(a, b) {
return ((a % b) + b) % b;
}
// returns a coordinates array which contains the segments of the feature's
// outer ring between the start and end points
// Note: this assumes the base feature is a single polygon
function getPartialRingCoords(feature, startPoint, endPoint) {
let polygon = feature.getGeometry();
if (polygon.getType() === 'MultiPolygon') {
polygon = polygon.getPolygon(0);
}
const ringCoords = polygon.getLinearRing().getCoordinates();
let i,
pointA,
pointB,
startSegmentIndex = -1;
for (i = 0; i < ringCoords.length; i++) {
pointA = ringCoords[i];
pointB = ringCoords[mod(i + 1, ringCoords.length)];
// check if this is the start segment dot product
if (isOnSegment(startPoint, pointA, pointB)) {
startSegmentIndex = i;
break;
}
}
const cwCoordinates = [];
let cwLength = 0;
const ccwCoordinates = [];
let ccwLength = 0;
// build clockwise coordinates
for (i = 0; i < ringCoords.length; i++) {
pointA =
i === 0
? startPoint
: ringCoords[mod(i + startSegmentIndex, ringCoords.length)];
pointB = ringCoords[mod(i + startSegmentIndex + 1, ringCoords.length)];
cwCoordinates.push(pointA);
if (isOnSegment(endPoint, pointA, pointB)) {
cwCoordinates.push(endPoint);
cwLength += length(pointA, endPoint);
break;
} else {
cwLength += length(pointA, pointB);
}
}
// build counter-clockwise coordinates
for (i = 0; i < ringCoords.length; i++) {
pointA = ringCoords[mod(startSegmentIndex - i, ringCoords.length)];
pointB =
i === 0
? startPoint
: ringCoords[mod(startSegmentIndex - i + 1, ringCoords.length)];
ccwCoordinates.push(pointB);
if (isOnSegment(endPoint, pointA, pointB)) {
ccwCoordinates.push(endPoint);
ccwLength += length(endPoint, pointB);
break;
} else {
ccwLength += length(pointA, pointB);
}
}
// keep the shortest path
return ccwLength < cwLength ? ccwCoordinates : cwCoordinates;
}
// layers definition
const raster = new TileLayer({
source: new OSM(),
});
@@ -157,86 +62,7 @@ const map = new Map({
}),
});
let drawInteraction, tracingFeature, startPoint, endPoint;
let drawing = false;
const getFeatureOptions = {
hitTolerance: 10,
layerFilter: (layer) => {
return layer === baseVector;
},
};
// the click event is used to start/end tracing around a feature
map.on('click', (event) => {
if (!drawing) {
return;
}
let hit = false;
map.forEachFeatureAtPixel(
event.pixel,
(feature) => {
if (tracingFeature && feature !== tracingFeature) {
return;
}
hit = true;
const coord = map.getCoordinateFromPixel(event.pixel);
// second click on the tracing feature: append the ring coordinates
if (feature === tracingFeature) {
endPoint = tracingFeature.getGeometry().getClosestPoint(coord);
const appendCoords = getPartialRingCoords(
tracingFeature,
startPoint,
endPoint
);
drawInteraction.removeLastPoint();
drawInteraction.appendCoordinates(appendCoords);
tracingFeature = null;
}
// start tracing on the feature ring
tracingFeature = feature;
startPoint = tracingFeature.getGeometry().getClosestPoint(coord);
},
getFeatureOptions
);
if (!hit) {
// clear current tracing feature & preview
previewLine.getGeometry().setCoordinates([]);
tracingFeature = null;
}
});
// the pointermove event is used to show a preview of the result of the tracing
map.on('pointermove', (event) => {
if (tracingFeature && drawing) {
let coord = null;
map.forEachFeatureAtPixel(
event.pixel,
(feature) => {
if (tracingFeature === feature) {
coord = map.getCoordinateFromPixel(event.pixel);
}
},
getFeatureOptions
);
let previewCoords = [];
if (coord) {
endPoint = tracingFeature.getGeometry().getClosestPoint(coord);
previewCoords = getPartialRingCoords(
tracingFeature,
startPoint,
endPoint
);
}
previewLine.getGeometry().setCoordinates(previewCoords);
}
});
let drawInteraction;
const snapInteraction = new Snap({
source: baseVector.getSource(),
@@ -248,16 +74,10 @@ function addInteraction() {
const value = typeSelect.value;
if (value !== 'None') {
drawInteraction = new Draw({
source: drawVector.getSource(),
type: value,
});
drawInteraction.on('drawstart', () => {
drawing = true;
});
drawInteraction.on('drawend', () => {
drawing = false;
previewLine.getGeometry().setCoordinates([]);
tracingFeature = null;
source: drawVector.getSource(),
trace: true,
traceSource: baseVector.getSource(),
});
map.addInteraction(drawInteraction);
map.addInteraction(snapInteraction);