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');