diff --git a/src/ol/geom/circle.exports b/src/ol/geom/circle.exports new file mode 100644 index 0000000000..e08d6f83be --- /dev/null +++ b/src/ol/geom/circle.exports @@ -0,0 +1,11 @@ +@exportSymbol ol.geom.Circle +@exportProperty ol.geom.Circle.prototype.clone +@exportProperty ol.geom.Circle.prototype.getCenter +@exportProperty ol.geom.Circle.prototype.getExtent +@exportProperty ol.geom.Circle.prototype.getRadius +@exportProperty ol.geom.Circle.prototype.getSimplifiedGeometry +@exportProperty ol.geom.Circle.prototype.getType +@exportProperty ol.geom.Circle.prototype.setCenter +@exportProperty ol.geom.Circle.prototype.setCenterAndRadius +@exportProperty ol.geom.Circle.prototype.setRadius +@exportProperty ol.geom.Circle.prototype.transform diff --git a/src/ol/geom/circle.js b/src/ol/geom/circle.js new file mode 100644 index 0000000000..ea3a3343d8 --- /dev/null +++ b/src/ol/geom/circle.js @@ -0,0 +1,192 @@ +goog.provide('ol.geom.Circle'); + +goog.require('goog.asserts'); +goog.require('ol.extent'); +goog.require('ol.geom.GeometryType'); +goog.require('ol.geom.SimpleGeometry'); +goog.require('ol.geom.flat'); + + + +/** + * @constructor + * @extends {ol.geom.SimpleGeometry} + * @param {ol.geom.RawPoint} center Center. + * @param {number=} opt_radius Radius. + * @param {ol.geom.GeometryLayout=} opt_layout Layout. + */ +ol.geom.Circle = function(center, opt_radius, opt_layout) { + goog.base(this); + var radius = goog.isDef(opt_radius) ? opt_radius : 0; + this.setCenterAndRadius(center, radius, opt_layout); +}; +goog.inherits(ol.geom.Circle, ol.geom.SimpleGeometry); + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.clone = function() { + var circle = new ol.geom.Circle(null); + circle.setFlatCoordinates(this.layout, this.flatCoordinates.slice()); + return circle; +}; + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.closestPointXY = + function(x, y, closestPoint, minSquaredDistance) { + var flatCoordinates = this.flatCoordinates; + var radius = flatCoordinates[this.stride] - flatCoordinates[0]; + var dx = x - flatCoordinates[0]; + var dy = y - flatCoordinates[1]; + var distance = Math.max(Math.sqrt(dx * dx + dy * dy) - radius, 0); + var squaredDistance = distance * distance; + if (squaredDistance < minSquaredDistance) { + // FIXME it must be possible to do this without trigonometric functions + var theta = Math.atan2(dy, dx); + closestPoint[0] = flatCoordinates[0] + radius * Math.cos(theta); + closestPoint[1] = flatCoordinates[1] + radius * Math.sin(theta); + return squaredDistance; + } else { + return minSquaredDistance; + } +}; + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.containsXY = function(x, y) { + var flatCoordinates = this.flatCoordinates; + var dx = x - flatCoordinates[0]; + var dy = y - flatCoordinates[1]; + var r = flatCoordinates[this.stride] - flatCoordinates[0]; + return dx * dx + dy * dy <= r; +}; + + +/** + * @return {ol.geom.RawPoint} Center. + */ +ol.geom.Circle.prototype.getCenter = function() { + return this.flatCoordinates.slice(0, this.stride); +}; + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.getExtent = function(opt_extent) { + if (this.extentRevision != this.revision) { + var flatCoordinates = this.flatCoordinates; + var radius = flatCoordinates[this.stride] - flatCoordinates[0]; + this.extent = ol.extent.createOrUpdate( + flatCoordinates[0] - radius, flatCoordinates[1] - radius, + flatCoordinates[0] + radius, flatCoordinates[1] + radius, + this.extent); + this.extentRevision = this.revision; + } + goog.asserts.assert(goog.isDef(this.extent)); + return ol.extent.returnOrUpdate(this.extent, opt_extent); +}; + + +/** + * @return {number} Radius. + */ +ol.geom.Circle.prototype.getRadius = function() { + var dx = this.flatCoordinates[this.stride] - this.flatCoordinates[0]; + var dy = this.flatCoordinates[this.stride + 1] - this.flatCoordinates[1]; + return Math.sqrt(dx * dx + dy * dy); +}; + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.getSimplifiedGeometry = function(squaredTolerance) { + return this; +}; + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.getType = function() { + return ol.geom.GeometryType.CIRCLE; +}; + + +/** + * @param {ol.geom.RawPoint} center Center. + */ +ol.geom.Circle.prototype.setCenter = function(center) { + var stride = this.stride; + goog.asserts.assert(center.length == stride); + var radius = this.flatCoordinates[stride] - this.flatCoordinates[0]; + var flatCoordinates = center.slice(); + flatCoordinates[stride] = flatCoordinates[0] + radius; + var i; + for (i = 1; i < stride; ++i) { + flatCoordinates[stride + i] = center[i]; + } + this.setFlatCoordinates(this.layout, flatCoordinates); +}; + + +/** + * @param {ol.geom.RawPoint} center Center. + * @param {number} radius Radius. + * @param {ol.geom.GeometryLayout=} opt_layout Layout. + */ +ol.geom.Circle.prototype.setCenterAndRadius = + function(center, radius, opt_layout) { + if (goog.isNull(center)) { + this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null); + } else { + this.setLayout(opt_layout, center, 0); + if (goog.isNull(this.flatCoordinates)) { + this.flatCoordinates = []; + } + var flatCoordinates = this.flatCoordinates; + var offset = ol.geom.flat.deflateCoordinate( + flatCoordinates, 0, center, this.stride); + flatCoordinates[offset++] = flatCoordinates[0] + radius; + var i, ii; + for (i = 1, ii = this.stride; i < ii; ++i) { + flatCoordinates[offset++] = flatCoordinates[i]; + } + flatCoordinates.length = offset; + this.dispatchChangeEvent(); + } +}; + + +/** + * @param {ol.geom.GeometryLayout} layout Layout. + * @param {Array.} flatCoordinates Flat coordinates. + */ +ol.geom.Circle.prototype.setFlatCoordinates = + function(layout, flatCoordinates) { + this.setFlatCoordinatesInternal(layout, flatCoordinates); + this.dispatchChangeEvent(); +}; + + +/** + * @param {number} radius Radius. + */ +ol.geom.Circle.prototype.setRadius = function(radius) { + goog.asserts.assert(!goog.isNull(this.flatCoordinates)); + this.flatCoordinates[this.stride] = this.flatCoordinates[0] + radius; + this.dispatchChangeEvent(); +}; + + +/** + * @inheritDoc + */ +ol.geom.Circle.prototype.transform = goog.abstractMethod; diff --git a/src/ol/geom/geometry.js b/src/ol/geom/geometry.js index bb93809b62..7a41e08503 100644 --- a/src/ol/geom/geometry.js +++ b/src/ol/geom/geometry.js @@ -18,7 +18,8 @@ ol.geom.GeometryType = { MULTI_POINT: 'MultiPoint', MULTI_LINE_STRING: 'MultiLineString', MULTI_POLYGON: 'MultiPolygon', - GEOMETRY_COLLECTION: 'GeometryCollection' + GEOMETRY_COLLECTION: 'GeometryCollection', + CIRCLE: 'Circle' }; diff --git a/test/spec/ol/geom/circle.test.js b/test/spec/ol/geom/circle.test.js new file mode 100644 index 0000000000..e7e6fa119d --- /dev/null +++ b/test/spec/ol/geom/circle.test.js @@ -0,0 +1,192 @@ +goog.provide('ol.test.geom.Circle'); + + +describe('ol.geom.Circle', function() { + + describe('with a unit circle', function() { + + var circle; + beforeEach(function() { + circle = new ol.geom.Circle([0, 0], 1); + }); + + describe('#clone', function() { + + it('returns a clone', function() { + var clone = circle.clone(); + expect(clone).to.be.an(ol.geom.Circle); + expect(clone.getCenter()).to.eql(circle.getCenter()); + expect(clone.getCenter()).not.to.be(circle.getCenter()); + expect(clone.getRadius()).to.be(circle.getRadius()); + }); + + }); + + describe('#containsCoordinate', function() { + + it('contains the center', function() { + expect(circle.containsCoordinate([0, 0])).to.be(true); + }); + + it('contains points inside the perimeter', function() { + expect(circle.containsCoordinate([0.5, 0.5])).to.be(true); + expect(circle.containsCoordinate([-0.5, 0.5])).to.be(true); + expect(circle.containsCoordinate([-0.5, -0.5])).to.be(true); + expect(circle.containsCoordinate([0.5, -0.5])).to.be(true); + }); + + it('contains points on the perimeter', function() { + expect(circle.containsCoordinate([1, 0])).to.be(true); + expect(circle.containsCoordinate([0, 1])).to.be(true); + expect(circle.containsCoordinate([-1, 0])).to.be(true); + expect(circle.containsCoordinate([0, -1])).to.be(true); + }); + + it('does not contain points outside the perimeter', function() { + expect(circle.containsCoordinate([2, 0])).to.be(false); + expect(circle.containsCoordinate([1, 1])).to.be(false); + expect(circle.containsCoordinate([-2, 0])).to.be(false); + expect(circle.containsCoordinate([0, -2])).to.be(false); + }); + + }); + + describe('#getCenter', function() { + + it('returns the expected value', function() { + expect(circle.getCenter()).to.eql([0, 0]); + }); + + }); + + describe('#getClosestPoint', function() { + + it('returns the closest point on the perimeter', function() { + var closestPoint; + closestPoint = circle.getClosestPoint([2, 0]); + expect(closestPoint[0]).to.roughlyEqual(1, 1e-15); + expect(closestPoint[1]).to.roughlyEqual(0, 1e-15); + closestPoint = circle.getClosestPoint([2, 2]); + expect(closestPoint[0]).to.roughlyEqual(Math.sqrt(0.5), 1e-15); + expect(closestPoint[1]).to.roughlyEqual(Math.sqrt(0.5), 1e-15); + closestPoint = circle.getClosestPoint([0, 2]); + expect(closestPoint[0]).to.roughlyEqual(0, 1e-15); + expect(closestPoint[1]).to.roughlyEqual(1, 1e-15); + closestPoint = circle.getClosestPoint([-2, 2]); + expect(closestPoint[0]).to.roughlyEqual(-Math.sqrt(0.5), 1e-15); + expect(closestPoint[1]).to.roughlyEqual(Math.sqrt(0.5), 1e-15); + closestPoint = circle.getClosestPoint([-2, 0]); + expect(closestPoint[0]).to.roughlyEqual(-1, 1e-15); + expect(closestPoint[1]).to.roughlyEqual(0, 1e-15); + closestPoint = circle.getClosestPoint([-2, -2]); + expect(closestPoint[0]).to.roughlyEqual(-Math.sqrt(0.5), 1e-15); + expect(closestPoint[1]).to.roughlyEqual(-Math.sqrt(0.5), 1e-15); + closestPoint = circle.getClosestPoint([0, -2]); + expect(closestPoint[0]).to.roughlyEqual(0, 1e-15); + expect(closestPoint[1]).to.roughlyEqual(-1, 1e-15); + closestPoint = circle.getClosestPoint([2, -2]); + expect(closestPoint[0]).to.roughlyEqual(Math.sqrt(0.5), 1e-15); + expect(closestPoint[1]).to.roughlyEqual(-Math.sqrt(0.5), 1e-15); + }); + + }); + + describe('#getExtent', function() { + + it('returns the expected value', function() { + expect(circle.getExtent()).to.eql([-1, -1, 1, 1]); + }); + + }); + + describe('#getRadius', function() { + + it('returns the expected value', function() { + expect(circle.getRadius()).to.be(1); + }); + + }); + + describe('#getSimplifiedGeometry', function() { + + it('returns the same geometry', function() { + expect(circle.getSimplifiedGeometry(1)).to.be(circle); + }); + + }); + + describe('#getType', function() { + + it('returns the expected value', function() { + expect(circle.getType()).to.be(ol.geom.GeometryType.CIRCLE); + }); + + }); + + describe('#setCenter', function() { + + it('sets the center', function() { + circle.setCenter([1, 2]); + expect(circle.getCenter()).to.eql([1, 2]); + }); + + it('fires a change event', function() { + var spy = sinon.spy(); + circle.on('change', spy); + circle.setCenter([1, 2]); + expect(spy.calledOnce).to.be(true); + }); + + }); + + describe('#setFlatCoordinates', function() { + + it('sets both center and radius', function() { + circle.setFlatCoordinates(ol.geom.GeometryLayout.XY, [1, 2, 4, 2]); + expect(circle.getCenter()).to.eql([1, 2]); + expect(circle.getRadius()).to.be(3); + }); + + it('fires a single change event', function() { + var spy = sinon.spy(); + circle.on('change', spy); + circle.setFlatCoordinates(ol.geom.GeometryLayout.XY, [1, 2, 4, 2]); + expect(spy.calledOnce).to.be(true); + }); + + }); + + describe('#setRadius', function() { + + it('sets the radius', function() { + circle.setRadius(2); + expect(circle.getRadius()).to.be(2); + }); + + it('fires a change event', function() { + var spy = sinon.spy(); + circle.on('change', spy); + circle.setRadius(2); + expect(spy.calledOnce).to.be(true); + }); + + }); + + describe('#transform', function() { + + it('throws an exception', function() { + expect(function() { + circle.transform(ol.proj.identityTransform); + }).to.throwException(); + }); + + }); + + }); + +}); + + +goog.require('ol.geom.Circle'); +goog.require('ol.geom.GeometryType'); +goog.require('ol.proj');