Merge pull request #14046 from tschaub/trace
Support tracing with the draw interaction
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,6 +5,7 @@ import Circle from '../geom/Circle.js';
|
||||
import Event from '../events/Event.js';
|
||||
import EventType from '../events/EventType.js';
|
||||
import Feature from '../Feature.js';
|
||||
import GeometryCollection from '../geom/GeometryCollection.js';
|
||||
import InteractionProperty from './Property.js';
|
||||
import LineString from '../geom/LineString.js';
|
||||
import MapBrowserEvent from '../MapBrowserEvent.js';
|
||||
@@ -18,7 +19,12 @@ import Polygon, {fromCircle, makeRegular} from '../geom/Polygon.js';
|
||||
import VectorLayer from '../layer/Vector.js';
|
||||
import VectorSource from '../source/Vector.js';
|
||||
import {FALSE, TRUE} from '../functions.js';
|
||||
import {always, noModifierKeys, shiftKeyOnly} from '../events/condition.js';
|
||||
import {
|
||||
always,
|
||||
never,
|
||||
noModifierKeys,
|
||||
shiftKeyOnly,
|
||||
} from '../events/condition.js';
|
||||
import {
|
||||
boundingExtent,
|
||||
getBottomLeft,
|
||||
@@ -26,10 +32,14 @@ import {
|
||||
getTopLeft,
|
||||
getTopRight,
|
||||
} from '../extent.js';
|
||||
import {clamp, squaredDistance, toFixed} from '../math.js';
|
||||
import {createEditingStyle} from '../style/Style.js';
|
||||
import {
|
||||
distance,
|
||||
squaredDistance as squaredCoordinateDistance,
|
||||
} from '../coordinate.js';
|
||||
import {fromUserCoordinate, getUserProjection} from '../proj.js';
|
||||
import {getStrideForLayout} from '../geom/SimpleGeometry.js';
|
||||
import {squaredDistance as squaredCoordinateDistance} from '../coordinate.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Options
|
||||
@@ -80,6 +90,11 @@ import {squaredDistance as squaredCoordinateDistance} from '../coordinate.js';
|
||||
* returns a boolean to indicate whether that event should be handled. The
|
||||
* default is {@link module:ol/events/condition.shiftKeyOnly}, meaning that the
|
||||
* Shift key activates freehand drawing.
|
||||
* @property {boolean|import("../events/condition.js").Condition} [trace=false] Trace a portion of another geometry.
|
||||
* Ignored when in freehand mode.
|
||||
* @property {VectorSource} [traceSource] Source for features to trace. If tracing is active and a `traceSource` is
|
||||
* not provided, the interaction's `source` will be used. Tracing requires that the interaction is configured with
|
||||
* either a `traceSource` or a `source`.
|
||||
* @property {boolean} [wrapX=false] Wrap the world horizontally on the sketch
|
||||
* overlay.
|
||||
* @property {import("../geom/Geometry.js").GeometryLayout} [geometryLayout='XY'] Layout of the
|
||||
@@ -106,6 +121,24 @@ import {squaredDistance as squaredCoordinateDistance} from '../coordinate.js';
|
||||
* @typedef {PointCoordType|LineCoordType|PolyCoordType} SketchCoordType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TraceState
|
||||
* @property {boolean} active Tracing active.
|
||||
* @property {import("../pixel.js").Pixel} [startPx] The initially clicked pixel location.
|
||||
* @property {Array<TraceTarget>} [targets] Targets available for tracing.
|
||||
* @property {number} [targetIndex] The index of the currently traced target. A value of -1 indicates
|
||||
* that no trace target is active.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TraceTarget
|
||||
* @property {Array<import("../coordinate.js").Coordinate>} coordinates Target coordinates.
|
||||
* @property {boolean} ring The target coordinates are a linear ring.
|
||||
* @property {number} startIndex The index of first traced coordinate. A fractional index represents an
|
||||
* edge intersection. Index values for rings will wrap (may be negative or larger than coordinates length).
|
||||
* @property {number} endIndex The index of last traced coordinate. Details from startIndex also apply here.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function that takes an array of coordinates and an optional existing geometry
|
||||
* and a projection as arguments, and returns a geometry. The optional existing
|
||||
@@ -168,6 +201,359 @@ export class DrawEvent extends Event {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../coordinate.js").Coordinate} coordinate The coordinate.
|
||||
* @param {Array<Feature>} features The candidate features.
|
||||
* @return {Array<TraceTarget>} The trace targets.
|
||||
*/
|
||||
function getTraceTargets(coordinate, features) {
|
||||
/**
|
||||
* @type {Array<TraceTarget>}
|
||||
*/
|
||||
const targets = [];
|
||||
|
||||
for (let i = 0; i < features.length; ++i) {
|
||||
const feature = features[i];
|
||||
const geometry = feature.getGeometry();
|
||||
appendGeometryTraceTargets(coordinate, geometry, targets);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../coordinate.js").Coordinate} a One coordinate.
|
||||
* @param {import("../coordinate.js").Coordinate} b Another coordinate.
|
||||
* @return {number} The squared distance between the two coordinates.
|
||||
*/
|
||||
function getSquaredDistance(a, b) {
|
||||
return squaredDistance(a[0], a[1], b[0], b[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LineCoordType} coordinates The ring coordinates.
|
||||
* @param {number} index The index. May be wrapped.
|
||||
* @return {import("../coordinate.js").Coordinate} The coordinate.
|
||||
*/
|
||||
function getCoordinate(coordinates, index) {
|
||||
const count = coordinates.length;
|
||||
if (index < 0) {
|
||||
return coordinates[index + count];
|
||||
}
|
||||
if (index >= count) {
|
||||
return coordinates[index - count];
|
||||
}
|
||||
return coordinates[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cumulative squared distance along a ring path. The end index index may be "wrapped" and it may
|
||||
* be less than the start index to indicate the direction of travel. The start and end index may have
|
||||
* a fractional part to indicate a point between two coordinates.
|
||||
* @param {LineCoordType} coordinates Ring coordinates.
|
||||
* @param {number} startIndex The start index.
|
||||
* @param {number} endIndex The end index.
|
||||
* @return {number} The cumulative squared distance along the ring path.
|
||||
*/
|
||||
function getCumulativeSquaredDistance(coordinates, startIndex, endIndex) {
|
||||
let lowIndex, highIndex;
|
||||
if (startIndex < endIndex) {
|
||||
lowIndex = startIndex;
|
||||
highIndex = endIndex;
|
||||
} else {
|
||||
lowIndex = endIndex;
|
||||
highIndex = startIndex;
|
||||
}
|
||||
const lowWholeIndex = Math.ceil(lowIndex);
|
||||
const highWholeIndex = Math.floor(highIndex);
|
||||
|
||||
if (lowWholeIndex > highWholeIndex) {
|
||||
// both start and end are on the same segment
|
||||
const start = interpolateCoordinate(coordinates, lowIndex);
|
||||
const end = interpolateCoordinate(coordinates, highIndex);
|
||||
return getSquaredDistance(start, end);
|
||||
}
|
||||
|
||||
let sd = 0;
|
||||
|
||||
if (lowIndex < lowWholeIndex) {
|
||||
const start = interpolateCoordinate(coordinates, lowIndex);
|
||||
const end = getCoordinate(coordinates, lowWholeIndex);
|
||||
sd += getSquaredDistance(start, end);
|
||||
}
|
||||
|
||||
if (highWholeIndex < highIndex) {
|
||||
const start = getCoordinate(coordinates, highWholeIndex);
|
||||
const end = interpolateCoordinate(coordinates, highIndex);
|
||||
sd += getSquaredDistance(start, end);
|
||||
}
|
||||
|
||||
for (let i = lowWholeIndex; i < highWholeIndex - 1; ++i) {
|
||||
const start = getCoordinate(coordinates, i);
|
||||
const end = getCoordinate(coordinates, i + 1);
|
||||
sd += getSquaredDistance(start, end);
|
||||
}
|
||||
|
||||
return sd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../coordinate.js").Coordinate} coordinate The coordinate.
|
||||
* @param {import("../geom/Geometry.js").default} geometry The candidate geometry.
|
||||
* @param {Array<TraceTarget>} targets The trace targets.
|
||||
*/
|
||||
function appendGeometryTraceTargets(coordinate, geometry, targets) {
|
||||
if (geometry instanceof LineString) {
|
||||
appendTraceTarget(coordinate, geometry.getCoordinates(), false, targets);
|
||||
return;
|
||||
}
|
||||
if (geometry instanceof MultiLineString) {
|
||||
const coordinates = geometry.getCoordinates();
|
||||
for (let i = 0, ii = coordinates.length; i < ii; ++i) {
|
||||
appendTraceTarget(coordinate, coordinates[i], false, targets);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (geometry instanceof Polygon) {
|
||||
const coordinates = geometry.getCoordinates();
|
||||
for (let i = 0, ii = coordinates.length; i < ii; ++i) {
|
||||
appendTraceTarget(coordinate, coordinates[i], true, targets);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (geometry instanceof MultiPolygon) {
|
||||
const polys = geometry.getCoordinates();
|
||||
for (let i = 0, ii = polys.length; i < ii; ++i) {
|
||||
const coordinates = polys[i];
|
||||
for (let j = 0, jj = coordinates.length; j < jj; ++j) {
|
||||
appendTraceTarget(coordinate, coordinates[j], true, targets);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (geometry instanceof GeometryCollection) {
|
||||
const geometries = geometry.getGeometries();
|
||||
for (let i = 0; i < geometries.length; ++i) {
|
||||
appendGeometryTraceTargets(coordinate, geometries[i], targets);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// other types cannot be traced
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} TraceTargetUpdateInfo
|
||||
* @property {number} index The new target index.
|
||||
* @property {number} endIndex The new segment end index.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {TraceTargetUpdateInfo}
|
||||
*/
|
||||
const sharedUpdateInfo = {index: -1, endIndex: NaN};
|
||||
|
||||
/**
|
||||
* @param {import("../coordinate.js").Coordinate} coordinate The coordinate.
|
||||
* @param {TraceState} traceState The trace state.
|
||||
* @return {TraceTargetUpdateInfo} Information about the new trace target. The returned
|
||||
* object is reused between calls and must not be modified by the caller.
|
||||
*/
|
||||
function getTraceTargetUpdate(coordinate, traceState) {
|
||||
const x = coordinate[0];
|
||||
const y = coordinate[1];
|
||||
|
||||
let closestTargetDistance = Infinity;
|
||||
|
||||
let newTargetIndex = -1;
|
||||
let newEndIndex = NaN;
|
||||
|
||||
for (
|
||||
let targetIndex = 0;
|
||||
targetIndex < traceState.targets.length;
|
||||
++targetIndex
|
||||
) {
|
||||
const target = traceState.targets[targetIndex];
|
||||
const coordinates = target.coordinates;
|
||||
|
||||
let minSegmentDistance = Infinity;
|
||||
let endIndex;
|
||||
for (
|
||||
let coordinateIndex = 0;
|
||||
coordinateIndex < coordinates.length - 1;
|
||||
++coordinateIndex
|
||||
) {
|
||||
const start = coordinates[coordinateIndex];
|
||||
const end = coordinates[coordinateIndex + 1];
|
||||
const rel = getPointSegmentRelationship(x, y, start, end);
|
||||
if (rel.squaredDistance < minSegmentDistance) {
|
||||
minSegmentDistance = rel.squaredDistance;
|
||||
endIndex = coordinateIndex + rel.along;
|
||||
}
|
||||
}
|
||||
|
||||
if (minSegmentDistance < closestTargetDistance) {
|
||||
closestTargetDistance = minSegmentDistance;
|
||||
if (target.ring && traceState.targetIndex === targetIndex) {
|
||||
// same target, maintain the same trace direction
|
||||
if (target.endIndex > target.startIndex) {
|
||||
// forward trace
|
||||
if (endIndex < target.startIndex) {
|
||||
endIndex += coordinates.length;
|
||||
}
|
||||
} else if (target.endIndex < target.startIndex) {
|
||||
// reverse trace
|
||||
if (endIndex > target.startIndex) {
|
||||
endIndex -= coordinates.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
newEndIndex = endIndex;
|
||||
newTargetIndex = targetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
traceState.targetIndex !== newTargetIndex &&
|
||||
traceState.targets[newTargetIndex].ring
|
||||
) {
|
||||
const target = traceState.targets[newTargetIndex];
|
||||
const coordinates = target.coordinates;
|
||||
const count = coordinates.length;
|
||||
const startIndex = target.startIndex;
|
||||
const endIndex = newEndIndex;
|
||||
if (startIndex < endIndex) {
|
||||
const forwardDistance = getCumulativeSquaredDistance(
|
||||
coordinates,
|
||||
startIndex,
|
||||
endIndex
|
||||
);
|
||||
const reverseDistance = getCumulativeSquaredDistance(
|
||||
coordinates,
|
||||
startIndex,
|
||||
endIndex - count
|
||||
);
|
||||
if (reverseDistance < forwardDistance) {
|
||||
newEndIndex -= count;
|
||||
}
|
||||
} else {
|
||||
const reverseDistance = getCumulativeSquaredDistance(
|
||||
coordinates,
|
||||
startIndex,
|
||||
endIndex
|
||||
);
|
||||
const forwardDistance = getCumulativeSquaredDistance(
|
||||
coordinates,
|
||||
startIndex,
|
||||
endIndex + count
|
||||
);
|
||||
if (forwardDistance < reverseDistance) {
|
||||
newEndIndex += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedUpdateInfo.index = newTargetIndex;
|
||||
sharedUpdateInfo.endIndex = newEndIndex;
|
||||
return sharedUpdateInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../coordinate.js").Coordinate} coordinate The clicked coordinate.
|
||||
* @param {Array<import("../coordinate.js").Coordinate>} coordinates The geometry component coordinates.
|
||||
* @param {boolean} ring The coordinates represent a linear ring.
|
||||
* @param {Array<TraceTarget>} targets The trace targets.
|
||||
*/
|
||||
function appendTraceTarget(coordinate, coordinates, ring, targets) {
|
||||
const x = coordinate[0];
|
||||
const y = coordinate[1];
|
||||
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
|
||||
const start = coordinates[i];
|
||||
const end = coordinates[i + 1];
|
||||
const rel = getPointSegmentRelationship(x, y, start, end);
|
||||
if (rel.squaredDistance === 0) {
|
||||
const index = i + rel.along;
|
||||
targets.push({
|
||||
coordinates: coordinates,
|
||||
ring: ring,
|
||||
startIndex: index,
|
||||
endIndex: index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PointSegmentRelationship
|
||||
* @property {number} along The closest point expressed as a fraction along the segment length.
|
||||
* @property {number} squaredDistance The squared distance of the point to the segment.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {PointSegmentRelationship}
|
||||
*/
|
||||
const sharedRel = {along: 0, squaredDistance: 0};
|
||||
|
||||
/**
|
||||
* @param {number} x The point x.
|
||||
* @param {number} y The point y.
|
||||
* @param {import("../coordinate.js").Coordinate} start The segment start.
|
||||
* @param {import("../coordinate.js").Coordinate} end The segment end.
|
||||
* @return {PointSegmentRelationship} The point segment relationship. The returned object is
|
||||
* shared between calls and must not be modified by the caller.
|
||||
*/
|
||||
function getPointSegmentRelationship(x, y, start, end) {
|
||||
const x1 = start[0];
|
||||
const y1 = start[1];
|
||||
const x2 = end[0];
|
||||
const y2 = end[1];
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
let along = 0;
|
||||
let px = x1;
|
||||
let py = y1;
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
along = clamp(((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy), 0, 1);
|
||||
px += dx * along;
|
||||
py += dy * along;
|
||||
}
|
||||
|
||||
sharedRel.along = along;
|
||||
sharedRel.squaredDistance = toFixed(squaredDistance(x, y, px, py), 10);
|
||||
return sharedRel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LineCoordType} coordinates The coordinates.
|
||||
* @param {number} index The index. May be fractional and may wrap.
|
||||
* @return {import("../coordinate.js").Coordinate} The interpolated coordinate.
|
||||
*/
|
||||
function interpolateCoordinate(coordinates, index) {
|
||||
const count = coordinates.length;
|
||||
|
||||
let startIndex = Math.floor(index);
|
||||
const along = index - startIndex;
|
||||
if (startIndex >= count) {
|
||||
startIndex -= count;
|
||||
} else if (startIndex < 0) {
|
||||
startIndex += count;
|
||||
}
|
||||
|
||||
let endIndex = startIndex + 1;
|
||||
if (endIndex >= count) {
|
||||
endIndex -= count;
|
||||
}
|
||||
|
||||
const start = coordinates[startIndex];
|
||||
const x0 = start[0];
|
||||
const y0 = start[1];
|
||||
const end = coordinates[endIndex];
|
||||
const dx = end[0] - x0;
|
||||
const dy = end[1] - y0;
|
||||
|
||||
return [x0 + dx * along, y0 + dy * along];
|
||||
}
|
||||
|
||||
/***
|
||||
* @template Return
|
||||
* @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
|
||||
@@ -514,9 +900,46 @@ class Draw extends PointerInteraction {
|
||||
: shiftKeyOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("../events/condition.js").Condition}
|
||||
* @private
|
||||
*/
|
||||
this.traceCondition_;
|
||||
this.setTrace(options.trace || false);
|
||||
|
||||
/**
|
||||
* @type {TraceState}
|
||||
* @private
|
||||
*/
|
||||
this.traceState_ = {active: false};
|
||||
|
||||
/**
|
||||
* @type {VectorSource|null}
|
||||
* @private
|
||||
*/
|
||||
this.traceSource_ = options.traceSource || options.source || null;
|
||||
|
||||
this.addChangeListener(InteractionProperty.ACTIVE, this.updateState_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle tracing mode or set a tracing condition.
|
||||
*
|
||||
* @param {boolean|import("../events/condition.js").Condition} trace A boolean to toggle tracing mode or an event
|
||||
* condition that will be checked when a feature is clicked to determine if tracing should be active.
|
||||
*/
|
||||
setTrace(trace) {
|
||||
let condition;
|
||||
if (!trace) {
|
||||
condition = never;
|
||||
} else if (trace === true) {
|
||||
condition = always;
|
||||
} else {
|
||||
condition = trace;
|
||||
}
|
||||
this.traceCondition_ = condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the interaction from its current map and attach it to the new map.
|
||||
* Subclasses may set up event handlers to get notified about changes to
|
||||
@@ -617,28 +1040,241 @@ class Draw extends PointerInteraction {
|
||||
this.startDrawing_(event.coordinate);
|
||||
}
|
||||
return true;
|
||||
} else if (this.condition_(event)) {
|
||||
this.lastDragTime_ = Date.now();
|
||||
this.downTimeout_ = setTimeout(
|
||||
function () {
|
||||
this.handlePointerMove_(
|
||||
new MapBrowserEvent(
|
||||
MapBrowserEventType.POINTERMOVE,
|
||||
event.map,
|
||||
event.originalEvent,
|
||||
false,
|
||||
event.frameState
|
||||
)
|
||||
);
|
||||
}.bind(this),
|
||||
this.dragVertexDelay_
|
||||
);
|
||||
this.downPx_ = event.pixel;
|
||||
return true;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!this.condition_(event)) {
|
||||
this.lastDragTime_ = undefined;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.lastDragTime_ = Date.now();
|
||||
this.downTimeout_ = setTimeout(
|
||||
function () {
|
||||
this.handlePointerMove_(
|
||||
new MapBrowserEvent(
|
||||
MapBrowserEventType.POINTERMOVE,
|
||||
event.map,
|
||||
event.originalEvent,
|
||||
false,
|
||||
event.frameState
|
||||
)
|
||||
);
|
||||
}.bind(this),
|
||||
this.dragVertexDelay_
|
||||
);
|
||||
this.downPx_ = event.pixel;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
deactivateTrace_() {
|
||||
this.traceState_ = {active: false};
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate or deactivate trace state based on a browser event.
|
||||
* @param {import("../MapBrowserEvent.js").default} event Event.
|
||||
* @private
|
||||
*/
|
||||
toggleTraceState_(event) {
|
||||
if (!this.traceSource_ || !this.traceCondition_(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.traceState_.active) {
|
||||
this.deactivateTrace_();
|
||||
return;
|
||||
}
|
||||
|
||||
const map = this.getMap();
|
||||
const lowerLeft = map.getCoordinateFromPixel([
|
||||
event.pixel[0] - this.snapTolerance_,
|
||||
event.pixel[1] + this.snapTolerance_,
|
||||
]);
|
||||
const upperRight = map.getCoordinateFromPixel([
|
||||
event.pixel[0] + this.snapTolerance_,
|
||||
event.pixel[1] - this.snapTolerance_,
|
||||
]);
|
||||
const extent = boundingExtent([lowerLeft, upperRight]);
|
||||
const features = this.traceSource_.getFeaturesInExtent(extent);
|
||||
if (features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targets = getTraceTargets(event.coordinate, features);
|
||||
if (targets.length) {
|
||||
this.traceState_ = {
|
||||
active: true,
|
||||
startPx: event.pixel.slice(),
|
||||
targets: targets,
|
||||
targetIndex: -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TraceTarget} target The trace target.
|
||||
* @param {number} endIndex The new end index of the trace.
|
||||
* @private
|
||||
*/
|
||||
addOrRemoveTracedCoordinates_(target, endIndex) {
|
||||
// three cases to handle:
|
||||
// 1. traced in the same direction and points need adding
|
||||
// 2. traced in the same direction and points need removing
|
||||
// 3. traced in a new direction
|
||||
const previouslyForward = target.startIndex <= target.endIndex;
|
||||
const currentlyForward = target.startIndex <= endIndex;
|
||||
if (previouslyForward === currentlyForward) {
|
||||
// same direction
|
||||
if (
|
||||
(previouslyForward && endIndex > target.endIndex) ||
|
||||
(!previouslyForward && endIndex < target.endIndex)
|
||||
) {
|
||||
// case 1 - add new points
|
||||
this.addTracedCoordinates_(target, target.endIndex, endIndex);
|
||||
} else if (
|
||||
(previouslyForward && endIndex < target.endIndex) ||
|
||||
(!previouslyForward && endIndex > target.endIndex)
|
||||
) {
|
||||
// case 2 - remove old points
|
||||
this.removeTracedCoordinates_(endIndex, target.endIndex);
|
||||
}
|
||||
} else {
|
||||
// case 3 - remove old points, add new points
|
||||
this.removeTracedCoordinates_(target.startIndex, target.endIndex);
|
||||
this.addTracedCoordinates_(target, target.startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fromIndex The start index.
|
||||
* @param {number} toIndex The end index.
|
||||
* @private
|
||||
*/
|
||||
removeTracedCoordinates_(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
let remove = 0;
|
||||
if (fromIndex < toIndex) {
|
||||
const start = Math.ceil(fromIndex);
|
||||
let end = Math.floor(toIndex);
|
||||
if (end === toIndex) {
|
||||
end -= 1;
|
||||
}
|
||||
remove = end - start + 1;
|
||||
} else {
|
||||
const start = Math.floor(fromIndex);
|
||||
let end = Math.ceil(toIndex);
|
||||
if (end === toIndex) {
|
||||
end += 1;
|
||||
}
|
||||
remove = start - end + 1;
|
||||
}
|
||||
|
||||
if (remove > 0) {
|
||||
this.removeLastPoints_(remove);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TraceTarget} target The trace target.
|
||||
* @param {number} fromIndex The start index.
|
||||
* @param {number} toIndex The end index.
|
||||
* @private
|
||||
*/
|
||||
addTracedCoordinates_(target, fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinates = [];
|
||||
if (fromIndex < toIndex) {
|
||||
// forward trace
|
||||
const start = Math.ceil(fromIndex);
|
||||
let end = Math.floor(toIndex);
|
||||
if (end === toIndex) {
|
||||
// if end is snapped to a vertex, it will be added later
|
||||
end -= 1;
|
||||
}
|
||||
for (let i = start; i <= end; ++i) {
|
||||
coordinates.push(getCoordinate(target.coordinates, i));
|
||||
}
|
||||
} else {
|
||||
// reverse trace
|
||||
const start = Math.floor(fromIndex);
|
||||
let end = Math.ceil(toIndex);
|
||||
if (end === toIndex) {
|
||||
end += 1;
|
||||
}
|
||||
for (let i = start; i >= end; --i) {
|
||||
coordinates.push(getCoordinate(target.coordinates, i));
|
||||
}
|
||||
}
|
||||
if (coordinates.length) {
|
||||
this.appendCoordinates(coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the trace.
|
||||
* @param {import("../MapBrowserEvent.js").default} event Event.
|
||||
* @private
|
||||
*/
|
||||
updateTrace_(event) {
|
||||
const traceState = this.traceState_;
|
||||
if (!traceState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (traceState.targetIndex === -1) {
|
||||
// check if we are ready to pick a target
|
||||
if (distance(traceState.startPx, event.pixel) < this.snapTolerance_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedTraceTarget = getTraceTargetUpdate(
|
||||
event.coordinate,
|
||||
traceState
|
||||
);
|
||||
|
||||
if (traceState.targetIndex !== updatedTraceTarget.index) {
|
||||
// target changed
|
||||
if (traceState.targetIndex !== -1) {
|
||||
// remove points added during previous trace
|
||||
const oldTarget = traceState.targets[traceState.targetIndex];
|
||||
this.removeTracedCoordinates_(oldTarget.startIndex, oldTarget.endIndex);
|
||||
}
|
||||
// add points for the new target
|
||||
const newTarget = traceState.targets[updatedTraceTarget.index];
|
||||
this.addTracedCoordinates_(
|
||||
newTarget,
|
||||
newTarget.startIndex,
|
||||
updatedTraceTarget.endIndex
|
||||
);
|
||||
} else {
|
||||
// target stayed the same
|
||||
const target = traceState.targets[traceState.targetIndex];
|
||||
this.addOrRemoveTracedCoordinates_(target, updatedTraceTarget.endIndex);
|
||||
}
|
||||
|
||||
// modify the state with updated info
|
||||
traceState.targetIndex = updatedTraceTarget.index;
|
||||
const target = traceState.targets[traceState.targetIndex];
|
||||
target.endIndex = updatedTraceTarget.endIndex;
|
||||
|
||||
// update event coordinate and pixel to match end point of final segment
|
||||
const coordinate = interpolateCoordinate(
|
||||
target.coordinates,
|
||||
target.endIndex
|
||||
);
|
||||
const pixel = this.getMap().getPixelFromCoordinate(coordinate);
|
||||
event.coordinate = coordinate;
|
||||
event.pixel = [Math.round(pixel[0]), Math.round(pixel[1])];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -656,6 +1292,8 @@ class Draw extends PointerInteraction {
|
||||
}
|
||||
|
||||
this.handlePointerMove_(event);
|
||||
const tracing = this.traceState_.active;
|
||||
this.toggleTraceState_(event);
|
||||
|
||||
if (this.shouldHandle_) {
|
||||
const startingToDraw = !this.finishCoordinate_;
|
||||
@@ -668,7 +1306,7 @@ class Draw extends PointerInteraction {
|
||||
!this.freehand_ &&
|
||||
(!startingToDraw || this.mode_ === 'Point')
|
||||
) {
|
||||
if (this.atFinish_(event.pixel)) {
|
||||
if (this.atFinish_(event.pixel, tracing)) {
|
||||
if (this.finishCondition_(event)) {
|
||||
this.finishDrawing();
|
||||
}
|
||||
@@ -713,20 +1351,23 @@ class Draw extends PointerInteraction {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.finishCoordinate_) {
|
||||
this.modifyDrawing_(event.coordinate);
|
||||
} else {
|
||||
if (!this.finishCoordinate_) {
|
||||
this.createOrUpdateSketchPoint_(event.coordinate.slice());
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateTrace_(event);
|
||||
this.modifyDrawing_(event.coordinate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an event is within the snapping tolerance of the start coord.
|
||||
* @param {import("../pixel.js").Pixel} pixel Pixel.
|
||||
* @param {boolean} [tracing] Drawing in trace mode (only stop if at the starting point).
|
||||
* @return {boolean} The event is within the snapping tolerance of the start.
|
||||
* @private
|
||||
*/
|
||||
atFinish_(pixel) {
|
||||
atFinish_(pixel, tracing) {
|
||||
let at = false;
|
||||
if (this.sketchFeature_) {
|
||||
let potentiallyDone = false;
|
||||
@@ -737,7 +1378,8 @@ class Draw extends PointerInteraction {
|
||||
} else if (mode === 'Circle') {
|
||||
at = this.sketchCoords_.length === 2;
|
||||
} else if (mode === 'LineString') {
|
||||
potentiallyDone = this.sketchCoords_.length > this.minPoints_;
|
||||
potentiallyDone =
|
||||
!tracing && this.sketchCoords_.length > this.minPoints_;
|
||||
} else if (mode === 'Polygon') {
|
||||
const sketchCoords = /** @type {PolyCoordType} */ (this.sketchCoords_);
|
||||
potentiallyDone = sketchCoords[0].length > this.minPoints_;
|
||||
@@ -745,6 +1387,14 @@ class Draw extends PointerInteraction {
|
||||
sketchCoords[0][0],
|
||||
sketchCoords[0][sketchCoords[0].length - 2],
|
||||
];
|
||||
if (tracing) {
|
||||
potentiallyFinishCoordinates = [sketchCoords[0][0]];
|
||||
} else {
|
||||
potentiallyFinishCoordinates = [
|
||||
sketchCoords[0][0],
|
||||
sketchCoords[0][sketchCoords[0].length - 2],
|
||||
];
|
||||
}
|
||||
}
|
||||
if (potentiallyDone) {
|
||||
const map = this.getMap();
|
||||
@@ -936,51 +1586,63 @@ class Draw extends PointerInteraction {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove last point of the feature currently being drawn. Does not do anything when
|
||||
* drawing POINT or MULTI_POINT geometries.
|
||||
* @api
|
||||
* @param {number} n The number of points to remove.
|
||||
*/
|
||||
removeLastPoint() {
|
||||
removeLastPoints_(n) {
|
||||
if (!this.sketchFeature_) {
|
||||
return;
|
||||
}
|
||||
const geometry = this.sketchFeature_.getGeometry();
|
||||
const projection = this.getMap().getView().getProjection();
|
||||
let coordinates;
|
||||
const mode = this.mode_;
|
||||
if (mode === 'LineString' || mode === 'Circle') {
|
||||
coordinates = /** @type {LineCoordType} */ (this.sketchCoords_);
|
||||
coordinates.splice(-2, 1);
|
||||
if (coordinates.length >= 2) {
|
||||
this.finishCoordinate_ = coordinates[coordinates.length - 2].slice();
|
||||
const finishCoordinate = this.finishCoordinate_.slice();
|
||||
coordinates[coordinates.length - 1] = finishCoordinate;
|
||||
this.createOrUpdateSketchPoint_(finishCoordinate);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
let coordinates;
|
||||
if (mode === 'LineString' || mode === 'Circle') {
|
||||
coordinates = /** @type {LineCoordType} */ (this.sketchCoords_);
|
||||
coordinates.splice(-2, 1);
|
||||
if (coordinates.length >= 2) {
|
||||
this.finishCoordinate_ = coordinates[coordinates.length - 2].slice();
|
||||
const finishCoordinate = this.finishCoordinate_.slice();
|
||||
coordinates[coordinates.length - 1] = finishCoordinate;
|
||||
this.createOrUpdateSketchPoint_(finishCoordinate);
|
||||
}
|
||||
this.geometryFunction_(coordinates, geometry, projection);
|
||||
if (geometry.getType() === 'Polygon' && this.sketchLine_) {
|
||||
this.createOrUpdateCustomSketchLine_(
|
||||
/** @type {Polygon} */ (geometry)
|
||||
);
|
||||
}
|
||||
} else if (mode === 'Polygon') {
|
||||
coordinates = /** @type {PolyCoordType} */ (this.sketchCoords_)[0];
|
||||
coordinates.splice(-2, 1);
|
||||
const sketchLineGeom = this.sketchLine_.getGeometry();
|
||||
if (coordinates.length >= 2) {
|
||||
const finishCoordinate = coordinates[coordinates.length - 2].slice();
|
||||
coordinates[coordinates.length - 1] = finishCoordinate;
|
||||
this.createOrUpdateSketchPoint_(finishCoordinate);
|
||||
}
|
||||
sketchLineGeom.setCoordinates(coordinates);
|
||||
this.geometryFunction_(this.sketchCoords_, geometry, projection);
|
||||
}
|
||||
this.geometryFunction_(coordinates, geometry, projection);
|
||||
if (geometry.getType() === 'Polygon' && this.sketchLine_) {
|
||||
this.createOrUpdateCustomSketchLine_(/** @type {Polygon} */ (geometry));
|
||||
}
|
||||
} else if (mode === 'Polygon') {
|
||||
coordinates = /** @type {PolyCoordType} */ (this.sketchCoords_)[0];
|
||||
coordinates.splice(-2, 1);
|
||||
const sketchLineGeom = this.sketchLine_.getGeometry();
|
||||
if (coordinates.length >= 2) {
|
||||
const finishCoordinate = coordinates[coordinates.length - 2].slice();
|
||||
coordinates[coordinates.length - 1] = finishCoordinate;
|
||||
this.createOrUpdateSketchPoint_(finishCoordinate);
|
||||
}
|
||||
sketchLineGeom.setCoordinates(coordinates);
|
||||
this.geometryFunction_(this.sketchCoords_, geometry, projection);
|
||||
}
|
||||
|
||||
if (coordinates.length === 1) {
|
||||
this.abortDrawing();
|
||||
if (coordinates.length === 1) {
|
||||
this.abortDrawing();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSketchFeatures_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove last point of the feature currently being drawn. Does not do anything when
|
||||
* drawing POINT or MULTI_POINT geometries.
|
||||
* @api
|
||||
*/
|
||||
removeLastPoint() {
|
||||
this.removeLastPoints_(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop drawing and add the sketch feature to the target layer.
|
||||
* The {@link module:ol/interaction/Draw~DrawEventType.DRAWEND} event is
|
||||
@@ -1045,6 +1707,7 @@ class Draw extends PointerInteraction {
|
||||
this.sketchPoint_ = null;
|
||||
this.sketchLine_ = null;
|
||||
this.overlay_.getSource().clear(true);
|
||||
this.deactivateTrace_();
|
||||
return sketchFeature;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,21 @@ import {listen} from '../../../../../src/ol/events.js';
|
||||
import {register} from '../../../../../src/ol/proj/proj4.js';
|
||||
import {unByKey} from '../../../../../src/ol/Observable.js';
|
||||
|
||||
describe('ol.interaction.Draw', function () {
|
||||
let target, map, source;
|
||||
describe('ol/interaction/Draw', function () {
|
||||
/**
|
||||
* @type {VectorSource}
|
||||
*/
|
||||
let source;
|
||||
|
||||
/**
|
||||
* @type {Map}
|
||||
*/
|
||||
let map;
|
||||
|
||||
/**
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
let target;
|
||||
|
||||
const width = 360;
|
||||
const height = 180;
|
||||
@@ -135,6 +148,78 @@ describe('ol.interaction.Draw', function () {
|
||||
expect(draw.freehandCondition_(event)).to.be(true);
|
||||
});
|
||||
|
||||
describe('trace option', function () {
|
||||
it('always goes in trace mode if true', function () {
|
||||
const draw = new Draw({
|
||||
source: source,
|
||||
type: 'LineString',
|
||||
trace: true,
|
||||
});
|
||||
|
||||
const event = new MapBrowserEvent({
|
||||
map: map,
|
||||
type: 'pointerup',
|
||||
originalEvent: new PointerEvent('pointerup', {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(draw.traceCondition_(event)).to.be(true);
|
||||
});
|
||||
|
||||
it('never goes in trace mode if false', function () {
|
||||
const draw = new Draw({
|
||||
source: source,
|
||||
type: 'LineString',
|
||||
trace: false,
|
||||
});
|
||||
|
||||
const event = new MapBrowserEvent(
|
||||
map,
|
||||
'pointerup',
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
})
|
||||
);
|
||||
|
||||
expect(draw.traceCondition_(event)).to.be(false);
|
||||
});
|
||||
|
||||
it('accepts a condition', function () {
|
||||
const draw = new Draw({
|
||||
source: source,
|
||||
type: 'LineString',
|
||||
trace: shiftKeyOnly,
|
||||
});
|
||||
|
||||
const goodEvent = new MapBrowserEvent(
|
||||
map,
|
||||
'pointerup',
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
shiftKey: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(draw.traceCondition_(goodEvent)).to.be(true);
|
||||
|
||||
const badEvent = new MapBrowserEvent(
|
||||
map,
|
||||
'pointerup',
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
shiftKey: false,
|
||||
})
|
||||
);
|
||||
|
||||
expect(draw.traceCondition_(badEvent)).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a dragVertexDelay option', function () {
|
||||
const draw = new Draw({
|
||||
source: source,
|
||||
@@ -1012,6 +1097,84 @@ describe('ol.interaction.Draw', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracing polygons', function () {
|
||||
let draw;
|
||||
|
||||
beforeEach(function () {
|
||||
draw = new Draw({
|
||||
source: source,
|
||||
type: 'Polygon',
|
||||
trace: true,
|
||||
});
|
||||
map.addInteraction(draw);
|
||||
});
|
||||
|
||||
it('starts tracing with first edge click, stops tracing with second edge click', function () {
|
||||
source.addFeatures([
|
||||
new Feature(
|
||||
new Polygon([
|
||||
[
|
||||
[0, -50],
|
||||
[100, -50],
|
||||
[100, -100],
|
||||
[0, -100],
|
||||
[0, -50],
|
||||
],
|
||||
])
|
||||
),
|
||||
]);
|
||||
|
||||
// first click adds a point
|
||||
simulateEvent('pointermove', 50, 0);
|
||||
simulateEvent('pointerdown', 50, 0);
|
||||
simulateEvent('pointerup', 50, 0);
|
||||
expect(draw.traceState_.active).to.be(false);
|
||||
draw.shouldHandle_ = false;
|
||||
|
||||
// second click activates tracing (center of bottom edge)
|
||||
simulateEvent('pointermove', 50, 50);
|
||||
simulateEvent('pointerdown', 50, 50);
|
||||
simulateEvent('pointerup', 50, 50);
|
||||
expect(draw.traceState_.active).to.be(true);
|
||||
expect(draw.traceState_.targetIndex).to.be(-1);
|
||||
draw.shouldHandle_ = false;
|
||||
|
||||
// move to pick a target
|
||||
simulateEvent('pointermove', 75, 10);
|
||||
expect(draw.traceState_.active).to.be(true);
|
||||
expect(draw.traceState_.targetIndex).to.be(0);
|
||||
draw.shouldHandle_ = false;
|
||||
|
||||
// third click ends tracing (right half of top edge)
|
||||
simulateEvent('pointermove', 75, 100);
|
||||
simulateEvent('pointerdown', 75, 100);
|
||||
simulateEvent('pointerup', 75, 100);
|
||||
expect(draw.traceState_.active).to.be(false);
|
||||
draw.shouldHandle_ = false;
|
||||
|
||||
// finish on first point
|
||||
simulateEvent('pointermove', 50, 0);
|
||||
simulateEvent('pointerdown', 50, 0);
|
||||
simulateEvent('pointerup', 50, 0);
|
||||
|
||||
const features = source.getFeatures();
|
||||
expect(features).to.have.length(2);
|
||||
const geometry = features[1].getGeometry();
|
||||
expect(geometry).to.be.a(Polygon);
|
||||
|
||||
expect(geometry.getCoordinates()).to.eql([
|
||||
[
|
||||
[50, 0],
|
||||
[50, -50],
|
||||
[100, -50], // traced point
|
||||
[100, -100], // traced point
|
||||
[75, -100],
|
||||
[50, 0],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('drawing multi-polygons', function () {
|
||||
let draw;
|
||||
|
||||
Reference in New Issue
Block a user