Alternatively manage features in an ol.Collection
ol.layer.Vector can now manage both an RTree and a Collection of features. The new useSpatialIndex option allows to opt out of RTree management, and the new ol.Collection type of the features option allows to opt in for Collection management.
This commit is contained in:
@@ -4977,12 +4977,13 @@ olx.source.TileWMSOptions.prototype.wrapX;
|
||||
|
||||
/**
|
||||
* @typedef {{attributions: (Array.<ol.Attribution>|undefined),
|
||||
* features: (Array.<ol.Feature>|undefined),
|
||||
* features: (Array.<ol.Feature>|ol.Collection.<ol.Feature>|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.<ol.Feature>|undefined}
|
||||
* Features. If provided as {@link ol.Collection}, the features in the source
|
||||
* and the collection will stay in sync.
|
||||
* @type {Array.<ol.Feature>|ol.Collection.<ol.Feature>|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
|
||||
|
||||
@@ -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.<ol.Feature>}
|
||||
*/
|
||||
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.<ol.Feature>}
|
||||
*/
|
||||
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.<ol.Feature>} 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.<ol.Feature>}
|
||||
* @api
|
||||
*/
|
||||
ol.source.Vector.prototype.getFeaturesCollection = function() {
|
||||
return this.featuresCollection_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get all features on the source.
|
||||
* @return {Array.<ol.Feature>} 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.<ol.Feature>} 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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user