diff --git a/examples/select-features.html b/examples/select-features.html new file mode 100644 index 0000000000..2ff31fb9dd --- /dev/null +++ b/examples/select-features.html @@ -0,0 +1,56 @@ + + + + + + + + + + + Select features example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Select features example

+

Example of using the Select interaction. Select features by clicking polygons. Hold the Shift-key to add to the selection.

+
+

See the select-features.js source to see how this is done.

+
+
select, vector
+
+ +
+ +
+ + + + + + diff --git a/examples/select-features.js b/examples/select-features.js new file mode 100644 index 0000000000..ab54750dbd --- /dev/null +++ b/examples/select-features.js @@ -0,0 +1,63 @@ +goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.interaction.Select'); +goog.require('ol.interaction.defaults'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.layer.Vector'); +goog.require('ol.parser.ogc.GML_v3'); +goog.require('ol.source.MapQuestOpenAerial'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Rule'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +var raster = new ol.layer.TileLayer({ + source: new ol.source.MapQuestOpenAerial() +}); + +var vector = new ol.layer.Vector({ + id: 'vector', + source: new ol.source.Vector({ + parser: new ol.parser.ogc.GML_v3(), + url: 'data/gml/topp-states-wfs.xml' + }), + style: new ol.style.Style({ + rules: [ + new ol.style.Rule({ + filter: 'renderIntent("selected")', + symbolizers: [ + new ol.style.Fill({ + color: '#ffffff', + opacity: 0.5 + }) + ] + }) + ], + symbolizers: [ + new ol.style.Fill({ + color: '#ffffff', + opacity: 0.25 + }), + new ol.style.Stroke({ + color: '#6666ff' + }) + ] + }) +}); + +var selectInteraction = new ol.interaction.Select({ + layerFilter: function(layer) { return layer.get('id') == 'vector'; } +}); + +var map = new ol.Map({ + interactions: ol.interaction.defaults().extend([selectInteraction]), + layers: [raster, vector], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: [-11000000, 4600000], + zoom: 4 + }) +}); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 973fd47471..8b94acbd1f 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -244,7 +244,7 @@ /** * @typedef {Object} ol.interaction.DragPanOptions * @property {ol.Kinetic|undefined} kinetic Kinetic. - * @property {ol.interaction.ConditionType|undefined} condition Conditon. + * @property {ol.interaction.ConditionType|undefined} condition Condition. */ /** @@ -295,6 +295,13 @@ * @property {number|undefined} delta Delta. */ +/** + * @typedef {Object} ol.interaction.SelectOptions + * @property {ol.interaction.ConditionType|undefined} condition Condition. + * @property {undefined|function(ol.layer.Layer):boolean} layerFilter Filter + * function to restrict selection to a subset of layers. + */ + /** * @typedef {Object} ol.interaction.TouchPanOptions * @property {ol.Kinetic|undefined} kinetic Kinetic. diff --git a/src/ol/expr/expression.js b/src/ol/expr/expression.js index dda7330ea6..cdbe012936 100644 --- a/src/ol/expr/expression.js +++ b/src/ol/expr/expression.js @@ -99,6 +99,7 @@ ol.expr.functions = { EXTENT: 'extent', FID: 'fid', GEOMETRY_TYPE: 'geometryType', + RENDER_INTENT: 'renderIntent', INTERSECTS: 'intersects', CONTAINS: 'contains', DWITHIN: 'dwithin', @@ -252,6 +253,17 @@ ol.expr.lib[ol.expr.functions.GEOMETRY_TYPE] = function(type) { }; +/** + * Determine if a feature's renderIntent matches the given one. + * @param {string} renderIntent Render intent. + * @return {boolean} The feature's renderIntent matches the given one. + * @this {ol.Feature} + */ +ol.expr.lib[ol.expr.functions.RENDER_INTENT] = function(renderIntent) { + return this.renderIntent == renderIntent; +}; + + ol.expr.lib[ol.expr.functions.INTERSECTS] = function(geom, opt_projection, opt_attribute) { throw new Error('Spatial function not implemented: ' + diff --git a/src/ol/feature.js b/src/ol/feature.js index 24de9c40e3..ad4465cce4 100644 --- a/src/ol/feature.js +++ b/src/ol/feature.js @@ -2,6 +2,7 @@ goog.provide('ol.Feature'); goog.require('ol.Object'); goog.require('ol.geom.Geometry'); +goog.require('ol.layer.VectorLayerRenderIntent'); @@ -34,6 +35,12 @@ ol.Feature = function(opt_values) { */ this.geometryName_; + /** + * The render intent for this feature. + * @type {ol.layer.VectorLayerRenderIntent|string} + */ + this.renderIntent = ol.layer.VectorLayerRenderIntent.DEFAULT; + /** * @type {Array.} * @private @@ -109,7 +116,7 @@ ol.Feature.prototype.set = function(key, value) { * Set the feature's commonly used identifier. This identifier is usually the * unique id in the source store. * - * @param {string} featureId The feature's identifier. + * @param {string|undefined} featureId The feature's identifier. */ ol.Feature.prototype.setFeatureId = function(featureId) { this.featureId_ = featureId; diff --git a/src/ol/geom/geometry.js b/src/ol/geom/geometry.js index 964e6fa4ce..383c255fa1 100644 --- a/src/ol/geom/geometry.js +++ b/src/ol/geom/geometry.js @@ -27,6 +27,19 @@ ol.geom.Geometry = function() { ol.geom.Geometry.prototype.dimension; +/** + * Create a clone of this geometry. The clone will not be represented in any + * shared structure. + * @return {ol.geom.Geometry} The cloned geometry. + */ +ol.geom.Geometry.prototype.clone = function() { + var clone = new this.constructor(this.getCoordinates()); + clone.bounds_ = this.bounds_; + clone.dimension = this.dimension; + return clone; +}; + + /** * Get the rectangular 2D envelope for this geoemtry. * @return {ol.Extent} The bounding rectangular envelope. diff --git a/src/ol/interaction/condition.js b/src/ol/interaction/condition.js index 78719a8c89..70702ab5db 100644 --- a/src/ol/interaction/condition.js +++ b/src/ol/interaction/condition.js @@ -2,6 +2,7 @@ goog.provide('ol.interaction.ConditionType'); goog.provide('ol.interaction.condition'); goog.require('goog.dom.TagName'); +goog.require('goog.events.EventType'); goog.require('goog.functions'); @@ -42,6 +43,15 @@ ol.interaction.condition.altShiftKeysOnly = function(browserEvent) { ol.interaction.condition.always = goog.functions.TRUE; +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @return {boolean} True only the event is a click event. + */ +ol.interaction.condition.clickOnly = function(browserEvent) { + return browserEvent.type == goog.events.EventType.CLICK; +}; + + /** * @param {goog.events.BrowserEvent} browserEvent Browser event. * @return {boolean} True if only the no modifier keys are pressed. diff --git a/src/ol/interaction/interaction.js b/src/ol/interaction/interaction.js index 4fe73093b4..853b3c68f2 100644 --- a/src/ol/interaction/interaction.js +++ b/src/ol/interaction/interaction.js @@ -2,6 +2,7 @@ goog.provide('ol.interaction.Interaction'); +goog.require('goog.Disposable'); goog.require('ol.MapBrowserEvent'); goog.require('ol.animation.pan'); goog.require('ol.animation.rotate'); @@ -12,9 +13,12 @@ goog.require('ol.easing'); /** * @constructor + * @extends {goog.Disposable} */ ol.interaction.Interaction = function() { + goog.base(this); }; +goog.inherits(ol.interaction.Interaction, goog.Disposable); /** diff --git a/src/ol/interaction/selectinteraction.exports b/src/ol/interaction/selectinteraction.exports new file mode 100644 index 0000000000..a5cd323f10 --- /dev/null +++ b/src/ol/interaction/selectinteraction.exports @@ -0,0 +1 @@ +@exportClass ol.interaction.Select ol.interaction.SelectOptions diff --git a/src/ol/interaction/selectinteraction.js b/src/ol/interaction/selectinteraction.js new file mode 100644 index 0000000000..45fa13aef0 --- /dev/null +++ b/src/ol/interaction/selectinteraction.js @@ -0,0 +1,172 @@ +goog.provide('ol.interaction.Select'); + +goog.require('goog.array'); +goog.require('ol.Feature'); +goog.require('ol.interaction.ConditionType'); +goog.require('ol.interaction.Interaction'); +goog.require('ol.interaction.condition'); +goog.require('ol.layer.Vector'); +goog.require('ol.layer.VectorLayerRenderIntent'); +goog.require('ol.source.Vector'); + + + +/** + * @constructor + * @extends {ol.interaction.Interaction} + * @param {ol.interaction.SelectOptions=} opt_options Options. + */ +ol.interaction.Select = function(opt_options) { + var options = goog.isDef(opt_options) ? opt_options : {}; + + /** + * @private + * @type {ol.interaction.ConditionType} + */ + this.condition_ = goog.isDef(options.condition) ? + options.condition : ol.interaction.condition.clickOnly; + + /** + * Mapping between original features and cloned features on selection layers. + * @type {Object.<*,Object.<*,ol.Feature>>} + * @private + */ + this.featureMap_ = {}; + + /** + * Mapping between original layers and selection layers, by map. + * @type {Object.<*,{map:ol.Map,layers:Object.<*,ol.layer.Vector>}>} + * @protected + */ + this.selectionLayers = {}; + + /** + * @type {null|function(ol.layer.Layer):boolean} + * @private + */ + this.layerFilter_ = goog.isDef(options.layerFilter) ? + options.layerFilter : null; + + goog.base(this); +}; +goog.inherits(ol.interaction.Select, ol.interaction.Interaction); + + +/** + * @inheritDoc + */ +ol.interaction.Select.prototype.disposeInternal = function() { + for (var m in this.selectionLayers) { + var selectionLayers = this.selectionLayers[m].layers; + var map = this.selectionLayers[m].map; + for (var l in selectionLayers) { + map.removeLayer(selectionLayers[l]); + } + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc. + */ +ol.interaction.Select.prototype.handleMapBrowserEvent = function(evt) { + var browserEvent = evt.browserEvent; + if (this.condition_(browserEvent)) { + var map = evt.map; + var layers = map.getLayerGroup().getLayersArray(); + if (!goog.isNull(this.layerFilter_)) { + layers = goog.array.filter(layers, this.layerFilter_); + } + var clear = !ol.interaction.condition.shiftKeyOnly(browserEvent); + + var that = this; + var select = function(featuresByLayer) { + that.select(map, featuresByLayer, layers, clear); + }; + + map.getFeatures({ + layers: layers, + pixel: evt.getPixel(), + success: select + }); + } + // TODO: Implement box selection + return true; +}; + + +/** + * @param {ol.Map} map The map where the selction event originated. + * @param {Array.>} featuresByLayer Features by layer. + * @param {Array.} layers The queried layers. + * @param {boolean} clear Whether the current layer content should be cleared. + */ +ol.interaction.Select.prototype.select = + function(map, featuresByLayer, layers, clear) { + var mapId = goog.getUid(map); + for (var i = 0, ii = featuresByLayer.length; i < ii; ++i) { + var layer = layers[i]; + var layerId = goog.getUid(layer); + if (!(mapId in this.selectionLayers)) { + this.selectionLayers[mapId] = {map: map, layers: {}}; + } + var selectionLayer = this.selectionLayers[mapId].layers[layerId]; + if (!goog.isDef(selectionLayer)) { + selectionLayer = new ol.layer.Vector({ + source: new ol.source.Vector({parser: null}), + style: goog.isFunction(layer.getStyle) ? layer.getStyle() : null + }); + selectionLayer.setTemporary(true); + map.addLayer(selectionLayer); + this.selectionLayers[mapId].layers[layerId] = selectionLayer; + this.featureMap_[layerId] = {}; + } + + var features = featuresByLayer[i]; + var numFeatures = features.length; + var selectedFeatures = []; + var featuresToAdd = []; + var unselectedFeatures = []; + var featuresToRemove = []; + var featureMap = this.featureMap_[layerId]; + var oldFeatureMap = featureMap; + if (clear) { + for (var f in featureMap) { + unselectedFeatures.push(layer.getFeatureWithUid(f)); + featuresToRemove.push(featureMap[f]); + } + featureMap = {}; + this.featureMap_[layerId] = featureMap; + } + for (var j = 0; j < numFeatures; ++j) { + var feature = features[j]; + var featureId = goog.getUid(feature); + var clone = featureMap[featureId]; + if (clone) { + // TODO: make toggle configurable + unselectedFeatures.push(feature); + featuresToRemove.push(clone); + delete featureMap[featureId]; + } else if (!(featureId in oldFeatureMap)) { + clone = new ol.Feature(feature.getAttributes()); + clone.setGeometry(feature.getGeometry().clone()); + clone.setFeatureId(feature.getFeatureId()); + clone.setSymbolizers(feature.getSymbolizers()); + clone.renderIntent = ol.layer.VectorLayerRenderIntent.SELECTED; + featureMap[featureId] = clone; + selectedFeatures.push(feature); + featuresToAdd.push(clone); + } + } + if (goog.isFunction(layer.setRenderIntent)) { + layer.setRenderIntent(ol.layer.VectorLayerRenderIntent.HIDDEN, + selectedFeatures); + layer.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT, + unselectedFeatures); + } + selectionLayer.removeFeatures(featuresToRemove); + selectionLayer.addFeatures(featuresToAdd); + // TODO: Dispatch an event with selectedFeatures and unselectedFeatures + } +}; diff --git a/src/ol/layer/vectorlayer.js b/src/ol/layer/vectorlayer.js index 397d6768e1..31f1cfb5cd 100644 --- a/src/ol/layer/vectorlayer.js +++ b/src/ol/layer/vectorlayer.js @@ -3,6 +3,7 @@ goog.provide('ol.layer.VectorLayerEventType'); goog.require('goog.array'); goog.require('goog.asserts'); +goog.require('goog.events.EventType'); goog.require('goog.object'); goog.require('ol.Feature'); goog.require('ol.expr'); @@ -201,6 +202,16 @@ ol.layer.FeatureCache.prototype.getFeaturesByIds_ = function(ids) { }; +/** + * @param {string} uid Feature uid. + * @return {ol.Feature|undefined} The feature with the provided uid if it is in + * the cache, otherwise undefined. + */ +ol.layer.FeatureCache.prototype.getFeatureWithUid = function(uid) { + return this.idLookup_[uid]; +}; + + /** * Remove a feature from the cache. * @param {ol.Feature} feature Feature. @@ -225,7 +236,9 @@ ol.layer.FeatureCache.prototype.remove = function(feature) { */ ol.layer.VectorLayerEventType = { ADD: 'add', - REMOVE: 'remove' + CHANGE: goog.events.EventType.CHANGE, + REMOVE: 'remove', + INTENTCHANGE: 'intentchange' }; @@ -287,6 +300,13 @@ ol.layer.Vector = function(options) { */ this.polygonVertices_ = new ol.geom.SharedVertices(); + /** + * True if this is a temporary layer. + * @type {boolean} + * @private + */ + this.temp_ = false; + }; goog.inherits(ol.layer.Vector, ol.layer.Layer); @@ -313,6 +333,25 @@ ol.layer.Vector.prototype.addFeatures = function(features) { }; +/** + * Remove all features from the layer. + */ +ol.layer.Vector.prototype.clear = function() { + this.featureCache_.clear(); + this.dispatchEvent(/** @type {ol.layer.VectorLayerEventObject} */ ({ + type: ol.layer.VectorLayerEventType.CHANGE + })); +}; + + +/** + * @return {boolean} Whether this layer is temporary. + */ +ol.layer.Vector.prototype.getTemporary = function() { + return this.temp_; +}; + + /** * @return {ol.source.Vector} Source. */ @@ -321,6 +360,14 @@ ol.layer.Vector.prototype.getVectorSource = function() { }; +/** + * @return {ol.style.Style} This layer's style. + */ +ol.layer.Vector.prototype.getStyle = function() { + return this.style_; +}; + + /** * Get all features whose bounding box intersects the provided extent. This * method is intended for being called by the renderer. When null is returned, @@ -426,6 +473,16 @@ ol.layer.Vector.prototype.groupFeaturesBySymbolizerLiteral = }; +/** + * @param {string|number} uid Feature uid. + * @return {ol.Feature|undefined} The feature with the provided uid if it is on + * the layer, otherwise undefined. + */ +ol.layer.Vector.prototype.getFeatureWithUid = function(uid) { + return this.featureCache_.getFeatureWithUid(/** @type {string} */ (uid)); +}; + + /** * @param {Object|Element|Document|string} data Feature data. * @param {ol.parser.Parser} parser Feature parser. @@ -532,12 +589,48 @@ ol.layer.Vector.prototype.removeFeatures = function(features) { }; +/** + * Changes the renderIntent for an array of features. + * @param {string} renderIntent Render intent. + * @param {Array.=} opt_features Features to change the renderIntent + * for. If not provided, all features will be changed. + */ +ol.layer.Vector.prototype.setRenderIntent = + function(renderIntent, opt_features) { + var features = goog.isDef(opt_features) ? opt_features : + goog.object.getValues(this.featureCache_.getFeaturesObject()); + var extent = ol.extent.createEmpty(), + feature, geometry; + for (var i = features.length - 1; i >= 0; --i) { + feature = features[i]; + feature.renderIntent = renderIntent; + geometry = feature.getGeometry(); + if (!goog.isNull(geometry)) { + ol.extent.extend(extent, geometry.getBounds()); + } + } + this.dispatchEvent(/** @type {ol.layer.VectorLayerEventObject} */ ({ + extent: extent, + features: features, + type: ol.layer.VectorLayerEventType.INTENTCHANGE + })); +}; + + +/** + * @param {boolean} temp Whether this layer is temporary. + */ +ol.layer.Vector.prototype.setTemporary = function(temp) { + this.temp_ = temp; +}; + + /** * @param {Array.} features Features. * @return {string} Feature info. */ ol.layer.Vector.uidTransformFeatureInfo = function(features) { - var featureIds = goog.array.map(features, + var uids = goog.array.map(features, function(feature) { return goog.getUid(feature); }); - return featureIds.join(', '); + return uids.join(', '); }; diff --git a/src/ol/layer/vectorlayerrenderintent.js b/src/ol/layer/vectorlayerrenderintent.js new file mode 100644 index 0000000000..757fef5aba --- /dev/null +++ b/src/ol/layer/vectorlayerrenderintent.js @@ -0,0 +1,11 @@ +goog.provide('ol.layer.VectorLayerRenderIntent'); + + +/** + * @enum {string} + */ +ol.layer.VectorLayerRenderIntent = { + DEFAULT: 'default', + HIDDEN: 'hidden', + SELECTED: 'selected' +}; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 8d704eb2c0..eeb1b3cad7 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -88,7 +88,9 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) { ol.renderer.canvas.VectorLayer.TILECACHE_SIZE); goog.events.listen(layer, [ ol.layer.VectorLayerEventType.ADD, - ol.layer.VectorLayerEventType.REMOVE + ol.layer.VectorLayerEventType.CHANGE, + ol.layer.VectorLayerEventType.REMOVE, + ol.layer.VectorLayerEventType.INTENTCHANGE ], this.handleLayerChange_, false, this); diff --git a/src/ol/renderer/canvas/canvasvectorrenderer.js b/src/ol/renderer/canvas/canvasvectorrenderer.js index 354a91803e..1e4543f446 100644 --- a/src/ol/renderer/canvas/canvasvectorrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorrenderer.js @@ -17,6 +17,7 @@ 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'); @@ -176,6 +177,9 @@ ol.renderer.canvas.VectorRenderer.prototype.renderLineStringFeatures_ = context.beginPath(); for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; + if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { + continue; + } id = goog.getUid(feature); currentSize = goog.isDef(this.symbolSizes_[id]) ? this.symbolSizes_[id] : [0]; @@ -253,6 +257,9 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = context.globalAlpha = alpha; for (i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; + if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { + continue; + } id = goog.getUid(feature); size = this.symbolSizes_[id]; this.symbolSizes_[id] = goog.isDef(size) ? @@ -296,7 +303,7 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = ol.renderer.canvas.VectorRenderer.prototype.renderText_ = function(features, text, texts) { var context = this.context_, - vecs, vec; + feature, vecs, vec; if (context.fillStyle !== text.color) { context.fillStyle = text.color; @@ -309,8 +316,12 @@ ol.renderer.canvas.VectorRenderer.prototype.renderText_ = context.textBaseline = 'middle'; for (var i = 0, ii = features.length; i < ii; ++i) { + feature = features[i]; + if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { + continue; + } vecs = ol.renderer.canvas.VectorRenderer.getLabelVectors( - features[i].getGeometry()); + feature.getGeometry()); for (var j = 0, jj = vecs.length; j < jj; ++j) { vec = vecs[j]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); @@ -336,7 +347,7 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPolygonFeatures_ = fillOpacity = symbolizer.fillOpacity, globalAlpha, i, ii, geometry, components, j, jj, poly, - rings, numRings, ring, dim, k, kk, vec; + rings, numRings, ring, dim, k, kk, vec, feature; if (strokeColor) { context.strokeStyle = strokeColor; @@ -359,7 +370,11 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPolygonFeatures_ = */ context.beginPath(); for (i = 0, ii = features.length; i < ii; ++i) { - geometry = features[i].getGeometry(); + feature = features[i]; + if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { + continue; + } + geometry = feature.getGeometry(); if (geometry instanceof ol.geom.Polygon) { components = [geometry]; } else { diff --git a/src/ol/style/fillsymbolizer.js b/src/ol/style/fillsymbolizer.js index b5e079117e..c0acfa6161 100644 --- a/src/ol/style/fillsymbolizer.js +++ b/src/ol/style/fillsymbolizer.js @@ -116,10 +116,20 @@ ol.style.Fill.prototype.setOpacity = function(opacity) { /** - * @typedef {{fillColor: (string), - * fillOpacity: (number)}} + * @typedef {{color: (string), + * opacity: (number)}} */ ol.style.FillDefaults = { color: '#ffffff', opacity: 0.4 }; + + +/** + * @typedef {{color: (string), + * opacity: (number)}} + */ +ol.style.FillDefaultsSelect = { + color: '#ffffff', + opacity: 0.7 +}; diff --git a/src/ol/style/strokesymbolizer.js b/src/ol/style/strokesymbolizer.js index f1f0b389aa..339bae811b 100644 --- a/src/ol/style/strokesymbolizer.js +++ b/src/ol/style/strokesymbolizer.js @@ -158,12 +158,24 @@ ol.style.Stroke.prototype.setWidth = function(width) { /** - * @typedef {{strokeColor: (string), - * strokeOpacity: (number), - * strokeWidth: (number)}} + * @typedef {{color: (string), + * opacity: (number), + * width: (number)}} */ ol.style.StrokeDefaults = { color: '#696969', opacity: 0.75, width: 1.5 }; + + +/** + * @typedef {{color: (string), + * opacity: (number), + * width: (number)}} + */ +ol.style.StrokeDefaultsSelect = { + color: '#696969', + opacity: 0.9, + width: 2.0 +}; diff --git a/src/ol/style/style.js b/src/ol/style/style.js index e098cc75cf..0a11949201 100644 --- a/src/ol/style/style.js +++ b/src/ol/style/style.js @@ -67,6 +67,19 @@ ol.style.Style.prototype.createLiterals = function(feature) { * @type {ol.style.Style} */ ol.style.Style.defaults = new ol.style.Style({ + rules: [ + new ol.style.Rule({ + filter: 'renderIntent("select")', + symbolizers: [ + new ol.style.Shape({ + fill: new ol.style.Fill(ol.style.FillDefaultsSelect), + stroke: new ol.style.Stroke(ol.style.StrokeDefaultsSelect) + }), + new ol.style.Fill(ol.style.FillDefaultsSelect), + new ol.style.Stroke(ol.style.StrokeDefaultsSelect) + ] + }) + ], symbolizers: [ new ol.style.Shape({ fill: new ol.style.Fill(), diff --git a/test/spec/ol/expr/expression.test.js b/test/spec/ol/expr/expression.test.js index 9b396897e4..22927fa0ef 100644 --- a/test/spec/ol/expr/expression.test.js +++ b/test/spec/ol/expr/expression.test.js @@ -872,6 +872,24 @@ describe('ol.expr.lib', function() { }); + describe('renderIntent()', function() { + + var feature = new ol.Feature(); + feature.renderIntent = 'foo'; + + var isFoo = parse('renderIntent("foo")'); + var isBar = parse('renderIntent("bar")'); + + it('True when renderIntent matches', function() { + expect(evaluate(isFoo, feature), true); + }); + + it('False when renderIntent does not match', function() { + expect(evaluate(isBar, feature), false); + }); + + }); + }); describe('ol.expr.register()', function() { diff --git a/test/spec/ol/geom/geometrycollection.test.js b/test/spec/ol/geom/geometrycollection.test.js index d9c1616c33..622cd48943 100644 --- a/test/spec/ol/geom/geometrycollection.test.js +++ b/test/spec/ol/geom/geometrycollection.test.js @@ -55,6 +55,23 @@ describe('ol.geom.GeometryCollection', function() { }); + describe('#clone()', function() { + + it('has a working clone method', function() { + var point = new ol.geom.Point([10, 20]); + var line = new ol.geom.LineString([[10, 20], [30, 40]]); + var poly = new ol.geom.Polygon([outer, inner1, inner2]); + var multi = new ol.geom.GeometryCollection([point, line, poly]); + var clone = multi.clone(); + expect(clone).to.not.be(multi); + var components = clone.components; + expect(components[0]).to.eql([10, 20]); + expect(components[1]).to.eql([[10, 20], [30, 40]]); + expect(components[2]).to.eql([outer, inner1, inner2]); + }); + + }); + describe('#getBounds()', function() { it('returns the bounding extent', function() { diff --git a/test/spec/ol/interaction/selectinteraction.test.js b/test/spec/ol/interaction/selectinteraction.test.js new file mode 100644 index 0000000000..12de3a07a6 --- /dev/null +++ b/test/spec/ol/interaction/selectinteraction.test.js @@ -0,0 +1,84 @@ +goog.provide('ol.test.interaction.Select'); + +describe('ol.interaction.Select', function() { + var map, target, select, vector, features; + + beforeEach(function() { + target = document.createElement('div'); + target.style.width = '256px'; + target.style.height = '256px'; + document.body.appendChild(target); + 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); + select = new ol.interaction.Select({ + layerFilter: function(layer) { return layer === vector; } + }); + map.getInteractions().push(select); + }); + + afterEach(function() { + goog.dispose(select); + goog.dispose(map); + document.body.removeChild(target); + select = null; + map = null; + target = null; + }); + + describe('#select', function() { + + it('toggles selection of features', function() { + select.select(map, [features], [vector]); + var layer = select.selectionLayers[goog.getUid(map)] + .layers[goog.getUid(vector)]; + expect(goog.object.getCount(layer.featureCache_.idLookup_)).to.be(2); + select.select(map, [features], [vector]); + expect(goog.object.getCount(layer.featureCache_.idLookup_)).to.be(0); + }); + + it('can append features to an existing selection', function() { + select.select(map, [[features[0]]], [vector]); + select.select(map, [[features[1]]], [vector]); + var layer = select.selectionLayers[goog.getUid(map)] + .layers[goog.getUid(vector)]; + expect(goog.object.getCount(layer.featureCache_.idLookup_)).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); + var layer = select.selectionLayers[goog.getUid(map)] + .layers[goog.getUid(vector)]; + expect(goog.object.getCount(layer.featureCache_.idLookup_)).to.be(1); + }); + + }); + +}); + +goog.require('goog.dispose'); +goog.require('goog.object'); +goog.require('ol.Map'); +goog.require('ol.interaction.Select'); +goog.require('ol.layer.Vector'); +goog.require('ol.parser.GeoJSON'); +goog.require('ol.source.Vector');