Merge pull request #10632 from jahow/draw-interaction-append-coords
Draw interaction: Append coordinates to polygons and lines (reworked)
This commit is contained in:
22
examples/tracing.html
Normal file
22
examples/tracing.html
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Tracing around a polygon
|
||||
shortdesc: Example of setting up a draw interaction to easily snap to an existing feature.
|
||||
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"
|
||||
---
|
||||
<div id="map" class="map"></div>
|
||||
<form class="form-inline">
|
||||
<label>Geometry type </label>
|
||||
<select id="type">
|
||||
<option value="Polygon">Polygon</option>
|
||||
<option value="LineString">LineString</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</form>
|
||||
253
examples/tracing.js
Normal file
253
examples/tracing.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import Map from '../src/ol/Map.js';
|
||||
import View from '../src/ol/View.js';
|
||||
import Draw from '../src/ol/interaction/Draw.js';
|
||||
import Snap from '../src/ol/interaction/Snap.js';
|
||||
import Style from '../src/ol/style/Style.js';
|
||||
import Stroke from '../src/ol/style/Stroke.js';
|
||||
import Fill from '../src/ol/style/Fill.js';
|
||||
import GeoJSON from '../src/ol/format/GeoJSON.js';
|
||||
import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
|
||||
import LineString from '../src/ol/geom/LineString.js';
|
||||
import Feature from '../src/ol/Feature.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()
|
||||
});
|
||||
|
||||
// features in this layer will be snapped to
|
||||
const baseVector = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
format: new GeoJSON(),
|
||||
url: 'https://ahocevar.com/geoserver/wfs?service=wfs&request=getfeature&typename=topp:states&cql_filter=STATE_NAME=\'Idaho\'&outputformat=application/json'
|
||||
})
|
||||
});
|
||||
|
||||
// this is were the drawn features go
|
||||
const drawVector = new VectorLayer({
|
||||
source: new VectorSource(),
|
||||
style: new Style({
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(100, 255, 0, 1)',
|
||||
width: 2
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'rgba(100, 255, 0, 0.3)'
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// this line only appears when we're tracing a feature outer ring
|
||||
const previewLine = new Feature({
|
||||
geometry: new LineString([])
|
||||
});
|
||||
const previewVector = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [previewLine]
|
||||
}),
|
||||
style: new Style({
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(255, 0, 0, 1)',
|
||||
width: 2
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
layers: [raster, baseVector, drawVector, previewVector],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: [-12986427, 5678422],
|
||||
zoom: 5
|
||||
})
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
const snapInteraction = new Snap({
|
||||
source: baseVector.getSource()
|
||||
});
|
||||
|
||||
const typeSelect = document.getElementById('type');
|
||||
|
||||
function addInteraction() {
|
||||
const value = typeSelect.value;
|
||||
if (value !== 'None') {
|
||||
drawInteraction = new Draw({
|
||||
source: drawVector.getSource(),
|
||||
type: typeSelect.value
|
||||
});
|
||||
drawInteraction.on('drawstart', () => {
|
||||
drawing = true;
|
||||
});
|
||||
drawInteraction.on('drawend', () => {
|
||||
drawing = false;
|
||||
previewLine.getGeometry().setCoordinates([]);
|
||||
tracingFeature = null;
|
||||
});
|
||||
map.addInteraction(drawInteraction);
|
||||
map.addInteraction(snapInteraction);
|
||||
}
|
||||
}
|
||||
|
||||
typeSelect.onchange = function() {
|
||||
map.removeInteraction(drawInteraction);
|
||||
map.removeInteraction(snapInteraction);
|
||||
addInteraction();
|
||||
};
|
||||
addInteraction();
|
||||
@@ -505,7 +505,7 @@ class Draw extends PointerInteraction {
|
||||
if (this.freehand_ &&
|
||||
event.type === MapBrowserEventType.POINTERDRAG &&
|
||||
this.sketchFeature_ !== null) {
|
||||
this.addToDrawing_(event);
|
||||
this.addToDrawing_(event.coordinate);
|
||||
pass = false;
|
||||
} else if (this.freehand_ &&
|
||||
event.type === MapBrowserEventType.POINTERDOWN) {
|
||||
@@ -580,7 +580,7 @@ class Draw extends PointerInteraction {
|
||||
this.finishDrawing();
|
||||
}
|
||||
} else {
|
||||
this.addToDrawing_(event);
|
||||
this.addToDrawing_(event.coordinate);
|
||||
}
|
||||
pass = false;
|
||||
} else if (this.freehand_) {
|
||||
@@ -764,13 +764,12 @@ class Draw extends PointerInteraction {
|
||||
|
||||
/**
|
||||
* Add a new coordinate to the drawing.
|
||||
* @param {import("../MapBrowserEvent.js").default} event Event.
|
||||
* @param {!PointCoordType} coordinate Coordinate
|
||||
* @private
|
||||
*/
|
||||
addToDrawing_(event) {
|
||||
const coordinate = event.coordinate;
|
||||
addToDrawing_(coordinate) {
|
||||
const geometry = this.sketchFeature_.getGeometry();
|
||||
const projection = event.map.getView().getProjection();
|
||||
const projection = this.getMap().getView().getProjection();
|
||||
let done;
|
||||
let coordinates;
|
||||
if (this.mode_ === Mode.LINE_STRING) {
|
||||
@@ -903,9 +902,44 @@ class Draw extends PointerInteraction {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend an existing geometry by adding additional points. This only works
|
||||
* on features with `LineString` geometries, where the interaction will
|
||||
* extend lines by adding points to the end of the coordinates array.
|
||||
* Append coordinates to the end of the geometry that is currently being drawn.
|
||||
* This can be used when drawing LineStrings or Polygons. Coordinates will
|
||||
* either be appended to the current LineString or the outer ring of the current
|
||||
* Polygon.
|
||||
* @param {!LineCoordType} coordinates Linear coordinates to be appended into
|
||||
* the coordinate array.
|
||||
* @api
|
||||
*/
|
||||
appendCoordinates(coordinates) {
|
||||
const mode = this.mode_;
|
||||
let sketchCoords = [];
|
||||
if (mode === Mode.LINE_STRING) {
|
||||
sketchCoords = /** @type {LineCoordType} */ this.sketchCoords_;
|
||||
} else if (mode === Mode.POLYGON) {
|
||||
sketchCoords = this.sketchCoords_ && this.sketchCoords_.length ? /** @type {PolyCoordType} */ (this.sketchCoords_)[0] : [];
|
||||
}
|
||||
|
||||
// Remove last coordinate from sketch drawing (this coordinate follows cursor position)
|
||||
const ending = sketchCoords.pop();
|
||||
|
||||
// Append coordinate list
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
this.addToDrawing_(coordinates[i]);
|
||||
}
|
||||
|
||||
// Duplicate last coordinate for sketch drawing
|
||||
this.addToDrawing_(ending);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate draw mode by starting from an existing geometry which will
|
||||
* receive new additional points. This only works on features with
|
||||
* `LineString` geometries, where the interaction will extend lines by adding
|
||||
* points to the end of the coordinates array.
|
||||
* This will change the original feature, instead of drawing a copy.
|
||||
*
|
||||
* The function will dispatch a `drawstart` event.
|
||||
*
|
||||
* @param {!Feature<LineString>} feature Feature to be extended.
|
||||
* @api
|
||||
*/
|
||||
|
||||
@@ -1399,4 +1399,129 @@ describe('ol.interaction.Draw', function() {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('append coordinates when drawing a Polygon feature', function() {
|
||||
let draw;
|
||||
let coordinates;
|
||||
let coordinates2;
|
||||
|
||||
beforeEach(function() {
|
||||
draw = new Draw({
|
||||
source: source,
|
||||
type: 'Polygon'
|
||||
});
|
||||
map.addInteraction(draw);
|
||||
coordinates = [[0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4]];
|
||||
coordinates2 = [[10, 10], [11, 11], [12, 10], [10, 13], [13, 12], [14, 14]];
|
||||
});
|
||||
|
||||
function isClosed(polygon) {
|
||||
const first = polygon.getFirstCoordinate();
|
||||
const last = polygon.getLastCoordinate();
|
||||
expect(first).to.eql(last);
|
||||
}
|
||||
|
||||
it('draws polygon with clicks, adds coordinates to drawing, finishing on first point', function() {
|
||||
// first point
|
||||
simulateEvent('pointermove', 10, 20);
|
||||
simulateEvent('pointerdown', 10, 20);
|
||||
simulateEvent('pointerup', 10, 20);
|
||||
isClosed(draw.sketchFeature_.getGeometry());
|
||||
|
||||
// add coordinates
|
||||
draw.appendCoordinates(coordinates);
|
||||
|
||||
// finish on first point
|
||||
simulateEvent('pointermove', 10, 20);
|
||||
simulateEvent('pointerdown', 10, 20);
|
||||
simulateEvent('pointerup', 10, 20);
|
||||
|
||||
const features = source.getFeatures();
|
||||
expect(features).to.have.length(1);
|
||||
const geometry = features[0].getGeometry();
|
||||
expect(geometry).to.be.a(Polygon);
|
||||
|
||||
expect(geometry.getCoordinates()).to.eql([
|
||||
[[10, -20], [0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [10, -20]]
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds coordinates to empty drawing', function() {
|
||||
// first point
|
||||
simulateEvent('pointermove', 0, 0);
|
||||
simulateEvent('pointerdown', 0, 0);
|
||||
simulateEvent('pointerup', 0, 0);
|
||||
draw.removeLastPoint();
|
||||
draw.appendCoordinates(coordinates);
|
||||
isClosed(draw.sketchFeature_.getGeometry());
|
||||
|
||||
// finish drawing
|
||||
simulateEvent('pointerdown', 0, 0);
|
||||
simulateEvent('pointerup', 0, 0);
|
||||
|
||||
const features = source.getFeatures();
|
||||
expect(features).to.have.length(1);
|
||||
const geometry = features[0].getGeometry();
|
||||
expect(geometry).to.be.a(Polygon);
|
||||
|
||||
expect(geometry.getCoordinates()).to.eql([
|
||||
[[0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [0, 0]]
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps updating the sketch feature after appending coordinates', function() {
|
||||
// first point
|
||||
simulateEvent('pointermove', 10, 20);
|
||||
simulateEvent('pointerdown', 10, 20);
|
||||
simulateEvent('pointerup', 10, 20);
|
||||
isClosed(draw.sketchFeature_.getGeometry());
|
||||
|
||||
// add coordinates
|
||||
draw.appendCoordinates(coordinates);
|
||||
|
||||
// add another point
|
||||
simulateEvent('pointermove', 30, 20);
|
||||
simulateEvent('pointerdown', 30, 20);
|
||||
simulateEvent('pointerup', 30, 20);
|
||||
|
||||
// sketchGeom should have a complete ring, with a double coordinate for cursor
|
||||
const sketchGeom = draw.sketchFeature_.getGeometry();
|
||||
expect(sketchGeom.getCoordinates()).to.eql([
|
||||
[[10, -20], [0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [30, -20], [30, -20], [10, -20]]
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps updating the sketch feature after multiple appendiges', function() {
|
||||
// first point
|
||||
simulateEvent('pointermove', 10, 20);
|
||||
simulateEvent('pointerdown', 10, 20);
|
||||
simulateEvent('pointerup', 10, 20);
|
||||
isClosed(draw.sketchFeature_.getGeometry());
|
||||
|
||||
// add coordinates
|
||||
draw.appendCoordinates(coordinates);
|
||||
|
||||
// another point
|
||||
simulateEvent('pointermove', 100, 100);
|
||||
simulateEvent('pointerdown', 100, 100);
|
||||
simulateEvent('pointerup', 100, 100);
|
||||
|
||||
// add another array of coordinates
|
||||
draw.appendCoordinates(coordinates2);
|
||||
|
||||
// finish on first point
|
||||
simulateEvent('pointermove', 10, 20);
|
||||
simulateEvent('pointerdown', 10, 20);
|
||||
simulateEvent('pointerup', 10, 20);
|
||||
|
||||
const features = source.getFeatures();
|
||||
expect(features).to.have.length(1);
|
||||
const geometry = features[0].getGeometry();
|
||||
expect(geometry).to.be.a(Polygon);
|
||||
|
||||
expect(geometry.getCoordinates()).to.eql([
|
||||
[[10, -20], [0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [100, -100], [10, 10], [11, 11], [12, 10], [10, 13], [13, 12], [14, 14], [10, -20]]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user