diff --git a/examples/snap.html b/examples/snap.html
index 087335a3b5..c47b279255 100644
--- a/examples/snap.html
+++ b/examples/snap.html
@@ -29,6 +29,7 @@ tags: "draw, edit, modify, vector, snap"
+
diff --git a/examples/snap.js b/examples/snap.js
index a439333c4e..cd1764266d 100644
--- a/examples/snap.js
+++ b/examples/snap.js
@@ -81,6 +81,8 @@ var Draw = {
this.LineString.setActive(false);
map.addInteraction(this.Polygon);
this.Polygon.setActive(false);
+ map.addInteraction(this.Circle);
+ this.Circle.setActive(false);
},
Point: new ol.interaction.Draw({
source: vector.getSource(),
@@ -94,6 +96,10 @@ var Draw = {
source: vector.getSource(),
type: /** @type {ol.geom.GeometryType} */ ('Polygon')
}),
+ Circle: new ol.interaction.Draw({
+ source: vector.getSource(),
+ type: /** @type {ol.geom.GeometryType} */ ('Circle')
+ }),
getActive: function() {
return this.activeType ? this[this.activeType].getActive() : false;
},
diff --git a/src/ol/coordinate.js b/src/ol/coordinate.js
index eea13ca6c9..e1c7a7bc2c 100644
--- a/src/ol/coordinate.js
+++ b/src/ol/coordinate.js
@@ -26,6 +26,37 @@ ol.coordinate.add = function(coordinate, delta) {
};
+/**
+ * Calculates the point closest to the passed coordinate on the passed circle.
+ *
+ * @param {ol.Coordinate} coordinate The coordinate.
+ * @param {ol.geom.Circle} circle The circle.
+ * @return {ol.Coordinate} Closest point on the circumference
+ */
+ol.coordinate.closestOnCircle = function(coordinate, circle) {
+ var r = circle.getRadius();
+ var center = circle.getCenter();
+ var x0 = center[0];
+ var y0 = center[1];
+ var x1 = coordinate[0];
+ var y1 = coordinate[1];
+
+ var dx = x1 - x0;
+ var dy = y1 - y0;
+ if (dx === 0 && dy === 0) {
+ dx = 1;
+ }
+ var d = Math.sqrt(dx * dx + dy * dy);
+
+ var x, y;
+
+ x = x0 + r * dx / d;
+ y = y0 + r * dy / d;
+
+ return [x, y];
+};
+
+
/**
* Calculates the point closest to the passed coordinate on the passed segment.
* This is the foot of the perpendicular of the coordinate to the segment when
diff --git a/src/ol/interaction/snap.js b/src/ol/interaction/snap.js
index a623d48f77..50344ee36d 100644
--- a/src/ol/interaction/snap.js
+++ b/src/ol/interaction/snap.js
@@ -8,6 +8,8 @@ goog.require('ol.events');
goog.require('ol.events.EventType');
goog.require('ol.extent');
goog.require('ol.functions');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.Polygon');
goog.require('ol.interaction.Pointer');
goog.require('ol.obj');
goog.require('ol.source.Vector');
@@ -142,7 +144,8 @@ ol.interaction.Snap = function(opt_options) {
'MultiPoint': this.writeMultiPointGeometry_,
'MultiLineString': this.writeMultiLineStringGeometry_,
'MultiPolygon': this.writeMultiPolygonGeometry_,
- 'GeometryCollection': this.writeGeometryCollectionGeometry_
+ 'GeometryCollection': this.writeGeometryCollectionGeometry_,
+ 'Circle': this.writeCircleGeometry_
};
};
ol.inherits(ol.interaction.Snap, ol.interaction.Pointer);
@@ -345,6 +348,15 @@ ol.interaction.Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) {
var box = ol.extent.boundingExtent([lowerLeft, upperRight]);
var segments = this.rBush_.getInExtent(box);
+
+ // If snapping on vertices only, don't consider circles
+ if (this.vertex_ && !this.edge_) {
+ segments = segments.filter(function(segment) {
+ return segment.feature.getGeometry().getType() !==
+ ol.geom.GeometryType.CIRCLE;
+ });
+ }
+
var snappedToVertex = false;
var snapped = false;
var vertex = null;
@@ -354,6 +366,8 @@ ol.interaction.Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) {
this.pixelCoordinate_ = pixelCoordinate;
segments.sort(this.sortByDistance_);
var closestSegment = segments[0].segment;
+ var isCircle = segments[0].feature.getGeometry().getType() ===
+ ol.geom.GeometryType.CIRCLE;
if (this.vertex_ && !this.edge_) {
pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
@@ -368,12 +382,17 @@ ol.interaction.Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) {
vertexPixel = map.getPixelFromCoordinate(vertex);
}
} else if (this.edge_) {
- vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
- closestSegment));
+ if (isCircle) {
+ vertex = ol.coordinate.closestOnCircle(pixelCoordinate,
+ /** @type {ol.geom.Circle} */ (segments[0].feature.getGeometry()));
+ } else {
+ vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
+ closestSegment));
+ }
vertexPixel = map.getPixelFromCoordinate(vertex);
if (ol.coordinate.distance(pixel, vertexPixel) <= this.pixelTolerance_) {
snapped = true;
- if (this.vertex_) {
+ if (this.vertex_ && !isCircle) {
pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1);
@@ -410,6 +429,26 @@ ol.interaction.Snap.prototype.updateFeature_ = function(feature) {
};
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.Circle} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writeCircleGeometry_ = function(feature, geometry) {
+ var polygon = ol.geom.Polygon.fromCircle(geometry);
+ var coordinates = polygon.getCoordinates()[0];
+ var i, ii, segment, segmentData;
+ for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+ segment = coordinates.slice(i, i + 2);
+ segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+ feature: feature,
+ segment: segment
+ });
+ this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+ }
+};
+
+
/**
* @param {ol.Feature} feature Feature
* @param {ol.geom.GeometryCollection} geometry Geometry.
diff --git a/test/spec/ol/coordinate.test.js b/test/spec/ol/coordinate.test.js
index 526ab837c4..7cf496fb6e 100644
--- a/test/spec/ol/coordinate.test.js
+++ b/test/spec/ol/coordinate.test.js
@@ -1,6 +1,7 @@
goog.provide('ol.test.coordinate');
goog.require('ol.coordinate');
+goog.require('ol.geom.Circle');
describe('ol.coordinate', function() {
@@ -89,6 +90,19 @@ describe('ol.coordinate', function() {
});
});
+ describe('#closestOnCircle', function() {
+ var center = [5, 10];
+ var circle = new ol.geom.Circle(center, 10);
+ it('can find the closest point on circle', function() {
+ expect(ol.coordinate.closestOnCircle([-20, 10], circle))
+ .to.eql([-5, 10]);
+ });
+ it('can handle coordinate equal circle center', function() {
+ expect(ol.coordinate.closestOnCircle(center, circle))
+ .to.eql([15, 10]);
+ });
+ });
+
describe('#closestOnSegment', function() {
it('can handle points where the foot of the perpendicular is closest',
function() {
diff --git a/test/spec/ol/interaction/snap.test.js b/test/spec/ol/interaction/snap.test.js
index 891687585f..e28fa1f67f 100644
--- a/test/spec/ol/interaction/snap.test.js
+++ b/test/spec/ol/interaction/snap.test.js
@@ -4,6 +4,7 @@ goog.require('ol.Collection');
goog.require('ol.Feature');
goog.require('ol.Map');
goog.require('ol.View');
+goog.require('ol.geom.Circle');
goog.require('ol.geom.Point');
goog.require('ol.geom.LineString');
goog.require('ol.interaction.Snap');
@@ -109,6 +110,27 @@ describe('ol.interaction.Snap', function() {
expect(event.coordinate).to.eql([10, 0]);
});
+ it('snaps to circle', function() {
+ var circle = new ol.Feature(new ol.geom.Circle([0, 0], 10));
+ var snapInteraction = new ol.interaction.Snap({
+ features: new ol.Collection([circle]),
+ pixelTolerance: 5
+ });
+ snapInteraction.setMap(map);
+
+ var event = {
+ pixel: [5 + width / 2, height / 2 - 5],
+ coordinate: [5, 5],
+ map: map
+ };
+ ol.interaction.Snap.handleEvent_.call(snapInteraction, event);
+
+ expect(event.coordinate).to.eql([
+ Math.sin(Math.PI / 4) * 10,
+ Math.sin(Math.PI / 4) * 10
+ ]);
+ });
+
it('handle feature without geometry', function() {
var feature = new ol.Feature();
var snapInteraction = new ol.interaction.Snap({