From 1eeea2aa4d1284129c2afa7ec9dac7b879c4a95f Mon Sep 17 00:00:00 2001 From: ahocevar Date: Mon, 6 Aug 2018 15:23:09 +0200 Subject: [PATCH] Use class method syntax instead of .prototype.method = function --- src/ol/source/UTFGrid.js | 619 ++++++++-------- src/ol/source/Vector.js | 1332 +++++++++++++++++------------------ src/ol/source/VectorTile.js | 156 ++-- src/ol/source/WMTS.js | 222 +++--- src/ol/source/Zoomify.js | 47 +- 5 files changed, 1185 insertions(+), 1191 deletions(-) diff --git a/src/ol/source/UTFGrid.js b/src/ol/source/UTFGrid.js index d023140681..30c9407a3e 100644 --- a/src/ol/source/UTFGrid.js +++ b/src/ol/source/UTFGrid.js @@ -76,178 +76,176 @@ export class CustomTile extends Tile { } -} - - -/** - * Get the image element for this tile. - * @return {HTMLImageElement} Image. - */ -CustomTile.prototype.getImage = function() { - return null; -}; - - -/** - * Synchronously returns data at given coordinate (if available). - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @return {*} The data. - */ -CustomTile.prototype.getData = function(coordinate) { - if (!this.grid_ || !this.keys_) { - return null; - } - const xRelative = (coordinate[0] - this.extent_[0]) / - (this.extent_[2] - this.extent_[0]); - const yRelative = (coordinate[1] - this.extent_[1]) / - (this.extent_[3] - this.extent_[1]); - - const row = this.grid_[Math.floor((1 - yRelative) * this.grid_.length)]; - - if (typeof row !== 'string') { + /** + * Get the image element for this tile. + * @return {HTMLImageElement} Image. + */ + getImage() { return null; } - let code = row.charCodeAt(Math.floor(xRelative * row.length)); - if (code >= 93) { - code--; - } - if (code >= 35) { - code--; - } - code -= 32; - let data = null; - if (code in this.keys_) { - const id = this.keys_[code]; - if (this.data_ && id in this.data_) { - data = this.data_[id]; - } else { - data = id; + /** + * Synchronously returns data at given coordinate (if available). + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @return {*} The data. + */ + getData(coordinate) { + if (!this.grid_ || !this.keys_) { + return null; } + const xRelative = (coordinate[0] - this.extent_[0]) / + (this.extent_[2] - this.extent_[0]); + const yRelative = (coordinate[1] - this.extent_[1]) / + (this.extent_[3] - this.extent_[1]); + + const row = this.grid_[Math.floor((1 - yRelative) * this.grid_.length)]; + + if (typeof row !== 'string') { + return null; + } + + let code = row.charCodeAt(Math.floor(xRelative * row.length)); + if (code >= 93) { + code--; + } + if (code >= 35) { + code--; + } + code -= 32; + + let data = null; + if (code in this.keys_) { + const id = this.keys_[code]; + if (this.data_ && id in this.data_) { + data = this.data_[id]; + } else { + data = id; + } + } + return data; } - return data; -}; -/** - * Calls the callback (synchronously by default) with the available data - * for given coordinate (or `null` if not yet loaded). - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {function(this: T, *)} callback Callback. - * @param {T=} opt_this The object to use as `this` in the callback. - * @param {boolean=} opt_request If `true` the callback is always async. - * The tile data is requested if not yet loaded. - * @template T - */ -CustomTile.prototype.forDataAtCoordinate = function(coordinate, callback, opt_this, opt_request) { - if (this.state == TileState.IDLE && opt_request === true) { - listenOnce(this, EventType.CHANGE, function(e) { - callback.call(opt_this, this.getData(coordinate)); - }, this); - this.loadInternal_(); - } else { - if (opt_request === true) { - setTimeout(function() { + /** + * Calls the callback (synchronously by default) with the available data + * for given coordinate (or `null` if not yet loaded). + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {function(this: T, *)} callback Callback. + * @param {T=} opt_this The object to use as `this` in the callback. + * @param {boolean=} opt_request If `true` the callback is always async. + * The tile data is requested if not yet loaded. + * @template T + */ + forDataAtCoordinate(coordinate, callback, opt_this, opt_request) { + if (this.state == TileState.IDLE && opt_request === true) { + listenOnce(this, EventType.CHANGE, function(e) { callback.call(opt_this, this.getData(coordinate)); - }.bind(this), 0); + }, this); + this.loadInternal_(); } else { - callback.call(opt_this, this.getData(coordinate)); + if (opt_request === true) { + setTimeout(function() { + callback.call(opt_this, this.getData(coordinate)); + }.bind(this), 0); + } else { + callback.call(opt_this, this.getData(coordinate)); + } } } -}; -/** - * @inheritDoc - */ -CustomTile.prototype.getKey = function() { - return this.src_; -}; + /** + * @inheritDoc + */ + getKey() { + return this.src_; + } -/** - * @private - */ -CustomTile.prototype.handleError_ = function() { - this.state = TileState.ERROR; - this.changed(); -}; + /** + * @private + */ + handleError_() { + this.state = TileState.ERROR; + this.changed(); + } -/** - * @param {!UTFGridJSON} json UTFGrid data. - * @private - */ -CustomTile.prototype.handleLoad_ = function(json) { - this.grid_ = json.grid; - this.keys_ = json.keys; - this.data_ = json.data; + /** + * @param {!UTFGridJSON} json UTFGrid data. + * @private + */ + handleLoad_(json) { + this.grid_ = json.grid; + this.keys_ = json.keys; + this.data_ = json.data; - this.state = TileState.EMPTY; - this.changed(); -}; + this.state = TileState.EMPTY; + this.changed(); + } -/** - * @private - */ -CustomTile.prototype.loadInternal_ = function() { - if (this.state == TileState.IDLE) { - this.state = TileState.LOADING; - if (this.jsonp_) { - requestJSONP(this.src_, this.handleLoad_.bind(this), - this.handleError_.bind(this)); - } else { - const client = new XMLHttpRequest(); - client.addEventListener('load', this.onXHRLoad_.bind(this)); - client.addEventListener('error', this.onXHRError_.bind(this)); - client.open('GET', this.src_); - client.send(); + /** + * @private + */ + loadInternal_() { + if (this.state == TileState.IDLE) { + this.state = TileState.LOADING; + if (this.jsonp_) { + requestJSONP(this.src_, this.handleLoad_.bind(this), + this.handleError_.bind(this)); + } else { + const client = new XMLHttpRequest(); + client.addEventListener('load', this.onXHRLoad_.bind(this)); + client.addEventListener('error', this.onXHRError_.bind(this)); + client.open('GET', this.src_); + client.send(); + } } } -}; -/** - * @private - * @param {Event} event The load event. - */ -CustomTile.prototype.onXHRLoad_ = function(event) { - const client = /** @type {XMLHttpRequest} */ (event.target); - // status will be 0 for file:// urls - if (!client.status || client.status >= 200 && client.status < 300) { - let response; - try { - response = /** @type {!UTFGridJSON} */(JSON.parse(client.responseText)); - } catch (err) { + /** + * @private + * @param {Event} event The load event. + */ + onXHRLoad_(event) { + const client = /** @type {XMLHttpRequest} */ (event.target); + // status will be 0 for file:// urls + if (!client.status || client.status >= 200 && client.status < 300) { + let response; + try { + response = /** @type {!UTFGridJSON} */(JSON.parse(client.responseText)); + } catch (err) { + this.handleError_(); + return; + } + this.handleLoad_(response); + } else { this.handleError_(); - return; } - this.handleLoad_(response); - } else { + } + + + /** + * @private + * @param {Event} event The error event. + */ + onXHRError_(event) { this.handleError_(); } -}; -/** - * @private - * @param {Event} event The error event. - */ -CustomTile.prototype.onXHRError_ = function(event) { - this.handleError_(); -}; - - -/** - * @override - */ -CustomTile.prototype.load = function() { - if (this.preemptive_) { - this.loadInternal_(); + /** + * @override + */ + load() { + if (this.preemptive_) { + this.loadInternal_(); + } } -}; +} /** @@ -326,178 +324,177 @@ class UTFGrid extends TileSource { } + + /** + * @private + * @param {Event} event The load event. + */ + onXHRLoad_(event) { + const client = /** @type {XMLHttpRequest} */ (event.target); + // status will be 0 for file:// urls + if (!client.status || client.status >= 200 && client.status < 300) { + let response; + try { + response = /** @type {TileJSON} */(JSON.parse(client.responseText)); + } catch (err) { + this.handleTileJSONError(); + return; + } + this.handleTileJSONResponse(response); + } else { + this.handleTileJSONError(); + } + } + + + /** + * @private + * @param {Event} event The error event. + */ + onXHRError_(event) { + this.handleTileJSONError(); + } + + + /** + * Return the template from TileJSON. + * @return {string|undefined} The template from TileJSON. + * @api + */ + getTemplate() { + return this.template_; + } + + + /** + * Calls the callback (synchronously by default) with the available data + * for given coordinate and resolution (or `null` if not yet loaded or + * in case of an error). + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {number} resolution Resolution. + * @param {function(*)} callback Callback. + * @param {boolean=} opt_request If `true` the callback is always async. + * The tile data is requested if not yet loaded. + * @api + */ + forDataAtCoordinateAndResolution( + coordinate, resolution, callback, opt_request) { + if (this.tileGrid) { + const tileCoord = this.tileGrid.getTileCoordForCoordAndResolution( + coordinate, resolution); + const tile = /** @type {!module:ol/source/UTFGrid~CustomTile} */(this.getTile( + tileCoord[0], tileCoord[1], tileCoord[2], 1, this.getProjection())); + tile.forDataAtCoordinate(coordinate, callback, null, opt_request); + } else { + if (opt_request === true) { + setTimeout(function() { + callback(null); + }, 0); + } else { + callback(null); + } + } + } + + + /** + * @protected + */ + handleTileJSONError() { + this.setState(SourceState.ERROR); + } + + + /** + * TODO: very similar to ol/source/TileJSON#handleTileJSONResponse + * @protected + * @param {TileJSON} tileJSON Tile JSON. + */ + handleTileJSONResponse(tileJSON) { + + const epsg4326Projection = getProjection('EPSG:4326'); + + const sourceProjection = this.getProjection(); + let extent; + if (tileJSON.bounds !== undefined) { + const transform = getTransformFromProjections( + epsg4326Projection, sourceProjection); + extent = applyTransform(tileJSON.bounds, transform); + } + + const minZoom = tileJSON.minzoom || 0; + const maxZoom = tileJSON.maxzoom || 22; + const tileGrid = createXYZ({ + extent: extentFromProjection(sourceProjection), + maxZoom: maxZoom, + minZoom: minZoom + }); + this.tileGrid = tileGrid; + + this.template_ = tileJSON.template; + + const grids = tileJSON.grids; + if (!grids) { + this.setState(SourceState.ERROR); + return; + } + + this.tileUrlFunction_ = createFromTemplates(grids, tileGrid); + + if (tileJSON.attribution !== undefined) { + const attributionExtent = extent !== undefined ? + extent : epsg4326Projection.getExtent(); + + this.setAttributions(function(frameState) { + if (intersects(attributionExtent, frameState.extent)) { + return [tileJSON.attribution]; + } + return null; + }); + } + + this.setState(SourceState.READY); + + } + + + /** + * @inheritDoc + */ + getTile(z, x, y, pixelRatio, projection) { + const tileCoordKey = getKeyZXY(z, x, y); + if (this.tileCache.containsKey(tileCoordKey)) { + return ( + /** @type {!module:ol/Tile} */ (this.tileCache.get(tileCoordKey)) + ); + } else { + const tileCoord = [z, x, y]; + const urlTileCoord = + this.getTileCoordForTileUrlFunction(tileCoord, projection); + const tileUrl = this.tileUrlFunction_(urlTileCoord, pixelRatio, projection); + const tile = new CustomTile( + tileCoord, + tileUrl !== undefined ? TileState.IDLE : TileState.EMPTY, + tileUrl !== undefined ? tileUrl : '', + this.tileGrid.getTileCoordExtent(tileCoord), + this.preemptive_, + this.jsonp_); + this.tileCache.set(tileCoordKey, tile); + return tile; + } + } + + + /** + * @inheritDoc + */ + useTile(z, x, y) { + const tileCoordKey = getKeyZXY(z, x, y); + if (this.tileCache.containsKey(tileCoordKey)) { + this.tileCache.get(tileCoordKey); + } + } } -/** - * @private - * @param {Event} event The load event. - */ -UTFGrid.prototype.onXHRLoad_ = function(event) { - const client = /** @type {XMLHttpRequest} */ (event.target); - // status will be 0 for file:// urls - if (!client.status || client.status >= 200 && client.status < 300) { - let response; - try { - response = /** @type {TileJSON} */(JSON.parse(client.responseText)); - } catch (err) { - this.handleTileJSONError(); - return; - } - this.handleTileJSONResponse(response); - } else { - this.handleTileJSONError(); - } -}; - - -/** - * @private - * @param {Event} event The error event. - */ -UTFGrid.prototype.onXHRError_ = function(event) { - this.handleTileJSONError(); -}; - - -/** - * Return the template from TileJSON. - * @return {string|undefined} The template from TileJSON. - * @api - */ -UTFGrid.prototype.getTemplate = function() { - return this.template_; -}; - - -/** - * Calls the callback (synchronously by default) with the available data - * for given coordinate and resolution (or `null` if not yet loaded or - * in case of an error). - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {number} resolution Resolution. - * @param {function(*)} callback Callback. - * @param {boolean=} opt_request If `true` the callback is always async. - * The tile data is requested if not yet loaded. - * @api - */ -UTFGrid.prototype.forDataAtCoordinateAndResolution = function( - coordinate, resolution, callback, opt_request) { - if (this.tileGrid) { - const tileCoord = this.tileGrid.getTileCoordForCoordAndResolution( - coordinate, resolution); - const tile = /** @type {!module:ol/source/UTFGrid~CustomTile} */(this.getTile( - tileCoord[0], tileCoord[1], tileCoord[2], 1, this.getProjection())); - tile.forDataAtCoordinate(coordinate, callback, null, opt_request); - } else { - if (opt_request === true) { - setTimeout(function() { - callback(null); - }, 0); - } else { - callback(null); - } - } -}; - - -/** - * @protected - */ -UTFGrid.prototype.handleTileJSONError = function() { - this.setState(SourceState.ERROR); -}; - - -/** - * TODO: very similar to ol/source/TileJSON#handleTileJSONResponse - * @protected - * @param {TileJSON} tileJSON Tile JSON. - */ -UTFGrid.prototype.handleTileJSONResponse = function(tileJSON) { - - const epsg4326Projection = getProjection('EPSG:4326'); - - const sourceProjection = this.getProjection(); - let extent; - if (tileJSON.bounds !== undefined) { - const transform = getTransformFromProjections( - epsg4326Projection, sourceProjection); - extent = applyTransform(tileJSON.bounds, transform); - } - - const minZoom = tileJSON.minzoom || 0; - const maxZoom = tileJSON.maxzoom || 22; - const tileGrid = createXYZ({ - extent: extentFromProjection(sourceProjection), - maxZoom: maxZoom, - minZoom: minZoom - }); - this.tileGrid = tileGrid; - - this.template_ = tileJSON.template; - - const grids = tileJSON.grids; - if (!grids) { - this.setState(SourceState.ERROR); - return; - } - - this.tileUrlFunction_ = createFromTemplates(grids, tileGrid); - - if (tileJSON.attribution !== undefined) { - const attributionExtent = extent !== undefined ? - extent : epsg4326Projection.getExtent(); - - this.setAttributions(function(frameState) { - if (intersects(attributionExtent, frameState.extent)) { - return [tileJSON.attribution]; - } - return null; - }); - } - - this.setState(SourceState.READY); - -}; - - -/** - * @inheritDoc - */ -UTFGrid.prototype.getTile = function(z, x, y, pixelRatio, projection) { - const tileCoordKey = getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - return ( - /** @type {!module:ol/Tile} */ (this.tileCache.get(tileCoordKey)) - ); - } else { - const tileCoord = [z, x, y]; - const urlTileCoord = - this.getTileCoordForTileUrlFunction(tileCoord, projection); - const tileUrl = this.tileUrlFunction_(urlTileCoord, pixelRatio, projection); - const tile = new CustomTile( - tileCoord, - tileUrl !== undefined ? TileState.IDLE : TileState.EMPTY, - tileUrl !== undefined ? tileUrl : '', - this.tileGrid.getTileCoordExtent(tileCoord), - this.preemptive_, - this.jsonp_); - this.tileCache.set(tileCoordKey, tile); - return tile; - } -}; - - -/** - * @inheritDoc - */ -UTFGrid.prototype.useTile = function(z, x, y) { - const tileCoordKey = getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - this.tileCache.get(tileCoordKey); - } -}; - - export default UTFGrid; diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js index 2b6a882e52..28d2c61d9c 100644 --- a/src/ol/source/Vector.js +++ b/src/ol/source/Vector.js @@ -276,711 +276,711 @@ class VectorSource extends Source { } -} - - -/** - * Add a single feature to the source. If you want to add a batch of features - * at once, call {@link module:ol/source/Vector~VectorSource#addFeatures #addFeatures()} - * instead. A feature will not be added to the source if feature with - * the same id is already there. The reason for this behavior is to avoid - * feature duplication when using bbox or tile loading strategies. - * @param {module:ol/Feature} feature Feature to add. - * @api - */ -VectorSource.prototype.addFeature = function(feature) { - this.addFeatureInternal(feature); - this.changed(); -}; - - -/** - * Add a feature without firing a `change` event. - * @param {module:ol/Feature} feature Feature. - * @protected - */ -VectorSource.prototype.addFeatureInternal = function(feature) { - const featureKey = getUid(feature).toString(); - - if (!this.addToIndex_(featureKey, feature)) { - return; + /** + * Add a single feature to the source. If you want to add a batch of features + * at once, call {@link module:ol/source/Vector~VectorSource#addFeatures #addFeatures()} + * instead. A feature will not be added to the source if feature with + * the same id is already there. The reason for this behavior is to avoid + * feature duplication when using bbox or tile loading strategies. + * @param {module:ol/Feature} feature Feature to add. + * @api + */ + addFeature(feature) { + this.addFeatureInternal(feature); + this.changed(); } - this.setupChangeEvents_(featureKey, feature); - const geometry = feature.getGeometry(); - if (geometry) { - const extent = geometry.getExtent(); - if (this.featuresRtree_) { - this.featuresRtree_.insert(extent, feature); - } - } else { - this.nullGeometryFeatures_[featureKey] = feature; - } - - this.dispatchEvent( - new VectorSourceEvent(VectorEventType.ADDFEATURE, feature)); -}; - - -/** - * @param {string} featureKey Unique identifier for the feature. - * @param {module:ol/Feature} feature The feature. - * @private - */ -VectorSource.prototype.setupChangeEvents_ = function(featureKey, feature) { - this.featureChangeKeys_[featureKey] = [ - listen(feature, EventType.CHANGE, - this.handleFeatureChange_, this), - listen(feature, ObjectEventType.PROPERTYCHANGE, - this.handleFeatureChange_, this) - ]; -}; - - -/** - * @param {string} featureKey Unique identifier for the feature. - * @param {module:ol/Feature} feature The feature. - * @return {boolean} The feature is "valid", in the sense that it is also a - * candidate for insertion into the Rtree. - * @private - */ -VectorSource.prototype.addToIndex_ = function(featureKey, feature) { - let valid = true; - const id = feature.getId(); - if (id !== undefined) { - if (!(id.toString() in this.idIndex_)) { - this.idIndex_[id.toString()] = feature; - } else { - valid = false; - } - } else { - assert(!(featureKey in this.undefIdIndex_), - 30); // The passed `feature` was already added to the source - this.undefIdIndex_[featureKey] = feature; - } - return valid; -}; - - -/** - * Add a batch of features to the source. - * @param {Array} features Features to add. - * @api - */ -VectorSource.prototype.addFeatures = function(features) { - this.addFeaturesInternal(features); - this.changed(); -}; - - -/** - * Add features without firing a `change` event. - * @param {Array} features Features. - * @protected - */ -VectorSource.prototype.addFeaturesInternal = function(features) { - const extents = []; - const newFeatures = []; - const geometryFeatures = []; - - for (let i = 0, length = features.length; i < length; i++) { - const feature = features[i]; + /** + * Add a feature without firing a `change` event. + * @param {module:ol/Feature} feature Feature. + * @protected + */ + addFeatureInternal(feature) { const featureKey = getUid(feature).toString(); - if (this.addToIndex_(featureKey, feature)) { - newFeatures.push(feature); - } - } - for (let i = 0, length = newFeatures.length; i < length; i++) { - const feature = newFeatures[i]; - const featureKey = getUid(feature).toString(); + if (!this.addToIndex_(featureKey, feature)) { + return; + } + this.setupChangeEvents_(featureKey, feature); const geometry = feature.getGeometry(); if (geometry) { const extent = geometry.getExtent(); - extents.push(extent); - geometryFeatures.push(feature); - } else { - this.nullGeometryFeatures_[featureKey] = feature; - } - } - if (this.featuresRtree_) { - this.featuresRtree_.load(extents, geometryFeatures); - } - - for (let i = 0, length = newFeatures.length; i < length; i++) { - this.dispatchEvent(new VectorSourceEvent(VectorEventType.ADDFEATURE, newFeatures[i])); - } -}; - - -/** - * @param {!module:ol/Collection} collection Collection. - * @private - */ -VectorSource.prototype.bindFeaturesCollection_ = function(collection) { - let modifyingCollection = false; - listen(this, VectorEventType.ADDFEATURE, - function(evt) { - if (!modifyingCollection) { - modifyingCollection = true; - collection.push(evt.feature); - modifyingCollection = false; - } - }); - listen(this, VectorEventType.REMOVEFEATURE, - function(evt) { - if (!modifyingCollection) { - modifyingCollection = true; - collection.remove(evt.feature); - modifyingCollection = false; - } - }); - listen(collection, CollectionEventType.ADD, - function(evt) { - if (!modifyingCollection) { - modifyingCollection = true; - this.addFeature(/** @type {module:ol/Feature} */ (evt.element)); - modifyingCollection = false; - } - }, this); - listen(collection, CollectionEventType.REMOVE, - function(evt) { - if (!modifyingCollection) { - modifyingCollection = true; - this.removeFeature(/** @type {module:ol/Feature} */ (evt.element)); - modifyingCollection = false; - } - }, this); - this.featuresCollection_ = collection; -}; - - -/** - * Remove all features from the source. - * @param {boolean=} opt_fast Skip dispatching of {@link module:ol/source/Vector~VectorSourceEvent#removefeature} events. - * @api - */ -VectorSource.prototype.clear = function(opt_fast) { - if (opt_fast) { - for (const featureId in this.featureChangeKeys_) { - const keys = this.featureChangeKeys_[featureId]; - keys.forEach(unlistenByKey); - } - if (!this.featuresCollection_) { - this.featureChangeKeys_ = {}; - this.idIndex_ = {}; - this.undefIdIndex_ = {}; - } - } else { - if (this.featuresRtree_) { - this.featuresRtree_.forEach(this.removeFeatureInternal, this); - for (const id in this.nullGeometryFeatures_) { - this.removeFeatureInternal(this.nullGeometryFeatures_[id]); - } - } - } - if (this.featuresCollection_) { - this.featuresCollection_.clear(); - } - - if (this.featuresRtree_) { - this.featuresRtree_.clear(); - } - this.loadedExtentsRtree_.clear(); - this.nullGeometryFeatures_ = {}; - - const clearEvent = new VectorSourceEvent(VectorEventType.CLEAR); - this.dispatchEvent(clearEvent); - this.changed(); -}; - - -/** - * Iterate through all features on the source, calling the provided callback - * with each one. If the callback returns any "truthy" value, iteration will - * stop and the function will return the same value. - * Note: this function only iterate through the feature that have a defined geometry. - * - * @param {function(module:ol/Feature): T} callback Called with each feature - * on the source. Return a truthy value to stop iteration. - * @return {T|undefined} The return value from the last call to the callback. - * @template T - * @api - */ -VectorSource.prototype.forEachFeature = function(callback) { - if (this.featuresRtree_) { - return this.featuresRtree_.forEach(callback); - } else if (this.featuresCollection_) { - return this.featuresCollection_.forEach(callback); - } -}; - - -/** - * Iterate through all features whose geometries contain the provided - * coordinate, calling the callback with each feature. If the callback returns - * a "truthy" value, iteration will stop and the function will return the same - * value. - * - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {function(module:ol/Feature): T} callback Called with each feature - * whose goemetry contains the provided coordinate. - * @return {T|undefined} The return value from the last call to the callback. - * @template T - */ -VectorSource.prototype.forEachFeatureAtCoordinateDirect = function(coordinate, callback) { - const extent = [coordinate[0], coordinate[1], coordinate[0], coordinate[1]]; - return this.forEachFeatureInExtent(extent, function(feature) { - const geometry = feature.getGeometry(); - if (geometry.intersectsCoordinate(coordinate)) { - return callback(feature); - } else { - return undefined; - } - }); -}; - - -/** - * Iterate through all features whose bounding box intersects the provided - * extent (note that the feature's geometry may not intersect the extent), - * calling the callback with each feature. If the callback returns a "truthy" - * value, iteration will stop and the function will return the same value. - * - * If you are interested in features whose geometry intersects an extent, call - * the {@link module:ol/source/Vector~VectorSource#forEachFeatureIntersectingExtent #forEachFeatureIntersectingExtent()} method instead. - * - * When `useSpatialIndex` is set to false, this method will loop through all - * features, equivalent to {@link module:ol/source/Vector~VectorSource#forEachFeature #forEachFeature()}. - * - * @param {module:ol/extent~Extent} extent Extent. - * @param {function(module:ol/Feature): T} callback Called with each feature - * whose bounding box intersects the provided extent. - * @return {T|undefined} The return value from the last call to the callback. - * @template T - * @api - */ -VectorSource.prototype.forEachFeatureInExtent = function(extent, callback) { - if (this.featuresRtree_) { - return this.featuresRtree_.forEachInExtent(extent, callback); - } else if (this.featuresCollection_) { - return this.featuresCollection_.forEach(callback); - } -}; - - -/** - * Iterate through all features whose geometry intersects the provided extent, - * calling the callback with each feature. If the callback returns a "truthy" - * value, iteration will stop and the function will return the same value. - * - * If you only want to test for bounding box intersection, call the - * {@link module:ol/source/Vector~VectorSource#forEachFeatureInExtent #forEachFeatureInExtent()} method instead. - * - * @param {module:ol/extent~Extent} extent Extent. - * @param {function(module:ol/Feature): T} callback Called with each feature - * whose geometry intersects the provided extent. - * @return {T|undefined} The return value from the last call to the callback. - * @template T - * @api - */ -VectorSource.prototype.forEachFeatureIntersectingExtent = function(extent, callback) { - return this.forEachFeatureInExtent(extent, - /** - * @param {module:ol/Feature} feature Feature. - * @return {T|undefined} The return value from the last call to the callback. - * @template T - */ - function(feature) { - const geometry = feature.getGeometry(); - if (geometry.intersectsExtent(extent)) { - const result = callback(feature); - if (result) { - return result; - } - } - }); -}; - - -/** - * 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 module:ol/Collection} as `features`. - * @return {module:ol/Collection} The collection of features. - * @api - */ -VectorSource.prototype.getFeaturesCollection = function() { - return this.featuresCollection_; -}; - - -/** - * Get all features on the source in random order. - * @return {Array} Features. - * @api - */ -VectorSource.prototype.getFeatures = function() { - let features; - if (this.featuresCollection_) { - features = this.featuresCollection_.getArray(); - } else if (this.featuresRtree_) { - features = this.featuresRtree_.getAll(); - if (!isEmpty(this.nullGeometryFeatures_)) { - extend(features, getValues(this.nullGeometryFeatures_)); - } - } - return ( - /** @type {Array} */ (features) - ); -}; - - -/** - * Get all features whose geometry intersects the provided coordinate. - * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @return {Array} Features. - * @api - */ -VectorSource.prototype.getFeaturesAtCoordinate = function(coordinate) { - const features = []; - this.forEachFeatureAtCoordinateDirect(coordinate, function(feature) { - features.push(feature); - }); - return features; -}; - - -/** - * Get all features in the provided extent. Note that this returns an array of - * all features intersecting the given extent in random order (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 {module:ol/extent~Extent} extent Extent. - * @return {Array} Features. - * @api - */ -VectorSource.prototype.getFeaturesInExtent = function(extent) { - 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 {module:ol/coordinate~Coordinate} coordinate Coordinate. - * @param {function(module:ol/Feature):boolean=} opt_filter Feature filter function. - * The filter function will receive one argument, the {@link module:ol/Feature feature} - * and it should return a boolean value. By default, no filtering is made. - * @return {module:ol/Feature} Closest feature. - * @api - */ -VectorSource.prototype.getClosestFeatureToCoordinate = function(coordinate, opt_filter) { - // Find the closest feature using branch and bound. We start searching an - // infinite extent, and find the distance from the first feature found. This - // becomes the closest feature. We then compute a smaller extent which any - // closer feature must intersect. We continue searching with this smaller - // extent, trying to find a closer feature. Every time we find a closer - // feature, we update the extent being searched so that any even closer - // feature must intersect it. We continue until we run out of features. - const x = coordinate[0]; - const y = coordinate[1]; - let closestFeature = null; - const closestPoint = [NaN, NaN]; - let minSquaredDistance = Infinity; - const extent = [-Infinity, -Infinity, Infinity, Infinity]; - const filter = opt_filter ? opt_filter : TRUE; - this.featuresRtree_.forEachInExtent(extent, - /** - * @param {module:ol/Feature} feature Feature. - */ - function(feature) { - if (filter(feature)) { - const geometry = feature.getGeometry(); - const previousMinSquaredDistance = minSquaredDistance; - minSquaredDistance = geometry.closestPointXY( - x, y, closestPoint, minSquaredDistance); - if (minSquaredDistance < previousMinSquaredDistance) { - closestFeature = feature; - // This is sneaky. Reduce the extent that it is currently being - // searched while the R-Tree traversal using this same extent object - // is still in progress. This is safe because the new extent is - // strictly contained by the old extent. - const minDistance = Math.sqrt(minSquaredDistance); - extent[0] = x - minDistance; - extent[1] = y - minDistance; - extent[2] = x + minDistance; - extent[3] = y + minDistance; - } - } - }); - return closestFeature; -}; - - -/** - * 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`. - * @param {module:ol/extent~Extent=} opt_extent Destination extent. If provided, no new extent - * will be created. Instead, that extent's coordinates will be overwritten. - * @return {module:ol/extent~Extent} Extent. - * @api - */ -VectorSource.prototype.getExtent = function(opt_extent) { - return this.featuresRtree_.getExtent(opt_extent); -}; - - -/** - * Get a feature by its identifier (the value returned by feature.getId()). - * Note that the index treats string and numeric identifiers as the same. So - * `source.getFeatureById(2)` will return a feature with id `'2'` or `2`. - * - * @param {string|number} id Feature identifier. - * @return {module:ol/Feature} The feature (or `null` if not found). - * @api - */ -VectorSource.prototype.getFeatureById = function(id) { - const feature = this.idIndex_[id.toString()]; - return feature !== undefined ? feature : null; -}; - - -/** - * Get the format associated with this source. - * - * @return {module:ol/format/Feature|undefined} The feature format. - * @api - */ -VectorSource.prototype.getFormat = function() { - return this.format_; -}; - - -/** - * @return {boolean} The source can have overlapping geometries. - */ -VectorSource.prototype.getOverlaps = function() { - return this.overlaps_; -}; - - -/** - * @override - */ -VectorSource.prototype.getResolutions = function() {}; - - -/** - * Get the url associated with this source. - * - * @return {string|module:ol/featureloader~FeatureUrlFunction|undefined} The url. - * @api - */ -VectorSource.prototype.getUrl = function() { - return this.url_; -}; - - -/** - * @param {module:ol/events/Event} event Event. - * @private - */ -VectorSource.prototype.handleFeatureChange_ = function(event) { - const feature = /** @type {module:ol/Feature} */ (event.target); - const featureKey = getUid(feature).toString(); - const geometry = feature.getGeometry(); - if (!geometry) { - if (!(featureKey in this.nullGeometryFeatures_)) { - if (this.featuresRtree_) { - this.featuresRtree_.remove(feature); - } - this.nullGeometryFeatures_[featureKey] = feature; - } - } else { - const extent = geometry.getExtent(); - if (featureKey in this.nullGeometryFeatures_) { - delete this.nullGeometryFeatures_[featureKey]; if (this.featuresRtree_) { this.featuresRtree_.insert(extent, feature); } } else { - if (this.featuresRtree_) { - this.featuresRtree_.update(extent, feature); - } + this.nullGeometryFeatures_[featureKey] = feature; } + + this.dispatchEvent( + new VectorSourceEvent(VectorEventType.ADDFEATURE, feature)); } - const id = feature.getId(); - if (id !== undefined) { - const sid = id.toString(); - if (featureKey in this.undefIdIndex_) { - delete this.undefIdIndex_[featureKey]; - this.idIndex_[sid] = feature; - } else { - if (this.idIndex_[sid] !== feature) { - this.removeFromIdIndex_(feature); - this.idIndex_[sid] = feature; + + + /** + * @param {string} featureKey Unique identifier for the feature. + * @param {module:ol/Feature} feature The feature. + * @private + */ + setupChangeEvents_(featureKey, feature) { + this.featureChangeKeys_[featureKey] = [ + listen(feature, EventType.CHANGE, + this.handleFeatureChange_, this), + listen(feature, ObjectEventType.PROPERTYCHANGE, + this.handleFeatureChange_, this) + ]; + } + + + /** + * @param {string} featureKey Unique identifier for the feature. + * @param {module:ol/Feature} feature The feature. + * @return {boolean} The feature is "valid", in the sense that it is also a + * candidate for insertion into the Rtree. + * @private + */ + addToIndex_(featureKey, feature) { + let valid = true; + const id = feature.getId(); + if (id !== undefined) { + if (!(id.toString() in this.idIndex_)) { + this.idIndex_[id.toString()] = feature; + } else { + valid = false; } - } - } else { - if (!(featureKey in this.undefIdIndex_)) { - this.removeFromIdIndex_(feature); + } else { + assert(!(featureKey in this.undefIdIndex_), + 30); // The passed `feature` was already added to the source this.undefIdIndex_[featureKey] = feature; } + return valid; } - this.changed(); - this.dispatchEvent(new VectorSourceEvent( - VectorEventType.CHANGEFEATURE, feature)); -}; -/** - * Returns true if the feature is contained within the source. - * @param {module:ol/Feature} feature Feature. - * @return {boolean} Has feature. - * @api - */ -VectorSource.prototype.hasFeature = function(feature) { - const id = feature.getId(); - if (id !== undefined) { - return id in this.idIndex_; - } else { - const featureKey = getUid(feature).toString(); - return featureKey in this.undefIdIndex_; + + /** + * Add a batch of features to the source. + * @param {Array} features Features to add. + * @api + */ + addFeatures(features) { + this.addFeaturesInternal(features); + this.changed(); } -}; - -/** - * @return {boolean} Is empty. - */ -VectorSource.prototype.isEmpty = function() { - return this.featuresRtree_.isEmpty() && isEmpty(this.nullGeometryFeatures_); -}; -/** - * @param {module:ol/extent~Extent} extent Extent. - * @param {number} resolution Resolution. - * @param {module:ol/proj/Projection} projection Projection. - */ -VectorSource.prototype.loadFeatures = function(extent, resolution, projection) { - const loadedExtentsRtree = this.loadedExtentsRtree_; - const extentsToLoad = this.strategy_(extent, resolution); - for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) { - const extentToLoad = extentsToLoad[i]; - const alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad, - /** - * @param {{extent: module:ol/extent~Extent}} object Object. - * @return {boolean} Contains. - */ - function(object) { - return containsExtent(object.extent, extentToLoad); - }); - if (!alreadyLoaded) { - this.loader_.call(this, extentToLoad, resolution, projection); - loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()}); + /** + * Add features without firing a `change` event. + * @param {Array} features Features. + * @protected + */ + addFeaturesInternal(features) { + const extents = []; + const newFeatures = []; + const geometryFeatures = []; + + for (let i = 0, length = features.length; i < length; i++) { + const feature = features[i]; + const featureKey = getUid(feature).toString(); + if (this.addToIndex_(featureKey, feature)) { + newFeatures.push(feature); + } } - } -}; + for (let i = 0, length = newFeatures.length; i < length; i++) { + const feature = newFeatures[i]; + const featureKey = getUid(feature).toString(); + this.setupChangeEvents_(featureKey, feature); -/** - * Remove an extent from the list of loaded extents. - * @param {module:ol/extent~Extent} extent Extent. - * @api - */ -VectorSource.prototype.removeLoadedExtent = function(extent) { - const loadedExtentsRtree = this.loadedExtentsRtree_; - let obj; - loadedExtentsRtree.forEachInExtent(extent, function(object) { - if (equals(object.extent, extent)) { - obj = object; - return true; + const geometry = feature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + extents.push(extent); + geometryFeatures.push(feature); + } else { + this.nullGeometryFeatures_[featureKey] = feature; + } } - }); - if (obj) { - loadedExtentsRtree.remove(obj); - } -}; - - -/** - * Remove a single feature from the source. If you want to remove all features - * at once, use the {@link module:ol/source/Vector~VectorSource#clear #clear()} method - * instead. - * @param {module:ol/Feature} feature Feature to remove. - * @api - */ -VectorSource.prototype.removeFeature = function(feature) { - const featureKey = getUid(feature).toString(); - if (featureKey in this.nullGeometryFeatures_) { - delete this.nullGeometryFeatures_[featureKey]; - } else { if (this.featuresRtree_) { - this.featuresRtree_.remove(feature); + this.featuresRtree_.load(extents, geometryFeatures); + } + + for (let i = 0, length = newFeatures.length; i < length; i++) { + this.dispatchEvent(new VectorSourceEvent(VectorEventType.ADDFEATURE, newFeatures[i])); } } - this.removeFeatureInternal(feature); - this.changed(); -}; -/** - * Remove feature without firing a `change` event. - * @param {module:ol/Feature} feature Feature. - * @protected - */ -VectorSource.prototype.removeFeatureInternal = function(feature) { - const featureKey = getUid(feature).toString(); - this.featureChangeKeys_[featureKey].forEach(unlistenByKey); - delete this.featureChangeKeys_[featureKey]; - const id = feature.getId(); - if (id !== undefined) { - delete this.idIndex_[id.toString()]; - } else { - delete this.undefIdIndex_[featureKey]; + /** + * @param {!module:ol/Collection} collection Collection. + * @private + */ + bindFeaturesCollection_(collection) { + let modifyingCollection = false; + listen(this, VectorEventType.ADDFEATURE, + function(evt) { + if (!modifyingCollection) { + modifyingCollection = true; + collection.push(evt.feature); + modifyingCollection = false; + } + }); + listen(this, VectorEventType.REMOVEFEATURE, + function(evt) { + if (!modifyingCollection) { + modifyingCollection = true; + collection.remove(evt.feature); + modifyingCollection = false; + } + }); + listen(collection, CollectionEventType.ADD, + function(evt) { + if (!modifyingCollection) { + modifyingCollection = true; + this.addFeature(/** @type {module:ol/Feature} */ (evt.element)); + modifyingCollection = false; + } + }, this); + listen(collection, CollectionEventType.REMOVE, + function(evt) { + if (!modifyingCollection) { + modifyingCollection = true; + this.removeFeature(/** @type {module:ol/Feature} */ (evt.element)); + modifyingCollection = false; + } + }, this); + this.featuresCollection_ = collection; } - this.dispatchEvent(new VectorSourceEvent( - VectorEventType.REMOVEFEATURE, feature)); -}; -/** - * Remove a feature from the id index. Called internally when the feature id - * may have changed. - * @param {module:ol/Feature} feature The feature. - * @return {boolean} Removed the feature from the index. - * @private - */ -VectorSource.prototype.removeFromIdIndex_ = function(feature) { - let removed = false; - for (const id in this.idIndex_) { - if (this.idIndex_[id] === feature) { - delete this.idIndex_[id]; - removed = true; - break; + /** + * Remove all features from the source. + * @param {boolean=} opt_fast Skip dispatching of {@link module:ol/source/Vector~VectorSourceEvent#removefeature} events. + * @api + */ + clear(opt_fast) { + if (opt_fast) { + for (const featureId in this.featureChangeKeys_) { + const keys = this.featureChangeKeys_[featureId]; + keys.forEach(unlistenByKey); + } + if (!this.featuresCollection_) { + this.featureChangeKeys_ = {}; + this.idIndex_ = {}; + this.undefIdIndex_ = {}; + } + } else { + if (this.featuresRtree_) { + this.featuresRtree_.forEach(this.removeFeatureInternal, this); + for (const id in this.nullGeometryFeatures_) { + this.removeFeatureInternal(this.nullGeometryFeatures_[id]); + } + } + } + if (this.featuresCollection_) { + this.featuresCollection_.clear(); + } + + if (this.featuresRtree_) { + this.featuresRtree_.clear(); + } + this.loadedExtentsRtree_.clear(); + this.nullGeometryFeatures_ = {}; + + const clearEvent = new VectorSourceEvent(VectorEventType.CLEAR); + this.dispatchEvent(clearEvent); + this.changed(); + } + + + /** + * Iterate through all features on the source, calling the provided callback + * with each one. If the callback returns any "truthy" value, iteration will + * stop and the function will return the same value. + * Note: this function only iterate through the feature that have a defined geometry. + * + * @param {function(module:ol/Feature): T} callback Called with each feature + * on the source. Return a truthy value to stop iteration. + * @return {T|undefined} The return value from the last call to the callback. + * @template T + * @api + */ + forEachFeature(callback) { + if (this.featuresRtree_) { + return this.featuresRtree_.forEach(callback); + } else if (this.featuresCollection_) { + return this.featuresCollection_.forEach(callback); } } - return removed; -}; -/** - * Set the new loader of the source. The next loadFeatures call will use the - * new loader. - * @param {module:ol/featureloader~FeatureLoader} loader The loader to set. - * @api - */ -VectorSource.prototype.setLoader = function(loader) { - this.loader_ = loader; -}; + /** + * Iterate through all features whose geometries contain the provided + * coordinate, calling the callback with each feature. If the callback returns + * a "truthy" value, iteration will stop and the function will return the same + * value. + * + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {function(module:ol/Feature): T} callback Called with each feature + * whose goemetry contains the provided coordinate. + * @return {T|undefined} The return value from the last call to the callback. + * @template T + */ + forEachFeatureAtCoordinateDirect(coordinate, callback) { + const extent = [coordinate[0], coordinate[1], coordinate[0], coordinate[1]]; + return this.forEachFeatureInExtent(extent, function(feature) { + const geometry = feature.getGeometry(); + if (geometry.intersectsCoordinate(coordinate)) { + return callback(feature); + } else { + return undefined; + } + }); + } + + + /** + * Iterate through all features whose bounding box intersects the provided + * extent (note that the feature's geometry may not intersect the extent), + * calling the callback with each feature. If the callback returns a "truthy" + * value, iteration will stop and the function will return the same value. + * + * If you are interested in features whose geometry intersects an extent, call + * the {@link module:ol/source/Vector~VectorSource#forEachFeatureIntersectingExtent #forEachFeatureIntersectingExtent()} method instead. + * + * When `useSpatialIndex` is set to false, this method will loop through all + * features, equivalent to {@link module:ol/source/Vector~VectorSource#forEachFeature #forEachFeature()}. + * + * @param {module:ol/extent~Extent} extent Extent. + * @param {function(module:ol/Feature): T} callback Called with each feature + * whose bounding box intersects the provided extent. + * @return {T|undefined} The return value from the last call to the callback. + * @template T + * @api + */ + forEachFeatureInExtent(extent, callback) { + if (this.featuresRtree_) { + return this.featuresRtree_.forEachInExtent(extent, callback); + } else if (this.featuresCollection_) { + return this.featuresCollection_.forEach(callback); + } + } + + + /** + * Iterate through all features whose geometry intersects the provided extent, + * calling the callback with each feature. If the callback returns a "truthy" + * value, iteration will stop and the function will return the same value. + * + * If you only want to test for bounding box intersection, call the + * {@link module:ol/source/Vector~VectorSource#forEachFeatureInExtent #forEachFeatureInExtent()} method instead. + * + * @param {module:ol/extent~Extent} extent Extent. + * @param {function(module:ol/Feature): T} callback Called with each feature + * whose geometry intersects the provided extent. + * @return {T|undefined} The return value from the last call to the callback. + * @template T + * @api + */ + forEachFeatureIntersectingExtent(extent, callback) { + return this.forEachFeatureInExtent(extent, + /** + * @param {module:ol/Feature} feature Feature. + * @return {T|undefined} The return value from the last call to the callback. + * @template T + */ + function(feature) { + const geometry = feature.getGeometry(); + if (geometry.intersectsExtent(extent)) { + const result = callback(feature); + if (result) { + return result; + } + } + }); + } + + + /** + * 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 module:ol/Collection} as `features`. + * @return {module:ol/Collection} The collection of features. + * @api + */ + getFeaturesCollection() { + return this.featuresCollection_; + } + + + /** + * Get all features on the source in random order. + * @return {Array} Features. + * @api + */ + getFeatures() { + let features; + if (this.featuresCollection_) { + features = this.featuresCollection_.getArray(); + } else if (this.featuresRtree_) { + features = this.featuresRtree_.getAll(); + if (!isEmpty(this.nullGeometryFeatures_)) { + extend(features, getValues(this.nullGeometryFeatures_)); + } + } + return ( + /** @type {Array} */ (features) + ); + } + + + /** + * Get all features whose geometry intersects the provided coordinate. + * @param {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @return {Array} Features. + * @api + */ + getFeaturesAtCoordinate(coordinate) { + const features = []; + this.forEachFeatureAtCoordinateDirect(coordinate, function(feature) { + features.push(feature); + }); + return features; + } + + + /** + * Get all features in the provided extent. Note that this returns an array of + * all features intersecting the given extent in random order (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 {module:ol/extent~Extent} extent Extent. + * @return {Array} Features. + * @api + */ + getFeaturesInExtent(extent) { + 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 {module:ol/coordinate~Coordinate} coordinate Coordinate. + * @param {function(module:ol/Feature):boolean=} opt_filter Feature filter function. + * The filter function will receive one argument, the {@link module:ol/Feature feature} + * and it should return a boolean value. By default, no filtering is made. + * @return {module:ol/Feature} Closest feature. + * @api + */ + getClosestFeatureToCoordinate(coordinate, opt_filter) { + // Find the closest feature using branch and bound. We start searching an + // infinite extent, and find the distance from the first feature found. This + // becomes the closest feature. We then compute a smaller extent which any + // closer feature must intersect. We continue searching with this smaller + // extent, trying to find a closer feature. Every time we find a closer + // feature, we update the extent being searched so that any even closer + // feature must intersect it. We continue until we run out of features. + const x = coordinate[0]; + const y = coordinate[1]; + let closestFeature = null; + const closestPoint = [NaN, NaN]; + let minSquaredDistance = Infinity; + const extent = [-Infinity, -Infinity, Infinity, Infinity]; + const filter = opt_filter ? opt_filter : TRUE; + this.featuresRtree_.forEachInExtent(extent, + /** + * @param {module:ol/Feature} feature Feature. + */ + function(feature) { + if (filter(feature)) { + const geometry = feature.getGeometry(); + const previousMinSquaredDistance = minSquaredDistance; + minSquaredDistance = geometry.closestPointXY( + x, y, closestPoint, minSquaredDistance); + if (minSquaredDistance < previousMinSquaredDistance) { + closestFeature = feature; + // This is sneaky. Reduce the extent that it is currently being + // searched while the R-Tree traversal using this same extent object + // is still in progress. This is safe because the new extent is + // strictly contained by the old extent. + const minDistance = Math.sqrt(minSquaredDistance); + extent[0] = x - minDistance; + extent[1] = y - minDistance; + extent[2] = x + minDistance; + extent[3] = y + minDistance; + } + } + }); + return closestFeature; + } + + + /** + * 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`. + * @param {module:ol/extent~Extent=} opt_extent Destination extent. If provided, no new extent + * will be created. Instead, that extent's coordinates will be overwritten. + * @return {module:ol/extent~Extent} Extent. + * @api + */ + getExtent(opt_extent) { + return this.featuresRtree_.getExtent(opt_extent); + } + + + /** + * Get a feature by its identifier (the value returned by feature.getId()). + * Note that the index treats string and numeric identifiers as the same. So + * `source.getFeatureById(2)` will return a feature with id `'2'` or `2`. + * + * @param {string|number} id Feature identifier. + * @return {module:ol/Feature} The feature (or `null` if not found). + * @api + */ + getFeatureById(id) { + const feature = this.idIndex_[id.toString()]; + return feature !== undefined ? feature : null; + } + + + /** + * Get the format associated with this source. + * + * @return {module:ol/format/Feature|undefined} The feature format. + * @api + */ + getFormat() { + return this.format_; + } + + + /** + * @return {boolean} The source can have overlapping geometries. + */ + getOverlaps() { + return this.overlaps_; + } + + + /** + * @override + */ + getResolutions() {} + + + /** + * Get the url associated with this source. + * + * @return {string|module:ol/featureloader~FeatureUrlFunction|undefined} The url. + * @api + */ + getUrl() { + return this.url_; + } + + + /** + * @param {module:ol/events/Event} event Event. + * @private + */ + handleFeatureChange_(event) { + const feature = /** @type {module:ol/Feature} */ (event.target); + const featureKey = getUid(feature).toString(); + const geometry = feature.getGeometry(); + if (!geometry) { + if (!(featureKey in this.nullGeometryFeatures_)) { + if (this.featuresRtree_) { + this.featuresRtree_.remove(feature); + } + this.nullGeometryFeatures_[featureKey] = feature; + } + } else { + const extent = geometry.getExtent(); + if (featureKey in this.nullGeometryFeatures_) { + delete this.nullGeometryFeatures_[featureKey]; + if (this.featuresRtree_) { + this.featuresRtree_.insert(extent, feature); + } + } else { + if (this.featuresRtree_) { + this.featuresRtree_.update(extent, feature); + } + } + } + const id = feature.getId(); + if (id !== undefined) { + const sid = id.toString(); + if (featureKey in this.undefIdIndex_) { + delete this.undefIdIndex_[featureKey]; + this.idIndex_[sid] = feature; + } else { + if (this.idIndex_[sid] !== feature) { + this.removeFromIdIndex_(feature); + this.idIndex_[sid] = feature; + } + } + } else { + if (!(featureKey in this.undefIdIndex_)) { + this.removeFromIdIndex_(feature); + this.undefIdIndex_[featureKey] = feature; + } + } + this.changed(); + this.dispatchEvent(new VectorSourceEvent( + VectorEventType.CHANGEFEATURE, feature)); + } + + /** + * Returns true if the feature is contained within the source. + * @param {module:ol/Feature} feature Feature. + * @return {boolean} Has feature. + * @api + */ + hasFeature(feature) { + const id = feature.getId(); + if (id !== undefined) { + return id in this.idIndex_; + } else { + const featureKey = getUid(feature).toString(); + return featureKey in this.undefIdIndex_; + } + } + + /** + * @return {boolean} Is empty. + */ + isEmpty() { + return this.featuresRtree_.isEmpty() && isEmpty(this.nullGeometryFeatures_); + } + + + /** + * @param {module:ol/extent~Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {module:ol/proj/Projection} projection Projection. + */ + loadFeatures(extent, resolution, projection) { + const loadedExtentsRtree = this.loadedExtentsRtree_; + const extentsToLoad = this.strategy_(extent, resolution); + for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) { + const extentToLoad = extentsToLoad[i]; + const alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad, + /** + * @param {{extent: module:ol/extent~Extent}} object Object. + * @return {boolean} Contains. + */ + function(object) { + return containsExtent(object.extent, extentToLoad); + }); + if (!alreadyLoaded) { + this.loader_.call(this, extentToLoad, resolution, projection); + loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()}); + } + } + } + + + /** + * Remove an extent from the list of loaded extents. + * @param {module:ol/extent~Extent} extent Extent. + * @api + */ + removeLoadedExtent(extent) { + const loadedExtentsRtree = this.loadedExtentsRtree_; + let obj; + loadedExtentsRtree.forEachInExtent(extent, function(object) { + if (equals(object.extent, extent)) { + obj = object; + return true; + } + }); + if (obj) { + loadedExtentsRtree.remove(obj); + } + } + + + /** + * Remove a single feature from the source. If you want to remove all features + * at once, use the {@link module:ol/source/Vector~VectorSource#clear #clear()} method + * instead. + * @param {module:ol/Feature} feature Feature to remove. + * @api + */ + removeFeature(feature) { + const featureKey = getUid(feature).toString(); + if (featureKey in this.nullGeometryFeatures_) { + delete this.nullGeometryFeatures_[featureKey]; + } else { + if (this.featuresRtree_) { + this.featuresRtree_.remove(feature); + } + } + this.removeFeatureInternal(feature); + this.changed(); + } + + + /** + * Remove feature without firing a `change` event. + * @param {module:ol/Feature} feature Feature. + * @protected + */ + removeFeatureInternal(feature) { + const featureKey = getUid(feature).toString(); + this.featureChangeKeys_[featureKey].forEach(unlistenByKey); + delete this.featureChangeKeys_[featureKey]; + const id = feature.getId(); + if (id !== undefined) { + delete this.idIndex_[id.toString()]; + } else { + delete this.undefIdIndex_[featureKey]; + } + this.dispatchEvent(new VectorSourceEvent( + VectorEventType.REMOVEFEATURE, feature)); + } + + + /** + * Remove a feature from the id index. Called internally when the feature id + * may have changed. + * @param {module:ol/Feature} feature The feature. + * @return {boolean} Removed the feature from the index. + * @private + */ + removeFromIdIndex_(feature) { + let removed = false; + for (const id in this.idIndex_) { + if (this.idIndex_[id] === feature) { + delete this.idIndex_[id]; + removed = true; + break; + } + } + return removed; + } + + + /** + * Set the new loader of the source. The next loadFeatures call will use the + * new loader. + * @param {module:ol/featureloader~FeatureLoader} loader The loader to set. + * @api + */ + setLoader(loader) { + this.loader_ = loader; + } + +} + export default VectorSource; diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index b33a803acf..72c022acc0 100644 --- a/src/ol/source/VectorTile.js +++ b/src/ol/source/VectorTile.js @@ -41,7 +41,7 @@ import {createXYZ, extentFromProjection, createForProjection} from '../tilegrid. * })); * // the line below is only required for ol/format/MVT * tile.setExtent(format.getLastExtent()); - * }; + * } * }); * ``` * @property {module:ol/Tile~UrlFunction} [tileUrlFunction] Optional function to get tile URL given a tile coordinate and the projection. @@ -136,84 +136,84 @@ class VectorTile extends UrlTile { } + /** + * @return {boolean} The source can have overlapping geometries. + */ + getOverlaps() { + return this.overlaps_; + } + + /** + * clear {@link module:ol/TileCache~TileCache} and delete all source tiles + * @api + */ + clear() { + this.tileCache.clear(); + this.sourceTiles_ = {}; + } + + /** + * @inheritDoc + */ + getTile(z, x, y, pixelRatio, projection) { + const tileCoordKey = getKeyZXY(z, x, y); + if (this.tileCache.containsKey(tileCoordKey)) { + return ( + /** @type {!module:ol/Tile} */ (this.tileCache.get(tileCoordKey)) + ); + } else { + const tileCoord = [z, x, y]; + const urlTileCoord = this.getTileCoordForTileUrlFunction( + tileCoord, projection); + const tile = new VectorImageTile( + tileCoord, + urlTileCoord !== null ? TileState.IDLE : TileState.EMPTY, + this.getRevision(), + this.format_, this.tileLoadFunction, urlTileCoord, this.tileUrlFunction, + this.tileGrid, this.getTileGridForProjection(projection), + this.sourceTiles_, pixelRatio, projection, this.tileClass, + this.handleTileChange.bind(this), tileCoord[0]); + + this.tileCache.set(tileCoordKey, tile); + return tile; + } + } + + + /** + * @inheritDoc + */ + getTileGridForProjection(projection) { + const code = projection.getCode(); + let tileGrid = this.tileGrids_[code]; + if (!tileGrid) { + // A tile grid that matches the tile size of the source tile grid is more + // likely to have 1:1 relationships between source tiles and rendered tiles. + const sourceTileGrid = this.tileGrid; + tileGrid = this.tileGrids_[code] = createForProjection(projection, undefined, + sourceTileGrid ? sourceTileGrid.getTileSize(sourceTileGrid.getMinZoom()) : undefined); + } + return tileGrid; + } + + + /** + * @inheritDoc + */ + getTilePixelRatio(pixelRatio) { + return pixelRatio; + } + + + /** + * @inheritDoc + */ + getTilePixelSize(z, pixelRatio, projection) { + const tileGrid = this.getTileGridForProjection(projection); + const tileSize = toSize(tileGrid.getTileSize(z), this.tmpSize); + return [Math.round(tileSize[0] * pixelRatio), Math.round(tileSize[1] * pixelRatio)]; + } } -/** - * @return {boolean} The source can have overlapping geometries. - */ -VectorTile.prototype.getOverlaps = function() { - return this.overlaps_; -}; - -/** - * clear {@link module:ol/TileCache~TileCache} and delete all source tiles - * @api - */ -VectorTile.prototype.clear = function() { - this.tileCache.clear(); - this.sourceTiles_ = {}; -}; - -/** - * @inheritDoc - */ -VectorTile.prototype.getTile = function(z, x, y, pixelRatio, projection) { - const tileCoordKey = getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - return ( - /** @type {!module:ol/Tile} */ (this.tileCache.get(tileCoordKey)) - ); - } else { - const tileCoord = [z, x, y]; - const urlTileCoord = this.getTileCoordForTileUrlFunction( - tileCoord, projection); - const tile = new VectorImageTile( - tileCoord, - urlTileCoord !== null ? TileState.IDLE : TileState.EMPTY, - this.getRevision(), - this.format_, this.tileLoadFunction, urlTileCoord, this.tileUrlFunction, - this.tileGrid, this.getTileGridForProjection(projection), - this.sourceTiles_, pixelRatio, projection, this.tileClass, - this.handleTileChange.bind(this), tileCoord[0]); - - this.tileCache.set(tileCoordKey, tile); - return tile; - } -}; - - -/** - * @inheritDoc - */ -VectorTile.prototype.getTileGridForProjection = function(projection) { - const code = projection.getCode(); - let tileGrid = this.tileGrids_[code]; - if (!tileGrid) { - // A tile grid that matches the tile size of the source tile grid is more - // likely to have 1:1 relationships between source tiles and rendered tiles. - const sourceTileGrid = this.tileGrid; - tileGrid = this.tileGrids_[code] = createForProjection(projection, undefined, - sourceTileGrid ? sourceTileGrid.getTileSize(sourceTileGrid.getMinZoom()) : undefined); - } - return tileGrid; -}; - - -/** - * @inheritDoc - */ -VectorTile.prototype.getTilePixelRatio = function(pixelRatio) { - return pixelRatio; -}; - - -/** - * @inheritDoc - */ -VectorTile.prototype.getTilePixelSize = function(z, pixelRatio, projection) { - const tileGrid = this.getTileGridForProjection(projection); - const tileSize = toSize(tileGrid.getTileSize(z), this.tmpSize); - return [Math.round(tileSize[0] * pixelRatio), Math.round(tileSize[1] * pixelRatio)]; -}; export default VectorTile; diff --git a/src/ol/source/WMTS.js b/src/ol/source/WMTS.js index ebe3909c81..1105e2fc82 100644 --- a/src/ol/source/WMTS.js +++ b/src/ol/source/WMTS.js @@ -151,117 +151,118 @@ class WMTS extends TileImage { } + /** + * Set the URLs to use for requests. + * URLs may contain OCG conform URL Template Variables: {TileMatrix}, {TileRow}, {TileCol}. + * @override + */ + setUrls(urls) { + this.urls = urls; + const key = urls.join('\n'); + this.setTileUrlFunction(this.fixedTileUrlFunction ? + this.fixedTileUrlFunction.bind(this) : + createFromTileUrlFunctions(urls.map(createFromWMTSTemplate.bind(this))), key); + } + + /** + * Get the dimensions, i.e. those passed to the constructor through the + * "dimensions" option, and possibly updated using the updateDimensions + * method. + * @return {!Object} Dimensions. + * @api + */ + getDimensions() { + return this.dimensions_; + } + + + /** + * Return the image format of the WMTS source. + * @return {string} Format. + * @api + */ + getFormat() { + return this.format_; + } + + + /** + * Return the layer of the WMTS source. + * @return {string} Layer. + * @api + */ + getLayer() { + return this.layer_; + } + + + /** + * Return the matrix set of the WMTS source. + * @return {string} MatrixSet. + * @api + */ + getMatrixSet() { + return this.matrixSet_; + } + + + /** + * Return the request encoding, either "KVP" or "REST". + * @return {module:ol/source/WMTSRequestEncoding} Request encoding. + * @api + */ + getRequestEncoding() { + return this.requestEncoding_; + } + + + /** + * Return the style of the WMTS source. + * @return {string} Style. + * @api + */ + getStyle() { + return this.style_; + } + + + /** + * Return the version of the WMTS source. + * @return {string} Version. + * @api + */ + getVersion() { + return this.version_; + } + + + /** + * @private + * @return {string} The key for the current dimensions. + */ + getKeyForDimensions_() { + let i = 0; + const res = []; + for (const key in this.dimensions_) { + res[i++] = key + '-' + this.dimensions_[key]; + } + return res.join('/'); + } + + + /** + * Update the dimensions. + * @param {Object} dimensions Dimensions. + * @api + */ + updateDimensions(dimensions) { + assign(this.dimensions_, dimensions); + this.setKey(this.getKeyForDimensions_()); + } + } -/** - * Set the URLs to use for requests. - * URLs may contain OCG conform URL Template Variables: {TileMatrix}, {TileRow}, {TileCol}. - * @override - */ -WMTS.prototype.setUrls = function(urls) { - this.urls = urls; - const key = urls.join('\n'); - this.setTileUrlFunction(this.fixedTileUrlFunction ? - this.fixedTileUrlFunction.bind(this) : - createFromTileUrlFunctions(urls.map(createFromWMTSTemplate.bind(this))), key); -}; - -/** - * Get the dimensions, i.e. those passed to the constructor through the - * "dimensions" option, and possibly updated using the updateDimensions - * method. - * @return {!Object} Dimensions. - * @api - */ -WMTS.prototype.getDimensions = function() { - return this.dimensions_; -}; - - -/** - * Return the image format of the WMTS source. - * @return {string} Format. - * @api - */ -WMTS.prototype.getFormat = function() { - return this.format_; -}; - - -/** - * Return the layer of the WMTS source. - * @return {string} Layer. - * @api - */ -WMTS.prototype.getLayer = function() { - return this.layer_; -}; - - -/** - * Return the matrix set of the WMTS source. - * @return {string} MatrixSet. - * @api - */ -WMTS.prototype.getMatrixSet = function() { - return this.matrixSet_; -}; - - -/** - * Return the request encoding, either "KVP" or "REST". - * @return {module:ol/source/WMTSRequestEncoding} Request encoding. - * @api - */ -WMTS.prototype.getRequestEncoding = function() { - return this.requestEncoding_; -}; - - -/** - * Return the style of the WMTS source. - * @return {string} Style. - * @api - */ -WMTS.prototype.getStyle = function() { - return this.style_; -}; - - -/** - * Return the version of the WMTS source. - * @return {string} Version. - * @api - */ -WMTS.prototype.getVersion = function() { - return this.version_; -}; - - -/** - * @private - * @return {string} The key for the current dimensions. - */ -WMTS.prototype.getKeyForDimensions_ = function() { - let i = 0; - const res = []; - for (const key in this.dimensions_) { - res[i++] = key + '-' + this.dimensions_[key]; - } - return res.join('/'); -}; - - -/** - * Update the dimensions. - * @param {Object} dimensions Dimensions. - * @api - */ -WMTS.prototype.updateDimensions = function(dimensions) { - assign(this.dimensions_, dimensions); - this.setKey(this.getKeyForDimensions_()); -}; - +export default WMTS; /** * Generate source options from a capabilities object. @@ -523,6 +524,3 @@ function createFromWMTSTemplate(template) { } ); } - - -export default WMTS; diff --git a/src/ol/source/Zoomify.js b/src/ol/source/Zoomify.js index e655fb1a1f..a4f2e9bd81 100644 --- a/src/ol/source/Zoomify.js +++ b/src/ol/source/Zoomify.js @@ -52,32 +52,31 @@ export class CustomTile extends ImageTile { } -} - - -/** - * @inheritDoc - */ -CustomTile.prototype.getImage = function() { - if (this.zoomifyImage_) { - return this.zoomifyImage_; - } - const image = ImageTile.prototype.getImage.call(this); - if (this.state == TileState.LOADED) { - const tileSize = this.tileSize_; - if (image.width == tileSize[0] && image.height == tileSize[1]) { - this.zoomifyImage_ = image; - return image; - } else { - const context = createCanvasContext2D(tileSize[0], tileSize[1]); - context.drawImage(image, 0, 0); - this.zoomifyImage_ = context.canvas; - return context.canvas; + /** + * @inheritDoc + */ + getImage() { + if (this.zoomifyImage_) { + return this.zoomifyImage_; + } + const image = super.getImage(); + if (this.state == TileState.LOADED) { + const tileSize = this.tileSize_; + if (image.width == tileSize[0] && image.height == tileSize[1]) { + this.zoomifyImage_ = image; + return image; + } else { + const context = createCanvasContext2D(tileSize[0], tileSize[1]); + context.drawImage(image, 0, 0); + this.zoomifyImage_ = context.canvas; + return context.canvas; + } + } else { + return image; } - } else { - return image; } -}; + +} /**