diff --git a/examples/draw-and-modify-features.html b/examples/draw-and-modify-features.html index c2da444e95..551a9d18a2 100644 --- a/examples/draw-and-modify-features.html +++ b/examples/draw-and-modify-features.html @@ -13,5 +13,6 @@ tags: "draw, edit, modify, vector, featureoverlay" + diff --git a/src/ol/coordinate.js b/src/ol/coordinate.js index 70af3d8716..e908dcc476 100644 --- a/src/ol/coordinate.js +++ b/src/ol/coordinate.js @@ -250,6 +250,16 @@ ol.coordinate.squaredDistance = function(coord1, coord2) { }; +/** + * @param {ol.Coordinate} coord1 First coordinate. + * @param {ol.Coordinate} coord2 Second coordinate. + * @return {number} Distance between coord1 and coord2. + */ +ol.coordinate.distance = function(coord1, coord2) { + return Math.sqrt(ol.coordinate.squaredDistance(coord1, coord2)); +}; + + /** * Calculate the squared distance from a coordinate to a line segment. * diff --git a/src/ol/interaction/modify.js b/src/ol/interaction/modify.js index 3dd9283619..1cb34e445f 100644 --- a/src/ol/interaction/modify.js +++ b/src/ol/interaction/modify.js @@ -164,6 +164,7 @@ ol.interaction.Modify = function(options) { 'MultiPoint': this.writeMultiPointGeometry_, 'MultiLineString': this.writeMultiLineStringGeometry_, 'MultiPolygon': this.writeMultiPolygonGeometry_, + 'Circle': this.writeCircleGeometry_, 'GeometryCollection': this.writeGeometryCollectionGeometry_ }; @@ -189,6 +190,19 @@ ol.interaction.Modify = function(options) { ol.inherits(ol.interaction.Modify, ol.interaction.Pointer); +/** + * @define {number} The segment index assigned to a circle's center when + * breaking up a cicrle into ModifySegmentDataType segments. + */ +ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX = 0; + +/** + * @define {number} The segment index assigned to a circle's circumference when + * breaking up a circle into ModifySegmentDataType segments. + */ +ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX = 1; + + /** * @param {ol.Feature} feature Feature. * @private @@ -449,6 +463,38 @@ ol.interaction.Modify.prototype.writeMultiPolygonGeometry_ = function(feature, g }; +/** + * We convert a circle into two segments. The segment at index + * {@link ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX} is the + * circle's center (a point). The segment at index + * {@link ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX} is + * the circumference, and is not a line segment. + * + * @param {ol.Feature} feature Feature. + * @param {ol.geom.Circle} geometry Geometry. + * @private + */ +ol.interaction.Modify.prototype.writeCircleGeometry_ = function(feature, geometry) { + var coordinates = geometry.getCenter(); + var centerSegmentData = /** @type {ol.ModifySegmentDataType} */ ({ + feature: feature, + geometry: geometry, + index: ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX, + segment: [coordinates, coordinates] + }); + var circumferenceSegmentData = /** @type {ol.ModifySegmentDataType} */ ({ + feature: feature, + geometry: geometry, + index: ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX, + segment: [coordinates, coordinates] + }); + var featureSegments = [centerSegmentData, circumferenceSegmentData]; + centerSegmentData.featureSegments = circumferenceSegmentData.featureSegments = featureSegments; + this.rBush_.insert(ol.extent.createOrUpdateFromCoordinate(coordinates), centerSegmentData); + this.rBush_.insert(geometry.getExtent(), circumferenceSegmentData); +}; + + /** * @param {ol.Feature} feature Feature * @param {ol.geom.GeometryCollection} geometry Geometry. @@ -526,7 +572,15 @@ ol.interaction.Modify.handleDownEvent_ = function(evt) { if (!componentSegments[uid]) { componentSegments[uid] = new Array(2); } - if (ol.coordinate.equals(segment[0], vertex) && + if (segmentDataMatch.geometry.getType() === ol.geom.GeometryType.CIRCLE && + segmentDataMatch.index === ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) { + + var closestVertex = ol.interaction.Modify.closestOnSegmentData_(vertex, segmentDataMatch); + if (ol.coordinate.equals(closestVertex, vertex) && !componentSegments[uid][0]) { + this.dragSegments_.push([segmentDataMatch, 0]); + componentSegments[uid][0] = segmentDataMatch; + } + } else if (ol.coordinate.equals(segment[0], vertex) && !componentSegments[uid][0]) { this.dragSegments_.push([segmentDataMatch, 0]); componentSegments[uid][0] = segmentDataMatch; @@ -576,7 +630,7 @@ ol.interaction.Modify.handleDragEvent_ = function(evt) { var segmentData = dragSegment[0]; var depth = segmentData.depth; var geometry = segmentData.geometry; - var coordinates = geometry.getCoordinates(); + var coordinates; var segment = segmentData.segment; var index = dragSegment[1]; @@ -590,30 +644,49 @@ ol.interaction.Modify.handleDragEvent_ = function(evt) { segment[0] = segment[1] = vertex; break; case ol.geom.GeometryType.MULTI_POINT: + coordinates = geometry.getCoordinates(); coordinates[segmentData.index] = vertex; segment[0] = segment[1] = vertex; break; case ol.geom.GeometryType.LINE_STRING: + coordinates = geometry.getCoordinates(); coordinates[segmentData.index + index] = vertex; segment[index] = vertex; break; case ol.geom.GeometryType.MULTI_LINE_STRING: + coordinates = geometry.getCoordinates(); coordinates[depth[0]][segmentData.index + index] = vertex; segment[index] = vertex; break; case ol.geom.GeometryType.POLYGON: + coordinates = geometry.getCoordinates(); coordinates[depth[0]][segmentData.index + index] = vertex; segment[index] = vertex; break; case ol.geom.GeometryType.MULTI_POLYGON: + coordinates = geometry.getCoordinates(); coordinates[depth[1]][depth[0]][segmentData.index + index] = vertex; segment[index] = vertex; break; + case ol.geom.GeometryType.CIRCLE: + segment[0] = segment[1] = vertex; + if (segmentData.index === ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX) { + this.changingFeature_ = true; + geometry.setCenter(vertex); + this.changingFeature_ = false; + } else { // We're dragging the circle's circumference: + this.changingFeature_ = true; + geometry.setRadius(ol.coordinate.distance(geometry.getCenter(), vertex)); + this.changingFeature_ = false; + } + break; default: // pass } - this.setGeometryCoordinates_(geometry, coordinates); + if (coordinates) { + this.setGeometryCoordinates_(geometry, coordinates); + } } this.createOrUpdateVertexFeature_(vertex); }; @@ -627,10 +700,23 @@ ol.interaction.Modify.handleDragEvent_ = function(evt) { */ ol.interaction.Modify.handleUpEvent_ = function(evt) { var segmentData; + var geometry; for (var i = this.dragSegments_.length - 1; i >= 0; --i) { segmentData = this.dragSegments_[i][0]; - this.rBush_.update(ol.extent.boundingExtent(segmentData.segment), - segmentData); + geometry = segmentData.geometry; + if (geometry.getType() === ol.geom.GeometryType.CIRCLE) { + // Update a circle object in the R* bush: + var coordinates = geometry.getCenter(); + var centerSegmentData = segmentData.featureSegments[0]; + var circumferenceSegmentData = segmentData.featureSegments[1]; + centerSegmentData.segment[0] = centerSegmentData.segment[1] = coordinates; + circumferenceSegmentData.segment[0] = circumferenceSegmentData.segment[1] = coordinates; + this.rBush_.update(ol.extent.createOrUpdateFromCoordinate(coordinates), centerSegmentData); + this.rBush_.update(geometry.getExtent(), circumferenceSegmentData); + } else { + this.rBush_.update(ol.extent.boundingExtent(segmentData.segment), + segmentData); + } } if (this.modified_) { this.dispatchEvent(new ol.interaction.Modify.Event( @@ -697,8 +783,8 @@ ol.interaction.Modify.prototype.handlePointerMove_ = function(evt) { ol.interaction.Modify.prototype.handlePointerAtPixel_ = function(pixel, map) { var pixelCoordinate = map.getCoordinateFromPixel(pixel); var sortByDistance = function(a, b) { - return ol.coordinate.squaredDistanceToSegment(pixelCoordinate, a.segment) - - ol.coordinate.squaredDistanceToSegment(pixelCoordinate, b.segment); + return ol.interaction.Modify.pointDistanceToSegmentDataSquared_(pixelCoordinate, a) - + ol.interaction.Modify.pointDistanceToSegmentDataSquared_(pixelCoordinate, b); }; var box = ol.extent.buffer( @@ -711,36 +797,44 @@ ol.interaction.Modify.prototype.handlePointerAtPixel_ = function(pixel, map) { nodes.sort(sortByDistance); var node = nodes[0]; var closestSegment = node.segment; - var vertex = (ol.coordinate.closestOnSegment(pixelCoordinate, - closestSegment)); + var vertex = ol.interaction.Modify.closestOnSegmentData_(pixelCoordinate, node); var vertexPixel = map.getPixelFromCoordinate(vertex); - if (Math.sqrt(ol.coordinate.squaredDistance(pixel, vertexPixel)) <= - this.pixelTolerance_) { - var pixel1 = map.getPixelFromCoordinate(closestSegment[0]); - var pixel2 = map.getPixelFromCoordinate(closestSegment[1]); - var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1); - var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2); - var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); - this.snappedToVertex_ = dist <= this.pixelTolerance_; - if (this.snappedToVertex_) { - vertex = squaredDist1 > squaredDist2 ? - closestSegment[1] : closestSegment[0]; - } - this.createOrUpdateVertexFeature_(vertex); + var dist = ol.coordinate.distance(pixel, vertexPixel); + if (dist <= this.pixelTolerance_) { var vertexSegments = {}; - vertexSegments[ol.getUid(closestSegment)] = true; - var segment; - for (var i = 1, ii = nodes.length; i < ii; ++i) { - segment = nodes[i].segment; - if ((ol.coordinate.equals(closestSegment[0], segment[0]) && - ol.coordinate.equals(closestSegment[1], segment[1]) || - (ol.coordinate.equals(closestSegment[0], segment[1]) && - ol.coordinate.equals(closestSegment[1], segment[0])))) { - vertexSegments[ol.getUid(segment)] = true; - } else { - break; + + if (node.geometry.getType() === ol.geom.GeometryType.CIRCLE && + node.index === ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) { + + this.snappedToVertex_ = true; + this.createOrUpdateVertexFeature_(vertex); + } else { + var pixel1 = map.getPixelFromCoordinate(closestSegment[0]); + var pixel2 = map.getPixelFromCoordinate(closestSegment[1]); + var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1); + var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2); + dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); + this.snappedToVertex_ = dist <= this.pixelTolerance_; + if (this.snappedToVertex_) { + vertex = squaredDist1 > squaredDist2 ? + closestSegment[1] : closestSegment[0]; + } + this.createOrUpdateVertexFeature_(vertex); + var segment; + for (var i = 1, ii = nodes.length; i < ii; ++i) { + segment = nodes[i].segment; + if ((ol.coordinate.equals(closestSegment[0], segment[0]) && + ol.coordinate.equals(closestSegment[1], segment[1]) || + (ol.coordinate.equals(closestSegment[0], segment[1]) && + ol.coordinate.equals(closestSegment[1], segment[0])))) { + vertexSegments[ol.getUid(segment)] = true; + } else { + break; + } } } + + vertexSegments[ol.getUid(closestSegment)] = true; this.vertexSegments_ = vertexSegments; return; } @@ -752,6 +846,52 @@ ol.interaction.Modify.prototype.handlePointerAtPixel_ = function(pixel, map) { }; +/** + * Returns the distance from a point to a line segment. + * + * @param {ol.Coordinate} pointCoordinates The coordinates of the point from + * which to calculate the distance. + * @param {ol.ModifySegmentDataType} segmentData The object describing the line + * segment we are calculating the distance to. + * @return {number} The square of the distance between a point and a line segment. + */ +ol.interaction.Modify.pointDistanceToSegmentDataSquared_ = function(pointCoordinates, segmentData) { + var geometry = segmentData.geometry; + + if (geometry.getType() === ol.geom.GeometryType.CIRCLE) { + var circleGeometry = /** @type {ol.geom.Circle} */ (geometry); + + if (segmentData.index === ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) { + var distanceToCenterSquared = + ol.coordinate.squaredDistance(circleGeometry.getCenter(), pointCoordinates); + var distanceToCircumference = + Math.sqrt(distanceToCenterSquared) - circleGeometry.getRadius(); + return distanceToCircumference * distanceToCircumference; + } + } + return ol.coordinate.squaredDistanceToSegment(pointCoordinates, segmentData.segment); +}; + +/** + * Returns the point closest to a given line segment. + * + * @param {ol.Coordinate} pointCoordinates The point to which a closest point + * should be found. + * @param {ol.ModifySegmentDataType} segmentData The object describing the line + * segment which should contain the closest point. + * @return {ol.Coordinate} The point closest to the specified line segment. + */ +ol.interaction.Modify.closestOnSegmentData_ = function(pointCoordinates, segmentData) { + var geometry = segmentData.geometry; + + if (geometry.getType() === ol.geom.GeometryType.CIRCLE && + segmentData.index === ol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) { + return geometry.getClosestPoint(pointCoordinates); + } + return ol.coordinate.closestOnSegment(pointCoordinates, segmentData.segment); +}; + + /** * @param {ol.ModifySegmentDataType} segmentData Segment data. * @param {ol.Coordinate} vertex Vertex. diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index 659afd2a8a..b3cdc557d9 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -370,7 +370,8 @@ ol.Transform; * feature: ol.Feature, * geometry: ol.geom.SimpleGeometry, * index: (number), - * segment: Array.}} + * segment: Array., + * featureSegments: (Array.|undefined)}} */ ol.ModifySegmentDataType; diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 61d6935c8f..349c731ac9 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -370,6 +370,39 @@ describe('ol.interaction.Modify', function() { }); + describe('circle modification', function() { + it('changes the circle radius and center', function() { + var circleFeature = new ol.Feature(new ol.geom.Circle([10, 10], 20)); + features.length = 0; + features.push(circleFeature); + + var modify = new ol.interaction.Modify({ + features: new ol.Collection(features) + }); + map.addInteraction(modify); + + // Change center + simulateEvent('pointermove', 10, -10, false, 0); + simulateEvent('pointerdown', 10, -10, false, 0); + simulateEvent('pointermove', 5, -5, false, 0); + simulateEvent('pointerdrag', 5, -5, false, 0); + simulateEvent('pointerup', 5, -5, false, 0); + + expect(circleFeature.getGeometry().getRadius()).to.equal(20); + expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); + + // Increase radius + simulateEvent('pointermove', 25, -5, false, 0); + simulateEvent('pointerdown', 25, -5, false, 0); + simulateEvent('pointermove', 30, -5, false, 0); + simulateEvent('pointerdrag', 30, -5, false, 0); + simulateEvent('pointerup', 30, -5, false, 0); + + expect(circleFeature.getGeometry().getRadius()).to.equal(25); + expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); + }); + }); + describe('boundary modification', function() { var modify, feature, events; @@ -539,7 +572,47 @@ describe('ol.interaction.Modify', function() { }; }); - it('updates the segment data', function() { + it('updates circle segment data', function() { + var feature = new ol.Feature(new ol.geom.Circle([10, 10], 20)); + features.length = 0; + features.push(feature); + + var modify = new ol.interaction.Modify({ + features: new ol.Collection(features) + }); + map.addInteraction(modify); + + var listeners; + + listeners = getListeners(feature, modify); + expect(listeners).to.have.length(1); + + var firstSegmentData; + + firstSegmentData = modify.rBush_.forEachInExtent([0, 0, 5, 5], + function(node) { + return node; + }); + expect(firstSegmentData.segment[0]).to.eql([10, 10]); + expect(firstSegmentData.segment[1]).to.eql([10, 10]); + + var center = feature.getGeometry().getCenter(); + center[0] = 1; + center[1] = 1; + feature.getGeometry().setCenter(center); + + firstSegmentData = modify.rBush_.forEachInExtent([0, 0, 5, 5], + function(node) { + return node; + }); + expect(firstSegmentData.segment[0]).to.eql([1, 1]); + expect(firstSegmentData.segment[1]).to.eql([1, 1]); + + listeners = getListeners(feature, modify); + expect(listeners).to.have.length(1); + }); + + it('updates polygon segment data', function() { var modify = new ol.interaction.Modify({ features: new ol.Collection(features) });