Merge pull request #13446 from T-MAPY/interaction-snap-bug-13440

Snap Interaction can snap to Point on line segment
This commit is contained in:
MoonE
2022-03-07 19:30:13 +01:00
committed by GitHub
2 changed files with 113 additions and 104 deletions
+92 -104
View File
@@ -12,9 +12,7 @@ import {boundingExtent, createEmpty} from '../extent.js';
import { import {
closestOnCircle, closestOnCircle,
closestOnSegment, closestOnSegment,
distance as coordinateDistance, squaredDistance,
squaredDistance as squaredCoordinateDistance,
squaredDistanceToSegment,
} from '../coordinate.js'; } from '../coordinate.js';
import {fromCircle} from '../geom/Polygon.js'; import {fromCircle} from '../geom/Polygon.js';
import { import {
@@ -28,7 +26,6 @@ import {listen, unlistenByKey} from '../events.js';
/** /**
* @typedef {Object} Result * @typedef {Object} Result
* @property {boolean} snapped Snapped.
* @property {import("../coordinate.js").Coordinate|null} vertex Vertex. * @property {import("../coordinate.js").Coordinate|null} vertex Vertex.
* @property {import("../pixel.js").Pixel|null} vertexPixel VertexPixel. * @property {import("../pixel.js").Pixel|null} vertexPixel VertexPixel.
*/ */
@@ -266,7 +263,7 @@ class Snap extends PointerInteraction {
*/ */
handleEvent(evt) { handleEvent(evt) {
const result = this.snapTo(evt.pixel, evt.coordinate, evt.map); const result = this.snapTo(evt.pixel, evt.coordinate, evt.map);
if (result.snapped) { if (result) {
evt.coordinate = result.vertex.slice(0, 2); evt.coordinate = result.vertex.slice(0, 2);
evt.pixel = result.vertexPixel; evt.pixel = result.vertexPixel;
} }
@@ -411,7 +408,7 @@ class Snap extends PointerInteraction {
* @param {import("../pixel.js").Pixel} pixel Pixel * @param {import("../pixel.js").Pixel} pixel Pixel
* @param {import("../coordinate.js").Coordinate} pixelCoordinate Coordinate * @param {import("../coordinate.js").Coordinate} pixelCoordinate Coordinate
* @param {import("../PluggableMap.js").default} map Map. * @param {import("../PluggableMap.js").default} map Map.
* @return {Result} Snap result * @return {Result|null} Snap result
*/ */
snapTo(pixel, pixelCoordinate, map) { snapTo(pixel, pixelCoordinate, map) {
const lowerLeft = map.getCoordinateFromPixel([ const lowerLeft = map.getCoordinateFromPixel([
@@ -424,113 +421,107 @@ class Snap extends PointerInteraction {
]); ]);
const box = boundingExtent([lowerLeft, upperRight]); const box = boundingExtent([lowerLeft, upperRight]);
let segments = this.rBush_.getInExtent(box); const segments = this.rBush_.getInExtent(box);
// If snapping on vertices only, don't consider circles const segmentsLength = segments.length;
if (this.vertex_ && !this.edge_) { if (segmentsLength === 0) {
segments = segments.filter(function (segment) { return null;
return segment.feature.getGeometry().getType() !== GeometryType.CIRCLE;
});
}
let snapped = false;
let vertex = null;
let vertexPixel = null;
if (segments.length === 0) {
return {
snapped: snapped,
vertex: vertex,
vertexPixel: vertexPixel,
};
} }
const projection = map.getView().getProjection(); const projection = map.getView().getProjection();
const projectedCoordinate = fromUserCoordinate(pixelCoordinate, projection); const projectedCoordinate = fromUserCoordinate(pixelCoordinate, projection);
let closestSegmentData; let closestVertex;
let minSquaredDistance = Infinity; let minSquaredDistance = Infinity;
for (let i = 0; i < segments.length; ++i) {
const segmentData = segments[i]; const squaredPixelTolerance = this.pixelTolerance_ * this.pixelTolerance_;
tempSegment[0] = fromUserCoordinate(segmentData.segment[0], projection); const getResult = () => {
tempSegment[1] = fromUserCoordinate(segmentData.segment[1], projection); if (closestVertex) {
const delta = squaredDistanceToSegment(projectedCoordinate, tempSegment); const vertexPixel = map.getPixelFromCoordinate(closestVertex);
if (delta < minSquaredDistance) { const squaredPixelDistance = squaredDistance(pixel, vertexPixel);
closestSegmentData = segmentData; if (squaredPixelDistance <= squaredPixelTolerance) {
minSquaredDistance = delta; return {
vertex: closestVertex,
vertexPixel: [
Math.round(vertexPixel[0]),
Math.round(vertexPixel[1]),
],
};
}
}
return null;
};
if (this.vertex_) {
for (let i = 0; i < segmentsLength; ++i) {
const segmentData = segments[i];
if (
segmentData.feature.getGeometry().getType() !== GeometryType.CIRCLE
) {
segmentData.segment.forEach((vertex) => {
const tempVertexCoord = fromUserCoordinate(vertex, projection);
const delta = squaredDistance(projectedCoordinate, tempVertexCoord);
if (delta < minSquaredDistance) {
closestVertex = vertex;
minSquaredDistance = delta;
}
});
}
}
const result = getResult();
if (result) {
return result;
} }
} }
const closestSegment = closestSegmentData.segment;
if (this.vertex_ && !this.edge_) { if (this.edge_) {
const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); for (let i = 0; i < segmentsLength; ++i) {
const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); let vertex = null;
const squaredDist1 = squaredCoordinateDistance(pixel, pixel1); const segmentData = segments[i];
const squaredDist2 = squaredCoordinateDistance(pixel, pixel2); if (
const dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); segmentData.feature.getGeometry().getType() === GeometryType.CIRCLE
if (dist <= this.pixelTolerance_) { ) {
snapped = true; let circleGeometry = segmentData.feature.getGeometry();
vertex = const userProjection = getUserProjection();
squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; if (userProjection) {
vertexPixel = map.getPixelFromCoordinate(vertex); circleGeometry = circleGeometry
} .clone()
} else if (this.edge_) { .transform(userProjection, projection);
const isCircle = }
closestSegmentData.feature.getGeometry().getType() === vertex = toUserCoordinate(
GeometryType.CIRCLE; closestOnCircle(
if (isCircle) { projectedCoordinate,
let circleGeometry = closestSegmentData.feature.getGeometry(); /** @type {import("../geom/Circle.js").default} */ (
const userProjection = getUserProjection(); circleGeometry
if (userProjection) { )
circleGeometry = circleGeometry ),
.clone() projection
.transform(userProjection, projection); );
} else {
const [segmentStart, segmentEnd] = segmentData.segment;
// points have only one coordinate
if (segmentEnd) {
tempSegment[0] = fromUserCoordinate(segmentStart, projection);
tempSegment[1] = fromUserCoordinate(segmentEnd, projection);
vertex = closestOnSegment(projectedCoordinate, tempSegment);
}
} }
vertex = toUserCoordinate( if (vertex) {
closestOnCircle( const delta = squaredDistance(projectedCoordinate, vertex);
projectedCoordinate, if (delta < minSquaredDistance) {
/** @type {import("../geom/Circle.js").default} */ (circleGeometry) closestVertex = vertex;
), minSquaredDistance = delta;
projection
);
} else {
tempSegment[0] = fromUserCoordinate(closestSegment[0], projection);
tempSegment[1] = fromUserCoordinate(closestSegment[1], projection);
vertex = toUserCoordinate(
closestOnSegment(projectedCoordinate, tempSegment),
projection
);
}
vertexPixel = map.getPixelFromCoordinate(vertex);
if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) {
snapped = true;
if (this.vertex_ && !isCircle) {
const pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
const pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
const squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1);
const squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2);
const dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
if (dist <= this.pixelTolerance_) {
vertex =
squaredDist1 > squaredDist2
? closestSegment[1]
: closestSegment[0];
vertexPixel = map.getPixelFromCoordinate(vertex);
} }
} }
} }
const result = getResult();
if (result) {
return result;
}
} }
if (snapped) { return null;
vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])];
}
return {
snapped: snapped,
vertex: vertex,
vertexPixel: vertexPixel,
};
} }
/** /**
@@ -629,15 +620,13 @@ class Snap extends PointerInteraction {
* @private * @private
*/ */
writeMultiPointGeometry_(feature, geometry) { writeMultiPointGeometry_(feature, geometry) {
const points = geometry.getCoordinates(); geometry.getCoordinates().forEach((point) => {
for (let i = 0, ii = points.length; i < ii; ++i) {
const coordinates = points[i];
const segmentData = { const segmentData = {
feature: feature, feature: feature,
segment: [coordinates, coordinates], segment: [point],
}; };
this.rBush_.insert(geometry.getExtent(), segmentData); this.rBush_.insert(geometry.getExtent(), segmentData);
} });
} }
/** /**
@@ -669,10 +658,9 @@ class Snap extends PointerInteraction {
* @private * @private
*/ */
writePointGeometry_(feature, geometry) { writePointGeometry_(feature, geometry) {
const coordinates = geometry.getCoordinates();
const segmentData = { const segmentData = {
feature: feature, feature: feature,
segment: [coordinates, coordinates], segment: [geometry.getCoordinates()],
}; };
this.rBush_.insert(geometry.getExtent(), segmentData); this.rBush_.insert(geometry.getExtent(), segmentData);
} }
@@ -122,6 +122,27 @@ describe('ol.interaction.Snap', function () {
expect(event.coordinate).to.eql([10, 0]); expect(event.coordinate).to.eql([10, 0]);
}); });
it('snaps to vertex on line', function () {
const line = new Feature(
new LineString([
[0, 0],
[50, 0],
])
);
const point = new Feature(new Point([5, 0]));
const snap = new Snap({
features: new Collection([line, point]),
});
snap.setMap(map);
const event = {
pixel: [3 + width / 2, height / 2],
coordinate: [3, 0],
map: map,
};
snap.handleEvent(event);
expect(event.coordinate).to.eql([5, 0]);
});
it('snaps to circle', function () { it('snaps to circle', function () {
const circle = new Feature(new Circle([0, 0], 10)); const circle = new Feature(new Circle([0, 0], 10));
const snapInteraction = new Snap({ const snapInteraction = new Snap({