From 73a2ab2de1ec12a2f7e2293ffab099413df2c2d1 Mon Sep 17 00:00:00 2001 From: Bart van den Eijnden Date: Wed, 27 Nov 2013 16:09:36 +0100 Subject: [PATCH 01/18] make sure we can also read in CDATA fields with newlines --- src/ol/parser/kmlparser.js | 2 +- test/spec/ol/parser/kml.test.js | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/ol/parser/kmlparser.js b/src/ol/parser/kmlparser.js index 1fecd92d39..a815485d7f 100644 --- a/src/ol/parser/kmlparser.js +++ b/src/ol/parser/kmlparser.js @@ -75,7 +75,7 @@ ol.parser.KML = function(opt_options) { '*': function(node, obj) { if (this.extractAttributes === true) { var len = node.childNodes.length; - if ((len === 1 || len === 2) && (node.firstChild.nodeType === 3 || + if (len > 0 && (node.firstChild.nodeType === 3 || node.firstChild.nodeType === 4)) { var readers = this.readers[this.defaultNamespaceURI]; readers['_attribute'].apply(this, arguments); diff --git a/test/spec/ol/parser/kml.test.js b/test/spec/ol/parser/kml.test.js index 4fb7774edc..4a165f055b 100644 --- a/test/spec/ol/parser/kml.test.js +++ b/test/spec/ol/parser/kml.test.js @@ -163,6 +163,19 @@ describe('ol.parser.KML', function() { expect(obj.features[0].get('description')).to.eql('Full of text.'); expect(obj.features[0].get('name')).to.eql('Pezinok'); }); + it('Test CDATA attributes with newlines', function() { + var cdata = '' + + ' ' + + '\n' + + '' + + '\n' + + '#rel1.0' + + ' 17.266666, 48.283333' + + ''; + var obj = parser.read(cdata); + expect(obj.features[0].get('description')).to.eql('Full of text.'); + expect(obj.features[0].get('name')).to.eql('Pezinok'); + }); it('handles line style (read / write)', function() { var kml = ' Date: Thu, 14 Nov 2013 17:50:31 -0700 Subject: [PATCH 02/18] Vector layer/source refactor This moves the feature cache from ol.layer.Vector to ol.source.Vector. These are the minimum changes required to maintain the existing functionality and make tests pass. More refactoring to come. --- src/ol/interaction/drawinteraction.js | 6 +- src/ol/interaction/modifyinteraction.js | 31 +- src/ol/interaction/selectinteraction.js | 2 +- src/ol/layer/vectorlayer.js | 435 +---------------- .../canvas/canvasvectorlayerrenderer.js | 34 +- src/ol/source/vectorsource.js | 442 +++++++++++++++++- .../ol/interaction/drawinteraction.test.js | 37 +- .../ol/interaction/selectinteraction.test.js | 44 +- test/spec/ol/layer/vectorlayer.test.js | 185 +------- test/spec/ol/source/vectorsource.test.js | 199 +++++++- 10 files changed, 725 insertions(+), 690 deletions(-) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 274fa87977..a220934ea0 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -256,7 +256,7 @@ ol.interaction.Draw.prototype.startDrawing_ = function(event) { sketchFeature.setGeometry(geometry); this.sketchFeature_ = sketchFeature; - this.sketchLayer_.addFeatures(features); + this.sketchLayer_.getVectorSource().addFeatures(features); }; @@ -344,7 +344,7 @@ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { } else if (this.type_ === ol.geom.GeometryType.MULTIPOLYGON) { sketchFeature.setGeometry(new ol.geom.MultiPolygon([coordinates])); } - this.layer_.addFeatures([sketchFeature]); + this.layer_.getVectorSource().addFeatures([sketchFeature]); }; @@ -363,7 +363,7 @@ ol.interaction.Draw.prototype.abortDrawing_ = function() { features.push(this.sketchPoint_); this.sketchPoint_ = null; } - this.sketchLayer_.removeFeatures(features); + this.sketchLayer_.getVectorSource().removeFeatures(features); } return sketchFeature; }; diff --git a/src/ol/interaction/modifyinteraction.js b/src/ol/interaction/modifyinteraction.js index e473be2f9c..19053979e2 100644 --- a/src/ol/interaction/modifyinteraction.js +++ b/src/ol/interaction/modifyinteraction.js @@ -18,9 +18,9 @@ goog.require('ol.geom.Polygon'); goog.require('ol.interaction.Drag'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Vector'); -goog.require('ol.layer.VectorEventType'); goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.source.Vector'); +goog.require('ol.source.VectorEventType'); goog.require('ol.structs.RBush'); @@ -59,6 +59,13 @@ ol.interaction.Modify = function(opt_options) { */ this.layerFilter_ = layerFilter; + /** + * Layer lookup. Keys source id to layer. + * @type {Object.} + * @private + */ + this.layerLookup_ = null; + /** * Temporary sketch layer. * @type {ol.layer.Vector} @@ -121,6 +128,7 @@ ol.interaction.Modify.prototype.setMap = function(map) { } if (!goog.isNull(map)) { + this.layerLookup_ = {}; if (goog.isNull(this.rBush_)) { this.rBush_ = new ol.structs.RBush(); } @@ -141,6 +149,7 @@ ol.interaction.Modify.prototype.setMap = function(map) { false, this); } else { // removing from a map, clean up + this.layerLookup_ = null; this.rBush_ = null; this.sketchLayer_ = null; } @@ -168,9 +177,11 @@ ol.interaction.Modify.prototype.handleLayerAdded_ = function(evt) { ol.interaction.Modify.prototype.addLayer_ = function(layer) { if (this.layerFilter_(layer) && layer instanceof ol.layer.Vector && !layer.getTemporary()) { - this.addIndex_(layer.getFeatures(ol.layer.Vector.selectedFeaturesFilter), + var source = layer.getVectorSource(); + this.layerLookup_[goog.getUid(source)] = layer; + this.addIndex_(source.getFeatures(ol.layer.Vector.selectedFeaturesFilter), layer); - goog.events.listen(layer, ol.layer.VectorEventType.INTENTCHANGE, + goog.events.listen(source, ol.source.VectorEventType.INTENTCHANGE, this.handleIntentChange_, false, this); } }; @@ -195,9 +206,11 @@ ol.interaction.Modify.prototype.handleLayerRemoved_ = function(evt) { ol.interaction.Modify.prototype.removeLayer_ = function(layer) { if (this.layerFilter_(layer) && layer instanceof ol.layer.Vector && !layer.getTemporary()) { + var source = layer.getVectorSource(); + delete this.layerLookup_[goog.getUid(source)]; this.removeIndex_( - layer.getFeatures(ol.layer.Vector.selectedFeaturesFilter)); - goog.events.unlisten(layer, ol.layer.VectorEventType.INTENTCHANGE, + source.getFeatures(ol.layer.Vector.selectedFeaturesFilter)); + goog.events.unlisten(source, ol.source.VectorEventType.INTENTCHANGE, this.handleIntentChange_, false, this); } }; @@ -248,11 +261,13 @@ ol.interaction.Modify.prototype.removeIndex_ = function(features) { /** * Listen for feature additions. - * @param {ol.layer.VectorEvent} evt Event object. + * @param {ol.source.VectorEvent} evt Event object. * @private */ ol.interaction.Modify.prototype.handleIntentChange_ = function(evt) { - var layer = evt.target; + var source = evt.target; + goog.asserts.assertInstanceof(source, ol.source.Vector); + var layer = this.layerLookup_[goog.getUid(source)]; goog.asserts.assertInstanceof(layer, ol.layer.Vector); var features = evt.features; for (var i = 0, ii = features.length; i < ii; ++i) { @@ -322,7 +337,7 @@ ol.interaction.Modify.prototype.createOrUpdateVertexFeature_ = if (goog.isNull(vertexFeature)) { vertexFeature = new ol.Feature({g: new ol.geom.Point(coordinates)}); this.vertexFeature_ = vertexFeature; - this.sketchLayer_.addFeatures([vertexFeature]); + this.sketchLayer_.getVectorSource().addFeatures([vertexFeature]); } else { var geometry = vertexFeature.getGeometry(); geometry.setCoordinates(coordinates); diff --git a/src/ol/interaction/selectinteraction.js b/src/ol/interaction/selectinteraction.js index 41ecf96085..8f1b08d9df 100644 --- a/src/ol/interaction/selectinteraction.js +++ b/src/ol/interaction/selectinteraction.js @@ -97,7 +97,7 @@ ol.interaction.Select.prototype.select = } var featuresToSelect = featuresByLayer[i]; - var selectedFeatures = layer.getFeatures( + var selectedFeatures = layer.getVectorSource().getFeatures( ol.layer.Vector.selectedFeaturesFilter); if (clear) { for (var j = selectedFeatures.length - 1; j >= 0; --j) { diff --git a/src/ol/layer/vectorlayer.js b/src/ol/layer/vectorlayer.js index 28d1df9d60..87e3edf6b5 100644 --- a/src/ol/layer/vectorlayer.js +++ b/src/ol/layer/vectorlayer.js @@ -1,126 +1,14 @@ goog.provide('ol.layer.Vector'); -goog.provide('ol.layer.VectorEventType'); goog.require('goog.array'); -goog.require('goog.asserts'); -goog.require('goog.events'); -goog.require('goog.events.Event'); goog.require('goog.object'); goog.require('ol.Feature'); -goog.require('ol.FeatureEventType'); -goog.require('ol.extent'); goog.require('ol.layer.Layer'); goog.require('ol.layer.VectorLayerRenderIntent'); -goog.require('ol.proj'); goog.require('ol.source.Vector'); -goog.require('ol.structs.RTree'); +goog.require('ol.source.VectorEventType'); goog.require('ol.style'); goog.require('ol.style.Style'); -goog.require('ol.style.TextLiteral'); - - - -/** - * @constructor - */ -ol.layer.FeatureCache = function() { - - /** - * @type {Object.} - * @private - */ - this.idLookup_; - - /** - * @type {ol.structs.RTree} - * @private - */ - this.rTree_; - - this.clear(); - -}; - - -/** - * Clear the cache. - */ -ol.layer.FeatureCache.prototype.clear = function() { - this.idLookup_ = {}; - this.rTree_ = new ol.structs.RTree(); -}; - - -/** - * Add a feature to the cache. - * @param {ol.Feature} feature Feature to be cached. - */ -ol.layer.FeatureCache.prototype.add = function(feature) { - var id = goog.getUid(feature).toString(), - geometry = feature.getGeometry(); - - this.idLookup_[id] = feature; - - // index by bounding box - if (!goog.isNull(geometry)) { - this.rTree_.insert(geometry.getBounds(), feature); - } -}; - - -/** - * @return {Object.} Object of features, keyed by id. - */ -ol.layer.FeatureCache.prototype.getFeaturesObject = function() { - return this.idLookup_; -}; - - -/** - * Get all features whose bounding box intersects the provided extent. - * - * @param {ol.Extent} extent Bounding extent. - * @return {Object.} Features. - */ -ol.layer.FeatureCache.prototype.getFeaturesObjectForExtent = function(extent) { - return this.rTree_.searchReturningObject(extent); -}; - - -/** - * Get features by ids. - * @param {Array.} ids Array of (internal) identifiers. - * @return {Array.} Array of features. - * @private - */ -ol.layer.FeatureCache.prototype.getFeaturesByIds_ = function(ids) { - var len = ids.length, - features = new Array(len), - i; - for (i = 0; i < len; ++i) { - features[i] = this.idLookup_[ids[i]]; - } - return features; -}; - - -/** - * Remove a feature from the cache. - * @param {ol.Feature} feature Feature. - * @param {ol.Extent=} opt_extent Optional extent (used when the current feature - * extent is different than the one in the index). - */ -ol.layer.FeatureCache.prototype.remove = function(feature, opt_extent) { - var id = goog.getUid(feature).toString(), - geometry = feature.getGeometry(); - - delete this.idLookup_[id]; - // index by bounding box - if (!goog.isNull(geometry)) { - var extent = goog.isDef(opt_extent) ? opt_extent : geometry.getBounds(); - this.rTree_.remove(extent, feature); - } -}; @@ -132,19 +20,15 @@ ol.layer.FeatureCache.prototype.remove = function(feature, opt_extent) { */ ol.layer.Vector = function(options) { - goog.base(this, /** @type {ol.layer.LayerOptions} */ (options)); + var baseOptions = /** @type {ol.layer.VectorLayerOptions} */ + (goog.object.clone(options)); /** * @private * @type {ol.style.Style} */ this.style_ = goog.isDef(options.style) ? options.style : null; - - /** - * @type {ol.layer.FeatureCache} - * @private - */ - this.featureCache_ = new ol.layer.FeatureCache(); + delete baseOptions.style; /** * @type {function(Array.):string} @@ -152,6 +36,7 @@ ol.layer.Vector = function(options) { */ this.transformFeatureInfo_ = goog.isDef(options.transformFeatureInfo) ? options.transformFeatureInfo : ol.layer.Vector.uidTransformFeatureInfo; + delete baseOptions.transformFeatureInfo; /** * True if this is a temporary layer. @@ -160,83 +45,11 @@ ol.layer.Vector = function(options) { */ this.temporary_ = false; + goog.base(this, /** @type {ol.layer.LayerOptions} */ (baseOptions)); }; goog.inherits(ol.layer.Vector, ol.layer.Layer); -/** - * @param {Array.} features Array of features. - */ -ol.layer.Vector.prototype.addFeatures = function(features) { - var extent = ol.extent.createEmpty(), - feature, geometry; - for (var i = 0, ii = features.length; i < ii; ++i) { - feature = features[i]; - this.featureCache_.add(feature); - geometry = feature.getGeometry(); - if (!goog.isNull(geometry)) { - ol.extent.extend(extent, geometry.getBounds()); - } - goog.events.listen(feature, ol.FeatureEventType.CHANGE, - this.handleFeatureChange_, false, this); - goog.events.listen(feature, ol.FeatureEventType.INTENTCHANGE, - this.handleIntentChange_, false, this); - } - this.dispatchEvent(new ol.layer.VectorEvent(ol.layer.VectorEventType.ADD, - features, [extent])); -}; - - -/** - * Listener for feature change events. - * @param {ol.FeatureEvent} evt The feature change event. - * @private - */ -ol.layer.Vector.prototype.handleFeatureChange_ = function(evt) { - goog.asserts.assertInstanceof(evt.target, ol.Feature); - var feature = /** @type {ol.Feature} */ (evt.target); - var extents = []; - if (!goog.isNull(evt.oldExtent)) { - extents.push(evt.oldExtent); - } - var geometry = feature.getGeometry(); - if (!goog.isNull(geometry)) { - this.featureCache_.remove(feature, evt.oldExtent); - this.featureCache_.add(feature); - extents.push(geometry.getBounds()); - } - this.dispatchEvent(new ol.layer.VectorEvent(ol.layer.VectorEventType.CHANGE, - [feature], extents)); -}; - - -/** - * Listener for render intent change events of features. - * @param {ol.FeatureEvent} evt The feature intent change event. - * @private - */ -ol.layer.Vector.prototype.handleIntentChange_ = function(evt) { - goog.asserts.assertInstanceof(evt.target, ol.Feature); - var feature = /** @type {ol.Feature} */ (evt.target); - var geometry = feature.getGeometry(); - if (!goog.isNull(geometry)) { - this.dispatchEvent(new ol.layer.VectorEvent( - ol.layer.VectorEventType.INTENTCHANGE, [feature], - [geometry.getBounds()])); - } -}; - - -/** - * Remove all features from the layer. - */ -ol.layer.Vector.prototype.clear = function() { - this.featureCache_.clear(); - this.dispatchEvent( - new ol.layer.VectorEvent(ol.layer.VectorEventType.REMOVE, [], [])); -}; - - /** * @return {boolean} Whether this layer is temporary. */ @@ -267,165 +80,10 @@ ol.layer.Vector.prototype.getStyle = function() { */ ol.layer.Vector.prototype.setStyle = function(style) { this.style_ = style; - this.dispatchEvent( - new ol.layer.VectorEvent(ol.layer.VectorEventType.CHANGE, [], [])); -}; - - -/** - * Returns an array of features that match a filter. This will not fetch data, - * it only considers features that are loaded already. - * @param {(function(ol.Feature):boolean)=} opt_filter Filter function. - * @return {Array.} Features that match the filter, or all features - * if no filter was provided. - */ -ol.layer.Vector.prototype.getFeatures = function(opt_filter) { - var result; - var features = this.featureCache_.getFeaturesObject(); - if (goog.isDef(opt_filter)) { - result = []; - for (var f in features) { - if (opt_filter(features[f]) === true) { - result.push(features[f]); - } - } - } else { - result = goog.object.getValues(features); - } - return result; -}; - - -/** - * Get all features whose bounding box intersects the provided extent. This - * method is intended for being called by the renderer. When null is returned, - * the renderer should not waste time rendering, and `opt_callback` is - * usually a function that requests a renderFrame, which will be called as soon - * as the data for `extent` is available. - * - * @param {ol.Extent} extent Bounding extent. - * @param {ol.proj.Projection} projection Target projection. - * @param {Function=} opt_callback Callback to call when data is parsed. - * @return {Object.} Features or null if source is loading - * data for `extent`. - */ -ol.layer.Vector.prototype.getFeaturesObjectForExtent = function(extent, - projection, opt_callback) { - var source = this.getSource(); - return source.prepareFeatures(this, extent, projection, opt_callback) == - ol.source.VectorLoadState.LOADING ? - null : - this.featureCache_.getFeaturesObjectForExtent(extent); -}; - - -/** - * @param {Object.} features Features. - * @param {number} resolution Map resolution. - * @return {Array.} symbolizers for features. Each array in this array - * contains 3 items: an array of features, the symbolizer literal, and - * an array with optional additional data for each feature. - */ -ol.layer.Vector.prototype.groupFeaturesBySymbolizerLiteral = - function(features, resolution) { - var uniqueLiterals = {}, - featuresBySymbolizer = [], - style = this.style_, - i, j, l, feature, symbolizers, literals, numLiterals, literal, - uniqueLiteral, key, item; - for (i in features) { - feature = features[i]; - // feature level symbolizers take precedence - symbolizers = feature.getSymbolizers(); - if (!goog.isNull(symbolizers)) { - literals = ol.style.Style.createLiterals(symbolizers, feature); - } else { - // layer style second - if (goog.isNull(style)) { - style = ol.style.getDefault(); - } - literals = style.createLiterals(feature, resolution); - } - numLiterals = literals.length; - for (j = 0; j < numLiterals; ++j) { - literal = literals[j]; - for (l in uniqueLiterals) { - uniqueLiteral = featuresBySymbolizer[uniqueLiterals[l]][1]; - if (literal.equals(uniqueLiteral)) { - literal = uniqueLiteral; - break; - } - } - key = goog.getUid(literal); - if (!goog.object.containsKey(uniqueLiterals, key)) { - uniqueLiterals[key] = featuresBySymbolizer.length; - featuresBySymbolizer.push([ - /** @type {Array.} */ ([]), - /** @type {ol.style.Literal} */ (literal), - /** @type {Array} */ ([]) - ]); - } - item = featuresBySymbolizer[uniqueLiterals[key]]; - item[0].push(feature); - if (literal instanceof ol.style.TextLiteral) { - item[2].push(literals[j].text); - } - } - } - featuresBySymbolizer.sort(this.sortByZIndex_); - return featuresBySymbolizer; -}; - - -/** - * @param {Object|Element|Document|string} data Feature data. - * @param {ol.parser.Parser} parser Feature parser. - * @param {ol.proj.Projection} projection This sucks. The layer should be a - * view in one projection. - */ -ol.layer.Vector.prototype.parseFeatures = function(data, parser, projection) { - - var addFeatures = function(data) { - var features = data.features; - var sourceProjection = this.getSource().getProjection(); - if (goog.isNull(sourceProjection)) { - sourceProjection = data.metadata.projection; - } - var transform = ol.proj.getTransform(sourceProjection, projection); - var geometry = null; - for (var i = 0, ii = features.length; i < ii; ++i) { - geometry = features[i].getGeometry(); - if (!goog.isNull(geometry)) { - geometry.transform(transform); - } - } - this.addFeatures(features); - }; - - var result; - if (goog.isString(data)) { - if (goog.isFunction(parser.readFeaturesFromStringAsync)) { - parser.readFeaturesFromStringAsync(data, goog.bind(addFeatures, this)); - } else { - goog.asserts.assert( - goog.isFunction(parser.readFeaturesFromString), - 'Expected parser with a readFeaturesFromString method.'); - result = parser.readFeaturesFromString(data); - addFeatures.call(this, result); - } - } else if (goog.isObject(data)) { - if (goog.isFunction(parser.readFeaturesFromObjectAsync)) { - parser.readFeaturesFromObjectAsync(data, goog.bind(addFeatures, this)); - } else { - goog.asserts.assert( - goog.isFunction(parser.readFeaturesFromObject), - 'Expected parser with a readFeaturesFromObject method.'); - result = parser.readFeaturesFromObject(data); - addFeatures.call(this, result); - } - } else { - // TODO: parse more data types - throw new Error('Data type not supported: ' + data); + var source = this.getVectorSource(); + if (source) { + source.dispatchEvent( + new ol.source.VectorEvent(ol.source.VectorEventType.CHANGE, [], [])); } }; @@ -438,30 +96,6 @@ ol.layer.Vector.prototype.getTransformFeatureInfo = function() { }; -/** - * Remove features from the layer. - * @param {Array.} features Features to remove. - */ -ol.layer.Vector.prototype.removeFeatures = function(features) { - var extent = ol.extent.createEmpty(), - feature, geometry; - for (var i = 0, ii = features.length; i < ii; ++i) { - feature = features[i]; - this.featureCache_.remove(feature); - geometry = feature.getGeometry(); - if (!goog.isNull(geometry)) { - ol.extent.extend(extent, geometry.getBounds()); - } - goog.events.unlisten(feature, ol.FeatureEventType.CHANGE, - this.handleFeatureChange_, false, this); - goog.events.unlisten(feature, ol.FeatureEventType.INTENTCHANGE, - this.handleIntentChange_, false, this); - } - this.dispatchEvent(new ol.layer.VectorEvent(ol.layer.VectorEventType.REMOVE, - features, [extent])); -}; - - /** * @param {boolean} temporary Whether this layer is temporary. */ @@ -470,18 +104,6 @@ ol.layer.Vector.prototype.setTemporary = function(temporary) { }; -/** - * Sort function for `groupFeaturesBySymbolizerLiteral`. - * @private - * @param {Array} a 1st item for the sort comparison. - * @param {Array} b 2nd item for the sort comparison. - * @return {number} Comparison result. - */ -ol.layer.Vector.prototype.sortByZIndex_ = function(a, b) { - return a[1].zIndex - b[1].zIndex; -}; - - /** * @param {Array.} features Features. * @return {string} Feature info. @@ -500,40 +122,3 @@ ol.layer.Vector.uidTransformFeatureInfo = function(features) { ol.layer.Vector.selectedFeaturesFilter = function(feature) { return feature.getRenderIntent() == ol.layer.VectorLayerRenderIntent.SELECTED; }; - - - -/** - * @constructor - * @extends {goog.events.Event} - * @param {string} type Event type. - * @param {Array.} features Features associated with the event. - * @param {Array.} extents Any extents associated with the event. - */ -ol.layer.VectorEvent = function(type, features, extents) { - - goog.base(this, type); - - /** - * @type {Array.} - */ - this.features = features; - - /** - * @type {Array.} - */ - this.extents = extents; - -}; -goog.inherits(ol.layer.VectorEvent, goog.events.Event); - - -/** - * @enum {string} - */ -ol.layer.VectorEventType = { - ADD: 'featureadd', - CHANGE: 'featurechange', - INTENTCHANGE: 'featureintentchange', - REMOVE: 'featureremove' -}; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index f47ca092ad..bcde996390 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -14,10 +14,10 @@ goog.require('ol.ViewHint'); goog.require('ol.extent'); goog.require('ol.geom.GeometryType'); goog.require('ol.layer.Vector'); -goog.require('ol.layer.VectorEventType'); goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.Vector'); +goog.require('ol.source.VectorEventType'); goog.require('ol.tilegrid.TileGrid'); @@ -88,13 +88,15 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) { */ this.tileCache_ = new ol.TileCache( ol.renderer.canvas.VectorLayer.TILECACHE_SIZE); - goog.events.listen(layer, [ - ol.layer.VectorEventType.ADD, - ol.layer.VectorEventType.CHANGE, - ol.layer.VectorEventType.REMOVE, - ol.layer.VectorEventType.INTENTCHANGE + + var source = layer.getSource(); + goog.events.listen(source, [ + ol.source.VectorEventType.ADD, + ol.source.VectorEventType.CHANGE, + ol.source.VectorEventType.REMOVE, + ol.source.VectorEventType.INTENTCHANGE ], - this.handleLayerChange_, false, this); + this.handleSourceChange_, false, this); /** * @private @@ -245,7 +247,7 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = var map = this.getMap(); var result = []; - var layer = this.getLayer(); + var source = this.getVectorLayer().getSource(); var location = map.getCoordinateFromPixel(pixel); var tileCoord = this.tileGrid_.getTileCoordForCoordAndZ(location, 0); var key = tileCoord.toString(); @@ -259,7 +261,7 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = var locationMin = [location[0] - halfMaxWidth, location[1] - halfMaxHeight]; var locationMax = [location[0] + halfMaxWidth, location[1] + halfMaxHeight]; var locationBbox = ol.extent.boundingExtent([locationMin, locationMax]); - var candidates = layer.getFeaturesObjectForExtent(locationBbox, + var candidates = source.getFeaturesObjectForExtent(locationBbox, map.getView().getView2D().getProjection()); if (goog.isNull(candidates)) { // data is not loaded @@ -319,15 +321,16 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = } } } + var layer = this.getLayer(); goog.global.setTimeout(function() { success(result, layer); }, 0); }; /** - * @param {ol.layer.VectorEvent} event Vector layer event. + * @param {ol.source.VectorEvent} event Vector layer event. * @private */ -ol.renderer.canvas.VectorLayer.prototype.handleLayerChange_ = function(event) { +ol.renderer.canvas.VectorLayer.prototype.handleSourceChange_ = function(event) { if (goog.isDef(this.renderedResolution_)) { this.expireTiles_(event.extents); } @@ -348,7 +351,6 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = resolution = view2DState.resolution, projection = view2DState.projection, extent = frameState.extent, - layer = this.getVectorLayer(), tileGrid = this.tileGrid_, tileSize = [512, 512], idle = !frameState.viewHints[ol.ViewHint.ANIMATING] && @@ -471,6 +473,8 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = var tile, tileCoord, key, x, y, i, type; var deferred = false; var dirty = false; + var layer = this.getVectorLayer(); + var source = layer.getSource(); var tileExtent, groups, group, j, numGroups, featuresObject, tileHasFeatures; fetchTileData: for (x = tileRange.minX; x <= tileRange.maxX; ++x) { @@ -486,7 +490,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = tileExtent[1] -= tileGutter; tileExtent[3] += tileGutter; tileHasFeatures = false; - featuresObject = layer.getFeaturesObjectForExtent(tileExtent, + featuresObject = source.getFeaturesObjectForExtent(tileExtent, projection, this.requestMapRenderFrame_); if (goog.isNull(featuresObject)) { deferred = true; @@ -505,8 +509,8 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = } this.dirty_ = dirty; - groups = layer.groupFeaturesBySymbolizerLiteral(featuresToRender, - tileResolution); + groups = source.groupFeaturesBySymbolizerLiteral(layer.getStyle(), + featuresToRender, tileResolution); numGroups = groups.length; for (j = 0; j < numGroups; ++j) { group = groups[j]; diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 32afe70bfe..2e13555d6a 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -1,8 +1,21 @@ +goog.provide('ol.source.FeatureCache'); goog.provide('ol.source.Vector'); +goog.provide('ol.source.VectorEventType'); goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('goog.events.Event'); goog.require('goog.net.XhrIo'); +goog.require('goog.object'); +goog.require('ol.Feature'); +goog.require('ol.FeatureEventType'); +goog.require('ol.extent'); +goog.require('ol.proj'); goog.require('ol.source.Source'); +goog.require('ol.structs.RTree'); +goog.require('ol.style'); +goog.require('ol.style.Style'); +goog.require('ol.style.TextLiteral'); /** @@ -20,10 +33,11 @@ ol.source.VectorLoadState = { /** * @constructor * @extends {ol.source.Source} - * @param {ol.source.VectorOptions} options Vector source options. + * @param {ol.source.VectorOptions=} opt_options Vector source options. * @todo stability experimental */ -ol.source.Vector = function(options) { +ol.source.Vector = function(opt_options) { + var options = goog.isDef(opt_options) ? opt_options : {}; /** * @private @@ -49,6 +63,12 @@ ol.source.Vector = function(options) { */ this.url_ = options.url; + /** + * @type {ol.source.FeatureCache} + * @private + */ + this.featureCache_ = new ol.source.FeatureCache(); + goog.base(this, { attributions: options.attributions, extent: options.extent, @@ -60,14 +80,247 @@ goog.inherits(ol.source.Vector, ol.source.Source); /** - * @param {ol.layer.Vector} layer Layer that parses the data. + * @param {Array.} features Array of features. + */ +ol.source.Vector.prototype.addFeatures = function(features) { + var extent = ol.extent.createEmpty(), + feature, geometry; + for (var i = 0, ii = features.length; i < ii; ++i) { + feature = features[i]; + this.featureCache_.add(feature); + geometry = feature.getGeometry(); + if (!goog.isNull(geometry)) { + ol.extent.extend(extent, geometry.getBounds()); + } + goog.events.listen(feature, ol.FeatureEventType.CHANGE, + this.handleFeatureChange_, false, this); + goog.events.listen(feature, ol.FeatureEventType.INTENTCHANGE, + this.handleIntentChange_, false, this); + } + this.dispatchEvent(new ol.source.VectorEvent(ol.source.VectorEventType.ADD, + features, [extent])); +}; + + +/** + * Returns an array of features that match a filter. This will not fetch data, + * it only considers features that are loaded already. + * @param {(function(ol.Feature):boolean)=} opt_filter Filter function. + * @return {Array.} Features that match the filter, or all features + * if no filter was provided. + */ +ol.source.Vector.prototype.getFeatures = function(opt_filter) { + var result; + var features = this.featureCache_.getFeaturesObject(); + if (goog.isDef(opt_filter)) { + result = []; + for (var f in features) { + if (opt_filter(features[f]) === true) { + result.push(features[f]); + } + } + } else { + result = goog.object.getValues(features); + } + return result; +}; + + +/** + * Get all features whose bounding box intersects the provided extent. This + * method is intended for being called by the renderer. When null is returned, + * the renderer should not waste time rendering, and `opt_callback` is + * usually a function that requests a renderFrame, which will be called as soon + * as the data for `extent` is available. + * + * @param {ol.Extent} extent Bounding extent. + * @param {ol.proj.Projection} projection Target projection. + * @param {function()=} opt_callback Callback to call when data is parsed. + * @return {Object.} Features or null if source is loading + * data for `extent`. + */ +ol.source.Vector.prototype.getFeaturesObjectForExtent = function(extent, + projection, opt_callback) { + var state = this.prepareFeatures(extent, projection, opt_callback); + var lookup = null; + if (state !== ol.source.VectorLoadState.LOADING) { + lookup = this.featureCache_.getFeaturesObjectForExtent(extent); + } + return lookup; +}; + + +/** + * TODO: This should be a ol.style.Style method. + * @param {ol.style.Style} style Style. + * @param {Object.} features Features. + * @param {number} resolution Map resolution. + * @return {Array.} symbolizers for features. Each array in this array + * contains 3 items: an array of features, the symbolizer literal, and + * an array with optional additional data for each feature. + */ +ol.source.Vector.prototype.groupFeaturesBySymbolizerLiteral = + function(style, features, resolution) { + var uniqueLiterals = {}, + featuresBySymbolizer = [], + i, j, l, feature, symbolizers, literals, numLiterals, literal, + uniqueLiteral, key, item; + for (i in features) { + feature = features[i]; + // feature level symbolizers take precedence + symbolizers = feature.getSymbolizers(); + if (!goog.isNull(symbolizers)) { + literals = ol.style.Style.createLiterals(symbolizers, feature); + } else { + // layer style second + if (goog.isNull(style)) { + style = ol.style.getDefault(); + } + literals = style.createLiterals(feature, resolution); + } + numLiterals = literals.length; + for (j = 0; j < numLiterals; ++j) { + literal = literals[j]; + for (l in uniqueLiterals) { + uniqueLiteral = featuresBySymbolizer[uniqueLiterals[l]][1]; + if (literal.equals(uniqueLiteral)) { + literal = uniqueLiteral; + break; + } + } + key = goog.getUid(literal); + if (!goog.object.containsKey(uniqueLiterals, key)) { + uniqueLiterals[key] = featuresBySymbolizer.length; + featuresBySymbolizer.push([ + /** @type {Array.} */ ([]), + /** @type {ol.style.Literal} */ (literal), + /** @type {Array} */ ([]) + ]); + } + item = featuresBySymbolizer[uniqueLiterals[key]]; + item[0].push(feature); + if (literal instanceof ol.style.TextLiteral) { + item[2].push(literals[j].text); + } + } + } + // TODO: move sort function to ol.style.Style + featuresBySymbolizer.sort(this.sortByZIndex_); + return featuresBySymbolizer; +}; + + +/** + * @param {Object|Element|Document|string} data Feature data. + * @param {ol.proj.Projection} projection This sucks. The layer should be a + * view in one projection. + */ +ol.source.Vector.prototype.parseFeatures = function(data, projection) { + + var addFeatures = function(data) { + var features = data.features; + var sourceProjection = this.getProjection(); + if (goog.isNull(sourceProjection)) { + sourceProjection = data.metadata.projection; + } + var transform = ol.proj.getTransform(sourceProjection, projection); + var geometry = null; + for (var i = 0, ii = features.length; i < ii; ++i) { + geometry = features[i].getGeometry(); + if (!goog.isNull(geometry)) { + geometry.transform(transform); + } + } + this.addFeatures(features); + }; + + var result; + var parser = this.parser_; + if (goog.isString(data)) { + if (goog.isFunction(parser.readFeaturesFromStringAsync)) { + parser.readFeaturesFromStringAsync(data, goog.bind(addFeatures, this)); + } else { + goog.asserts.assert( + goog.isFunction(parser.readFeaturesFromString), + 'Expected parser with a readFeaturesFromString method.'); + result = parser.readFeaturesFromString(data); + addFeatures.call(this, result); + } + } else if (goog.isObject(data)) { + if (goog.isFunction(parser.readFeaturesFromObjectAsync)) { + parser.readFeaturesFromObjectAsync(data, goog.bind(addFeatures, this)); + } else { + goog.asserts.assert( + goog.isFunction(parser.readFeaturesFromObject), + 'Expected parser with a readFeaturesFromObject method.'); + result = parser.readFeaturesFromObject(data); + addFeatures.call(this, result); + } + } else { + // TODO: parse more data types + throw new Error('Data type not supported: ' + data); + } +}; + + +/** + * Listener for feature change events. + * @param {ol.FeatureEvent} evt The feature change event. + * @private + */ +ol.source.Vector.prototype.handleFeatureChange_ = function(evt) { + goog.asserts.assertInstanceof(evt.target, ol.Feature); + var feature = /** @type {ol.Feature} */ (evt.target); + var extents = []; + if (!goog.isNull(evt.oldExtent)) { + extents.push(evt.oldExtent); + } + var geometry = feature.getGeometry(); + if (!goog.isNull(geometry)) { + this.featureCache_.remove(feature, evt.oldExtent); + this.featureCache_.add(feature); + extents.push(geometry.getBounds()); + } + this.dispatchEvent(new ol.source.VectorEvent(ol.source.VectorEventType.CHANGE, + [feature], extents)); +}; + + +/** + * Listener for render intent change events of features. + * @param {ol.FeatureEvent} evt The feature intent change event. + * @private + */ +ol.source.Vector.prototype.handleIntentChange_ = function(evt) { + goog.asserts.assertInstanceof(evt.target, ol.Feature); + var feature = /** @type {ol.Feature} */ (evt.target); + var geometry = feature.getGeometry(); + if (!goog.isNull(geometry)) { + this.dispatchEvent(new ol.source.VectorEvent( + ol.source.VectorEventType.INTENTCHANGE, [feature], + [geometry.getBounds()])); + } +}; + + +/** + * Remove all features from the layer. + */ +ol.source.Vector.prototype.clear = function() { + this.featureCache_.clear(); + this.dispatchEvent( + new ol.source.VectorEvent(ol.source.VectorEventType.REMOVE, [], [])); +}; + + +/** * @param {ol.Extent} extent Extent that needs to be fetched. * @param {ol.proj.Projection} projection Projection of the view. * @param {function()=} opt_callback Callback which is called when features are * parsed after loading. * @return {ol.source.VectorLoadState} The current load state. */ -ol.source.Vector.prototype.prepareFeatures = function(layer, extent, projection, +ol.source.Vector.prototype.prepareFeatures = function(extent, projection, opt_callback) { // TODO: Implement strategies. BBOX aware strategies will need the extent. if (goog.isDef(this.url_) && @@ -77,7 +330,7 @@ ol.source.Vector.prototype.prepareFeatures = function(layer, extent, projection, var xhr = event.target; if (xhr.isSuccess()) { // TODO: Get source projection from data if supported by parser. - layer.parseFeatures(xhr.getResponseText(), this.parser_, projection); + this.parseFeatures(xhr.getResponseText(), projection); this.loadState_ = ol.source.VectorLoadState.LOADED; if (goog.isDef(opt_callback)) { opt_callback(); @@ -88,9 +341,186 @@ ol.source.Vector.prototype.prepareFeatures = function(layer, extent, projection, } }, this)); } else if (!goog.isNull(this.data_)) { - layer.parseFeatures(this.data_, this.parser_, projection); + this.parseFeatures(this.data_, projection); this.data_ = null; this.loadState_ = ol.source.VectorLoadState.LOADED; } return this.loadState_; }; + + +/** + * Remove features from the layer. + * @param {Array.} features Features to remove. + */ +ol.source.Vector.prototype.removeFeatures = function(features) { + var extent = ol.extent.createEmpty(), + feature, geometry; + for (var i = 0, ii = features.length; i < ii; ++i) { + feature = features[i]; + this.featureCache_.remove(feature); + geometry = feature.getGeometry(); + if (!goog.isNull(geometry)) { + ol.extent.extend(extent, geometry.getBounds()); + } + goog.events.unlisten(feature, ol.FeatureEventType.CHANGE, + this.handleFeatureChange_, false, this); + goog.events.unlisten(feature, ol.FeatureEventType.INTENTCHANGE, + this.handleIntentChange_, false, this); + } + this.dispatchEvent(new ol.source.VectorEvent(ol.source.VectorEventType.REMOVE, + features, [extent])); +}; + + +/** + * Sort function for `groupFeaturesBySymbolizerLiteral`. + * @private + * @param {Array} a 1st item for the sort comparison. + * @param {Array} b 2nd item for the sort comparison. + * @return {number} Comparison result. + */ +ol.source.Vector.prototype.sortByZIndex_ = function(a, b) { + return a[1].zIndex - b[1].zIndex; +}; + + + +/** + * @constructor + * @extends {goog.events.Event} + * @param {string} type Event type. + * @param {Array.} features Features associated with the event. + * @param {Array.} extents Any extents associated with the event. + */ +ol.source.VectorEvent = function(type, features, extents) { + + goog.base(this, type); + + /** + * @type {Array.} + */ + this.features = features; + + /** + * @type {Array.} + */ + this.extents = extents; + +}; +goog.inherits(ol.source.VectorEvent, goog.events.Event); + + +/** + * @enum {string} + */ +ol.source.VectorEventType = { + ADD: 'featureadd', + CHANGE: 'featurechange', + INTENTCHANGE: 'featureintentchange', + REMOVE: 'featureremove' +}; + + + +/** + * @constructor + */ +ol.source.FeatureCache = function() { + + /** + * @type {Object.} + * @private + */ + this.idLookup_; + + /** + * @type {ol.structs.RTree} + * @private + */ + this.rTree_; + + this.clear(); + +}; + + +/** + * Clear the cache. + */ +ol.source.FeatureCache.prototype.clear = function() { + this.idLookup_ = {}; + this.rTree_ = new ol.structs.RTree(); +}; + + +/** + * Add a feature to the cache. + * @param {ol.Feature} feature Feature to be cached. + */ +ol.source.FeatureCache.prototype.add = function(feature) { + var id = goog.getUid(feature).toString(), + geometry = feature.getGeometry(); + + this.idLookup_[id] = feature; + + // index by bounding box + if (!goog.isNull(geometry)) { + this.rTree_.insert(geometry.getBounds(), feature); + } +}; + + +/** + * @return {Object.} Object of features, keyed by id. + */ +ol.source.FeatureCache.prototype.getFeaturesObject = function() { + return this.idLookup_; +}; + + +/** + * Get all features whose bounding box intersects the provided extent. + * + * @param {ol.Extent} extent Bounding extent. + * @return {Object.} Features. + */ +ol.source.FeatureCache.prototype.getFeaturesObjectForExtent = function(extent) { + return this.rTree_.searchReturningObject(extent); +}; + + +/** + * Get features by ids. + * @param {Array.} ids Array of (internal) identifiers. + * @return {Array.} Array of features. + * @private + */ +ol.source.FeatureCache.prototype.getFeaturesByIds_ = function(ids) { + var len = ids.length, + features = new Array(len), + i; + for (i = 0; i < len; ++i) { + features[i] = this.idLookup_[ids[i]]; + } + return features; +}; + + +/** + * Remove a feature from the cache. + * @param {ol.Feature} feature Feature. + * @param {ol.Extent=} opt_extent Optional extent (used when the current feature + * extent is different than the one in the index). + */ +ol.source.FeatureCache.prototype.remove = function(feature, opt_extent) { + var id = goog.getUid(feature).toString(), + geometry = feature.getGeometry(); + + delete this.idLookup_[id]; + // index by bounding box + if (!goog.isNull(geometry)) { + var extent = goog.isDef(opt_extent) ? opt_extent : geometry.getBounds(); + this.rTree_.remove(extent, feature); + } +}; diff --git a/test/spec/ol/interaction/drawinteraction.test.js b/test/spec/ol/interaction/drawinteraction.test.js index f4c27f6b42..9f9867a7b2 100644 --- a/test/spec/ol/interaction/drawinteraction.test.js +++ b/test/spec/ol/interaction/drawinteraction.test.js @@ -1,7 +1,7 @@ goog.provide('ol.test.interaction.Draw'); describe('ol.interaction.Draw', function() { - var target, map, vector; + var target, map, source, layer; var width = 360; var height = 180; @@ -15,11 +15,12 @@ describe('ol.interaction.Draw', function() { style.width = width + 'px'; style.height = height + 'px'; document.body.appendChild(target); - vector = new ol.layer.Vector({source: new ol.source.Vector({})}); + source = new ol.source.Vector(); + layer = new ol.layer.Vector({source: source}); map = new ol.Map({ target: target, renderer: ol.RendererHint.CANVAS, - layers: [vector], + layers: [layer], view: new ol.View2D({ projection: 'EPSG:4326', center: [0, 0], @@ -56,7 +57,7 @@ describe('ol.interaction.Draw', function() { it('creates a new interaction', function() { var draw = new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.POINT }); expect(draw).to.be.a(ol.interaction.Draw); @@ -69,7 +70,7 @@ describe('ol.interaction.Draw', function() { beforeEach(function() { map.addInteraction(new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.POINT })); }); @@ -79,7 +80,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mousedown', 10, 20); simulateEvent('mouseup', 10, 20); simulateEvent('click', 10, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.Point); @@ -92,7 +93,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mousemove', 15, 20); simulateEvent('mouseup', 15, 20); simulateEvent('click', 15, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(0); }); @@ -102,7 +103,7 @@ describe('ol.interaction.Draw', function() { beforeEach(function() { map.addInteraction(new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.MULTIPOINT })); }); @@ -112,7 +113,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mousedown', 30, 15); simulateEvent('mouseup', 30, 15); simulateEvent('click', 30, 15); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.MultiPoint); @@ -125,7 +126,7 @@ describe('ol.interaction.Draw', function() { beforeEach(function() { map.addInteraction(new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.LINESTRING })); }); @@ -148,7 +149,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mouseup', 30, 20); simulateEvent('click', 30, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.LineString); @@ -181,7 +182,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mouseup', 30, 20); simulateEvent('click', 30, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.LineString); @@ -194,7 +195,7 @@ describe('ol.interaction.Draw', function() { beforeEach(function() { map.addInteraction(new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.MULTILINESTRING })); }); @@ -217,7 +218,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mouseup', 30, 20); simulateEvent('click', 30, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.MultiLineString); @@ -230,7 +231,7 @@ describe('ol.interaction.Draw', function() { beforeEach(function() { map.addInteraction(new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.POLYGON })); }); @@ -260,7 +261,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mouseup', 10, 20); simulateEvent('click', 10, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.Polygon); @@ -277,7 +278,7 @@ describe('ol.interaction.Draw', function() { beforeEach(function() { map.addInteraction(new ol.interaction.Draw({ - layer: vector, + layer: layer, type: ol.geom.GeometryType.MULTIPOLYGON })); }); @@ -307,7 +308,7 @@ describe('ol.interaction.Draw', function() { simulateEvent('mouseup', 10, 20); simulateEvent('click', 10, 20); - var features = vector.getFeatures(); + var features = source.getFeatures(); expect(features).to.have.length(1); var geometry = features[0].getGeometry(); expect(geometry).to.be.a(ol.geom.MultiPolygon); diff --git a/test/spec/ol/interaction/selectinteraction.test.js b/test/spec/ol/interaction/selectinteraction.test.js index 5c412af9dd..473653d352 100644 --- a/test/spec/ol/interaction/selectinteraction.test.js +++ b/test/spec/ol/interaction/selectinteraction.test.js @@ -1,7 +1,7 @@ goog.provide('ol.test.interaction.Select'); describe('ol.interaction.Select', function() { - var map, target, select, vector, features; + var map, target, select, source, vector, features; beforeEach(function() { target = document.createElement('div'); @@ -11,24 +11,19 @@ describe('ol.interaction.Select', function() { map = new ol.Map({ target: target }); - features = ol.parser.GeoJSON.read(JSON.stringify({ - 'type': 'FeatureCollection', - 'features': [{ - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [-1, 1] - } - }, { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [1, -1] - } - }] - })); - vector = new ol.layer.Vector({source: new ol.source.Vector({})}); - vector.addFeatures(features); + + features = [ + new ol.Feature({ + geometry: new ol.geom.Point([-1, 1]) + }), + new ol.Feature({ + geometry: new ol.geom.Point([1, -1]) + }) + ]; + + source = new ol.source.Vector({}); + source.addFeatures(features); + vector = new ol.layer.Vector({source: source}); select = new ol.interaction.Select({ layers: [vector] }); @@ -52,21 +47,21 @@ describe('ol.interaction.Select', function() { it('toggles selection of features', function() { select.select(map, [features], [vector]); - expect(vector.getFeatures(selectedFeaturesFilter).length).to.be(2); + expect(source.getFeatures(selectedFeaturesFilter).length).to.be(2); select.select(map, [features], [vector]); - expect(vector.getFeatures(selectedFeaturesFilter).length).to.be(0); + expect(source.getFeatures(selectedFeaturesFilter).length).to.be(0); }); it('can append features to an existing selection', function() { select.select(map, [[features[0]]], [vector], true); select.select(map, [[features[1]]], [vector]); - expect(vector.getFeatures(selectedFeaturesFilter).length).to.be(2); + expect(source.getFeatures(selectedFeaturesFilter).length).to.be(2); }); it('can clear a selection before selecting new features', function() { select.select(map, [[features[0]]], [vector], true); select.select(map, [[features[1]]], [vector], true); - expect(vector.getFeatures(selectedFeaturesFilter).length).to.be(1); + expect(source.getFeatures(selectedFeaturesFilter).length).to.be(1); }); }); @@ -74,8 +69,9 @@ describe('ol.interaction.Select', function() { }); goog.require('goog.dispose'); +goog.require('ol.Feature'); goog.require('ol.Map'); +goog.require('ol.geom.Point'); goog.require('ol.interaction.Select'); goog.require('ol.layer.Vector'); -goog.require('ol.parser.GeoJSON'); goog.require('ol.source.Vector'); diff --git a/test/spec/ol/layer/vectorlayer.test.js b/test/spec/ol/layer/vectorlayer.test.js index ea6977736e..f98cf65184 100644 --- a/test/spec/ol/layer/vectorlayer.test.js +++ b/test/spec/ol/layer/vectorlayer.test.js @@ -2,177 +2,15 @@ goog.provide('ol.test.layer.Vector'); describe('ol.layer.Vector', function() { - describe('#addFeatures()', function() { + describe('constructor', function() { + + it('creates a new layer', function() { - it('allows adding features', function() { var layer = new ol.layer.Vector({ - source: new ol.source.Vector({}) + source: new ol.source.Vector() }); - layer.addFeatures([new ol.Feature(), new ol.Feature()]); - expect(goog.object.getCount(layer.featureCache_.getFeaturesObject())) - .to.eql(2); - }); - }); - - describe('ol.layer.FeatureCache#getFeaturesObject()', function() { - - var layer, features; - - beforeEach(function() { - features = [ - new ol.Feature({ - g: new ol.geom.Point([16.0, 48.0]) - }), - new ol.Feature({ - g: new ol.geom.LineString([[17.0, 49.0], [17.1, 49.1]]) - }) - ]; - layer = new ol.layer.Vector({ - source: new ol.source.Vector({}) - }); - layer.addFeatures(features); - }); - - it('returns the features in an object', function() { - var featuresObject = layer.featureCache_.getFeaturesObject(); - expect(goog.object.getCount(featuresObject)).to.eql(features.length); - }); - - }); - - describe('#groupFeaturesBySymbolizerLiteral()', function() { - - var layer = new ol.layer.Vector({ - source: new ol.source.Vector({ - projection: ol.proj.get('EPSG:4326') - }), - style: new ol.style.Style({ - rules: [ - new ol.style.Rule({ - symbolizers: [ - new ol.style.Stroke({ - width: 2, - color: ol.expr.parse('colorProperty'), - opacity: 1 - }) - ] - }) - ] - }) - }); - var features; - - it('groups equal symbolizers', function() { - features = [ - new ol.Feature({ - g: new ol.geom.LineString([[-10, -10], [10, 10]]), - colorProperty: '#BADA55' - }), - new ol.Feature({ - g: new ol.geom.LineString([[-10, 10], [10, -10]]), - colorProperty: '#013' - }), - new ol.Feature({ - g: new ol.geom.LineString([[10, -10], [-10, -10]]), - colorProperty: '#013' - }) - ]; - - var groups = layer.groupFeaturesBySymbolizerLiteral(features, 1); - expect(groups.length).to.be(2); - expect(groups[0][0].length).to.be(1); - expect(groups[0][1].color).to.be('#BADA55'); - expect(groups[1][0].length).to.be(2); - expect(groups[1][1].color).to.be('#013'); - }); - - it('groups equal symbolizers also when defined on features', function() { - var symbolizer = new ol.style.Stroke({ - width: 3, - color: ol.expr.parse('colorProperty'), - opacity: 1 - }); - var anotherSymbolizer = new ol.style.Stroke({ - width: 3, - color: '#BADA55', - opacity: 1 - }); - var featureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, -10], [-10, 10]]), - colorProperty: '#BADA55' - }); - featureWithSymbolizers.setSymbolizers([symbolizer]); - var anotherFeatureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, 10], [-10, -10]]) - }); - anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); - features.push(featureWithSymbolizers, anotherFeatureWithSymbolizers); - - var groups = layer.groupFeaturesBySymbolizerLiteral(features, 1); - expect(groups).to.have.length(3); - expect(groups[2][0].length).to.be(2); - expect(groups[2][1].width).to.be(3); - - }); - - it('sorts groups by zIndex', function() { - var symbolizer = new ol.style.Stroke({ - width: 3, - color: '#BADA55', - opacity: 1, - zIndex: 1 - }); - var anotherSymbolizer = new ol.style.Stroke({ - width: 3, - color: '#BADA55', - opacity: 1 - }); - var featureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, -10], [-10, 10]]) - }); - featureWithSymbolizers.setSymbolizers([symbolizer]); - var anotherFeatureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, 10], [-10, -10]]) - }); - anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); - features = [featureWithSymbolizers, anotherFeatureWithSymbolizers]; - - var groups = layer.groupFeaturesBySymbolizerLiteral(features, 1); - expect(groups).to.have.length(2); - expect(groups[0][1].zIndex).to.be(0); - expect(groups[1][1].zIndex).to.be(1); - }); - - goog.dispose(layer); - - }); - - describe('ol.layer.VectorEvent', function() { - - var layer, features; - - beforeEach(function() { - features = [ - new ol.Feature({ - g: new ol.geom.Point([16.0, 48.0]) - }), - new ol.Feature({ - g: new ol.geom.LineString([[17.0, 49.0], [17.1, 49.1]]) - }) - ]; - layer = new ol.layer.Vector({ - source: new ol.source.Vector({}) - }); - layer.addFeatures(features); - }); - - it('dispatches events on feature change', function(done) { - layer.on('featurechange', function(evt) { - expect(evt.features[0]).to.be(features[0]); - expect(evt.extents[0]).to.eql(features[0].getGeometry().getBounds()); - done(); - }); - features[0].set('foo', 'bar'); + expect(layer).to.be.a(ol.layer.Vector); + expect(layer).to.be.a(ol.layer.Layer); }); @@ -180,15 +18,6 @@ describe('ol.layer.Vector', function() { }); -goog.require('goog.dispose'); -goog.require('goog.object'); -goog.require('ol.Feature'); -goog.require('ol.expr'); -goog.require('ol.geom.LineString'); -goog.require('ol.geom.Point'); -goog.require('ol.proj'); +goog.require('ol.layer.Layer'); goog.require('ol.layer.Vector'); goog.require('ol.source.Vector'); -goog.require('ol.style.Rule'); -goog.require('ol.style.Stroke'); -goog.require('ol.style.Style'); diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 8029a492aa..a1deeb5d51 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -5,27 +5,134 @@ describe('ol.source.Vector', function() { describe('constructor', function() { it('creates an instance', function() { - var source = new ol.source.Vector({}); + var source = new ol.source.Vector(); expect(source).to.be.a(ol.source.Vector); expect(source).to.be.a(ol.source.Source); }); }); + describe('#addFeatures()', function() { + + it('allows adding features', function() { + var source = new ol.source.Vector(); + source.addFeatures([new ol.Feature(), new ol.Feature()]); + expect(goog.object.getCount(source.featureCache_.getFeaturesObject())) + .to.eql(2); + }); + }); + + describe('#groupFeaturesBySymbolizerLiteral()', function() { + + var source = new ol.source.Vector({ + projection: ol.proj.get('EPSG:4326') + }); + + var style = new ol.style.Style({ + symbolizers: [ + new ol.style.Stroke({ + width: 2, + color: ol.expr.parse('colorProperty'), + opacity: 1 + }) + ] + }); + var features; + + it('groups equal symbolizers', function() { + features = [ + new ol.Feature({ + g: new ol.geom.LineString([[-10, -10], [10, 10]]), + colorProperty: '#BADA55' + }), + new ol.Feature({ + g: new ol.geom.LineString([[-10, 10], [10, -10]]), + colorProperty: '#013' + }), + new ol.Feature({ + g: new ol.geom.LineString([[10, -10], [-10, -10]]), + colorProperty: '#013' + }) + ]; + + var groups = source.groupFeaturesBySymbolizerLiteral(style, features, 1); + expect(groups.length).to.be(2); + expect(groups[0][0].length).to.be(1); + expect(groups[0][1].color).to.be('#BADA55'); + expect(groups[1][0].length).to.be(2); + expect(groups[1][1].color).to.be('#013'); + }); + + it('groups equal symbolizers also when defined on features', function() { + var symbolizer = new ol.style.Stroke({ + width: 3, + color: ol.expr.parse('colorProperty'), + opacity: 1 + }); + var anotherSymbolizer = new ol.style.Stroke({ + width: 3, + color: '#BADA55', + opacity: 1 + }); + var featureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, -10], [-10, 10]]), + colorProperty: '#BADA55' + }); + featureWithSymbolizers.setSymbolizers([symbolizer]); + var anotherFeatureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, 10], [-10, -10]]) + }); + anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); + features.push(featureWithSymbolizers, anotherFeatureWithSymbolizers); + + var groups = source.groupFeaturesBySymbolizerLiteral(style, features, 1); + expect(groups).to.have.length(3); + expect(groups[2][0].length).to.be(2); + expect(groups[2][1].width).to.be(3); + + }); + + it('sorts groups by zIndex', function() { + var symbolizer = new ol.style.Stroke({ + width: 3, + color: '#BADA55', + opacity: 1, + zIndex: 1 + }); + var anotherSymbolizer = new ol.style.Stroke({ + width: 3, + color: '#BADA55', + opacity: 1 + }); + var featureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, -10], [-10, 10]]) + }); + featureWithSymbolizers.setSymbolizers([symbolizer]); + var anotherFeatureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, 10], [-10, -10]]) + }); + anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); + features = [featureWithSymbolizers, anotherFeatureWithSymbolizers]; + + var groups = source.groupFeaturesBySymbolizerLiteral(style, features, 1); + expect(groups).to.have.length(2); + expect(groups[0][1].zIndex).to.be(0); + expect(groups[1][1].zIndex).to.be(1); + }); + + }); + describe('#prepareFeatures', function() { it('loads and parses data from a file', function(done) { var source = new ol.source.Vector({ url: 'spec/ol/parser/geojson/countries.geojson', parser: new ol.parser.GeoJSON() }); - var layer = new ol.layer.Vector({ - source: source - }); - source.prepareFeatures(layer, [-180, -90, 180, 90], + source.prepareFeatures([-180, -90, 180, 90], ol.proj.get('EPSG:4326'), function() { expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); expect(goog.object.getCount( - layer.featureCache_.getFeaturesObject())).to.be(179); + source.featureCache_.getFeaturesObject())).to.be(179); done(); }); }); @@ -63,25 +170,93 @@ describe('ol.source.Vector', function() { parser: new ol.parser.GeoJSON(), projection: ol.proj.get('EPSG:4326') }); - var layer = new ol.layer.Vector({ - source: source - }); - source.prepareFeatures(layer, [-180, -90, 180, 90], + source.prepareFeatures([-180, -90, 180, 90], ol.proj.get('EPSG:4326'), function() { expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); expect(goog.object.getCount( - layer.featureCache_.getFeaturesObject())).to.be(4); + source.featureCache_.getFeaturesObject())).to.be(4); done(); }); }); + + + + }); + + describe('featurechange event', function() { + + var source, features; + + beforeEach(function() { + features = [ + new ol.Feature({ + g: new ol.geom.Point([16.0, 48.0]) + }), + new ol.Feature({ + g: new ol.geom.LineString([[17.0, 49.0], [17.1, 49.1]]) + }) + ]; + source = new ol.source.Vector(); + source.addFeatures(features); + }); + + it('is dispatched on attribute changes', function(done) { + goog.events.listen(source, ol.source.VectorEventType.CHANGE, + function(evt) { + var expected = features[0]; + expect(evt.features[0]).to.be(expected); + expect(evt.extents[0]).to.eql(expected.getGeometry().getBounds()); + done(); + + }); + + features[0].set('foo', 'bar'); + }); + }); }); +describe('ol.source.FeatureCache', function() { + + describe('#getFeaturesObject()', function() { + var source, features; + + beforeEach(function() { + features = [ + new ol.Feature({ + g: new ol.geom.Point([16.0, 48.0]) + }), + new ol.Feature({ + g: new ol.geom.LineString([[17.0, 49.0], [17.1, 49.1]]) + }) + ]; + source = new ol.source.Vector(); + source.addFeatures(features); + }); + + it('returns the features in an object', function() { + var featuresObject = source.featureCache_.getFeaturesObject(); + expect(goog.object.getCount(featuresObject)).to.eql(features.length); + }); + + }); + +}); + +goog.require('goog.dispose'); +goog.require('goog.events'); goog.require('goog.object'); -goog.require('ol.layer.Vector'); +goog.require('ol.Feature'); +goog.require('ol.expr'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point'); goog.require('ol.parser.GeoJSON'); goog.require('ol.proj'); +goog.require('ol.source.FeatureCache'); goog.require('ol.source.Source'); goog.require('ol.source.Vector'); +goog.require('ol.source.VectorEventType'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); From 01a0b9ff8d77ceacb9bce2939abf9064a28dc68d Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 15 Nov 2013 13:37:06 -0700 Subject: [PATCH 03/18] Private source methods --- src/ol/source/vectorsource.js | 12 +++++++----- test/spec/ol/source/vectorsource.test.js | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 2e13555d6a..2303f735a3 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -141,7 +141,7 @@ ol.source.Vector.prototype.getFeatures = function(opt_filter) { */ ol.source.Vector.prototype.getFeaturesObjectForExtent = function(extent, projection, opt_callback) { - var state = this.prepareFeatures(extent, projection, opt_callback); + var state = this.prepareFeatures_(extent, projection, opt_callback); var lookup = null; if (state !== ol.source.VectorLoadState.LOADING) { lookup = this.featureCache_.getFeaturesObjectForExtent(extent); @@ -214,8 +214,9 @@ ol.source.Vector.prototype.groupFeaturesBySymbolizerLiteral = * @param {Object|Element|Document|string} data Feature data. * @param {ol.proj.Projection} projection This sucks. The layer should be a * view in one projection. + * @private */ -ol.source.Vector.prototype.parseFeatures = function(data, projection) { +ol.source.Vector.prototype.parseFeatures_ = function(data, projection) { var addFeatures = function(data) { var features = data.features; @@ -319,8 +320,9 @@ ol.source.Vector.prototype.clear = function() { * @param {function()=} opt_callback Callback which is called when features are * parsed after loading. * @return {ol.source.VectorLoadState} The current load state. + * @private */ -ol.source.Vector.prototype.prepareFeatures = function(extent, projection, +ol.source.Vector.prototype.prepareFeatures_ = function(extent, projection, opt_callback) { // TODO: Implement strategies. BBOX aware strategies will need the extent. if (goog.isDef(this.url_) && @@ -330,7 +332,7 @@ ol.source.Vector.prototype.prepareFeatures = function(extent, projection, var xhr = event.target; if (xhr.isSuccess()) { // TODO: Get source projection from data if supported by parser. - this.parseFeatures(xhr.getResponseText(), projection); + this.parseFeatures_(xhr.getResponseText(), projection); this.loadState_ = ol.source.VectorLoadState.LOADED; if (goog.isDef(opt_callback)) { opt_callback(); @@ -341,7 +343,7 @@ ol.source.Vector.prototype.prepareFeatures = function(extent, projection, } }, this)); } else if (!goog.isNull(this.data_)) { - this.parseFeatures(this.data_, projection); + this.parseFeatures_(this.data_, projection); this.data_ = null; this.loadState_ = ol.source.VectorLoadState.LOADED; } diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index a1deeb5d51..24e1a3d7f0 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -121,13 +121,13 @@ describe('ol.source.Vector', function() { }); - describe('#prepareFeatures', function() { + describe('#prepareFeatures_', function() { it('loads and parses data from a file', function(done) { var source = new ol.source.Vector({ url: 'spec/ol/parser/geojson/countries.geojson', parser: new ol.parser.GeoJSON() }); - source.prepareFeatures([-180, -90, 180, 90], + source.prepareFeatures_([-180, -90, 180, 90], ol.proj.get('EPSG:4326'), function() { expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); @@ -170,7 +170,7 @@ describe('ol.source.Vector', function() { parser: new ol.parser.GeoJSON(), projection: ol.proj.get('EPSG:4326') }); - source.prepareFeatures([-180, -90, 180, 90], + source.prepareFeatures_([-180, -90, 180, 90], ol.proj.get('EPSG:4326'), function() { expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); From 307e425891a15c5c09603d74dc4beaf22b47f1da Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 15 Nov 2013 13:38:33 -0700 Subject: [PATCH 04/18] Remove unused method --- src/ol/source/vectorsource.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 2303f735a3..0d5d91a45f 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -304,16 +304,6 @@ ol.source.Vector.prototype.handleIntentChange_ = function(evt) { }; -/** - * Remove all features from the layer. - */ -ol.source.Vector.prototype.clear = function() { - this.featureCache_.clear(); - this.dispatchEvent( - new ol.source.VectorEvent(ol.source.VectorEventType.REMOVE, [], [])); -}; - - /** * @param {ol.Extent} extent Extent that needs to be fetched. * @param {ol.proj.Projection} projection Projection of the view. From bfaed4d52d556dfa6a07089c3c3ed9636d297dea Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 15 Nov 2013 14:19:57 -0700 Subject: [PATCH 05/18] Moving all style functionality from source to style --- .../canvas/canvasvectorlayerrenderer.js | 14 ++- src/ol/source/vectorsource.js | 75 ------------- src/ol/style/style.js | 66 +++++++++++ test/spec/ol/source/vectorsource.test.js | 103 ------------------ test/spec/ol/style/style.test.js | 96 ++++++++++++++++ 5 files changed, 172 insertions(+), 182 deletions(-) diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index bcde996390..e3e5550481 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -18,6 +18,7 @@ goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.Vector'); goog.require('ol.source.VectorEventType'); +goog.require('ol.style'); goog.require('ol.tilegrid.TileGrid'); @@ -475,7 +476,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = var dirty = false; var layer = this.getVectorLayer(); var source = layer.getSource(); - var tileExtent, groups, group, j, numGroups, featuresObject, tileHasFeatures; + var tileExtent, featuresObject, tileHasFeatures; fetchTileData: for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { @@ -509,10 +510,15 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = } this.dirty_ = dirty; - groups = source.groupFeaturesBySymbolizerLiteral(layer.getStyle(), + var style = layer.getStyle(); + if (goog.isNull(style)) { + style = ol.style.getDefault(); + } + var groups = style.groupFeaturesBySymbolizerLiteral( featuresToRender, tileResolution); - numGroups = groups.length; - for (j = 0; j < numGroups; ++j) { + var numGroups = groups.length; + var group; + for (var j = 0; j < numGroups; ++j) { group = groups[j]; deferred = sketchCanvasRenderer.renderFeatures(group[0], group[1], group[2]); diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 0d5d91a45f..0e049b8e93 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -13,9 +13,6 @@ goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.source.Source'); goog.require('ol.structs.RTree'); -goog.require('ol.style'); -goog.require('ol.style.Style'); -goog.require('ol.style.TextLiteral'); /** @@ -150,66 +147,6 @@ ol.source.Vector.prototype.getFeaturesObjectForExtent = function(extent, }; -/** - * TODO: This should be a ol.style.Style method. - * @param {ol.style.Style} style Style. - * @param {Object.} features Features. - * @param {number} resolution Map resolution. - * @return {Array.} symbolizers for features. Each array in this array - * contains 3 items: an array of features, the symbolizer literal, and - * an array with optional additional data for each feature. - */ -ol.source.Vector.prototype.groupFeaturesBySymbolizerLiteral = - function(style, features, resolution) { - var uniqueLiterals = {}, - featuresBySymbolizer = [], - i, j, l, feature, symbolizers, literals, numLiterals, literal, - uniqueLiteral, key, item; - for (i in features) { - feature = features[i]; - // feature level symbolizers take precedence - symbolizers = feature.getSymbolizers(); - if (!goog.isNull(symbolizers)) { - literals = ol.style.Style.createLiterals(symbolizers, feature); - } else { - // layer style second - if (goog.isNull(style)) { - style = ol.style.getDefault(); - } - literals = style.createLiterals(feature, resolution); - } - numLiterals = literals.length; - for (j = 0; j < numLiterals; ++j) { - literal = literals[j]; - for (l in uniqueLiterals) { - uniqueLiteral = featuresBySymbolizer[uniqueLiterals[l]][1]; - if (literal.equals(uniqueLiteral)) { - literal = uniqueLiteral; - break; - } - } - key = goog.getUid(literal); - if (!goog.object.containsKey(uniqueLiterals, key)) { - uniqueLiterals[key] = featuresBySymbolizer.length; - featuresBySymbolizer.push([ - /** @type {Array.} */ ([]), - /** @type {ol.style.Literal} */ (literal), - /** @type {Array} */ ([]) - ]); - } - item = featuresBySymbolizer[uniqueLiterals[key]]; - item[0].push(feature); - if (literal instanceof ol.style.TextLiteral) { - item[2].push(literals[j].text); - } - } - } - // TODO: move sort function to ol.style.Style - featuresBySymbolizer.sort(this.sortByZIndex_); - return featuresBySymbolizer; -}; - - /** * @param {Object|Element|Document|string} data Feature data. * @param {ol.proj.Projection} projection This sucks. The layer should be a @@ -365,18 +302,6 @@ ol.source.Vector.prototype.removeFeatures = function(features) { }; -/** - * Sort function for `groupFeaturesBySymbolizerLiteral`. - * @private - * @param {Array} a 1st item for the sort comparison. - * @param {Array} b 2nd item for the sort comparison. - * @return {number} Comparison result. - */ -ol.source.Vector.prototype.sortByZIndex_ = function(a, b) { - return a[1].zIndex - b[1].zIndex; -}; - - /** * @constructor diff --git a/src/ol/style/style.js b/src/ol/style/style.js index c8d70ef635..e9bfee8657 100644 --- a/src/ol/style/style.js +++ b/src/ol/style/style.js @@ -15,6 +15,7 @@ goog.require('ol.style.Rule'); goog.require('ol.style.Shape'); goog.require('ol.style.Stroke'); goog.require('ol.style.Symbolizer'); +goog.require('ol.style.TextLiteral'); @@ -82,6 +83,71 @@ ol.style.Style.prototype.createLiterals = function(feature, resolution) { }; +/** + * @param {Object.} features Features. + * @param {number} resolution Map resolution. + * @return {Array.} symbolizers for features. Each array in this array + * contains 3 items: an array of features, the symbolizer literal, and + * an array with optional additional data for each feature. + */ +ol.style.Style.prototype.groupFeaturesBySymbolizerLiteral = + function(features, resolution) { + var uniqueLiterals = {}, + featuresBySymbolizer = [], + i, j, l, feature, symbolizers, literals, numLiterals, literal, + uniqueLiteral, key, item; + for (i in features) { + feature = features[i]; + // feature level symbolizers take precedence + symbolizers = feature.getSymbolizers(); + if (!goog.isNull(symbolizers)) { + literals = ol.style.Style.createLiterals(symbolizers, feature); + } else { + literals = this.createLiterals(feature, resolution); + } + numLiterals = literals.length; + for (j = 0; j < numLiterals; ++j) { + literal = literals[j]; + for (l in uniqueLiterals) { + uniqueLiteral = featuresBySymbolizer[uniqueLiterals[l]][1]; + if (literal.equals(uniqueLiteral)) { + literal = uniqueLiteral; + break; + } + } + key = goog.getUid(literal); + if (!goog.object.containsKey(uniqueLiterals, key)) { + uniqueLiterals[key] = featuresBySymbolizer.length; + featuresBySymbolizer.push([ + /** @type {Array.} */ ([]), + /** @type {ol.style.Literal} */ (literal), + /** @type {Array} */ ([]) + ]); + } + item = featuresBySymbolizer[uniqueLiterals[key]]; + item[0].push(feature); + if (literal instanceof ol.style.TextLiteral) { + item[2].push(literals[j].text); + } + } + } + featuresBySymbolizer.sort(this.sortByZIndex_); + return featuresBySymbolizer; +}; + + +/** + * Sort function for `groupFeaturesBySymbolizerLiteral`. + * @private + * @param {Array} a 1st item for the sort comparison. + * @param {Array} b 2nd item for the sort comparison. + * @return {number} Comparison result. + */ +ol.style.Style.prototype.sortByZIndex_ = function(a, b) { + return a[1].zIndex - b[1].zIndex; +}; + + /** * The default style. * @type {ol.style.Style} diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 24e1a3d7f0..d40c31cc9d 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -21,106 +21,6 @@ describe('ol.source.Vector', function() { }); }); - describe('#groupFeaturesBySymbolizerLiteral()', function() { - - var source = new ol.source.Vector({ - projection: ol.proj.get('EPSG:4326') - }); - - var style = new ol.style.Style({ - symbolizers: [ - new ol.style.Stroke({ - width: 2, - color: ol.expr.parse('colorProperty'), - opacity: 1 - }) - ] - }); - var features; - - it('groups equal symbolizers', function() { - features = [ - new ol.Feature({ - g: new ol.geom.LineString([[-10, -10], [10, 10]]), - colorProperty: '#BADA55' - }), - new ol.Feature({ - g: new ol.geom.LineString([[-10, 10], [10, -10]]), - colorProperty: '#013' - }), - new ol.Feature({ - g: new ol.geom.LineString([[10, -10], [-10, -10]]), - colorProperty: '#013' - }) - ]; - - var groups = source.groupFeaturesBySymbolizerLiteral(style, features, 1); - expect(groups.length).to.be(2); - expect(groups[0][0].length).to.be(1); - expect(groups[0][1].color).to.be('#BADA55'); - expect(groups[1][0].length).to.be(2); - expect(groups[1][1].color).to.be('#013'); - }); - - it('groups equal symbolizers also when defined on features', function() { - var symbolizer = new ol.style.Stroke({ - width: 3, - color: ol.expr.parse('colorProperty'), - opacity: 1 - }); - var anotherSymbolizer = new ol.style.Stroke({ - width: 3, - color: '#BADA55', - opacity: 1 - }); - var featureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, -10], [-10, 10]]), - colorProperty: '#BADA55' - }); - featureWithSymbolizers.setSymbolizers([symbolizer]); - var anotherFeatureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, 10], [-10, -10]]) - }); - anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); - features.push(featureWithSymbolizers, anotherFeatureWithSymbolizers); - - var groups = source.groupFeaturesBySymbolizerLiteral(style, features, 1); - expect(groups).to.have.length(3); - expect(groups[2][0].length).to.be(2); - expect(groups[2][1].width).to.be(3); - - }); - - it('sorts groups by zIndex', function() { - var symbolizer = new ol.style.Stroke({ - width: 3, - color: '#BADA55', - opacity: 1, - zIndex: 1 - }); - var anotherSymbolizer = new ol.style.Stroke({ - width: 3, - color: '#BADA55', - opacity: 1 - }); - var featureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, -10], [-10, 10]]) - }); - featureWithSymbolizers.setSymbolizers([symbolizer]); - var anotherFeatureWithSymbolizers = new ol.Feature({ - g: new ol.geom.LineString([[-10, 10], [-10, -10]]) - }); - anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); - features = [featureWithSymbolizers, anotherFeatureWithSymbolizers]; - - var groups = source.groupFeaturesBySymbolizerLiteral(style, features, 1); - expect(groups).to.have.length(2); - expect(groups[0][1].zIndex).to.be(0); - expect(groups[1][1].zIndex).to.be(1); - }); - - }); - describe('#prepareFeatures_', function() { it('loads and parses data from a file', function(done) { var source = new ol.source.Vector({ @@ -249,7 +149,6 @@ goog.require('goog.dispose'); goog.require('goog.events'); goog.require('goog.object'); goog.require('ol.Feature'); -goog.require('ol.expr'); goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); goog.require('ol.parser.GeoJSON'); @@ -258,5 +157,3 @@ goog.require('ol.source.FeatureCache'); goog.require('ol.source.Source'); goog.require('ol.source.Vector'); goog.require('ol.source.VectorEventType'); -goog.require('ol.style.Stroke'); -goog.require('ol.style.Style'); diff --git a/test/spec/ol/style/style.test.js b/test/spec/ol/style/style.test.js index d282a06dcd..a58e68275d 100644 --- a/test/spec/ol/style/style.test.js +++ b/test/spec/ol/style/style.test.js @@ -122,6 +122,102 @@ describe('ol.style.Style', function() { }); + describe('#groupFeaturesBySymbolizerLiteral()', function() { + + var style = new ol.style.Style({ + symbolizers: [ + new ol.style.Stroke({ + width: 2, + color: ol.expr.parse('colorProperty'), + opacity: 1 + }) + ] + }); + var features; + + it('groups equal symbolizers', function() { + features = [ + new ol.Feature({ + g: new ol.geom.LineString([[-10, -10], [10, 10]]), + colorProperty: '#BADA55' + }), + new ol.Feature({ + g: new ol.geom.LineString([[-10, 10], [10, -10]]), + colorProperty: '#013' + }), + new ol.Feature({ + g: new ol.geom.LineString([[10, -10], [-10, -10]]), + colorProperty: '#013' + }) + ]; + + var groups = style.groupFeaturesBySymbolizerLiteral(features, 1); + expect(groups.length).to.be(2); + expect(groups[0][0].length).to.be(1); + expect(groups[0][1].color).to.be('#BADA55'); + expect(groups[1][0].length).to.be(2); + expect(groups[1][1].color).to.be('#013'); + }); + + it('groups equal symbolizers also when defined on features', function() { + var symbolizer = new ol.style.Stroke({ + width: 3, + color: ol.expr.parse('colorProperty'), + opacity: 1 + }); + var anotherSymbolizer = new ol.style.Stroke({ + width: 3, + color: '#BADA55', + opacity: 1 + }); + var featureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, -10], [-10, 10]]), + colorProperty: '#BADA55' + }); + featureWithSymbolizers.setSymbolizers([symbolizer]); + var anotherFeatureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, 10], [-10, -10]]) + }); + anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); + features.push(featureWithSymbolizers, anotherFeatureWithSymbolizers); + + var groups = style.groupFeaturesBySymbolizerLiteral(features, 1); + expect(groups).to.have.length(3); + expect(groups[2][0].length).to.be(2); + expect(groups[2][1].width).to.be(3); + + }); + + it('sorts groups by zIndex', function() { + var symbolizer = new ol.style.Stroke({ + width: 3, + color: '#BADA55', + opacity: 1, + zIndex: 1 + }); + var anotherSymbolizer = new ol.style.Stroke({ + width: 3, + color: '#BADA55', + opacity: 1 + }); + var featureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, -10], [-10, 10]]) + }); + featureWithSymbolizers.setSymbolizers([symbolizer]); + var anotherFeatureWithSymbolizers = new ol.Feature({ + g: new ol.geom.LineString([[-10, 10], [-10, -10]]) + }); + anotherFeatureWithSymbolizers.setSymbolizers([anotherSymbolizer]); + features = [featureWithSymbolizers, anotherFeatureWithSymbolizers]; + + var groups = style.groupFeaturesBySymbolizerLiteral(features, 1); + expect(groups).to.have.length(2); + expect(groups[0][1].zIndex).to.be(0); + expect(groups[1][1].zIndex).to.be(1); + }); + + }); + describe('ol.style.getDefault()', function() { var style = ol.style.getDefault(); From ec02e09ce8de19a7800ed9d83609fa0edf6e40dc Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 15 Nov 2013 17:01:11 -0700 Subject: [PATCH 06/18] Move render intent enum to feature --- src/ol/feature.js | 18 +++++++++++++++--- src/ol/interaction/drawinteraction.js | 8 ++++---- src/ol/interaction/modifyinteraction.js | 14 +++++++------- src/ol/interaction/selectinteraction.js | 10 +++++----- src/ol/layer/vectorlayer.js | 4 ++-- src/ol/layer/vectorlayerrenderintent.js | 13 ------------- .../canvas/canvasvectorlayerrenderer.js | 4 ++-- src/ol/renderer/canvas/canvasvectorrenderer.js | 10 +++++----- 8 files changed, 40 insertions(+), 41 deletions(-) delete mode 100644 src/ol/layer/vectorlayerrenderintent.js diff --git a/src/ol/feature.js b/src/ol/feature.js index 787922c46d..cb9a478d63 100644 --- a/src/ol/feature.js +++ b/src/ol/feature.js @@ -1,6 +1,7 @@ goog.provide('ol.Feature'); goog.provide('ol.FeatureEvent'); goog.provide('ol.FeatureEventType'); +goog.provide('ol.FeatureRenderIntent'); goog.require('goog.events'); goog.require('goog.events.Event'); @@ -8,7 +9,6 @@ goog.require('goog.events.EventType'); goog.require('ol.Object'); goog.require('ol.geom.Geometry'); goog.require('ol.geom.GeometryEvent'); -goog.require('ol.layer.VectorLayerRenderIntent'); @@ -51,10 +51,10 @@ ol.Feature = function(opt_values) { /** * The render intent for this feature. - * @type {ol.layer.VectorLayerRenderIntent|string} + * @type {ol.FeatureRenderIntent|string} * @private */ - this.renderIntent_ = ol.layer.VectorLayerRenderIntent.DEFAULT; + this.renderIntent_ = ol.FeatureRenderIntent.DEFAULT; /** * @type {Array.} @@ -247,6 +247,18 @@ ol.Feature.prototype.setSymbolizers = function(symbolizers) { ol.Feature.DEFAULT_GEOMETRY = 'geometry'; +/** + * @enum {string} + */ +ol.FeatureRenderIntent = { + DEFAULT: 'default', + FUTURE: 'future', + HIDDEN: 'hidden', + SELECTED: 'selected', + TEMPORARY: 'temporary' +}; + + /** * @enum {string} */ diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index a220934ea0..b46e6d2050 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -4,6 +4,7 @@ goog.require('goog.asserts'); goog.require('ol.Coordinate'); goog.require('ol.Feature'); +goog.require('ol.FeatureRenderIntent'); goog.require('ol.Map'); goog.require('ol.MapBrowserEvent'); goog.require('ol.MapBrowserEvent.EventType'); @@ -16,7 +17,6 @@ goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); goog.require('ol.interaction.Interaction'); goog.require('ol.layer.Vector'); -goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.source.Vector'); @@ -233,7 +233,7 @@ ol.interaction.Draw.prototype.startDrawing_ = function(event) { var start = event.getCoordinate(); this.finishCoordinate_ = start; var sketchFeature = new ol.Feature(); - sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.SELECTED); + sketchFeature.setRenderIntent(ol.FeatureRenderIntent.SELECTED); var features = [sketchFeature]; var geometry; if (this.mode_ === ol.interaction.DrawMode.POINT) { @@ -242,7 +242,7 @@ ol.interaction.Draw.prototype.startDrawing_ = function(event) { var sketchPoint = new ol.Feature({ geom: new ol.geom.Point(start.slice()) }); - sketchPoint.setRenderIntent(ol.layer.VectorLayerRenderIntent.TEMPORARY); + sketchPoint.setRenderIntent(ol.FeatureRenderIntent.TEMPORARY); this.sketchPoint_ = sketchPoint; features.push(sketchPoint); @@ -325,7 +325,7 @@ ol.interaction.Draw.prototype.addToDrawing_ = function(event) { ol.interaction.Draw.prototype.finishDrawing_ = function(event) { var sketchFeature = this.abortDrawing_(); goog.asserts.assert(!goog.isNull(sketchFeature)); - sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); + sketchFeature.setRenderIntent(ol.FeatureRenderIntent.DEFAULT); var geometry = sketchFeature.getGeometry(); var coordinates = geometry.getCoordinates(); if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { diff --git a/src/ol/interaction/modifyinteraction.js b/src/ol/interaction/modifyinteraction.js index 19053979e2..020dd95d4e 100644 --- a/src/ol/interaction/modifyinteraction.js +++ b/src/ol/interaction/modifyinteraction.js @@ -6,6 +6,7 @@ goog.require('goog.events'); goog.require('goog.functions'); goog.require('ol.CollectionEventType'); goog.require('ol.Feature'); +goog.require('ol.FeatureRenderIntent'); goog.require('ol.MapBrowserEvent.EventType'); goog.require('ol.ViewHint'); goog.require('ol.coordinate'); @@ -18,7 +19,6 @@ goog.require('ol.geom.Polygon'); goog.require('ol.interaction.Drag'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Vector'); -goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.source.Vector'); goog.require('ol.source.VectorEventType'); goog.require('ol.structs.RBush'); @@ -273,7 +273,7 @@ ol.interaction.Modify.prototype.handleIntentChange_ = function(evt) { for (var i = 0, ii = features.length; i < ii; ++i) { var feature = features[i]; var renderIntent = feature.getRenderIntent(); - if (renderIntent == ol.layer.VectorLayerRenderIntent.SELECTED) { + if (renderIntent == ol.FeatureRenderIntent.SELECTED) { this.addIndex_([feature], layer); } else { this.removeIndex_([feature]); @@ -356,7 +356,7 @@ ol.interaction.Modify.prototype.handleDragStart = function(evt) { this.dragSegments_ = []; var vertexFeature = this.vertexFeature_; if (!goog.isNull(vertexFeature) && vertexFeature.getRenderIntent() != - ol.layer.VectorLayerRenderIntent.HIDDEN) { + ol.FeatureRenderIntent.HIDDEN) { var renderIntent = vertexFeature.getRenderIntent(); var insertVertices = []; var vertex = vertexFeature.getGeometry().getCoordinates(); @@ -375,7 +375,7 @@ ol.interaction.Modify.prototype.handleDragStart = function(evt) { original.setSymbolizers(feature.getSymbolizers()); feature.setOriginal(original); } - if (renderIntent == ol.layer.VectorLayerRenderIntent.TEMPORARY) { + if (renderIntent == ol.FeatureRenderIntent.TEMPORARY) { if (ol.coordinate.equals(segment[0], vertex)) { dragSegments.push([node, 0]); } else if (ol.coordinate.equals(segment[1], vertex)) { @@ -470,7 +470,7 @@ ol.interaction.Modify.prototype.handleMouseMove_ = function(evt) { var vertexFeature = this.vertexFeature_; var rBush = this.rBush_; var nodes = rBush.getAllInExtent(box); - var renderIntent = ol.layer.VectorLayerRenderIntent.HIDDEN; + var renderIntent = ol.FeatureRenderIntent.HIDDEN; if (nodes.length > 0) { nodes.sort(sortByDistance); var node = nodes[0]; @@ -484,10 +484,10 @@ ol.interaction.Modify.prototype.handleMouseMove_ = function(evt) { var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1); var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2); var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); - renderIntent = ol.layer.VectorLayerRenderIntent.FUTURE; + renderIntent = ol.FeatureRenderIntent.FUTURE; if (dist <= 10) { vertex = squaredDist1 > squaredDist2 ? segment[1] : segment[0]; - renderIntent = ol.layer.VectorLayerRenderIntent.TEMPORARY; + renderIntent = ol.FeatureRenderIntent.TEMPORARY; } vertexFeature = this.createOrUpdateVertexFeature_(node.style, vertex); this.modifiable_ = true; diff --git a/src/ol/interaction/selectinteraction.js b/src/ol/interaction/selectinteraction.js index 8f1b08d9df..6442cd9d5b 100644 --- a/src/ol/interaction/selectinteraction.js +++ b/src/ol/interaction/selectinteraction.js @@ -3,11 +3,11 @@ goog.provide('ol.interaction.Select'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('ol.Feature'); +goog.require('ol.FeatureRenderIntent'); goog.require('ol.events.ConditionType'); goog.require('ol.events.condition'); goog.require('ol.interaction.Interaction'); goog.require('ol.layer.Vector'); -goog.require('ol.layer.VectorLayerRenderIntent'); @@ -102,16 +102,16 @@ ol.interaction.Select.prototype.select = if (clear) { for (var j = selectedFeatures.length - 1; j >= 0; --j) { selectedFeatures[j].setRenderIntent( - ol.layer.VectorLayerRenderIntent.DEFAULT); + ol.FeatureRenderIntent.DEFAULT); } } for (var j = featuresToSelect.length - 1; j >= 0; --j) { var feature = featuresToSelect[j]; // TODO: Make toggle configurable feature.setRenderIntent(feature.getRenderIntent() == - ol.layer.VectorLayerRenderIntent.SELECTED ? - ol.layer.VectorLayerRenderIntent.DEFAULT : - ol.layer.VectorLayerRenderIntent.SELECTED); + ol.FeatureRenderIntent.SELECTED ? + ol.FeatureRenderIntent.DEFAULT : + ol.FeatureRenderIntent.SELECTED); } // TODO: Dispatch an event with selectedFeatures and unselectedFeatures } diff --git a/src/ol/layer/vectorlayer.js b/src/ol/layer/vectorlayer.js index 87e3edf6b5..11feb64ecd 100644 --- a/src/ol/layer/vectorlayer.js +++ b/src/ol/layer/vectorlayer.js @@ -3,8 +3,8 @@ goog.provide('ol.layer.Vector'); goog.require('goog.array'); goog.require('goog.object'); goog.require('ol.Feature'); +goog.require('ol.FeatureRenderIntent'); goog.require('ol.layer.Layer'); -goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.source.Vector'); goog.require('ol.source.VectorEventType'); goog.require('ol.style'); @@ -120,5 +120,5 @@ ol.layer.Vector.uidTransformFeatureInfo = function(features) { * @return {boolean} Whether the feature is selected. */ ol.layer.Vector.selectedFeaturesFilter = function(feature) { - return feature.getRenderIntent() == ol.layer.VectorLayerRenderIntent.SELECTED; + return feature.getRenderIntent() == ol.FeatureRenderIntent.SELECTED; }; diff --git a/src/ol/layer/vectorlayerrenderintent.js b/src/ol/layer/vectorlayerrenderintent.js deleted file mode 100644 index 6b84c28700..0000000000 --- a/src/ol/layer/vectorlayerrenderintent.js +++ /dev/null @@ -1,13 +0,0 @@ -goog.provide('ol.layer.VectorLayerRenderIntent'); - - -/** - * @enum {string} - */ -ol.layer.VectorLayerRenderIntent = { - DEFAULT: 'default', - FUTURE: 'future', - HIDDEN: 'hidden', - SELECTED: 'selected', - TEMPORARY: 'temporary' -}; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index e3e5550481..605018c03f 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -6,6 +6,7 @@ goog.require('goog.dom.TagName'); goog.require('goog.events'); goog.require('goog.object'); goog.require('goog.vec.Mat4'); +goog.require('ol.FeatureRenderIntent'); goog.require('ol.Pixel'); goog.require('ol.TileCache'); goog.require('ol.TileCoord'); @@ -14,7 +15,6 @@ goog.require('ol.ViewHint'); goog.require('ol.extent'); goog.require('ol.geom.GeometryType'); goog.require('ol.layer.Vector'); -goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.Vector'); goog.require('ol.source.VectorEventType'); @@ -277,7 +277,7 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = for (var id in candidates) { candidate = candidates[id]; if (candidate.getRenderIntent() == - ol.layer.VectorLayerRenderIntent.HIDDEN) { + ol.FeatureRenderIntent.HIDDEN) { continue; } geom = candidate.getGeometry(); diff --git a/src/ol/renderer/canvas/canvasvectorrenderer.js b/src/ol/renderer/canvas/canvasvectorrenderer.js index 30d7a3ebca..213f0e6cbc 100644 --- a/src/ol/renderer/canvas/canvasvectorrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorrenderer.js @@ -8,6 +8,7 @@ goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.vec.Mat4'); goog.require('ol.Feature'); +goog.require('ol.FeatureRenderIntent'); goog.require('ol.geom.AbstractCollection'); goog.require('ol.geom.Geometry'); goog.require('ol.geom.GeometryType'); @@ -17,7 +18,6 @@ goog.require('ol.geom.MultiPoint'); goog.require('ol.geom.MultiPolygon'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); -goog.require('ol.layer.VectorLayerRenderIntent'); goog.require('ol.style.IconLiteral'); goog.require('ol.style.LineLiteral'); goog.require('ol.style.Literal'); @@ -160,7 +160,7 @@ ol.renderer.canvas.Vector.prototype.renderLineStringFeatures_ = context.beginPath(); for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; - if (feature.getRenderIntent() == ol.layer.VectorLayerRenderIntent.HIDDEN) { + if (feature.getRenderIntent() == ol.FeatureRenderIntent.HIDDEN) { continue; } id = goog.getUid(feature); @@ -249,7 +249,7 @@ ol.renderer.canvas.Vector.prototype.renderPointFeatures_ = context.globalAlpha = alpha; for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; - if (feature.getRenderIntent() == ol.layer.VectorLayerRenderIntent.HIDDEN) { + if (feature.getRenderIntent() == ol.FeatureRenderIntent.HIDDEN) { continue; } id = goog.getUid(feature); @@ -325,7 +325,7 @@ ol.renderer.canvas.Vector.prototype.renderText_ = for (var i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; - if (feature.getRenderIntent() == ol.layer.VectorLayerRenderIntent.HIDDEN) { + if (feature.getRenderIntent() == ol.FeatureRenderIntent.HIDDEN) { continue; } vecs = ol.renderer.canvas.Vector.getLabelVectors( @@ -393,7 +393,7 @@ ol.renderer.canvas.Vector.prototype.renderPolygonFeatures_ = context.beginPath(); for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; - if (feature.getRenderIntent() == ol.layer.VectorLayerRenderIntent.HIDDEN) { + if (feature.getRenderIntent() == ol.FeatureRenderIntent.HIDDEN) { continue; } geometry = feature.getGeometry(); From 8cc4ae8dbd29ad0506c9299a8deb009aa8b82d87 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 18 Nov 2013 17:54:04 -0700 Subject: [PATCH 07/18] Separate load requests from feature requests This separates the action of requesting an extent to be loaded from the action of requesting cached features. The renderer (or any other consumer of a vector source) calls load to request a data extent. A `featureload` event fires when new features are loaded. The renderer (or any other consumer) separately asks for cached features given an extent. This vector source only loads features once, but this separation will also work with sources that make multiple requests for data in different extents. This also removes the `data` option from the vector source in favor of a `features` option. Since we no longer have shared data structures for geometries, people can manually create features and pass them to a vector source. The `addFeatures` method is exported as well. This is used to add features to a source that don't have a representation on the "remote" (or server). --- examples/draw-features.js | 2 +- examples/icon.js | 29 +- examples/style-rules.js | 161 ++++------- src/objectliterals.jsdoc | 11 +- src/ol/interaction/drawinteraction.js | 2 +- src/ol/interaction/modifyinteraction.js | 2 +- .../canvas/canvasvectorlayerrenderer.js | 16 +- src/ol/source/vectorsource.exports | 1 + src/ol/source/vectorsource.js | 251 +++++++++--------- test/spec/ol/source/vectorsource.test.js | 65 ----- 10 files changed, 214 insertions(+), 326 deletions(-) diff --git a/examples/draw-features.js b/examples/draw-features.js index 637035f756..1d48f6d9b4 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -18,7 +18,7 @@ var raster = new ol.layer.Tile({ }); var vector = new ol.layer.Vector({ - source: new ol.source.Vector({parser: null}), + source: new ol.source.Vector(), style: new ol.style.Style({ rules: [ new ol.style.Rule({ diff --git a/examples/icon.js b/examples/icon.js index 07ef45ce88..d41c2f5517 100644 --- a/examples/icon.js +++ b/examples/icon.js @@ -1,11 +1,12 @@ +goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.Overlay'); goog.require('ol.OverlayPositioning'); goog.require('ol.RendererHint'); goog.require('ol.View2D'); +goog.require('ol.geom.Point'); goog.require('ol.layer.Tile'); goog.require('ol.layer.Vector'); -goog.require('ol.parser.GeoJSON'); goog.require('ol.source.TileJSON'); goog.require('ol.source.Vector'); goog.require('ol.style.Icon'); @@ -18,22 +19,6 @@ var raster = new ol.layer.Tile({ }) }); -var data = { - type: 'FeatureCollection', - features: [{ - type: 'Feature', - properties: { - name: 'Null Island', - population: 4000, - rainfall: 500 - }, - geometry: { - type: 'Point', - coordinates: [0, 0] - } - }] -}; - var style = new ol.style.Style({ symbolizers: [ new ol.style.Icon({ @@ -45,8 +30,14 @@ var style = new ol.style.Style({ var vector = new ol.layer.Vector({ source: new ol.source.Vector({ - parser: new ol.parser.GeoJSON(), - data: data + features: [ + new ol.Feature({ + name: 'Null Island', + population: 4000, + rainfall: 500, + geometry: new ol.geom.Point([0, 0]) + }) + ] }), style: style }); diff --git a/examples/style-rules.js b/examples/style-rules.js index c730704afc..c78bcb207f 100644 --- a/examples/style-rules.js +++ b/examples/style-rules.js @@ -1,11 +1,12 @@ +goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.RendererHint'); goog.require('ol.View2D'); goog.require('ol.control'); goog.require('ol.expr'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point'); goog.require('ol.layer.Vector'); -goog.require('ol.parser.GeoJSON'); -goog.require('ol.proj'); goog.require('ol.source.Vector'); goog.require('ol.style.Fill'); goog.require('ol.style.Rule'); @@ -62,108 +63,60 @@ var style = new ol.style.Style({rules: [ var vector = new ol.layer.Vector({ style: style, source: new ol.source.Vector({ - data: { - 'type': 'FeatureCollection', - 'features': [{ - 'type': 'Feature', - 'properties': { - 'color': '#BADA55', - 'where': 'inner' - }, - 'geometry': { - 'type': 'LineString', - 'coordinates': [[-10000000, -10000000], [10000000, 10000000]] - } - }, { - 'type': 'Feature', - 'properties': { - 'color': '#BADA55', - 'where': 'inner' - }, - 'geometry': { - 'type': 'LineString', - 'coordinates': [[-10000000, 10000000], [10000000, -10000000]] - } - }, { - 'type': 'Feature', - 'properties': { - 'color': '#013', - 'where': 'outer' - }, - 'geometry': { - 'type': 'LineString', - 'coordinates': [[-10000000, -10000000], [-10000000, 10000000]] - } - }, { - 'type': 'Feature', - 'properties': { - 'color': '#013', - 'where': 'outer' - }, - 'geometry': { - 'type': 'LineString', - 'coordinates': [[-10000000, 10000000], [10000000, 10000000]] - } - }, { - 'type': 'Feature', - 'properties': { - 'color': '#013', - 'where': 'outer' - }, - 'geometry': { - 'type': 'LineString', - 'coordinates': [[10000000, 10000000], [10000000, -10000000]] - } - }, { - 'type': 'Feature', - 'properties': { - 'color': '#013', - 'where': 'outer' - }, - 'geometry': { - 'type': 'LineString', - 'coordinates': [[10000000, -10000000], [-10000000, -10000000]] - } - }, { - 'type': 'Feature', - 'properties': { - 'label': 'South' - }, - 'geometry': { - 'type': 'Point', - 'coordinates': [0, -6000000] - } - }, { - 'type': 'Feature', - 'properties': { - 'label': 'West' - }, - 'geometry': { - 'type': 'Point', - 'coordinates': [-6000000, 0] - } - }, { - 'type': 'Feature', - 'properties': { - 'label': 'North' - }, - 'geometry': { - 'type': 'Point', - 'coordinates': [0, 6000000] - } - }, { - 'type': 'Feature', - 'properties': { - 'label': 'East' - }, - 'geometry': { - 'type': 'Point', - 'coordinates': [6000000, 0] - } - }] - }, - parser: new ol.parser.GeoJSON(), - projection: ol.proj.get('EPSG:3857') + features: [ + new ol.Feature({ + color: '#BADA55', + where: 'inner', + geometry: new ol.geom.LineString( + [[-10000000, -10000000], [10000000, 10000000]]) + }), + new ol.Feature({ + color: '#BADA55', + where: 'inner', + geometry: new ol.geom.LineString( + [[-10000000, 10000000], [10000000, -10000000]]) + }), + new ol.Feature({ + color: '#013', + where: 'outer', + geometry: new ol.geom.LineString( + [[-10000000, -10000000], [-10000000, 10000000]]) + }), + new ol.Feature({ + color: '#013', + where: 'outer', + geometry: new ol.geom.LineString( + [[-10000000, 10000000], [10000000, 10000000]]) + }), + new ol.Feature({ + color: '#013', + where: 'outer', + geometry: new ol.geom.LineString( + [[10000000, 10000000], [10000000, -10000000]]) + }), + new ol.Feature({ + color: '#013', + where: 'outer', + geometry: new ol.geom.LineString( + [[10000000, -10000000], [-10000000, -10000000]]) + }), + new ol.Feature({ + label: 'South', + geometry: new ol.geom.Point([0, -6000000]) + }), + new ol.Feature({ + label: 'West', + geometry: new ol.geom.Point([-6000000, 0]) + }), + new ol.Feature({ + label: 'North', + geometry: new ol.geom.Point([0, 6000000]) + }), + new ol.Feature({ + label: 'East', + geometry: new ol.geom.Point([6000000, 0]) + }) + ] }) }); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index d416c8b63e..83e0e9a6e1 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -718,16 +718,19 @@ /** * @typedef {Object} ol.source.VectorOptions * @property {Array.|undefined} attributions Attributions. - * @property {Object|string|undefined} data Data to parse. + * @property {Array.|undefined} features Any features to be added + * to the source. Providing features is an alternative to providing + * `url` and `parser` options. * @property {ol.Extent|undefined} extent Extent. * @property {string|undefined} logo Logo. - * @property {ol.parser.Parser} parser Parser instance to parse data - * provided as `data` or fetched from `url`. + * @property {ol.parser.Parser|undefined} parser Parser instance to parse data + * fetched from `url`. * @property {ol.proj.ProjectionLike|undefined} projection Projection. Usually the * projection is provided by the parser, so this only needs to be set if * the parser does not know the SRS (e.g. in some GML flavors), or if the * projection determined by the parser needs to be overridden. - * @property {string|undefined} url Server url providing the vector data. + * @property {string|undefined} url Server url providing the vector data. If + * provided, the `parser` option must also be supplied. * @todo stability experimental */ diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index b46e6d2050..8a698818a1 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -113,7 +113,7 @@ ol.interaction.Draw.prototype.setMap = function(map) { if (!goog.isNull(map)) { if (goog.isNull(this.sketchLayer_)) { var layer = new ol.layer.Vector({ - source: new ol.source.Vector({parser: null}), + source: new ol.source.Vector(), style: this.layer_.getStyle() }); layer.setTemporary(true); diff --git a/src/ol/interaction/modifyinteraction.js b/src/ol/interaction/modifyinteraction.js index 020dd95d4e..692c0cd74d 100644 --- a/src/ol/interaction/modifyinteraction.js +++ b/src/ol/interaction/modifyinteraction.js @@ -134,7 +134,7 @@ ol.interaction.Modify.prototype.setMap = function(map) { } if (goog.isNull(this.sketchLayer_)) { var sketchLayer = new ol.layer.Vector({ - source: new ol.source.Vector({parser: null}) + source: new ol.source.Vector() }); this.sketchLayer_ = sketchLayer; sketchLayer.setTemporary(true); diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 605018c03f..6ec6e528dc 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -92,6 +92,7 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) { var source = layer.getSource(); goog.events.listen(source, [ + ol.source.VectorEventType.LOAD, ol.source.VectorEventType.ADD, ol.source.VectorEventType.CHANGE, ol.source.VectorEventType.REMOVE, @@ -427,6 +428,15 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = this.tileArchetype_.height = tileSize[1]; } + /** + * Let the source to know what data extent we want loaded. As there may + * already be features loaded, we continue with rendering after this request. + * If this results in loading new features, a new rendering will be triggered. + */ + var layer = this.getVectorLayer(); + var source = layer.getVectorSource(); + source.load(tileRangeExtent, projection); + /** * Prepare the sketch canvas. This covers the currently visible tile range * and will have rendered all newly visible features. @@ -474,8 +484,6 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = var tile, tileCoord, key, x, y, i, type; var deferred = false; var dirty = false; - var layer = this.getVectorLayer(); - var source = layer.getSource(); var tileExtent, featuresObject, tileHasFeatures; fetchTileData: for (x = tileRange.minX; x <= tileRange.maxX; ++x) { @@ -491,8 +499,8 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = tileExtent[1] -= tileGutter; tileExtent[3] += tileGutter; tileHasFeatures = false; - featuresObject = source.getFeaturesObjectForExtent(tileExtent, - projection, this.requestMapRenderFrame_); + featuresObject = source.getFeaturesObjectForExtent( + tileExtent, projection); if (goog.isNull(featuresObject)) { deferred = true; break fetchTileData; diff --git a/src/ol/source/vectorsource.exports b/src/ol/source/vectorsource.exports index d7842f5b84..b864def335 100644 --- a/src/ol/source/vectorsource.exports +++ b/src/ol/source/vectorsource.exports @@ -1 +1,2 @@ @exportClass ol.source.Vector ol.source.VectorOptions +@exportProperty ol.source.Vector.prototype.addFeatures diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 0e049b8e93..fb2a20f57a 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -3,6 +3,7 @@ goog.provide('ol.source.Vector'); goog.provide('ol.source.VectorEventType'); goog.require('goog.asserts'); +goog.require('goog.async.nextTick'); goog.require('goog.events'); goog.require('goog.events.Event'); goog.require('goog.net.XhrIo'); @@ -36,17 +37,12 @@ ol.source.VectorLoadState = { ol.source.Vector = function(opt_options) { var options = goog.isDef(opt_options) ? opt_options : {}; - /** - * @private - * @type {Object|string} - */ - this.data_ = goog.isDef(options.data) ? options.data : null; - - /** - * @private - * @type {ol.source.VectorLoadState} - */ - this.loadState_ = ol.source.VectorLoadState.IDLE; + goog.base(this, { + attributions: options.attributions, + extent: options.extent, + logo: options.logo, + projection: options.projection + }); /** * @private @@ -60,23 +56,126 @@ ol.source.Vector = function(opt_options) { */ this.url_ = options.url; + /** + * @private + * @type {ol.source.VectorLoadState} + */ + this.loadState_ = goog.isDef(this.url_) ? + ol.source.VectorLoadState.IDLE : ol.source.VectorLoadState.LOADED; + /** * @type {ol.source.FeatureCache} * @private */ this.featureCache_ = new ol.source.FeatureCache(); - goog.base(this, { - attributions: options.attributions, - extent: options.extent, - logo: options.logo, - projection: options.projection - }); + // add any user provided features + if (goog.isDef(options.features)) { + this.addFeatures(options.features); + } + }; goog.inherits(ol.source.Vector, ol.source.Source); /** + * Request for new features to be loaded. + * @param {ol.Extent} extent Desired extent. + * @param {ol.proj.Projection} projection Desired projection. + * @return {boolean} New features will be loaded. + */ +ol.source.Vector.prototype.load = function(extent, projection) { + var requested = false; + if (this.loadState_ === ol.source.VectorLoadState.IDLE) { + goog.asserts.assertString(this.url_); + this.loadState_ = ol.source.VectorLoadState.LOADING; + goog.net.XhrIo.send(this.url_, goog.bind(function(event) { + var xhr = event.target; + if (xhr.isSuccess()) { + // parsing may be asynchronous, so we don't set load state here + this.parseFeaturesString_(xhr.getResponseText(), projection); + } else { + this.loadState_ = ol.source.VectorLoadState.ERROR; + } + }, this)); + requested = true; + } + return requested; +}; + + +/** + * Parse features from a string. + * @param {string} data Feature data. + * @param {ol.proj.Projection} projection The target projection. + * @private + */ +ol.source.Vector.prototype.parseFeaturesString_ = function(data, projection) { + if (goog.isFunction(this.parser_.readFeaturesFromStringAsync)) { + this.parser_.readFeaturesFromStringAsync(data, goog.bind(function(result) { + this.handleReadResult_(result, projection); + }, this)); + } else { + goog.asserts.assert( + goog.isFunction(this.parser_.readFeaturesFromString), + 'Expected parser with a readFeaturesFromString method.'); + this.handleReadResult_( + this.parser_.readFeaturesFromString(data), projection); + } +}; + + +/** + * Handle the read result from a parser. + * TODO: make parsers accept a target projection (see #1287) + * @param {ol.parser.ReadFeaturesResult} result Read features result. + * @param {ol.proj.Projection} projection The desired projection. + * @private + */ +ol.source.Vector.prototype.handleReadResult_ = function(result, projection) { + var features = result.features; + var sourceProjection = this.getProjection(); + if (goog.isNull(sourceProjection)) { + sourceProjection = result.metadata.projection; + } + var transform = ol.proj.getTransform(sourceProjection, projection); + var extent = ol.extent.createEmpty(); + var geometry = null; + var feature; + for (var i = 0, ii = features.length; i < ii; ++i) { + feature = features[i]; + geometry = feature.getGeometry(); + if (!goog.isNull(geometry)) { + geometry.transform(transform); + ol.extent.extend(extent, geometry.getBounds()); + } + this.loadFeature_(feature); + } + this.loadState_ = ol.source.VectorLoadState.LOADED; + // called in the next tick to normalize load event for sync/async parsing + goog.async.nextTick(function() { + this.dispatchEvent(new ol.source.VectorEvent(ol.source.VectorEventType.LOAD, + features, [extent])); + }, this); +}; + + +/** + * Load a feature. + * @param {ol.Feature} feature Feature to load. + * @private + */ +ol.source.Vector.prototype.loadFeature_ = function(feature) { + goog.events.listen(feature, ol.FeatureEventType.CHANGE, + this.handleFeatureChange_, false, this); + goog.events.listen(feature, ol.FeatureEventType.INTENTCHANGE, + this.handleIntentChange_, false, this); + this.featureCache_.add(feature); +}; + + +/** + * Add newly created features to the source. * @param {Array.} features Array of features. */ ol.source.Vector.prototype.addFeatures = function(features) { @@ -84,15 +183,11 @@ ol.source.Vector.prototype.addFeatures = function(features) { feature, geometry; for (var i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; - this.featureCache_.add(feature); + this.loadFeature_(feature); geometry = feature.getGeometry(); if (!goog.isNull(geometry)) { ol.extent.extend(extent, geometry.getBounds()); } - goog.events.listen(feature, ol.FeatureEventType.CHANGE, - this.handleFeatureChange_, false, this); - goog.events.listen(feature, ol.FeatureEventType.INTENTCHANGE, - this.handleIntentChange_, false, this); } this.dispatchEvent(new ol.source.VectorEvent(ol.source.VectorEventType.ADD, features, [extent])); @@ -125,79 +220,17 @@ ol.source.Vector.prototype.getFeatures = function(opt_filter) { /** * Get all features whose bounding box intersects the provided extent. This - * method is intended for being called by the renderer. When null is returned, - * the renderer should not waste time rendering, and `opt_callback` is - * usually a function that requests a renderFrame, which will be called as soon - * as the data for `extent` is available. + * method is intended for being called by the renderer. * * @param {ol.Extent} extent Bounding extent. * @param {ol.proj.Projection} projection Target projection. - * @param {function()=} opt_callback Callback to call when data is parsed. - * @return {Object.} Features or null if source is loading - * data for `extent`. + * @return {Object.} Features lookup object. */ ol.source.Vector.prototype.getFeaturesObjectForExtent = function(extent, - projection, opt_callback) { - var state = this.prepareFeatures_(extent, projection, opt_callback); - var lookup = null; - if (state !== ol.source.VectorLoadState.LOADING) { - lookup = this.featureCache_.getFeaturesObjectForExtent(extent); - } - return lookup; -}; - - -/** - * @param {Object|Element|Document|string} data Feature data. - * @param {ol.proj.Projection} projection This sucks. The layer should be a - * view in one projection. - * @private - */ -ol.source.Vector.prototype.parseFeatures_ = function(data, projection) { - - var addFeatures = function(data) { - var features = data.features; - var sourceProjection = this.getProjection(); - if (goog.isNull(sourceProjection)) { - sourceProjection = data.metadata.projection; - } - var transform = ol.proj.getTransform(sourceProjection, projection); - var geometry = null; - for (var i = 0, ii = features.length; i < ii; ++i) { - geometry = features[i].getGeometry(); - if (!goog.isNull(geometry)) { - geometry.transform(transform); - } - } - this.addFeatures(features); - }; - - var result; - var parser = this.parser_; - if (goog.isString(data)) { - if (goog.isFunction(parser.readFeaturesFromStringAsync)) { - parser.readFeaturesFromStringAsync(data, goog.bind(addFeatures, this)); - } else { - goog.asserts.assert( - goog.isFunction(parser.readFeaturesFromString), - 'Expected parser with a readFeaturesFromString method.'); - result = parser.readFeaturesFromString(data); - addFeatures.call(this, result); - } - } else if (goog.isObject(data)) { - if (goog.isFunction(parser.readFeaturesFromObjectAsync)) { - parser.readFeaturesFromObjectAsync(data, goog.bind(addFeatures, this)); - } else { - goog.asserts.assert( - goog.isFunction(parser.readFeaturesFromObject), - 'Expected parser with a readFeaturesFromObject method.'); - result = parser.readFeaturesFromObject(data); - addFeatures.call(this, result); - } - } else { - // TODO: parse more data types - throw new Error('Data type not supported: ' + data); - } + projection) { + // TODO: create forEachFeatureInExtent method instead + // TODO: transform if requested project is different than loaded projection + return this.featureCache_.getFeaturesObjectForExtent(extent); }; @@ -241,43 +274,6 @@ ol.source.Vector.prototype.handleIntentChange_ = function(evt) { }; -/** - * @param {ol.Extent} extent Extent that needs to be fetched. - * @param {ol.proj.Projection} projection Projection of the view. - * @param {function()=} opt_callback Callback which is called when features are - * parsed after loading. - * @return {ol.source.VectorLoadState} The current load state. - * @private - */ -ol.source.Vector.prototype.prepareFeatures_ = function(extent, projection, - opt_callback) { - // TODO: Implement strategies. BBOX aware strategies will need the extent. - if (goog.isDef(this.url_) && - this.loadState_ == ol.source.VectorLoadState.IDLE) { - this.loadState_ = ol.source.VectorLoadState.LOADING; - goog.net.XhrIo.send(this.url_, goog.bind(function(event) { - var xhr = event.target; - if (xhr.isSuccess()) { - // TODO: Get source projection from data if supported by parser. - this.parseFeatures_(xhr.getResponseText(), projection); - this.loadState_ = ol.source.VectorLoadState.LOADED; - if (goog.isDef(opt_callback)) { - opt_callback(); - } - } else { - // TODO: Error handling. - this.loadState_ = ol.source.VectorLoadState.ERROR; - } - }, this)); - } else if (!goog.isNull(this.data_)) { - this.parseFeatures_(this.data_, projection); - this.data_ = null; - this.loadState_ = ol.source.VectorLoadState.LOADED; - } - return this.loadState_; -}; - - /** * Remove features from the layer. * @param {Array.} features Features to remove. @@ -332,6 +328,7 @@ goog.inherits(ol.source.VectorEvent, goog.events.Event); * @enum {string} */ ol.source.VectorEventType = { + LOAD: 'featureload', ADD: 'featureadd', CHANGE: 'featurechange', INTENTCHANGE: 'featureintentchange', diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index d40c31cc9d..3ed5181df2 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -21,69 +21,6 @@ describe('ol.source.Vector', function() { }); }); - describe('#prepareFeatures_', function() { - it('loads and parses data from a file', function(done) { - var source = new ol.source.Vector({ - url: 'spec/ol/parser/geojson/countries.geojson', - parser: new ol.parser.GeoJSON() - }); - source.prepareFeatures_([-180, -90, 180, 90], - ol.proj.get('EPSG:4326'), - function() { - expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); - expect(goog.object.getCount( - source.featureCache_.getFeaturesObject())).to.be(179); - done(); - }); - }); - - it('parses inline data', function() { - var source = new ol.source.Vector({ - data: { - 'type': 'FeatureCollection', - 'features': [{ - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [0, -6000000] - } - }, { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [-6000000, 0] - } - }, { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [0, 6000000] - } - }, { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [6000000, 0] - } - }] - }, - parser: new ol.parser.GeoJSON(), - projection: ol.proj.get('EPSG:4326') - }); - source.prepareFeatures_([-180, -90, 180, 90], - ol.proj.get('EPSG:4326'), - function() { - expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); - expect(goog.object.getCount( - source.featureCache_.getFeaturesObject())).to.be(4); - done(); - }); - }); - - - - }); - describe('featurechange event', function() { var source, features; @@ -151,8 +88,6 @@ goog.require('goog.object'); goog.require('ol.Feature'); goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); -goog.require('ol.parser.GeoJSON'); -goog.require('ol.proj'); goog.require('ol.source.FeatureCache'); goog.require('ol.source.Source'); goog.require('ol.source.Vector'); From 1877f92d46ba8d7dae7badf3a055847b857c4a82 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 14:49:49 -0700 Subject: [PATCH 08/18] Add forEach method to rtree, use it in feature cache This saves having to create feature lookup objects and iterate through lookup properties multiple times. --- .../canvas/canvasvectorlayerrenderer.js | 60 ++++++++----------- src/ol/source/vectorsource.js | 27 ++++++--- src/ol/structs/rtree.js | 53 ++++++++-------- test/spec/ol/structs/rtree.test.js | 37 ++++++++++-- 4 files changed, 100 insertions(+), 77 deletions(-) diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 6ec6e528dc..3874e0c8a0 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -1,6 +1,7 @@ goog.provide('ol.renderer.canvas.VectorLayer'); goog.require('goog.asserts'); +goog.require('goog.async.nextTick'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.events'); @@ -247,12 +248,13 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = function(pixel, success, opt_error) { // TODO What do we want to pass to the error callback? var map = this.getMap(); - var result = []; + var features = []; var source = this.getVectorLayer().getSource(); var location = map.getCoordinateFromPixel(pixel); var tileCoord = this.tileGrid_.getTileCoordForCoordAndZ(location, 0); var key = tileCoord.toString(); + if (this.tileCache_.containsKey(key)) { var cachedTile = this.tileCache_.get(key); var symbolSizes = cachedTile[1]; @@ -262,24 +264,15 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = var halfMaxHeight = maxSymbolSize[1] / 2; var locationMin = [location[0] - halfMaxWidth, location[1] - halfMaxHeight]; var locationMax = [location[0] + halfMaxWidth, location[1] + halfMaxHeight]; - var locationBbox = ol.extent.boundingExtent([locationMin, locationMax]); - var candidates = source.getFeaturesObjectForExtent(locationBbox, - map.getView().getView2D().getProjection()); - if (goog.isNull(candidates)) { - // data is not loaded - if (goog.isDef(opt_error)) { - goog.global.setTimeout(function() { opt_error(); }, 0); - } - return; - } + var extent = ol.extent.boundingExtent([locationMin, locationMax]); + var projection = map.getView().getView2D().getProjection(); - var candidate, geom, type, symbolBounds, symbolSize, symbolOffset, - halfWidth, halfHeight, uid, coordinates, j; - for (var id in candidates) { - candidate = candidates[id]; - if (candidate.getRenderIntent() == - ol.FeatureRenderIntent.HIDDEN) { - continue; + source.forEachFeatureInExtent(extent, projection, function(candidate) { + var geom, type, symbolBounds, symbolSize, symbolOffset, + halfWidth, halfHeight, uid, coordinates, j; + + if (candidate.getRenderIntent() == ol.FeatureRenderIntent.HIDDEN) { + return; } geom = candidate.getGeometry(); type = geom.getType(); @@ -304,27 +297,30 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = } for (j = coordinates.length - 1; j >= 0; --j) { if (ol.extent.containsCoordinate(symbolBounds, coordinates[j])) { - result.push(candidate); + features.push(candidate); break; } } } else if (goog.isFunction(geom.containsCoordinate)) { // For polygons, check if the pixel location is inside the polygon if (geom.containsCoordinate(location)) { - result.push(candidate); + features.push(candidate); } } else if (goog.isFunction(geom.distanceFromCoordinate)) { // For lines, check if the distance to the pixel location is // within the rendered line width if (2 * geom.distanceFromCoordinate(location) <= symbolSizes[goog.getUid(candidate)][0]) { - result.push(candidate); + features.push(candidate); } } - } + + }); } var layer = this.getLayer(); - goog.global.setTimeout(function() { success(result, layer); }, 0); + goog.async.nextTick(function() { + success(features, layer); + }); }; @@ -482,10 +478,9 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = // TODO make gutter configurable? var tileGutter = 15 * tileResolution; var tile, tileCoord, key, x, y, i, type; - var deferred = false; var dirty = false; var tileExtent, featuresObject, tileHasFeatures; - fetchTileData: + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { tileCoord = new ol.TileCoord(0, x, y); @@ -499,15 +494,11 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = tileExtent[1] -= tileGutter; tileExtent[3] += tileGutter; tileHasFeatures = false; - featuresObject = source.getFeaturesObjectForExtent( - tileExtent, projection); - if (goog.isNull(featuresObject)) { - deferred = true; - break fetchTileData; - } - tileHasFeatures = tileHasFeatures || - !goog.object.isEmpty(featuresObject); - goog.object.extend(featuresToRender, featuresObject); + source.forEachFeatureInExtent( + tileExtent, projection, function(feature) { + featuresToRender[goog.getUid(feature)] = feature; + tileHasFeatures = true; + }); if (tileHasFeatures) { tilesOnSketchCanvas[key] = tileCoord; } @@ -525,6 +516,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = var groups = style.groupFeaturesBySymbolizerLiteral( featuresToRender, tileResolution); var numGroups = groups.length; + var deferred = false; var group; for (var j = 0; j < numGroups; ++j) { group = groups[j]; diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index fb2a20f57a..ec0085724f 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -224,13 +224,16 @@ ol.source.Vector.prototype.getFeatures = function(opt_filter) { * * @param {ol.Extent} extent Bounding extent. * @param {ol.proj.Projection} projection Target projection. - * @return {Object.} Features lookup object. + * @param {function(this: T, ol.Feature)} callback Callback called with each + * feature. + * @param {T=} opt_thisArg The object to be used as the value of 'this' for + * the callback. + * @template T */ -ol.source.Vector.prototype.getFeaturesObjectForExtent = function(extent, - projection) { - // TODO: create forEachFeatureInExtent method instead +ol.source.Vector.prototype.forEachFeatureInExtent = function(extent, + projection, callback, opt_thisArg) { // TODO: transform if requested project is different than loaded projection - return this.featureCache_.getFeaturesObjectForExtent(extent); + this.featureCache_.forEach(extent, callback, opt_thisArg); }; @@ -394,13 +397,19 @@ ol.source.FeatureCache.prototype.getFeaturesObject = function() { /** - * Get all features whose bounding box intersects the provided extent. + * Operate on each feature whose bounding box intersects the provided extent. * * @param {ol.Extent} extent Bounding extent. - * @return {Object.} Features. + * @param {function(this: T, ol.Feature)} callback Callback called with each + * feature. + * @param {T=} opt_thisArg The object to be used as the value of 'this' for + * the callback. + * @template T */ -ol.source.FeatureCache.prototype.getFeaturesObjectForExtent = function(extent) { - return this.rTree_.searchReturningObject(extent); +ol.source.FeatureCache.prototype.forEach = + function(extent, callback, opt_thisArg) { + this.rTree_.forEach( + extent, /** @type {function(Object)} */ (callback), opt_thisArg); }; diff --git a/src/ol/structs/rtree.js b/src/ol/structs/rtree.js index 6057998bc6..fc314e79c3 100644 --- a/src/ol/structs/rtree.js +++ b/src/ol/structs/rtree.js @@ -499,8 +499,7 @@ ol.structs.RTree.prototype.removeSubtree_ = function(rect, obj, root) { ol.structs.RTree.recalculateExtent_(tree); workingObject.target = undefined; if (tree.nodes.length < this.minWidth_) { // Underflow - workingObject.nodes = /** @type {Array} */ - (this.searchSubtree_(tree, true, [], tree)); + workingObject.nodes = this.searchSubtree_(tree, true, [], tree); } break; } else if (goog.isDef(lTree.nodes)) { @@ -528,15 +527,13 @@ ol.structs.RTree.prototype.removeSubtree_ = function(rect, obj, root) { workingObject.nodes.length = 0; if (hitStack.length === 0 && tree.nodes.length <= 1) { // Underflow..on root! - workingObject.nodes = /** @type {Array} */ - (this.searchSubtree_(tree, true, workingObject.nodes, tree)); + this.searchSubtree_(tree, true, workingObject.nodes, tree); tree.nodes.length = 0; hitStack.push(tree); countStack.push(1); } else if (hitStack.length > 0 && tree.nodes.length < this.minWidth_) { // Underflow..AGAIN! - workingObject.nodes = /** @type {Array} */ - (this.searchSubtree_(tree, true, workingObject.nodes, tree)); + this.searchSubtree_(tree, true, workingObject.nodes, tree); tree.nodes.length = 0; } else { workingObject.nodes = undefined; // Just start resizing @@ -562,24 +559,24 @@ ol.structs.RTree.prototype.removeSubtree_ = function(rect, obj, root) { */ ol.structs.RTree.prototype.search = function(extent, opt_type) { var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); - return /** @type {Array} */ ( - this.searchSubtree_(rect, false, [], this.rootTree_, opt_type)); + return this.searchSubtree_(rect, false, [], this.rootTree_, opt_type); }; /** - * Non-recursive search function + * Search in the given extent and call the callback with each result. * - * @param {ol.Extent} extent Extent. - * @param {string|number=} opt_type Optional type of the objects we want to - * find. - * @return {Object} Result. Keys are UIDs of the values. + * @param {ol.Extent} extent Extent to search. + * @param {function(this: T, Object)} callback Callback called with each result. + * @param {T=} opt_thisArg The object to be used as the value of 'this' for + * the callback. * @this {ol.structs.RTree} + * @template T */ -ol.structs.RTree.prototype.searchReturningObject = function(extent, opt_type) { +ol.structs.RTree.prototype.forEach = function(extent, callback, opt_thisArg) { var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); - return /** @type {Object} */ ( - this.searchSubtree_(rect, false, [], this.rootTree_, opt_type, true)); + this.searchSubtree_( + rect, false, [], this.rootTree_, undefined, callback, opt_thisArg); }; @@ -588,17 +585,19 @@ ol.structs.RTree.prototype.searchReturningObject = function(extent, opt_type) { * * @param {ol.structs.RTreeNode} rect Rectangle. * @param {boolean} returnNode Do we return nodes? - * @param {Array|Object} result Result. + * @param {Array} result Result. * @param {ol.structs.RTreeNode} root Root. * @param {string|number=} opt_type Optional type to search for. - * @param {boolean=} opt_resultAsObject If set, result will be an object keyed - * by UID. + * @param {function(this: T, Object)=} opt_callback Callback called with each + * result. + * @param {T=} opt_thisArg The object to be used as the value of 'this' for + * the callback. * @private - * @return {Array|Object} Result. + * @template T + * @return {Array} Result. */ ol.structs.RTree.prototype.searchSubtree_ = function( - rect, returnNode, result, root, opt_type, opt_resultAsObject) { - var resultObject = {}; + rect, returnNode, result, root, opt_type, opt_callback, opt_thisArg) { var hitStack = []; // Contains the elements that overlap if (!ol.extent.intersects(rect.extent, root.extent)) { @@ -621,8 +620,8 @@ ol.structs.RTree.prototype.searchSubtree_ = function( // walk all the way in to the leaf to know that we don't need it if (!goog.isDef(opt_type) || lTree.type == opt_type) { var obj = lTree.leaf; - if (goog.isDef(opt_resultAsObject)) { - resultObject[goog.getUid(obj).toString()] = obj; + if (goog.isDef(opt_callback)) { + opt_callback.call(opt_thisArg, obj); } else { result.push(obj); } @@ -635,9 +634,5 @@ ol.structs.RTree.prototype.searchSubtree_ = function( } } while (hitStack.length > 0); - if (goog.isDef(opt_resultAsObject)) { - return resultObject; - } else { - return result; - } + return result; }; diff --git a/test/spec/ol/structs/rtree.test.js b/test/spec/ol/structs/rtree.test.js index 0a65afb978..08a15373d0 100644 --- a/test/spec/ol/structs/rtree.test.js +++ b/test/spec/ol/structs/rtree.test.js @@ -109,11 +109,38 @@ describe('ol.structs.RTree', function() { expect(result.length).to.be(3); }); - it('can return objects instead of arrays', function() { - var obj = {foo: 'bar'}; - rTree.insert([5, 5, 5, 5], obj); - var result = rTree.searchReturningObject([4, 4, 6, 6]); - expect(result[goog.getUid(obj)]).to.equal(obj); + }); + + describe('#forEach()', function() { + var tree; + beforeEach(function() { + tree = new ol.structs.RTree(); + }); + + it('calls a callback for each result in the search extent', function() { + var one = {}; + tree.insert([4.5, 4.5, 5, 5], one); + + var two = {}; + tree.insert([5, 5, 5.5, 5.5], two); + + var callback = sinon.spy(); + tree.forEach([4, 4, 6, 6], callback); + expect(callback.callCount).to.be(2); + expect(callback.calledWith(one)).to.be(true); + expect(callback.calledWith(two)).to.be(true); + }); + + it('accepts a this argument', function() { + var obj = {}; + tree.insert([5, 5, 5, 5], obj); + + var callback = sinon.spy(); + var thisArg = {}; + tree.forEach([4, 4, 6, 6], callback, thisArg); + expect(callback.callCount).to.be(1); + expect(callback.calledWith(obj)).to.be(true); + expect(callback.calledOn(thisArg)).to.be(true); }); }); From 6229c0a1d47e07cce743868115851b48158b6c3f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 14:56:08 -0700 Subject: [PATCH 09/18] Unused getFeaturesByIds_ method --- src/ol/source/vectorsource.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index ec0085724f..07c14a1f97 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -413,23 +413,6 @@ ol.source.FeatureCache.prototype.forEach = }; -/** - * Get features by ids. - * @param {Array.} ids Array of (internal) identifiers. - * @return {Array.} Array of features. - * @private - */ -ol.source.FeatureCache.prototype.getFeaturesByIds_ = function(ids) { - var len = ids.length, - features = new Array(len), - i; - for (i = 0; i < len; ++i) { - features[i] = this.idLookup_[ids[i]]; - } - return features; -}; - - /** * Remove a feature from the cache. * @param {ol.Feature} feature Feature. From 1f6d9fc5aef823b1fc0f763ed4dabe68a021ce19 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 15:29:31 -0700 Subject: [PATCH 10/18] Test for vector source load method --- src/ol/source/vectorsource.js | 1 + test/spec/ol/source/vectorsource.test.js | 53 +++++++++++++++++-- .../source/vectorsource/single-feature.json | 16 ++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/spec/ol/source/vectorsource/single-feature.json diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 07c14a1f97..a2098845a4 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -1,6 +1,7 @@ goog.provide('ol.source.FeatureCache'); goog.provide('ol.source.Vector'); goog.provide('ol.source.VectorEventType'); +goog.provide('ol.source.VectorLoadState'); goog.require('goog.asserts'); goog.require('goog.async.nextTick'); diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 3ed5181df2..527d8e2aeb 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -3,21 +3,65 @@ goog.provide('ol.test.source.Vector'); describe('ol.source.Vector', function() { + var url = 'spec/ol/source/vectorsource/single-feature.json'; + describe('constructor', function() { it('creates an instance', function() { var source = new ol.source.Vector(); expect(source).to.be.a(ol.source.Vector); expect(source).to.be.a(ol.source.Source); }); + + it('accepts features', function() { + var features = [new ol.Feature()]; + var source = new ol.source.Vector({ + features: features + }); + expect(source).to.be.a(ol.source.Vector); + expect(source.getFeatures()).to.eql(features); + }); + + it('accepts url and parser', function() { + var source = new ol.source.Vector({ + url: url, + parser: new ol.parser.GeoJSON() + }); + expect(source).to.be.a(ol.source.Vector); + }); + }); + + describe('#load()', function() { + it('triggers loading of features', function() { + var source = new ol.source.Vector({ + url: url, + parser: new ol.parser.GeoJSON() + }); + expect(source.loadState_).to.be(ol.source.VectorLoadState.IDLE); + var triggered = source.load([-1, -1, 1, 1], ol.proj.get('EPSG:4326')); + expect(triggered).to.be(true); + expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADING); + }); + + it('returns false when already loading', function() { + var source = new ol.source.Vector({ + url: url, + parser: new ol.parser.GeoJSON() + }); + source.load([-1, -1, 1, 1], ol.proj.get('EPSG:4326')); + // second call with same extent + var triggered = source.load([-1, -1, 1, 1], ol.proj.get('EPSG:4326')); + expect(triggered).to.be(false); + expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADING); + }); }); describe('#addFeatures()', function() { it('allows adding features', function() { var source = new ol.source.Vector(); - source.addFeatures([new ol.Feature(), new ol.Feature()]); - expect(goog.object.getCount(source.featureCache_.getFeaturesObject())) - .to.eql(2); + var features = [new ol.Feature()]; + source.addFeatures(features); + expect(source.getFeatures()).to.eql(features); }); }); @@ -88,7 +132,10 @@ goog.require('goog.object'); goog.require('ol.Feature'); goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); +goog.require('ol.parser.GeoJSON'); +goog.require('ol.proj'); goog.require('ol.source.FeatureCache'); goog.require('ol.source.Source'); goog.require('ol.source.Vector'); goog.require('ol.source.VectorEventType'); +goog.require('ol.source.VectorLoadState'); diff --git a/test/spec/ol/source/vectorsource/single-feature.json b/test/spec/ol/source/vectorsource/single-feature.json new file mode 100644 index 0000000000..5d7944efbe --- /dev/null +++ b/test/spec/ol/source/vectorsource/single-feature.json @@ -0,0 +1,16 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "point_1", + "geometry": { + "coordinates": [1, 2], + "type": "Point" + }, + "properties": { + "name": "point" + } + } + ] +} From 3bcd4bf833516bc84d8cf67036709ccd13f85ff1 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 15:38:19 -0700 Subject: [PATCH 11/18] Tests for getFeatures method --- test/spec/ol/source/vectorsource.test.js | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 527d8e2aeb..4d92fd9311 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -63,6 +63,41 @@ describe('ol.source.Vector', function() { source.addFeatures(features); expect(source.getFeatures()).to.eql(features); }); + + }); + + describe('#getFeatures()', function() { + + it('gets features cached on the source', function() { + var source = new ol.source.Vector({ + features: [new ol.Feature()] + }); + source.addFeatures([new ol.Feature()]); + + var features = source.getFeatures(); + expect(features).to.be.an('array'); + expect(features).to.have.length(2); + }); + + it('accepts a filter function', function() { + var features = [ + new ol.Feature({name: 'a'}), + new ol.Feature({name: 'b'}), + new ol.Feature({name: 'c'}), + new ol.Feature({name: 'd'}) + ]; + var source = new ol.source.Vector({features: features}); + + var results = source.getFeatures(function(feature) { + return feature.get('name') > 'b'; + }); + + expect(results).to.be.an('array'); + expect(results).to.have.length(2); + expect(results).to.contain(features[2]); + expect(results).to.contain(features[3]); + }); + }); describe('featurechange event', function() { From f9710b6698421d9b8637c5ac59a5ba78f072343a Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 16:00:26 -0700 Subject: [PATCH 12/18] Test forEachFeatureInExtent --- test/spec/ol/source/vectorsource.test.js | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 4d92fd9311..edf7114139 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -100,6 +100,51 @@ describe('ol.source.Vector', function() { }); + describe('#forEachFeatureInExtent()', function() { + + var features = [ + new ol.Feature({geom: new ol.geom.Point([-100, 50])}), + new ol.Feature({geom: new ol.geom.Point([100, 50])}), + new ol.Feature({geom: new ol.geom.Point([100, -50])}), + new ol.Feature({geom: new ol.geom.Point([-100, -50])}) + ]; + var source = new ol.source.Vector({features: features}); + var gg = ol.proj.get('EPSG:4326'); + + it('calls callback with each feature in the extent', function() { + var callback = sinon.spy(); + source.forEachFeatureInExtent([-180, -90, 180, 90], gg, callback); + expect(callback.callCount).to.be(4); + expect(callback.calledWith(sinon.match.same(features[0]))).to.be(true); + expect(callback.calledWith(sinon.match.same(features[1]))).to.be(true); + expect(callback.calledWith(sinon.match.same(features[2]))).to.be(true); + expect(callback.calledWith(sinon.match.same(features[3]))).to.be(true); + }); + + it('accepts a this argument', function() { + var callback = sinon.spy(); + var thisArg = {}; + source.forEachFeatureInExtent( + [-180, -90, 180, 90], gg, callback, thisArg); + expect(callback.calledOn(thisArg)).to.be(true); + }); + + it('works with a subset of features', function() { + var callback = sinon.spy(); + source.forEachFeatureInExtent([-100, -50, -100, 50], gg, callback); + expect(callback.callCount).to.be(2); + expect(callback.calledWith(sinon.match.same(features[0]))).to.be(true); + expect(callback.calledWith(sinon.match.same(features[3]))).to.be(true); + }); + + it('works with no features', function() { + var callback = sinon.spy(); + source.forEachFeatureInExtent([-110, -50, -110, -50], gg, callback); + expect(callback.called).to.be(false); + }); + + }); + describe('featurechange event', function() { var source, features; From a5cadacb78a3d080011a4f712271e0b4db90bad4 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 16:18:40 -0700 Subject: [PATCH 13/18] Test featureload event --- test/spec/ol/source/vectorsource.test.js | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index edf7114139..97197136f3 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -145,6 +145,39 @@ describe('ol.source.Vector', function() { }); + describe('featureload event', function() { + + var gg = ol.proj.get('EPSG:4326'); + var world = [-180, -90, 180, 90]; + + it('is dispatched after features load', function(done) { + var source = new ol.source.Vector({ + url: url, + parser: new ol.parser.GeoJSON() + }); + expect(source.loadState_).to.be(ol.source.VectorLoadState.IDLE); + var triggered = source.load(world, gg); + expect(triggered).to.be(true); + expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADING); + goog.events.listen(source, ol.source.VectorEventType.LOAD, + function(evt) { + var features = evt.features; + expect(features).to.be.an('array'); + expect(features).to.have.length(1); + expect(features[0]).to.be.an(ol.Feature); + + var extents = evt.extents; + expect(extents).to.be.an('array'); + expect(extents).to.have.length(1); + expect(extents[0]).to.be.eql([1, 2, 1, 2]); + + expect(source.loadState_).to.be(ol.source.VectorLoadState.LOADED); + done(); + }); + }); + + }); + describe('featurechange event', function() { var source, features; From 8b5dccb742fa6c5aa9886a2ab096f425262bd21a Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 16:23:11 -0700 Subject: [PATCH 14/18] Test featureadd event --- test/spec/ol/source/vectorsource.test.js | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 97197136f3..5606ee2250 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -178,6 +178,36 @@ describe('ol.source.Vector', function() { }); + describe('featureadd event', function() { + + it('is dispatched after features load', function(done) { + var source = new ol.source.Vector(); + var features = [ + new ol.Feature({g: new ol.geom.Point([10, 5])}), + new ol.Feature({g: new ol.geom.Point([-10, -5])}) + ]; + + goog.events.listen(source, ol.source.VectorEventType.ADD, + function(evt) { + var features = evt.features; + expect(features).to.be.an('array'); + expect(features).to.have.length(2); + expect(features).to.contain(features[0]); + expect(features).to.contain(features[1]); + + var extents = evt.extents; + expect(extents).to.be.an('array'); + expect(extents).to.have.length(1); + expect(extents[0]).to.be.eql([-10, -5, 10, 5]); + + done(); + }); + + source.addFeatures(features); + }); + + }); + describe('featurechange event', function() { var source, features; From b7b14420a4d55a37dcca91a7f39522898d8e434f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 19 Nov 2013 16:51:41 -0700 Subject: [PATCH 15/18] Test removeFeatures and featureremove event --- test/spec/ol/source/vectorsource.test.js | 54 +++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/spec/ol/source/vectorsource.test.js b/test/spec/ol/source/vectorsource.test.js index 5606ee2250..860543fe85 100644 --- a/test/spec/ol/source/vectorsource.test.js +++ b/test/spec/ol/source/vectorsource.test.js @@ -100,6 +100,28 @@ describe('ol.source.Vector', function() { }); + describe('#removeFeatures()', function() { + + it('removes cached features', function() { + var features = [new ol.Feature(), new ol.Feature()]; + var source = new ol.source.Vector({features: features}); + + expect(source.getFeatures()).to.have.length(2); + source.removeFeatures(features); + expect(source.getFeatures()).to.have.length(0); + }); + + it('removes cached features', function() { + var features = [new ol.Feature(), new ol.Feature()]; + var source = new ol.source.Vector({features: features}); + + expect(source.getFeatures()).to.have.length(2); + source.removeFeatures([features[0]]); + expect(source.getFeatures()).to.eql([features[1]]); + }); + + }); + describe('#forEachFeatureInExtent()', function() { var features = [ @@ -180,7 +202,7 @@ describe('ol.source.Vector', function() { describe('featureadd event', function() { - it('is dispatched after features load', function(done) { + it('is dispatched after features are added', function(done) { var source = new ol.source.Vector(); var features = [ new ol.Feature({g: new ol.geom.Point([10, 5])}), @@ -208,6 +230,36 @@ describe('ol.source.Vector', function() { }); + describe('featureremove event', function() { + + it('is dispatched after features are removed', function(done) { + var features = [ + new ol.Feature({g: new ol.geom.Point([10, 5])}), + new ol.Feature({g: new ol.geom.Point([-10, -5])}) + ]; + var source = new ol.source.Vector({features: features}); + + goog.events.listen(source, ol.source.VectorEventType.REMOVE, + function(evt) { + var features = evt.features; + expect(features).to.be.an('array'); + expect(features).to.have.length(2); + expect(features).to.contain(features[0]); + expect(features).to.contain(features[1]); + + var extents = evt.extents; + expect(extents).to.be.an('array'); + expect(extents).to.have.length(1); + expect(extents[0]).to.be.eql([-10, -5, 10, 5]); + + done(); + }); + + source.removeFeatures(features); + }); + + }); + describe('featurechange event', function() { var source, features; From 92a30bcbf7a1a224cde084a09d873ec1183a147c Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 28 Nov 2013 16:16:52 +0100 Subject: [PATCH 16/18] Use ol.structs.RBush in ol.source.Vector --- src/ol/source/vectorsource.js | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index a2098845a4..7e25a17b55 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -14,7 +14,7 @@ goog.require('ol.FeatureEventType'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.source.Source'); -goog.require('ol.structs.RTree'); +goog.require('ol.structs.RBush'); /** @@ -251,10 +251,10 @@ ol.source.Vector.prototype.handleFeatureChange_ = function(evt) { extents.push(evt.oldExtent); } var geometry = feature.getGeometry(); + var extent = geometry.getBounds(); if (!goog.isNull(geometry)) { - this.featureCache_.remove(feature, evt.oldExtent); - this.featureCache_.add(feature); - extents.push(geometry.getBounds()); + this.featureCache_.updateExtent(feature, extent); + extents.push(extent); } this.dispatchEvent(new ol.source.VectorEvent(ol.source.VectorEventType.CHANGE, [feature], extents)); @@ -353,10 +353,10 @@ ol.source.FeatureCache = function() { this.idLookup_; /** - * @type {ol.structs.RTree} + * @type {ol.structs.RBush} * @private */ - this.rTree_; + this.rBush_; this.clear(); @@ -368,7 +368,7 @@ ol.source.FeatureCache = function() { */ ol.source.FeatureCache.prototype.clear = function() { this.idLookup_ = {}; - this.rTree_ = new ol.structs.RTree(); + this.rBush_ = new ol.structs.RBush(); }; @@ -384,7 +384,7 @@ ol.source.FeatureCache.prototype.add = function(feature) { // index by bounding box if (!goog.isNull(geometry)) { - this.rTree_.insert(geometry.getBounds(), feature); + this.rBush_.insert(geometry.getBounds(), feature); } }; @@ -409,7 +409,7 @@ ol.source.FeatureCache.prototype.getFeaturesObject = function() { */ ol.source.FeatureCache.prototype.forEach = function(extent, callback, opt_thisArg) { - this.rTree_.forEach( + this.rBush_.forEachInExtent( extent, /** @type {function(Object)} */ (callback), opt_thisArg); }; @@ -417,17 +417,24 @@ ol.source.FeatureCache.prototype.forEach = /** * Remove a feature from the cache. * @param {ol.Feature} feature Feature. - * @param {ol.Extent=} opt_extent Optional extent (used when the current feature - * extent is different than the one in the index). */ -ol.source.FeatureCache.prototype.remove = function(feature, opt_extent) { +ol.source.FeatureCache.prototype.remove = function(feature) { var id = goog.getUid(feature).toString(), geometry = feature.getGeometry(); delete this.idLookup_[id]; // index by bounding box if (!goog.isNull(geometry)) { - var extent = goog.isDef(opt_extent) ? opt_extent : geometry.getBounds(); - this.rTree_.remove(extent, feature); + this.rBush_.remove(feature); } }; + + +/** + * Updates a feature's extent in the spatial index. + * @param {ol.Feature} feature Feature. + * @param {ol.Extent} extent Extent. + */ +ol.source.FeatureCache.prototype.updateExtent = function(feature, extent) { + this.rBush_.update(extent, feature); +}; From d4bdac715b55f6e1d03e82453519798c5dfb0615 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 28 Nov 2013 16:17:17 +0100 Subject: [PATCH 17/18] Get rid of ol.structs.RTree --- src/ol/structs/rtree.js | 638 ----------------------------- test/spec/ol/structs/rtree.test.js | 151 ------- 2 files changed, 789 deletions(-) delete mode 100644 src/ol/structs/rtree.js delete mode 100644 test/spec/ol/structs/rtree.test.js diff --git a/src/ol/structs/rtree.js b/src/ol/structs/rtree.js deleted file mode 100644 index fc314e79c3..0000000000 --- a/src/ol/structs/rtree.js +++ /dev/null @@ -1,638 +0,0 @@ -// rtree.js - General-Purpose Non-Recursive Javascript R-Tree Library -// Version 0.6.2, December 5st 2009 -// -// Copyright (c) 2009 Jon-Carlos Rivera -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// Jon-Carlos Rivera - imbcmdth@hotmail.com - - -goog.provide('ol.structs.RTree'); - -goog.require('goog.array'); -goog.require('ol.extent'); - - -/** - * @typedef {{extent: ol.Extent, - * leaf: (Object|undefined), - * nodes: (Array.|undefined), - * target: (Object|undefined), - * type: (string|number|undefined)}} - */ -ol.structs.RTreeNode; - - - -/** - * @constructor - * @param {number=} opt_maxWidth Width before a node is split. Default is `6`. - */ -ol.structs.RTree = function(opt_maxWidth) { - - /** - * Maximum width of any node before a split. - * @private - * @type {number} - */ - this.maxWidth_ = goog.isDef(opt_maxWidth) ? opt_maxWidth : 6; - - /** - * Minimum width of any node before a merge. - * @private - * @type {number} - */ - this.minWidth_ = Math.floor(this.maxWidth_ / 2); - - /** - * Start with an empty root-tree. - * @private - * @type {ol.structs.RTreeNode} - */ - this.rootTree_ = /** @type {ol.structs.RTreeNode} */ - ({extent: ol.extent.createEmpty(), nodes: []}); - -}; - - -/** - * @param {ol.structs.RTreeNode} node Node. - * @private - */ -ol.structs.RTree.recalculateExtent_ = function(node) { - var n = node.nodes.length; - var extent = node.extent; - if (n === 0) { - ol.extent.empty(extent); - } else { - var firstNodeExtent = node.nodes[0].extent; - extent[0] = firstNodeExtent[0]; - extent[2] = firstNodeExtent[2]; - extent[1] = firstNodeExtent[1]; - extent[3] = firstNodeExtent[3]; - var i; - for (i = 1; i < n; ++i) { - ol.extent.extend(extent, node.nodes[i].extent); - } - } -}; - - -/** - * This is Jon-Carlos Rivera's special addition to the world of r-trees. - * Every other (simple) method he found produced poor trees. - * This skews insertions to prefering squarer and emptier nodes. - * - * @param {number} l L. - * @param {number} w W. - * @param {number} fill Fill. - * @private - * @return {number} Squarified ratio. - */ -ol.structs.RTree.squarifiedRatio_ = function(l, w, fill) { - // Area of new enlarged rectangle - var peri = (l + w) / 2; // Average size of a side of the new rectangle - var area = l * w; // Area of new rectangle - // return the ratio of the perimeter to the area - the closer to 1 we are, - // the more "square" a rectangle is. conversly, when approaching zero the - // more elongated a rectangle is - var geo = area / (peri * peri); - return area * fill / geo; -}; - - -/** - * Choose the best for rectangle to be inserted into. - * - * @param {ol.structs.RTreeNode} rect Rectangle. - * @param {ol.structs.RTreeNode} root Root to start search. - * @private - * @return {Array} Leaf node parent. - */ -ol.structs.RTree.prototype.chooseLeafSubtree_ = function(rect, root) { - var bestChoiceIndex = -1; - var bestChoiceStack = []; - var bestChoiceArea; - - bestChoiceStack.push(root); - var nodes = root.nodes; - - do { - if (bestChoiceIndex != -1) { - bestChoiceStack.push(nodes[bestChoiceIndex]); - nodes = nodes[bestChoiceIndex].nodes; - bestChoiceIndex = -1; - } - - for (var i = nodes.length - 1; i >= 0; --i) { - var lTree = nodes[i]; - if (goog.isDef(lTree.leaf)) { - // Bail out of everything and start inserting - bestChoiceIndex = -1; - break; - } - // Area of new enlarged rectangle - var oldLRatio = ol.structs.RTree.squarifiedRatio_( - lTree.extent[2] - lTree.extent[0], - lTree.extent[3] - lTree.extent[1], - lTree.nodes.length + 1); - - // Enlarge rectangle to fit new rectangle - var nw = (lTree.extent[2] > rect.extent[2] ? - lTree.extent[2] : rect.extent[2]) - - (lTree.extent[0] < rect.extent[0] ? - lTree.extent[0] : rect.extent[0]); - var nh = (lTree.extent[3] > rect.extent[3] ? - lTree.extent[3] : rect.extent[3]) - - (lTree.extent[1] < rect.extent[1] ? - lTree.extent[1] : rect.extent[1]); - - // Area of new enlarged rectangle - var lRatio = ol.structs.RTree.squarifiedRatio_( - nw, nh, lTree.nodes.length + 2); - - if (bestChoiceIndex < 0 || - Math.abs(lRatio - oldLRatio) < bestChoiceArea) { - bestChoiceArea = Math.abs(lRatio - oldLRatio); - bestChoiceIndex = i; - } - } - } while (bestChoiceIndex != -1); - - return bestChoiceStack; -}; - - -/** - * Non-recursive insert function. - * - * @param {ol.Extent} extent Extent. - * @param {Object} obj Object to insert. - * @param {string|number=} opt_type Optional type to store along with the - * object. - */ -ol.structs.RTree.prototype.insert = function(extent, obj, opt_type) { - var node = /** @type {ol.structs.RTreeNode} */ - ({extent: extent, leaf: obj}); - if (goog.isDef(opt_type)) { - node.type = opt_type; - } - this.insertSubtree_(node, this.rootTree_); -}; - - -/** - * Non-recursive internal insert function. - * - * @param {ol.structs.RTreeNode} node Node to insert. - * @param {ol.structs.RTreeNode} root Root to begin insertion at. - * @private - */ -ol.structs.RTree.prototype.insertSubtree_ = function(node, root) { - var bc; // Best Current node - // Initial insertion is special because we resize the Tree and we don't - // care about any overflow (seriously, how can the first object overflow?) - if (root.nodes.length === 0) { - root.extent = ol.extent.clone(node.extent); - root.nodes.push(node); - return; - } - - // Find the best fitting leaf node - // chooseLeaf returns an array of all tree levels (including root) - // that were traversed while trying to find the leaf - var treeStack = this.chooseLeafSubtree_(node, root); - var workingObject = node; - - // Walk back up the tree resizing and inserting as needed - do { - //handle the case of an empty node (from a split) - if (bc && goog.isDef(bc.nodes) && bc.nodes.length === 0) { - var pbc = bc; // Past bc - bc = treeStack.pop(); - for (var t = 0, tt = bc.nodes.length; t < tt; ++t) { - if (bc.nodes[t] === pbc || bc.nodes[t].nodes.length === 0) { - bc.nodes.splice(t, 1); - break; - } - } - } else { - bc = treeStack.pop(); - } - - // If there is data attached to this workingObject - var isArray = goog.isArray(workingObject); - if (goog.isDef(workingObject.leaf) || - goog.isDef(workingObject.nodes) || isArray) { - // Do Insert - if (isArray) { - for (var ai = 0, aii = workingObject.length; ai < aii; ++ai) { - ol.extent.extend(bc.extent, workingObject[ai].extent); - } - bc.nodes = bc.nodes.concat(workingObject); - } else { - ol.extent.extend(bc.extent, workingObject.extent); - bc.nodes.push(workingObject); // Do Insert - } - - if (bc.nodes.length <= this.maxWidth_) { // Start Resizeing Up the Tree - workingObject = {extent: ol.extent.clone(bc.extent)}; - } else { // Otherwise Split this Node - // linearSplit_() returns an array containing two new nodes - // formed from the split of the previous node's overflow - var a = this.linearSplit_(bc.nodes); - workingObject = a;//[1]; - - if (treeStack.length < 1) { // If are splitting the root.. - bc.nodes.push(a[0]); - treeStack.push(bc); // Reconsider the root element - workingObject = a[1]; - } - } - } else { // Otherwise Do Resize - //Just keep applying the new bounding rectangle to the parents.. - ol.extent.extend(bc.extent, workingObject.extent); - workingObject = ({extent: ol.extent.clone(bc.extent)}); - } - } while (treeStack.length > 0); -}; - - -/** - * Split a set of nodes into two roughly equally-filled nodes. - * - * @param {Array.} nodes Array of nodes. - * @private - * @return {Array.} An array of two nodes. - */ -ol.structs.RTree.prototype.linearSplit_ = function(nodes) { - var n = this.pickLinear_(nodes); - while (nodes.length > 0) { - this.pickNext_(nodes, n[0], n[1]); - } - return n; -}; - - -/** - * Pick the "best" two starter nodes to use as seeds using the "linear" - * criteria. - * - * @param {Array.} nodes Array of source nodes. - * @private - * @return {Array.} An array of two nodes. - */ -ol.structs.RTree.prototype.pickLinear_ = function(nodes) { - var lowestHighX = nodes.length - 1; - var highestLowX = 0; - var lowestHighY = nodes.length - 1; - var highestLowY = 0; - var t1, t2; - - for (var i = nodes.length - 2; i >= 0; --i) { - var l = nodes[i]; - if (l.extent[0] > nodes[highestLowX].extent[0]) { - highestLowX = i; - } else if (l.extent[2] < nodes[lowestHighX].extent[1]) { - lowestHighX = i; - } - if (l.extent[1] > nodes[highestLowY].extent[1]) { - highestLowY = i; - } else if (l.extent[3] < nodes[lowestHighY].extent[3]) { - lowestHighY = i; - } - } - var dx = Math.abs(nodes[lowestHighX].extent[2] - - nodes[highestLowX].extent[0]); - var dy = Math.abs(nodes[lowestHighY].extent[3] - - nodes[highestLowY].extent[1]); - if (dx > dy) { - if (lowestHighX > highestLowX) { - t1 = nodes.splice(lowestHighX, 1)[0]; - t2 = nodes.splice(highestLowX, 1)[0]; - } else { - t2 = nodes.splice(highestLowX, 1)[0]; - t1 = nodes.splice(lowestHighX, 1)[0]; - } - } else { - if (lowestHighY > highestLowY) { - t1 = nodes.splice(lowestHighY, 1)[0]; - t2 = nodes.splice(highestLowY, 1)[0]; - } else { - t2 = nodes.splice(highestLowY, 1)[0]; - t1 = nodes.splice(lowestHighY, 1)[0]; - } - } - return [ - /** @type {ol.structs.RTreeNode} */ - ({extent: ol.extent.clone(t1.extent), nodes: [t1]}), - /** @type {ol.structs.RTreeNode} */ - ({extent: ol.extent.clone(t2.extent), nodes: [t2]}) - ]; -}; - - -/** - * Insert the best source rectangle into the best fitting parent node: a or b. - * - * @param {Array.} nodes Source node array. - * @param {ol.structs.RTreeNode} a Target node array a. - * @param {ol.structs.RTreeNode} b Target node array b. - * @private - */ -ol.structs.RTree.prototype.pickNext_ = function(nodes, a, b) { - // Area of new enlarged rectangle - var areaA = ol.structs.RTree.squarifiedRatio_(a.extent[2] - a.extent[0], - a.extent[3] - a.extent[1], a.nodes.length + 1); - var areaB = ol.structs.RTree.squarifiedRatio_(b.extent[2] - b.extent[0], - b.extent[3] - b.extent[1], b.nodes.length + 1); - var highAreaDelta; - var highAreaNode; - var lowestGrowthGroup; - - for (var i = nodes.length - 1; i >= 0; --i) { - var l = nodes[i]; - - var newAreaA = [ - a.extent[0] < l.extent[0] ? a.extent[0] : l.extent[0], - a.extent[2] > l.extent[2] ? a.extent[2] : l.extent[2], - a.extent[1] < l.extent[1] ? a.extent[1] : l.extent[1], - a.extent[3] > l.extent[3] ? a.extent[3] : l.extent[3] - ]; - var changeNewAreaA = Math.abs(ol.structs.RTree.squarifiedRatio_( - newAreaA[1] - newAreaA[0], - newAreaA[3] - newAreaA[2], a.nodes.length + 2) - areaA); - - var newAreaB = [ - b.extent[0] < l.extent[0] ? b.extent[0] : l.extent[0], - b.extent[2] > l.extent[2] ? b.extent[2] : l.extent[2], - b.extent[1] < l.extent[1] ? b.extent[1] : l.extent[1], - b.extent[3] > l.extent[3] ? b.extent[3] : l.extent[3] - ]; - var changeNewAreaB = Math.abs(ol.structs.RTree.squarifiedRatio_( - newAreaB[1] - newAreaB[0], newAreaB[3] - newAreaB[2], - b.nodes.length + 2) - areaB); - - var changeNewAreaDelta = Math.abs(changeNewAreaB - changeNewAreaA); - if (!highAreaNode || !highAreaDelta || - changeNewAreaDelta < highAreaDelta) { - highAreaNode = i; - highAreaDelta = changeNewAreaDelta; - lowestGrowthGroup = changeNewAreaB < changeNewAreaA ? b : a; - } - } - var tempNode = nodes.splice(highAreaNode, 1)[0]; - if (a.nodes.length + nodes.length + 1 <= this.minWidth_) { - a.nodes.push(tempNode); - ol.extent.extend(a.extent, tempNode.extent); - } else if (b.nodes.length + nodes.length + 1 <= this.minWidth_) { - b.nodes.push(tempNode); - ol.extent.extend(b.extent, tempNode.extent); - } - else { - lowestGrowthGroup.nodes.push(tempNode); - ol.extent.extend(lowestGrowthGroup.extent, tempNode.extent); - } -}; - - -/** - * Non-recursive function that deletes a specific region. - * - * @param {ol.Extent} extent Extent. - * @param {Object=} opt_obj Object. - * @return {Array} Result. - * @this {ol.structs.RTree} - */ -ol.structs.RTree.prototype.remove = function(extent, opt_obj) { - arguments[0] = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); - switch (arguments.length) { - case 1: - arguments[1] = false; // opt_obj == false for conditionals - case 2: - arguments[2] = this.rootTree_; // Add root node to end of argument list - default: - arguments.length = 3; - } - if (arguments[1] === false) { // Do area-wide † - var numberDeleted = 0; - var result = []; - do { - numberDeleted = result.length; - result = result.concat(this.removeSubtree_.apply(this, arguments)); - } while (numberDeleted != result.length); - return result; - } else { // Delete a specific item - return this.removeSubtree_.apply(this, arguments); - } -}; - - -/** - * Find the best specific node(s) for object to be deleted from. - * - * @param {ol.structs.RTreeNode} rect Rectangle. - * @param {Object} obj Object. - * @param {ol.structs.RTreeNode} root Root to start search. - * @private - * @return {Array} Leaf node parent. - */ -ol.structs.RTree.prototype.removeSubtree_ = function(rect, obj, root) { - var hitStack = []; // Contains the elements that overlap - var countStack = []; // Contains the elements that overlap - var returnArray = []; - var currentDepth = 1; - - if (!rect || !ol.extent.intersects(rect.extent, root.extent)) { - return returnArray; - } - - /** @type {ol.structs.RTreeNode} */ - var workingObject = /** @type {ol.structs.RTreeNode} */ - ({extent: ol.extent.clone(rect.extent), target: obj}); - - countStack.push(root.nodes.length); - hitStack.push(root); - - do { - var tree = hitStack.pop(); - var i = countStack.pop() - 1; - - if (goog.isDef(workingObject.target)) { - // We are searching for a target - while (i >= 0) { - var lTree = tree.nodes[i]; - if (ol.extent.intersects(workingObject.extent, lTree.extent)) { - if ((workingObject.target && goog.isDef(lTree.leaf) && - lTree.leaf === workingObject.target) || - (!workingObject.target && (goog.isDef(lTree.leaf) || - ol.extent.containsExtent(workingObject.extent, lTree.extent)))) - { // A Match !! - // Yup we found a match... - // we can cancel search and start walking up the list - if (goog.isDef(lTree.nodes)) { - // If we are deleting a node not a leaf... - returnArray = this.searchSubtree_(lTree, true, [], lTree); - tree.nodes.splice(i, 1); - } else { - returnArray = tree.nodes.splice(i, 1); - } - // Resize MBR down... - ol.structs.RTree.recalculateExtent_(tree); - workingObject.target = undefined; - if (tree.nodes.length < this.minWidth_) { // Underflow - workingObject.nodes = this.searchSubtree_(tree, true, [], tree); - } - break; - } else if (goog.isDef(lTree.nodes)) { - // Not a Leaf - currentDepth += 1; - countStack.push(i); - hitStack.push(tree); - tree = lTree; - i = lTree.nodes.length; - } - } - i -= 1; - } - } else if (goog.isDef(workingObject.nodes)) { - // We are unsplitting - tree.nodes.splice(i + 1, 1); // Remove unsplit node - // workingObject.nodes contains a list of elements removed from the - // tree so far - if (tree.nodes.length > 0) { - ol.structs.RTree.recalculateExtent_(tree); - } - for (var t = 0, tt = workingObject.nodes.length; t < tt; ++t) { - this.insertSubtree_(workingObject.nodes[t], tree); - } - workingObject.nodes.length = 0; - if (hitStack.length === 0 && tree.nodes.length <= 1) { - // Underflow..on root! - this.searchSubtree_(tree, true, workingObject.nodes, tree); - tree.nodes.length = 0; - hitStack.push(tree); - countStack.push(1); - } else if (hitStack.length > 0 && tree.nodes.length < this.minWidth_) { - // Underflow..AGAIN! - this.searchSubtree_(tree, true, workingObject.nodes, tree); - tree.nodes.length = 0; - } else { - workingObject.nodes = undefined; // Just start resizing - } - } else { // we are just resizing - ol.structs.RTree.recalculateExtent_(tree); - } - currentDepth -= 1; - } while (hitStack.length > 0); - - return returnArray; -}; - - -/** - * Non-recursive search function - * - * @param {ol.Extent} extent Extent. - * @param {string|number=} opt_type Optional type of the objects we want to - * find. - * @return {Array} Result. - * @this {ol.structs.RTree} - */ -ol.structs.RTree.prototype.search = function(extent, opt_type) { - var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); - return this.searchSubtree_(rect, false, [], this.rootTree_, opt_type); -}; - - -/** - * Search in the given extent and call the callback with each result. - * - * @param {ol.Extent} extent Extent to search. - * @param {function(this: T, Object)} callback Callback called with each result. - * @param {T=} opt_thisArg The object to be used as the value of 'this' for - * the callback. - * @this {ol.structs.RTree} - * @template T - */ -ol.structs.RTree.prototype.forEach = function(extent, callback, opt_thisArg) { - var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); - this.searchSubtree_( - rect, false, [], this.rootTree_, undefined, callback, opt_thisArg); -}; - - -/** - * Non-recursive internal search function - * - * @param {ol.structs.RTreeNode} rect Rectangle. - * @param {boolean} returnNode Do we return nodes? - * @param {Array} result Result. - * @param {ol.structs.RTreeNode} root Root. - * @param {string|number=} opt_type Optional type to search for. - * @param {function(this: T, Object)=} opt_callback Callback called with each - * result. - * @param {T=} opt_thisArg The object to be used as the value of 'this' for - * the callback. - * @private - * @template T - * @return {Array} Result. - */ -ol.structs.RTree.prototype.searchSubtree_ = function( - rect, returnNode, result, root, opt_type, opt_callback, opt_thisArg) { - var hitStack = []; // Contains the elements that overlap - - if (!ol.extent.intersects(rect.extent, root.extent)) { - return result; - } - - hitStack.push(root.nodes); - - do { - var nodes = hitStack.pop(); - - for (var i = nodes.length - 1; i >= 0; --i) { - var lTree = nodes[i]; - if (ol.extent.intersects(rect.extent, lTree.extent)) { - if (goog.isDef(lTree.nodes)) { // Not a Leaf - hitStack.push(lTree.nodes); - } else if (goog.isDef(lTree.leaf)) { // A Leaf !! - if (!returnNode) { - // TODO keep track of type on all nodes so we don't have to - // walk all the way in to the leaf to know that we don't need it - if (!goog.isDef(opt_type) || lTree.type == opt_type) { - var obj = lTree.leaf; - if (goog.isDef(opt_callback)) { - opt_callback.call(opt_thisArg, obj); - } else { - result.push(obj); - } - } - } else { - result.push(lTree); - } - } - } - } - } while (hitStack.length > 0); - - return result; -}; diff --git a/test/spec/ol/structs/rtree.test.js b/test/spec/ol/structs/rtree.test.js deleted file mode 100644 index 08a15373d0..0000000000 --- a/test/spec/ol/structs/rtree.test.js +++ /dev/null @@ -1,151 +0,0 @@ -goog.provide('ol.test.structs.RTree'); - - -describe('ol.structs.RTree', function() { - - var rTree = new ol.structs.RTree(); - - describe('creation', function() { - it('can insert 1k objects', function() { - var i = 1000; - while (i > 0) { - var min = [Math.random() * 10000, Math.random() * 10000]; - var max = [min[0] + Math.random() * 500, min[1] + Math.random() * 500]; - var bounds = [min[0], min[1], max[0], max[1]]; - rTree.insert(bounds, 'JUST A TEST OBJECT!_' + i); - i--; - } - expect(goog.object.getCount(rTree.search([0, 0, 10600, 10600]))) - .to.be(1000); - }); - it('can insert 1k more objects', function() { - var i = 1000; - while (i > 0) { - var min = [Math.random() * 10000, Math.random() * 10000]; - var max = [min[0] + Math.random() * 500, min[1] + Math.random() * 500]; - var bounds = [min[0], min[1], max[0], max[1]]; - rTree.insert(bounds, 'JUST A TEST OBJECT!_' + i); - i--; - } - expect(goog.object.getCount(rTree.search([0, 0, 10600, 10600]))) - .to.be(2000); - }); - }); - - describe('search', function() { - it('can perform 1k out-of-bounds searches', function() { - var i = 1000; - var len = 0; - while (i > 0) { - var min = [-(Math.random() * 10000 + 501), - -(Math.random() * 10000 + 501)]; - var max = [min[0] + Math.random() * 500, min[1] + Math.random() * 500]; - var bounds = [min[0], min[1], max[0], max[1]]; - len += rTree.search(bounds).length; - i--; - } - expect(len).to.be(0); - }); - it('can perform 1k in-bounds searches', function() { - var i = 1000; - var len = 0; - while (i > 0) { - var min = [Math.random() * 10000, Math.random() * 10000]; - var max = [min[0] + Math.random() * 500, min[1] + Math.random() * 500]; - var bounds = [min[0], min[1], max[0], max[1]]; - len += rTree.search(bounds).length; - i--; - } - expect(len).not.to.be(0); - }); - }); - - describe('deletion', function() { - var len = 0; - it('can delete half the RTree', function() { - var bounds = [5000, 0, 10500, 10500]; - len += rTree.remove(bounds).length; - expect(len).to.not.be(0); - }); - it('can delete the other half of the RTree', function() { - var bounds = [0, 0, 5000, 10500]; - len += rTree.remove(bounds).length; - expect(len).to.be(2000); - }); - }); - - describe('result plausibility and structure', function() { - - it('filters by rectangle', function() { - rTree.insert([0, 0, 1, 1], 1); - rTree.insert([1, 1, 4, 4], 2); - rTree.insert([2, 2, 3, 3], 3); - rTree.insert([-5, -5, -4, -4], 4); - rTree.insert([-4, -4, -1, -1], 5); - rTree.insert([-3, -3, -2, -2], 6); - - var result; - result = goog.object.getValues(rTree.search([2, 2, 3, 3])); - expect(result).to.contain(2); - expect(result).to.contain(3); - expect(result.length).to.be(2); - result = goog.object.getValues(rTree.search([-1, -1, 2, 2])); - expect(result).to.contain(1); - expect(result).to.contain(2); - expect(result).to.contain(3); - expect(result).to.contain(5); - expect(result.length).to.be(4); - expect(goog.object.getCount(rTree.search([5, 5, 6, 6]))).to.be(0); - }); - - it('filters by type', function() { - rTree.insert([2, 2, 3, 3], 7, 'type1'); - - var result; - result = rTree.search([1, 2, 4, 4], 'type1'); - expect(result).to.contain(7); - expect(result.length).to.be(1); - result = rTree.search([1, 2, 4, 4]); - expect(result.length).to.be(3); - }); - - }); - - describe('#forEach()', function() { - var tree; - beforeEach(function() { - tree = new ol.structs.RTree(); - }); - - it('calls a callback for each result in the search extent', function() { - var one = {}; - tree.insert([4.5, 4.5, 5, 5], one); - - var two = {}; - tree.insert([5, 5, 5.5, 5.5], two); - - var callback = sinon.spy(); - tree.forEach([4, 4, 6, 6], callback); - expect(callback.callCount).to.be(2); - expect(callback.calledWith(one)).to.be(true); - expect(callback.calledWith(two)).to.be(true); - }); - - it('accepts a this argument', function() { - var obj = {}; - tree.insert([5, 5, 5, 5], obj); - - var callback = sinon.spy(); - var thisArg = {}; - tree.forEach([4, 4, 6, 6], callback, thisArg); - expect(callback.callCount).to.be(1); - expect(callback.calledWith(obj)).to.be(true); - expect(callback.calledOn(thisArg)).to.be(true); - }); - - }); - -}); - -goog.require('goog.object'); -goog.require('ol.structs.RTree'); From 45d9f6ce0b4103a894bdb63fa8566e1b9c685ee8 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 28 Nov 2013 16:17:29 +0100 Subject: [PATCH 18/18] Rename rbush test to match the naming scheme --- test/spec/ol/structs/{rbush.js => rbush.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/spec/ol/structs/{rbush.js => rbush.test.js} (100%) diff --git a/test/spec/ol/structs/rbush.js b/test/spec/ol/structs/rbush.test.js similarity index 100% rename from test/spec/ol/structs/rbush.js rename to test/spec/ol/structs/rbush.test.js