diff --git a/externs/olx.js b/externs/olx.js index 4253a2082c..9a9771e74e 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3984,6 +3984,7 @@ olx.source.BingMapsOptions.prototype.wrapX; * distance: (number|undefined), * extent: (ol.Extent|undefined), * format: (ol.format.Feature|undefined), + * geometryFunction: (undefined|function(ol.Feature):ol.geom.Point), * logo: (string|undefined), * projection: ol.proj.ProjectionLike, * source: ol.source.Vector, @@ -4017,6 +4018,25 @@ olx.source.ClusterOptions.prototype.distance; olx.source.ClusterOptions.prototype.extent; +/** + * Function that takes an {@link ol.Feature} as argument and returns an + * {@link ol.geom.Point} as cluster calculation point for the feature. When a + * feature should not be considered for clustering, the function should return + * `null`. The default, which works when the underyling source contains point + * features only, is + * ```js + * function(feature) { + * return feature.getGeometry(); + * } + * ``` + * See {@link ol.geom.Polygon#getInteriorPoint} for a way to get a cluster + * calculation point for polygons. + * @type {undefined|function(ol.Feature):ol.geom.Point} + * @api + */ +olx.source.ClusterOptions.prototype.geometryFunction; + + /** * Format. * @type {ol.format.Feature|undefined} @@ -6174,8 +6194,8 @@ olx.style.FillOptions; /** - * A color, gradient or pattern. See {@link ol.color} - * and {@link ol.colorlike} for possible formats. Default null; + * A color, gradient or pattern. See {@link ol.color} + * and {@link ol.colorlike} for possible formats. Default null; * if null, the Canvas/renderer default black will be used. * @type {ol.Color|ol.ColorLike|undefined} * @api diff --git a/src/ol/source/clustersource.js b/src/ol/source/clustersource.js index 45a80283f1..d01f64e3c7 100644 --- a/src/ol/source/clustersource.js +++ b/src/ol/source/clustersource.js @@ -14,7 +14,9 @@ goog.require('ol.source.Vector'); /** * @classdesc - * Layer source to cluster vector data. + * Layer source to cluster vector data. Works out of the box with point + * geometries. For other geometry types, or if not all geometries should be + * considered for clustering, a custom `geometryFunction` can be defined. * * @constructor * @param {olx.source.ClusterOptions} options Constructor options. @@ -48,6 +50,17 @@ ol.source.Cluster = function(options) { */ this.features_ = []; + /** + * @param {ol.Feature} feature Feature. + * @return {ol.geom.Point} Cluster calculation point. + */ + this.geometryFunction_ = options.geometryFunction || function(feature) { + var geometry = feature.getGeometry(); + goog.asserts.assert(geometry instanceof ol.geom.Point, + 'feature geometry is a ol.geom.Point instance'); + return geometry; + }; + /** * @type {ol.source.Vector} * @private @@ -117,25 +130,25 @@ ol.source.Cluster.prototype.cluster_ = function() { for (var i = 0, ii = features.length; i < ii; i++) { var feature = features[i]; if (!(goog.getUid(feature).toString() in clustered)) { - var geometry = feature.getGeometry(); - goog.asserts.assert(geometry instanceof ol.geom.Point, - 'feature geometry is a ol.geom.Point instance'); - var coordinates = geometry.getCoordinates(); - ol.extent.createOrUpdateFromCoordinate(coordinates, extent); - ol.extent.buffer(extent, mapDistance, extent); + var geometry = this.geometryFunction_(feature); + if (geometry) { + var coordinates = geometry.getCoordinates(); + ol.extent.createOrUpdateFromCoordinate(coordinates, extent); + ol.extent.buffer(extent, mapDistance, extent); - var neighbors = this.source_.getFeaturesInExtent(extent); - goog.asserts.assert(neighbors.length >= 1, 'at least one neighbor found'); - neighbors = neighbors.filter(function(neighbor) { - var uid = goog.getUid(neighbor).toString(); - if (!(uid in clustered)) { - clustered[uid] = true; - return true; - } else { - return false; - } - }); - this.features_.push(this.createCluster_(neighbors)); + var neighbors = this.source_.getFeaturesInExtent(extent); + goog.asserts.assert(neighbors.length >= 1, 'at least one neighbor found'); + neighbors = neighbors.filter(function(neighbor) { + var uid = goog.getUid(neighbor).toString(); + if (!(uid in clustered)) { + clustered[uid] = true; + return true; + } else { + return false; + } + }); + this.features_.push(this.createCluster_(neighbors)); + } } } goog.asserts.assert( @@ -150,16 +163,16 @@ ol.source.Cluster.prototype.cluster_ = function() { * @private */ ol.source.Cluster.prototype.createCluster_ = function(features) { - var length = features.length; var centroid = [0, 0]; - for (var i = 0; i < length; i++) { - var geometry = features[i].getGeometry(); - goog.asserts.assert(geometry instanceof ol.geom.Point, - 'feature geometry is a ol.geom.Point instance'); - var coordinates = geometry.getCoordinates(); - ol.coordinate.add(centroid, coordinates); + for (var i = features.length - 1; i >= 0; --i) { + var geometry = this.geometryFunction_(features[i]); + if (geometry) { + ol.coordinate.add(centroid, geometry.getCoordinates()); + } else { + features.splice(i, 1); + } } - ol.coordinate.scale(centroid, 1 / length); + ol.coordinate.scale(centroid, 1 / features.length); var cluster = new ol.Feature(new ol.geom.Point(centroid)); cluster.set('features', features); diff --git a/test/spec/ol/source/clustersource.test.js b/test/spec/ol/source/clustersource.test.js index 100206abf2..19b66b8cb6 100644 --- a/test/spec/ol/source/clustersource.test.js +++ b/test/spec/ol/source/clustersource.test.js @@ -14,8 +14,54 @@ describe('ol.source.Cluster', function() { expect(source).to.be.a(ol.source.Cluster); }); }); + + describe('#loadFeatures', function() { + var extent = [-1, -1, 1, 1]; + var projection = ol.proj.get('EPSG:3857'); + it('clusters a source with point features', function() { + var source = new ol.source.Cluster({ + source: new ol.source.Vector({ + features: [ + new ol.Feature(new ol.geom.Point([0, 0])), + new ol.Feature(new ol.geom.Point([0, 0])) + ] + }) + }); + source.loadFeatures(extent, 1, projection); + expect(source.getFeatures().length).to.be(1); + expect(source.getFeatures()[0].get('features').length).to.be(2); + }); + it('clusters with a custom geometryFunction', function() { + var source = new ol.source.Cluster({ + geometryFunction: function(feature) { + var geom = feature.getGeometry(); + if (geom.getType() == 'Point') { + return geom; + } else if (geom.getType() == 'Polygon') { + return geom.getInteriorPoint(); + } + return null; + }, + source: new ol.source.Vector({ + features: [ + new ol.Feature(new ol.geom.Point([0, 0])), + new ol.Feature(new ol.geom.LineString([[0, 0], [1, 1]])), + new ol.Feature(new ol.geom.Polygon( + [[[-1, -1], [-1, 1], [1, 1], [1, -1], [-1, -1]]])) + ] + }) + }); + source.loadFeatures(extent, 1, projection); + expect(source.getFeatures().length).to.be(1); + expect(source.getFeatures()[0].get('features').length).to.be(2); + }); + }) }); +goog.require('ol.Feature'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.Polygon'); goog.require('ol.proj'); goog.require('ol.source.Cluster'); goog.require('ol.source.Source');