diff --git a/externs/olx.js b/externs/olx.js
index 9700718064..840d4f2434 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -4977,12 +4977,13 @@ olx.source.TileWMSOptions.prototype.wrapX;
/**
* @typedef {{attributions: (Array.
|undefined),
- * features: (Array.|undefined),
+ * features: (Array.|ol.Collection.|undefined),
* format: (ol.format.Feature|undefined),
* loader: (ol.FeatureLoader|undefined),
* logo: (string|olx.LogoOptions|undefined),
* strategy: (ol.LoadingStrategy|undefined),
* url: (string|undefined),
+ * useSpatialIndex: (boolean|undefined),
* wrapX: (boolean|undefined)}}
* @api
*/
@@ -4998,8 +4999,9 @@ olx.source.VectorOptions.prototype.attributions;
/**
- * Features.
- * @type {Array.|undefined}
+ * Features. If provided as {@link ol.Collection}, the features in the source
+ * and the collection will stay in sync.
+ * @type {Array.|ol.Collection.|undefined}
* @api stable
*/
olx.source.VectorOptions.prototype.features;
@@ -5052,6 +5054,29 @@ olx.source.VectorOptions.prototype.strategy;
olx.source.VectorOptions.prototype.url;
+/**
+ * By default, an RTree is used as spatial index. When features are removed and
+ * added frequently, and the total number of features is low, setting this to
+ * `false` may improve performance.
+ *
+ * Note that
+ * {@link ol.source.Vector#getFeaturesInExtent},
+ * {@link ol.source.Vector#getClosestFeatureToCoordinate} and
+ * {@link ol.source.Vector#getExtent} cannot be used when `useSpatialIndex` is
+ * set to `false`, and {@link ol.source.Vector#forEachFeatureInExtent} will loop
+ * through all features.
+ *
+ * When set to `false`, the features will be maintained in an
+ * {@link ol.Collection}, which can be retrieved through
+ * {@link ol.source.Vector#getFeaturesCollection}.
+ *
+ * The default is `true`.
+ * @type {boolean|undefined}
+ * @api
+ */
+olx.source.VectorOptions.prototype.useSpatialIndex;
+
+
/**
* Wrap the world horizontally. Default is `true`. For vector editing across the
* -180° and 180° meridians to work properly, this should be set to `false`. The
diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js
index 42f945f9d7..60976af89b 100644
--- a/src/ol/source/vectorsource.js
+++ b/src/ol/source/vectorsource.js
@@ -1,5 +1,4 @@
// FIXME bulk feature upload - suppress events
-// FIXME put features in an ol.Collection
// FIXME make change-detection more refined (notably, geometry hint)
goog.provide('ol.source.Vector');
@@ -12,7 +11,10 @@ goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.object');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEventType');
goog.require('ol.Extent');
+goog.require('ol.Feature');
goog.require('ol.FeatureLoader');
goog.require('ol.LoadingStrategy');
goog.require('ol.ObjectEventType');
@@ -105,11 +107,14 @@ ol.source.Vector = function(opt_options) {
this.strategy_ = goog.isDef(options.strategy) ? options.strategy :
ol.loadingstrategy.all;
+ var useSpatialIndex =
+ goog.isDef(options.useSpatialIndex) ? options.useSpatialIndex : true;
+
/**
* @private
* @type {ol.structs.RBush.}
*/
- this.featuresRtree_ = new ol.structs.RBush();
+ this.featuresRtree_ = useSpatialIndex ? new ol.structs.RBush() : null;
/**
* @private
@@ -143,8 +148,27 @@ ol.source.Vector = function(opt_options) {
*/
this.featureChangeKeys_ = {};
- if (goog.isDef(options.features)) {
- this.addFeaturesInternal(options.features);
+ /**
+ * @private
+ * @type {ol.Collection.}
+ */
+ this.featuresCollection_ = null;
+
+ var collection, features;
+ if (options.features instanceof ol.Collection) {
+ collection = options.features;
+ features = collection.getArray();
+ } else if (goog.isArray(options.features)) {
+ features = options.features;
+ }
+ if (!useSpatialIndex && !goog.isDef(collection)) {
+ collection = new ol.Collection(features);
+ }
+ if (goog.isDef(features)) {
+ this.addFeaturesInternal(features);
+ }
+ if (goog.isDef(collection)) {
+ this.bindFeaturesCollection_(collection);
}
};
@@ -181,7 +205,9 @@ ol.source.Vector.prototype.addFeatureInternal = function(feature) {
var geometry = feature.getGeometry();
if (goog.isDefAndNotNull(geometry)) {
var extent = geometry.getExtent();
- this.featuresRtree_.insert(extent, feature);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.insert(extent, feature);
+ }
} else {
this.nullGeometryFeatures_[featureKey] = feature;
}
@@ -280,7 +306,9 @@ ol.source.Vector.prototype.addFeaturesInternal = function(features) {
this.nullGeometryFeatures_[featureKey] = feature;
}
}
- this.featuresRtree_.load(extents, geometryFeatures);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.load(extents, geometryFeatures);
+ }
for (i = 0, length = newFeatures.length; i < length; i++) {
this.dispatchEvent(new ol.source.VectorEvent(
@@ -289,6 +317,54 @@ ol.source.Vector.prototype.addFeaturesInternal = function(features) {
};
+/**
+ * @param {!ol.Collection.} collection Collection.
+ * @private
+ */
+ol.source.Vector.prototype.bindFeaturesCollection_ = function(collection) {
+ goog.asserts.assert(goog.isNull(this.featuresCollection_),
+ 'bindFeaturesCollection can only be called once');
+ var modifyingCollection = false;
+ goog.events.listen(this, ol.source.VectorEventType.ADDFEATURE,
+ function(evt) {
+ if (!modifyingCollection) {
+ modifyingCollection = true;
+ collection.push(evt.feature);
+ modifyingCollection = false;
+ }
+ });
+ goog.events.listen(this, ol.source.VectorEventType.REMOVEFEATURE,
+ function(evt) {
+ if (!modifyingCollection) {
+ modifyingCollection = true;
+ collection.remove(evt.feature);
+ modifyingCollection = false;
+ }
+ });
+ goog.events.listen(collection, ol.CollectionEventType.ADD,
+ function(evt) {
+ if (!modifyingCollection) {
+ var feature = evt.element;
+ goog.asserts.assertInstanceof(feature, ol.Feature);
+ modifyingCollection = true;
+ this.addFeature(feature);
+ modifyingCollection = false;
+ }
+ }, false, this);
+ goog.events.listen(collection, ol.CollectionEventType.REMOVE,
+ function(evt) {
+ if (!modifyingCollection) {
+ var feature = evt.element;
+ goog.asserts.assertInstanceof(feature, ol.Feature);
+ modifyingCollection = true;
+ this.removeFeature(feature);
+ modifyingCollection = false;
+ }
+ }, false, this);
+ this.featuresCollection_ = collection;
+};
+
+
/**
* Remove all features from the source.
* @param {boolean=} opt_fast Skip dispatching of {@link removefeature} events.
@@ -305,8 +381,10 @@ ol.source.Vector.prototype.clear = function(opt_fast) {
this.undefIdIndex_ = {};
} else {
var rmFeatureInternal = this.removeFeatureInternal;
- this.featuresRtree_.forEach(rmFeatureInternal, this);
- goog.object.forEach(this.nullGeometryFeatures_, rmFeatureInternal, this);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.forEach(rmFeatureInternal, this);
+ goog.object.forEach(this.nullGeometryFeatures_, rmFeatureInternal, this);
+ }
goog.asserts.assert(goog.object.isEmpty(this.featureChangeKeys_),
'featureChangeKeys is an empty object now');
goog.asserts.assert(goog.object.isEmpty(this.idIndex_),
@@ -315,7 +393,9 @@ ol.source.Vector.prototype.clear = function(opt_fast) {
'undefIdIndex is an empty object now');
}
- this.featuresRtree_.clear();
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.clear();
+ }
this.loadedExtentsRtree_.clear();
this.nullGeometryFeatures_ = {};
@@ -338,7 +418,11 @@ ol.source.Vector.prototype.clear = function(opt_fast) {
* @api stable
*/
ol.source.Vector.prototype.forEachFeature = function(callback, opt_this) {
- return this.featuresRtree_.forEach(callback, opt_this);
+ if (!goog.isNull(this.featuresRtree_)) {
+ return this.featuresRtree_.forEach(callback, opt_this);
+ } else if (!goog.isNull(this.featuresCollection_)) {
+ return this.featuresCollection_.forEach(callback, opt_this);
+ }
};
@@ -381,6 +465,9 @@ ol.source.Vector.prototype.forEachFeatureAtCoordinateDirect =
* the {@link ol.source.Vector#forEachFeatureIntersectingExtent
* source.forEachFeatureIntersectingExtent()} method instead.
*
+ * When `useSpatialIndex` is set to false, this method will loop through all
+ * features, equivalent to {@link ol.source.Vector#forEachFeature}.
+ *
* @param {ol.Extent} extent Extent.
* @param {function(this: T, ol.Feature): S} callback Called with each feature
* whose bounding box intersects the provided extent.
@@ -391,7 +478,11 @@ ol.source.Vector.prototype.forEachFeatureAtCoordinateDirect =
*/
ol.source.Vector.prototype.forEachFeatureInExtent =
function(extent, callback, opt_this) {
- return this.featuresRtree_.forEachInExtent(extent, callback, opt_this);
+ if (!goog.isNull(this.featuresRtree_)) {
+ return this.featuresRtree_.forEachInExtent(extent, callback, opt_this);
+ } else if (!goog.isNull(this.featuresCollection_)) {
+ return this.featuresCollection_.forEach(callback, opt_this);
+ }
};
@@ -448,17 +539,36 @@ ol.source.Vector.prototype.forEachFeatureIntersectingExtent =
};
+/**
+ * Get the features collection associated with this source. Will be `null`
+ * unless the source was configured with `useSpatialIndex` set to `false`, or
+ * with an {@link ol.Collection} as `features`.
+ * @return {ol.Collection.}
+ * @api
+ */
+ol.source.Vector.prototype.getFeaturesCollection = function() {
+ return this.featuresCollection_;
+};
+
+
/**
* Get all features on the source.
* @return {Array.} Features.
* @api stable
*/
ol.source.Vector.prototype.getFeatures = function() {
- var features = this.featuresRtree_.getAll();
- if (!goog.object.isEmpty(this.nullGeometryFeatures_)) {
- goog.array.extend(
- features, goog.object.getValues(this.nullGeometryFeatures_));
+ var features;
+ if (!goog.isNull(this.featuresCollection_)) {
+ features = this.featuresCollection_.getArray();
+ } else if (!goog.isNull(this.featuresRtree_)) {
+ features = this.featuresRtree_.getAll();
+ if (!goog.object.isEmpty(this.nullGeometryFeatures_)) {
+ goog.array.extend(
+ features, goog.object.getValues(this.nullGeometryFeatures_));
+ }
}
+ goog.asserts.assert(goog.isDef(features),
+ 'Neither featuresRtree_ nor featuresCollection_ are available');
return features;
};
@@ -482,17 +592,25 @@ ol.source.Vector.prototype.getFeaturesAtCoordinate = function(coordinate) {
* Get all features in the provided extent. Note that this returns all features
* whose bounding boxes intersect the given extent (so it may include features
* whose geometries do not intersect the extent).
+ *
+ * This method is not available when the source is configured with
+ * `useSpatialIndex` set to `false`.
* @param {ol.Extent} extent Extent.
* @return {Array.} Features.
* @api
*/
ol.source.Vector.prototype.getFeaturesInExtent = function(extent) {
+ goog.asserts.assert(!goog.isNull(this.featuresRtree_),
+ 'getFeaturesInExtent does not work when useSpatialIndex is set to false');
return this.featuresRtree_.getInExtent(extent);
};
/**
* Get the closest feature to the provided coordinate.
+ *
+ * This method is not available when the source is configured with
+ * `useSpatialIndex` set to `false`.
* @param {ol.Coordinate} coordinate Coordinate.
* @return {ol.Feature} Closest feature.
* @api stable
@@ -512,6 +630,9 @@ ol.source.Vector.prototype.getClosestFeatureToCoordinate =
var closestPoint = [NaN, NaN];
var minSquaredDistance = Infinity;
var extent = [-Infinity, -Infinity, Infinity, Infinity];
+ goog.asserts.assert(!goog.isNull(this.featuresRtree_),
+ 'getClosestFeatureToCoordinate does not work with useSpatialIndex set ' +
+ 'to false');
this.featuresRtree_.forEachInExtent(extent,
/**
* @param {ol.Feature} feature Feature.
@@ -542,10 +663,15 @@ ol.source.Vector.prototype.getClosestFeatureToCoordinate =
/**
* Get the extent of the features currently in the source.
+ *
+ * This method is not available when the source is configured with
+ * `useSpatialIndex` set to `false`.
* @return {ol.Extent} Extent.
* @api stable
*/
ol.source.Vector.prototype.getExtent = function() {
+ goog.asserts.assert(!goog.isNull(this.featuresRtree_),
+ 'getExtent does not work when useSpatialIndex is set to false');
return this.featuresRtree_.getExtent();
};
@@ -575,16 +701,22 @@ ol.source.Vector.prototype.handleFeatureChange_ = function(event) {
var geometry = feature.getGeometry();
if (!goog.isDefAndNotNull(geometry)) {
if (!(featureKey in this.nullGeometryFeatures_)) {
- this.featuresRtree_.remove(feature);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.remove(feature);
+ }
this.nullGeometryFeatures_[featureKey] = feature;
}
} else {
var extent = geometry.getExtent();
if (featureKey in this.nullGeometryFeatures_) {
delete this.nullGeometryFeatures_[featureKey];
- this.featuresRtree_.insert(extent, feature);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.insert(extent, feature);
+ }
} else {
- this.featuresRtree_.update(extent, feature);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.update(extent, feature);
+ }
}
}
var id = feature.getId();
@@ -668,7 +800,9 @@ ol.source.Vector.prototype.removeFeature = function(feature) {
if (featureKey in this.nullGeometryFeatures_) {
delete this.nullGeometryFeatures_[featureKey];
} else {
- this.featuresRtree_.remove(feature);
+ if (!goog.isNull(this.featuresRtree_)) {
+ this.featuresRtree_.remove(feature);
+ }
}
this.removeFeatureInternal(feature);
this.changed();
diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js
index df782e7a67..74bad15d13 100644
--- a/test/spec/ol/source/vectorsource.test.js
+++ b/test/spec/ol/source/vectorsource.test.js
@@ -427,10 +427,69 @@ describe('ol.source.Vector', function() {
});
});
+ describe('with useSpatialIndex set to false', function() {
+ var source;
+ beforeEach(function() {
+ source = new ol.source.Vector({useSpatialIndex: false});
+ });
+
+ it('returns a features collection', function() {
+ expect(source.getFeaturesCollection()).to.be.a(ol.Collection);
+ });
+
+ it('#forEachFeatureInExtent loops through all features', function() {
+ source.addFeatures([new ol.Feature(), new ol.Feature()]);
+ var spy = sinon.spy();
+ source.forEachFeatureInExtent([0, 0, 0, 0], spy);
+ expect(spy.callCount).to.be(2);
+ });
+
+ });
+
+ describe('with a collection of features', function() {
+ var collection, source;
+ beforeEach(function() {
+ collection = new ol.Collection();
+ source = new ol.source.Vector({
+ features: collection
+ });
+ });
+
+ it('#getFeaturesCollection returns the configured collection', function() {
+ expect(source.getFeaturesCollection()).to.equal(collection);
+ });
+
+ it('keeps the collection in sync with the source\'s features', function() {
+ var feature = new ol.Feature();
+ source.addFeature(feature);
+ expect(collection.getLength()).to.be(1);
+ source.removeFeature(feature);
+ expect(collection.getLength()).to.be(0);
+ source.addFeatures([feature]);
+ expect(collection.getLength()).to.be(1);
+ source.clear();
+ expect(collection.getLength()).to.be(0);
+ });
+
+ it('keeps the source\'s features in sync with the collection', function() {
+ var feature = new ol.Feature();
+ collection.push(feature);
+ expect(source.getFeatures().length).to.be(1);
+ collection.remove(feature);
+ expect(source.getFeatures().length).to.be(0);
+ collection.extend([feature]);
+ expect(source.getFeatures().length).to.be(1);
+ collection.clear();
+ expect(source.getFeatures().length).to.be(0);
+ });
+
+ });
+
});
goog.require('goog.events');
+goog.require('ol.Collection');
goog.require('ol.Feature');
goog.require('ol.geom.Point');
goog.require('ol.proj');