Merge pull request #10043 from tschaub/snap-geographic

User coordinates during snapping
This commit is contained in:
Tim Schaub
2019-09-27 17:20:16 +02:00
committed by GitHub
5 changed files with 200 additions and 97 deletions

View File

@@ -1,6 +1,6 @@
import {Map, View} from '../src/ol/index.js';
import GeoJSON from '../src/ol/format/GeoJSON.js';
import {Modify, Select, Draw} from '../src/ol/interaction.js';
import {Modify, Select, Draw, Snap} from '../src/ol/interaction.js';
import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
import {useGeographic} from '../src/ol/proj.js';
@@ -39,19 +39,30 @@ const draw = new Draw({
source: source
});
const snap = new Snap({
source: source
});
function removeInteractions() {
map.removeInteraction(modify);
map.removeInteraction(select);
map.removeInteraction(draw);
map.removeInteraction(select);
}
const mode = document.getElementById('mode');
function onChange() {
removeInteractions();
switch (mode.value) {
case 'draw': {
map.removeInteraction(modify);
map.removeInteraction(select);
map.addInteraction(draw);
map.addInteraction(snap);
break;
}
case 'modify': {
map.removeInteraction(draw);
map.addInteraction(select);
map.addInteraction(modify);
map.addInteraction(snap);
break;
}
default: {

View File

@@ -920,12 +920,12 @@ class Modify extends PointerInteraction {
*/
handlePointerAtPixel_(pixel, map) {
const pixelCoordinate = map.getCoordinateFromPixel(pixel);
const projection = map.getView().getProjection();
const sortByDistance = function(a, b) {
return pointDistanceToSegmentDataSquared(pixelCoordinate, a) -
pointDistanceToSegmentDataSquared(pixelCoordinate, b);
return projectedDistanceToSegmentDataSquared(pixelCoordinate, a, projection) -
projectedDistanceToSegmentDataSquared(pixelCoordinate, b, projection);
};
const projection = map.getView().getProjection();
const viewExtent = fromUserExtent(createExtent(pixelCoordinate, tempExtent), projection);
const buffer = map.getView().getResolution() * this.pixelTolerance_;
const box = toUserExtent(bufferExtent(viewExtent, buffer, tempExtent), projection);
@@ -1235,9 +1235,10 @@ function compareIndexes(a, b) {
* which to calculate the distance.
* @param {SegmentData} segmentData The object describing the line
* segment we are calculating the distance to.
* @param {import("../proj/Projection.js").default} projection The view projection.
* @return {number} The square of the distance between a point and a line segment.
*/
function pointDistanceToSegmentDataSquared(pointCoordinates, segmentData) {
function projectedDistanceToSegmentDataSquared(pointCoordinates, segmentData, projection) {
const geometry = segmentData.geometry;
if (geometry.getType() === GeometryType.CIRCLE) {
@@ -1251,7 +1252,11 @@ function pointDistanceToSegmentDataSquared(pointCoordinates, segmentData) {
return distanceToCircumference * distanceToCircumference;
}
}
return squaredDistanceToSegment(pointCoordinates, segmentData.segment);
const coordinate = fromUserCoordinate(pointCoordinates, projection);
tempSegment[0] = fromUserCoordinate(segmentData.segment[0], projection);
tempSegment[1] = fromUserCoordinate(segmentData.segment[1], projection);
return squaredDistanceToSegment(coordinate, tempSegment);
}
/**

View File

@@ -14,6 +14,7 @@ import PointerInteraction from './Pointer.js';
import {getValues} from '../obj.js';
import VectorEventType from '../source/VectorEventType.js';
import RBush from '../structs/RBush.js';
import {fromUserCoordinate, toUserCoordinate} from '../proj.js';
/**
@@ -52,9 +53,10 @@ function getFeatureFromEvent(evt) {
} else if (/** @type {import("../Collection.js").CollectionEvent} */ (evt).element) {
return /** @type {import("../Feature.js").default} */ (/** @type {import("../Collection.js").CollectionEvent} */ (evt).element);
}
}
const tempSegment = [];
/**
* @classdesc
* Handles snapping of vector features while modifying or drawing them. The
@@ -70,10 +72,12 @@ function getFeatureFromEvent(evt) {
*
* import Snap from 'ol/interaction/Snap';
*
* var snap = new Snap({
* const snap = new Snap({
* source: source
* });
*
* map.addInteraction(snap);
*
* @api
*/
class Snap extends PointerInteraction {
@@ -149,13 +153,6 @@ class Snap extends PointerInteraction {
*/
this.pendingFeatures_ = {};
/**
* Used for distance sorting in sortByDistance_
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.pixelCoordinate_ = null;
/**
* @type {number}
* @private
@@ -163,13 +160,6 @@ class Snap extends PointerInteraction {
this.pixelTolerance_ = options.pixelTolerance !== undefined ?
options.pixelTolerance : 10;
/**
* @type {function(SegmentData, SegmentData): number}
* @private
*/
this.sortByDistance_ = sortByDistance.bind(this);
/**
* Segment RTree for each layer
* @type {import("../structs/RBush.js").default<SegmentData>}
@@ -177,22 +167,21 @@ class Snap extends PointerInteraction {
*/
this.rBush_ = new RBush();
/**
* @const
* @private
* @type {Object<string, function(import("../Feature.js").default, import("../geom/Geometry.js").default): void>}
*/
this.SEGMENT_WRITERS_ = {
'Point': this.writePointGeometry_,
'LineString': this.writeLineStringGeometry_,
'LinearRing': this.writeLineStringGeometry_,
'Polygon': this.writePolygonGeometry_,
'MultiPoint': this.writeMultiPointGeometry_,
'MultiLineString': this.writeMultiLineStringGeometry_,
'MultiPolygon': this.writeMultiPolygonGeometry_,
'GeometryCollection': this.writeGeometryCollectionGeometry_,
'Circle': this.writeCircleGeometry_
'Point': this.writePointGeometry_.bind(this),
'LineString': this.writeLineStringGeometry_.bind(this),
'LinearRing': this.writeLineStringGeometry_.bind(this),
'Polygon': this.writePolygonGeometry_.bind(this),
'MultiPoint': this.writeMultiPointGeometry_.bind(this),
'MultiLineString': this.writeMultiLineStringGeometry_.bind(this),
'MultiPolygon': this.writeMultiPolygonGeometry_.bind(this),
'GeometryCollection': this.writeGeometryCollectionGeometry_.bind(this),
'Circle': this.writeCircleGeometry_.bind(this)
};
}
@@ -211,7 +200,7 @@ class Snap extends PointerInteraction {
const segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()];
if (segmentWriter) {
this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(createEmpty());
segmentWriter.call(this, feature, geometry);
segmentWriter(feature, geometry);
}
}
@@ -383,10 +372,9 @@ class Snap extends PointerInteraction {
* @return {Result} Snap result
*/
snapTo(pixel, pixelCoordinate, map) {
const lowerLeft = map.getCoordinateFromPixelInternal(
const lowerLeft = map.getCoordinateFromPixel(
[pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]);
const upperRight = map.getCoordinateFromPixelInternal(
const upperRight = map.getCoordinateFromPixel(
[pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]);
const box = boundingExtent([lowerLeft, upperRight]);
@@ -400,57 +388,78 @@ class Snap extends PointerInteraction {
});
}
let snappedToVertex = false;
let snapped = false;
let vertex = null;
let vertexPixel = null;
let dist, pixel1, pixel2, squaredDist1, squaredDist2;
if (segments.length > 0) {
this.pixelCoordinate_ = pixelCoordinate;
segments.sort(this.sortByDistance_);
const closestSegment = segments[0].segment;
const isCircle = segments[0].feature.getGeometry().getType() ===
GeometryType.CIRCLE;
if (this.vertex_ && !this.edge_) {
pixel1 = map.getPixelFromCoordinateInternal(closestSegment[0]);
pixel2 = map.getPixelFromCoordinateInternal(closestSegment[1]);
squaredDist1 = squaredCoordinateDistance(pixel, pixel1);
squaredDist2 = squaredCoordinateDistance(pixel, pixel2);
dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
snappedToVertex = dist <= this.pixelTolerance_;
if (snappedToVertex) {
snapped = true;
vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0];
vertexPixel = map.getPixelFromCoordinateInternal(vertex);
}
} else if (this.edge_) {
if (isCircle) {
vertex = closestOnCircle(pixelCoordinate,
/** @type {import("../geom/Circle.js").default} */ (segments[0].feature.getGeometry()));
} else {
vertex = closestOnSegment(pixelCoordinate, closestSegment);
}
vertexPixel = map.getPixelFromCoordinateInternal(vertex);
if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) {
snapped = true;
if (this.vertex_ && !isCircle) {
pixel1 = map.getPixelFromCoordinateInternal(closestSegment[0]);
pixel2 = map.getPixelFromCoordinateInternal(closestSegment[1]);
squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1);
squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2);
dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
snappedToVertex = dist <= this.pixelTolerance_;
if (snappedToVertex) {
vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0];
vertexPixel = map.getPixelFromCoordinateInternal(vertex);
}
if (segments.length === 0) {
return {
snapped: snapped,
vertex: vertex,
vertexPixel: vertexPixel
};
}
const projection = map.getView().getProjection();
const projectedCoordinate = fromUserCoordinate(pixelCoordinate, projection);
let closestSegmentData;
let minSquaredDistance = Infinity;
for (let i = 0; i < segments.length; ++i) {
const segmentData = segments[i];
tempSegment[0] = fromUserCoordinate(segmentData.segment[0], projection);
tempSegment[1] = fromUserCoordinate(segmentData.segment[1], projection);
const delta = squaredDistanceToSegment(projectedCoordinate, tempSegment);
if (delta < minSquaredDistance) {
closestSegmentData = segmentData;
minSquaredDistance = delta;
}
}
const closestSegment = closestSegmentData.segment;
if (this.vertex_ && !this.edge_) {
const pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
const pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
const squaredDist1 = squaredCoordinateDistance(pixel, pixel1);
const squaredDist2 = squaredCoordinateDistance(pixel, pixel2);
const dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
if (dist <= this.pixelTolerance_) {
snapped = true;
vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0];
vertexPixel = map.getPixelFromCoordinate(vertex);
}
} else if (this.edge_) {
const isCircle = closestSegmentData.feature.getGeometry().getType() === GeometryType.CIRCLE;
if (isCircle) {
vertex = closestOnCircle(pixelCoordinate,
/** @type {import("../geom/Circle.js").default} */ (closestSegmentData.feature.getGeometry()));
} 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);
}
}
}
if (snapped) {
vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])];
}
}
if (snapped) {
vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])];
}
return {
snapped: snapped,
vertex: vertex,
@@ -495,7 +504,7 @@ class Snap extends PointerInteraction {
for (let i = 0; i < geometries.length; ++i) {
const segmentWriter = this.SEGMENT_WRITERS_[geometries[i].getType()];
if (segmentWriter) {
segmentWriter.call(this, feature, geometries[i]);
segmentWriter(feature, geometries[i]);
}
}
}
@@ -613,17 +622,4 @@ class Snap extends PointerInteraction {
}
/**
* Sort segments by distance, helper function
* @param {SegmentData} a The first segment data.
* @param {SegmentData} b The second segment data.
* @return {number} The difference in distance.
* @this {Snap}
*/
function sortByDistance(a, b) {
const deltaA = squaredDistanceToSegment(this.pixelCoordinate_, a.segment);
const deltaB = squaredDistanceToSegment(this.pixelCoordinate_, b.segment);
return deltaA - deltaB;
}
export default Snap;

View File

@@ -6,6 +6,8 @@ import Circle from '../../../../src/ol/geom/Circle.js';
import Point from '../../../../src/ol/geom/Point.js';
import LineString from '../../../../src/ol/geom/LineString.js';
import Snap from '../../../../src/ol/interaction/Snap.js';
import {useGeographic, clearUserProjection} from '../../../../src/ol/proj.js';
import {overrideRAF} from '../../util.js';
describe('ol.interaction.Snap', function() {
@@ -190,6 +192,78 @@ describe('ol.interaction.Snap', function() {
expect(event.coordinate).to.eql([10, 0]);
});
});
describe('handleEvent - useGeographic', () => {
let target, map;
const size = 256;
let restoreRAF;
beforeEach(done => {
restoreRAF = overrideRAF();
useGeographic();
target = document.createElement('div');
Object.assign(target.style, {
position: 'absolute',
top: 0,
left: 0,
width: `${size}px`,
height: `${size}px`
});
document.body.appendChild(target);
map = new Map({
target: target,
view: new View({
center: [0, 0],
zoom: 0
})
});
map.once('postrender', () => {
done();
});
});
afterEach(() => {
map.dispose();
document.body.removeChild(target);
clearUserProjection();
restoreRAF();
});
it('snaps to user coordinates', () => {
const lon = -90;
const lat = 45;
const point = new Feature(new Point([lon, lat]));
const snap = new Snap({
features: new Collection([point])
});
snap.setMap(map);
const expectedPixel = map.getPixelFromCoordinate([lon, lat]).map(value => Math.round(value));
const delta = 5;
const pixel = expectedPixel.slice();
pixel[0] += delta;
pixel[1] += delta;
const coordinate = map.getCoordinateFromPixel(pixel);
const event = {
pixel: pixel,
coordinate: coordinate,
map: map
};
snap.handleEvent(event);
expect(event.coordinate).to.eql([lon, lat]);
expect(event.pixel).to.eql(expectedPixel);
});
});

17
test/spec/util.js Normal file
View File

@@ -0,0 +1,17 @@
export function overrideRAF() {
const raf = window.requestAnimationFrame;
const caf = window.cancelAnimationFrame;
window.requestAnimationFrame = function(callback) {
return setTimeout(callback, 1);
};
window.cancelAnimationFrame = function(key) {
return clearTimeout(key);
};
return function() {
window.requestAnimationFrame = raf;
window.cancelAnimationFrame = caf;
};
}