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
|
layout: example.html
|
||||||
title: Tracing around a polygon
|
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: >
|
docs: >
|
||||||
This example showcases how the draw interaction API can be set up to make snapping along
|
The draw interaction has a <code>trace</code> option to enable tracing
|
||||||
an existing geometry easier while preserving topology, which is sometimes called "tracing".
|
around existing features. This example uses the <code>traceSource</code> option
|
||||||
When the user clicks on two different points on the Idaho state border,
|
to trace features from one source and add them to another source. The first click
|
||||||
the part of the border comprised between these two points is added to
|
on an edge of the Idaho feature will start tracing. The second click on the edge
|
||||||
the currently drawn feature.
|
will stop tracing.
|
||||||
This leverages the `appendCoordinates` method of the `ol/interaction/Draw` interaction.
|
tags: "draw, trace, vector, snap, topology"
|
||||||
tags: "draw, trace, snap, vector, topology"
|
|
||||||
---
|
---
|
||||||
<div id="map" class="map"></div>
|
<div id="map" class="map"></div>
|
||||||
<form>
|
<form>
|
||||||
|
|||||||
@@ -11,101 +11,6 @@ import View from '../src/ol/View.js';
|
|||||||
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
|
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
|
||||||
import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.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({
|
const raster = new TileLayer({
|
||||||
source: new OSM(),
|
source: new OSM(),
|
||||||
});
|
});
|
||||||
@@ -157,86 +62,7 @@ const map = new Map({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
let drawInteraction, tracingFeature, startPoint, endPoint;
|
let drawInteraction;
|
||||||
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({
|
const snapInteraction = new Snap({
|
||||||
source: baseVector.getSource(),
|
source: baseVector.getSource(),
|
||||||
@@ -248,16 +74,10 @@ function addInteraction() {
|
|||||||
const value = typeSelect.value;
|
const value = typeSelect.value;
|
||||||
if (value !== 'None') {
|
if (value !== 'None') {
|
||||||
drawInteraction = new Draw({
|
drawInteraction = new Draw({
|
||||||
source: drawVector.getSource(),
|
|
||||||
type: value,
|
type: value,
|
||||||
});
|
source: drawVector.getSource(),
|
||||||
drawInteraction.on('drawstart', () => {
|
trace: true,
|
||||||
drawing = true;
|
traceSource: baseVector.getSource(),
|
||||||
});
|
|
||||||
drawInteraction.on('drawend', () => {
|
|
||||||
drawing = false;
|
|
||||||
previewLine.getGeometry().setCoordinates([]);
|
|
||||||
tracingFeature = null;
|
|
||||||
});
|
});
|
||||||
map.addInteraction(drawInteraction);
|
map.addInteraction(drawInteraction);
|
||||||
map.addInteraction(snapInteraction);
|
map.addInteraction(snapInteraction);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Circle from '../geom/Circle.js';
|
|||||||
import Event from '../events/Event.js';
|
import Event from '../events/Event.js';
|
||||||
import EventType from '../events/EventType.js';
|
import EventType from '../events/EventType.js';
|
||||||
import Feature from '../Feature.js';
|
import Feature from '../Feature.js';
|
||||||
|
import GeometryCollection from '../geom/GeometryCollection.js';
|
||||||
import InteractionProperty from './Property.js';
|
import InteractionProperty from './Property.js';
|
||||||
import LineString from '../geom/LineString.js';
|
import LineString from '../geom/LineString.js';
|
||||||
import MapBrowserEvent from '../MapBrowserEvent.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 VectorLayer from '../layer/Vector.js';
|
||||||
import VectorSource from '../source/Vector.js';
|
import VectorSource from '../source/Vector.js';
|
||||||
import {FALSE, TRUE} from '../functions.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 {
|
import {
|
||||||
boundingExtent,
|
boundingExtent,
|
||||||
getBottomLeft,
|
getBottomLeft,
|
||||||
@@ -26,10 +32,14 @@ import {
|
|||||||
getTopLeft,
|
getTopLeft,
|
||||||
getTopRight,
|
getTopRight,
|
||||||
} from '../extent.js';
|
} from '../extent.js';
|
||||||
|
import {clamp, squaredDistance, toFixed} from '../math.js';
|
||||||
import {createEditingStyle} from '../style/Style.js';
|
import {createEditingStyle} from '../style/Style.js';
|
||||||
|
import {
|
||||||
|
distance,
|
||||||
|
squaredDistance as squaredCoordinateDistance,
|
||||||
|
} from '../coordinate.js';
|
||||||
import {fromUserCoordinate, getUserProjection} from '../proj.js';
|
import {fromUserCoordinate, getUserProjection} from '../proj.js';
|
||||||
import {getStrideForLayout} from '../geom/SimpleGeometry.js';
|
import {getStrideForLayout} from '../geom/SimpleGeometry.js';
|
||||||
import {squaredDistance as squaredCoordinateDistance} from '../coordinate.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Options
|
* @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
|
* returns a boolean to indicate whether that event should be handled. The
|
||||||
* default is {@link module:ol/events/condition.shiftKeyOnly}, meaning that the
|
* default is {@link module:ol/events/condition.shiftKeyOnly}, meaning that the
|
||||||
* Shift key activates freehand drawing.
|
* 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
|
* @property {boolean} [wrapX=false] Wrap the world horizontally on the sketch
|
||||||
* overlay.
|
* overlay.
|
||||||
* @property {import("../geom/Geometry.js").GeometryLayout} [geometryLayout='XY'] Layout of the
|
* @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 {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
|
* Function that takes an array of coordinates and an optional existing geometry
|
||||||
* and a projection as arguments, and returns a geometry. The optional existing
|
* 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
|
* @template Return
|
||||||
* @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
|
* @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> &
|
||||||
@@ -514,9 +900,46 @@ class Draw extends PointerInteraction {
|
|||||||
: shiftKeyOnly;
|
: 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_);
|
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.
|
* 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
|
* Subclasses may set up event handlers to get notified about changes to
|
||||||
@@ -617,28 +1040,241 @@ class Draw extends PointerInteraction {
|
|||||||
this.startDrawing_(event.coordinate);
|
this.startDrawing_(event.coordinate);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if (this.condition_(event)) {
|
}
|
||||||
this.lastDragTime_ = Date.now();
|
|
||||||
this.downTimeout_ = setTimeout(
|
if (!this.condition_(event)) {
|
||||||
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 {
|
|
||||||
this.lastDragTime_ = undefined;
|
this.lastDragTime_ = undefined;
|
||||||
return false;
|
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);
|
this.handlePointerMove_(event);
|
||||||
|
const tracing = this.traceState_.active;
|
||||||
|
this.toggleTraceState_(event);
|
||||||
|
|
||||||
if (this.shouldHandle_) {
|
if (this.shouldHandle_) {
|
||||||
const startingToDraw = !this.finishCoordinate_;
|
const startingToDraw = !this.finishCoordinate_;
|
||||||
@@ -668,7 +1306,7 @@ class Draw extends PointerInteraction {
|
|||||||
!this.freehand_ &&
|
!this.freehand_ &&
|
||||||
(!startingToDraw || this.mode_ === 'Point')
|
(!startingToDraw || this.mode_ === 'Point')
|
||||||
) {
|
) {
|
||||||
if (this.atFinish_(event.pixel)) {
|
if (this.atFinish_(event.pixel, tracing)) {
|
||||||
if (this.finishCondition_(event)) {
|
if (this.finishCondition_(event)) {
|
||||||
this.finishDrawing();
|
this.finishDrawing();
|
||||||
}
|
}
|
||||||
@@ -713,20 +1351,23 @@ class Draw extends PointerInteraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.finishCoordinate_) {
|
if (!this.finishCoordinate_) {
|
||||||
this.modifyDrawing_(event.coordinate);
|
|
||||||
} else {
|
|
||||||
this.createOrUpdateSketchPoint_(event.coordinate.slice());
|
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.
|
* Determine if an event is within the snapping tolerance of the start coord.
|
||||||
* @param {import("../pixel.js").Pixel} pixel Pixel.
|
* @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.
|
* @return {boolean} The event is within the snapping tolerance of the start.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
atFinish_(pixel) {
|
atFinish_(pixel, tracing) {
|
||||||
let at = false;
|
let at = false;
|
||||||
if (this.sketchFeature_) {
|
if (this.sketchFeature_) {
|
||||||
let potentiallyDone = false;
|
let potentiallyDone = false;
|
||||||
@@ -737,7 +1378,8 @@ class Draw extends PointerInteraction {
|
|||||||
} else if (mode === 'Circle') {
|
} else if (mode === 'Circle') {
|
||||||
at = this.sketchCoords_.length === 2;
|
at = this.sketchCoords_.length === 2;
|
||||||
} else if (mode === 'LineString') {
|
} else if (mode === 'LineString') {
|
||||||
potentiallyDone = this.sketchCoords_.length > this.minPoints_;
|
potentiallyDone =
|
||||||
|
!tracing && this.sketchCoords_.length > this.minPoints_;
|
||||||
} else if (mode === 'Polygon') {
|
} else if (mode === 'Polygon') {
|
||||||
const sketchCoords = /** @type {PolyCoordType} */ (this.sketchCoords_);
|
const sketchCoords = /** @type {PolyCoordType} */ (this.sketchCoords_);
|
||||||
potentiallyDone = sketchCoords[0].length > this.minPoints_;
|
potentiallyDone = sketchCoords[0].length > this.minPoints_;
|
||||||
@@ -745,6 +1387,14 @@ class Draw extends PointerInteraction {
|
|||||||
sketchCoords[0][0],
|
sketchCoords[0][0],
|
||||||
sketchCoords[0][sketchCoords[0].length - 2],
|
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) {
|
if (potentiallyDone) {
|
||||||
const map = this.getMap();
|
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
|
* @param {number} n The number of points to remove.
|
||||||
* drawing POINT or MULTI_POINT geometries.
|
|
||||||
* @api
|
|
||||||
*/
|
*/
|
||||||
removeLastPoint() {
|
removeLastPoints_(n) {
|
||||||
if (!this.sketchFeature_) {
|
if (!this.sketchFeature_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const geometry = this.sketchFeature_.getGeometry();
|
const geometry = this.sketchFeature_.getGeometry();
|
||||||
const projection = this.getMap().getView().getProjection();
|
const projection = this.getMap().getView().getProjection();
|
||||||
let coordinates;
|
|
||||||
const mode = this.mode_;
|
const mode = this.mode_;
|
||||||
if (mode === 'LineString' || mode === 'Circle') {
|
for (let i = 0; i < n; ++i) {
|
||||||
coordinates = /** @type {LineCoordType} */ (this.sketchCoords_);
|
let coordinates;
|
||||||
coordinates.splice(-2, 1);
|
if (mode === 'LineString' || mode === 'Circle') {
|
||||||
if (coordinates.length >= 2) {
|
coordinates = /** @type {LineCoordType} */ (this.sketchCoords_);
|
||||||
this.finishCoordinate_ = coordinates[coordinates.length - 2].slice();
|
coordinates.splice(-2, 1);
|
||||||
const finishCoordinate = this.finishCoordinate_.slice();
|
if (coordinates.length >= 2) {
|
||||||
coordinates[coordinates.length - 1] = finishCoordinate;
|
this.finishCoordinate_ = coordinates[coordinates.length - 2].slice();
|
||||||
this.createOrUpdateSketchPoint_(finishCoordinate);
|
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) {
|
if (coordinates.length === 1) {
|
||||||
this.abortDrawing();
|
this.abortDrawing();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSketchFeatures_();
|
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.
|
* Stop drawing and add the sketch feature to the target layer.
|
||||||
* The {@link module:ol/interaction/Draw~DrawEventType.DRAWEND} event is
|
* The {@link module:ol/interaction/Draw~DrawEventType.DRAWEND} event is
|
||||||
@@ -1045,6 +1707,7 @@ class Draw extends PointerInteraction {
|
|||||||
this.sketchPoint_ = null;
|
this.sketchPoint_ = null;
|
||||||
this.sketchLine_ = null;
|
this.sketchLine_ = null;
|
||||||
this.overlay_.getSource().clear(true);
|
this.overlay_.getSource().clear(true);
|
||||||
|
this.deactivateTrace_();
|
||||||
return sketchFeature;
|
return sketchFeature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,21 @@ import {listen} from '../../../../../src/ol/events.js';
|
|||||||
import {register} from '../../../../../src/ol/proj/proj4.js';
|
import {register} from '../../../../../src/ol/proj/proj4.js';
|
||||||
import {unByKey} from '../../../../../src/ol/Observable.js';
|
import {unByKey} from '../../../../../src/ol/Observable.js';
|
||||||
|
|
||||||
describe('ol.interaction.Draw', function () {
|
describe('ol/interaction/Draw', function () {
|
||||||
let target, map, source;
|
/**
|
||||||
|
* @type {VectorSource}
|
||||||
|
*/
|
||||||
|
let source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Map}
|
||||||
|
*/
|
||||||
|
let map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
let target;
|
||||||
|
|
||||||
const width = 360;
|
const width = 360;
|
||||||
const height = 180;
|
const height = 180;
|
||||||
@@ -135,6 +148,78 @@ describe('ol.interaction.Draw', function () {
|
|||||||
expect(draw.freehandCondition_(event)).to.be(true);
|
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 () {
|
it('accepts a dragVertexDelay option', function () {
|
||||||
const draw = new Draw({
|
const draw = new Draw({
|
||||||
source: source,
|
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 () {
|
describe('drawing multi-polygons', function () {
|
||||||
let draw;
|
let draw;
|
||||||
|
|
||||||
Reference in New Issue
Block a user