From 38124d770b45803cb837b446fd78ec475e616ccc Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 25 Sep 2019 10:33:38 +0200 Subject: [PATCH] Revert delete Select interaction commit 3838b684276458dd0a6852d634a138c925c14800 --- changelog/upgrade-notes.md | 8 - examples/box-selection.html | 3 +- examples/box-selection.js | 40 +- examples/earthquake-clusters.html | 2 +- examples/earthquake-clusters.js | 92 +++-- examples/icon-negative.js | 47 +-- examples/modify-features.html | 2 +- examples/modify-features.js | 42 +-- examples/modify-test.js | 28 +- examples/select-features.js | 85 +++++ examples/snap.js | 50 +-- examples/translate-features.js | 40 +- examples/vector-esri-edit.js | 46 +-- src/ol/interaction.js | 1 + src/ol/interaction/Select.js | 483 ++++++++++++++++++++++++ test/spec/ol/interaction/select.test.js | 451 ++++++++++++++++++++++ 16 files changed, 1127 insertions(+), 293 deletions(-) create mode 100644 examples/select-features.js create mode 100644 src/ol/interaction/Select.js create mode 100644 test/spec/ol/interaction/select.test.js diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 60653966a3..b69be2d8ed 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -275,14 +275,6 @@ The `ol/source/Vector#refresh()` method now removes all features from the source The `getGetFeatureInfoUrl` of `ol/source/ImageWMS` and `ol/source/TileWMS` is now called `getFeatureInfoUrl`. -##### Removal of `SelectInteraction` - -The `SelectInteraction` is removed. There are two examples ([Select Features by Hover](https://openlayers.org/en/master/examples/select-hover-features.html) and [Select multiple Features](https://openlayers.org/en/master/examples/select-multiple-features.html) which show how similar results can be achieved by using more basic methods. - -##### `getFeaturesAtPixel` always returns an array - -`getFeaturesAtPixel` now returns an empty array instead of `null` if no features were found. - #### Other changes ##### Allow declutter in image render mode diff --git a/examples/box-selection.html b/examples/box-selection.html index 7eeecfdfdc..2660efa27c 100644 --- a/examples/box-selection.html +++ b/examples/box-selection.html @@ -3,7 +3,8 @@ layout: example.html title: Box Selection shortdesc: Using a DragBox interaction to select features. docs: > -

This example shows how to use a DragBox interaction to select features.

+

This example shows how to use a DragBox interaction to select features. Selected features are added + to the feature overlay of a select interaction (ol/interaction/Select) for highlighting.

Use Ctrl+Drag (Command+Drag on Mac) to draw boxes.

tags: "DragBox, feature, selection, box" --- diff --git a/examples/box-selection.js b/examples/box-selection.js index 821d1fa77b..5f6b3de1a8 100644 --- a/examples/box-selection.js +++ b/examples/box-selection.js @@ -2,13 +2,9 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import {platformModifierKeyOnly} from '../src/ol/events/condition.js'; import GeoJSON from '../src/ol/format/GeoJSON.js'; -import {DragBox} from '../src/ol/interaction.js'; +import {DragBox, Select} from '../src/ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import {OSM, Vector as VectorSource} from '../src/ol/source.js'; -import Collection from '../src/ol/Collection.js'; -import Style from '../src/ol/style/Style.js'; -import Fill from '../src/ol/style/Fill.js'; -import Stroke from '../src/ol/style/Stroke.js'; const vectorSource = new VectorSource({ @@ -34,37 +30,11 @@ const map = new Map({ }) }); -const selectedFeatures = new Collection(); +// a normal select interaction to handle click +const select = new Select(); +map.addInteraction(select); -// style features in collection - -const highlightStyle = new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.7)' - }), - stroke: new Stroke({ - color: '#3399CC', - width: 3 - }) -}); - -selectedFeatures.on('add', function(e) { - e.element.setStyle(highlightStyle); -}); - -selectedFeatures.on('remove', function(e) { - e.element.setStyle(undefined); -}); - - -// handle clicks - -map.on('singleclick', function(e) { - selectedFeatures.clear(); - map.forEachFeatureAtPixel(e.pixel, function(f) { - selectedFeatures.push(f); - }); -}); +const selectedFeatures = select.getFeatures(); // a DragBox interaction used to select features by drawing boxes const dragBox = new DragBox({ diff --git a/examples/earthquake-clusters.html b/examples/earthquake-clusters.html index aef52ea204..773711a2b3 100644 --- a/examples/earthquake-clusters.html +++ b/examples/earthquake-clusters.html @@ -5,7 +5,7 @@ shortdesc: Demonstrates the use of style geometries to render source features of docs: >

This example parses a KML file and renders the features as clusters on a vector layer. The styling in this example is quite involved. Single earthquake locations (rendered as stars) have a size relative to their magnitude. Clusters have an opacity relative to the number of features in the cluster, and a size that represents - the extent of the features that make up the cluster. When hovering over a cluster, the individual features that make up the cluster will be shown.

+ the extent of the features that make up the cluster. When clicking or hovering on a cluster, the individual features that make up the cluster will be shown.

To achieve this, we make heavy use of style functions.

tags: "KML, vector, style, geometry, cluster" --- diff --git a/examples/earthquake-clusters.js b/examples/earthquake-clusters.js index 840a429ac1..e328f75562 100644 --- a/examples/earthquake-clusters.js +++ b/examples/earthquake-clusters.js @@ -2,6 +2,7 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import {createEmpty, getWidth, getHeight, extend} from '../src/ol/extent.js'; import KML from '../src/ol/format/KML.js'; +import {defaults as defaultInteractions, Select} from '../src/ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import {Cluster, Stamen, Vector as VectorSource} from '../src/ol/source.js'; import {Circle as CircleStyle, Fill, RegularShape, Stroke, Style, Text} from '../src/ol/style.js'; @@ -68,50 +69,48 @@ const calculateClusterInfo = function(resolution) { }; let currentResolution; -let hovered = null; - function styleFunction(feature, resolution) { - if (feature !== hovered) { - if (resolution != currentResolution) { - calculateClusterInfo(resolution); - currentResolution = resolution; - } - let style; - const size = feature.get('features').length; - if (size > 1) { - style = new Style({ - image: new CircleStyle({ - radius: feature.get('radius'), - fill: new Fill({ - color: [255, 153, 0, Math.min(0.8, 0.4 + (size / maxFeatureCount))] - }) - }), - text: new Text({ - text: size.toString(), - fill: textFill, - stroke: textStroke - }) - }); - } else { - const originalFeature = feature.get('features')[0]; - style = createEarthquakeStyle(originalFeature); - } - return style; - } else { - const styles = [new Style({ + if (resolution != currentResolution) { + calculateClusterInfo(resolution); + currentResolution = resolution; + } + let style; + const size = feature.get('features').length; + if (size > 1) { + style = new Style({ image: new CircleStyle({ radius: feature.get('radius'), - fill: invisibleFill + fill: new Fill({ + color: [255, 153, 0, Math.min(0.8, 0.4 + (size / maxFeatureCount))] + }) + }), + text: new Text({ + text: size.toString(), + fill: textFill, + stroke: textStroke }) - })]; - const originalFeatures = feature.get('features'); - let originalFeature; - for (let i = originalFeatures.length - 1; i >= 0; --i) { - originalFeature = originalFeatures[i]; - styles.push(createEarthquakeStyle(originalFeature)); - } - return styles; + }); + } else { + const originalFeature = feature.get('features')[0]; + style = createEarthquakeStyle(originalFeature); } + return style; +} + +function selectStyleFunction(feature) { + const styles = [new Style({ + image: new CircleStyle({ + radius: feature.get('radius'), + fill: invisibleFill + }) + })]; + const originalFeatures = feature.get('features'); + let originalFeature; + for (let i = originalFeatures.length - 1; i >= 0; --i) { + originalFeature = originalFeatures[i]; + styles.push(createEarthquakeStyle(originalFeature)); + } + return styles; } vector = new VectorLayer({ @@ -135,17 +134,16 @@ const raster = new TileLayer({ const map = new Map({ layers: [raster, vector], + interactions: defaultInteractions().extend([new Select({ + condition: function(evt) { + return evt.type == 'pointermove' || + evt.type == 'singleclick'; + }, + style: selectStyleFunction + })]), target: 'map', view: new View({ center: [0, 0], zoom: 2 }) }); - -map.on('pointermove', function(e) { - hovered = null; - map.forEachFeatureAtPixel(e.pixel, function(f) { - hovered = f; - f.changed(); - }); -}); diff --git a/examples/icon-negative.js b/examples/icon-negative.js index 54da8dff40..dfa83db624 100644 --- a/examples/icon-negative.js +++ b/examples/icon-negative.js @@ -2,6 +2,7 @@ import Feature from '../src/ol/Feature.js'; import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import Point from '../src/ol/geom/Point.js'; +import Select from '../src/ol/interaction/Select.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import Stamen from '../src/ol/source/Stamen.js'; import VectorSource from '../src/ol/source/Vector.js'; @@ -43,35 +44,27 @@ const map = new Map({ }); const selectStyle = {}; -let selected = null; - -map.on('singleclick', function(e) { - if (selected !== null) { - selected.setStyle(undefined); - } - map.forEachFeatureAtPixel(e.pixel, function(f) { - f.setStyle(function(feature) { - const image = feature.get('style').getImage().getImage(); - if (!selectStyle[image.src]) { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.width = image.width; - canvas.height = image.height; - context.drawImage(image, 0, 0, image.width, image.height); - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; - for (let i = 0, ii = data.length; i < ii; i = i + (i % 4 == 2 ? 2 : 1)) { - data[i] = 255 - data[i]; - } - context.putImageData(imageData, 0, 0); - selectStyle[image.src] = createStyle(undefined, canvas); +const select = new Select({ + style: function(feature) { + const image = feature.get('style').getImage().getImage(); + if (!selectStyle[image.src]) { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = image.width; + canvas.height = image.height; + context.drawImage(image, 0, 0, image.width, image.height); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + for (let i = 0, ii = data.length; i < ii; i = i + (i % 4 == 2 ? 2 : 1)) { + data[i] = 255 - data[i]; } - return selectStyle[image.src]; - }); - selected = f; - return true; - }); + context.putImageData(imageData, 0, 0); + selectStyle[image.src] = createStyle(undefined, canvas); + } + return selectStyle[image.src]; + } }); +map.addInteraction(select); map.on('pointermove', function(evt) { map.getTargetElement().style.cursor = diff --git a/examples/modify-features.html b/examples/modify-features.html index 7db85bdfc0..47ccc9a82b 100644 --- a/examples/modify-features.html +++ b/examples/modify-features.html @@ -3,7 +3,7 @@ layout: example.html title: Modify Features shortdesc: Editing features with the modify interaction. docs: > -

This example demonstrates how the modify interaction can be used. Zoom in to an area of interest and select a feature for editing. +

This example demonstrates how the modify and select interactions can be used together. Zoom in to an area of interest and select a feature for editing. Then drag points around to modify the feature. You can preserve topology by selecting multiple features before editing (Shift+Click to select multiple features).

tags: "modify, edit, vector" --- diff --git a/examples/modify-features.js b/examples/modify-features.js index cb67fb19eb..3be37f11dc 100644 --- a/examples/modify-features.js +++ b/examples/modify-features.js @@ -1,14 +1,9 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import GeoJSON from '../src/ol/format/GeoJSON.js'; -import {defaults as defaultInteractions, Modify} from '../src/ol/interaction.js'; +import {defaults as defaultInteractions, Modify, Select} from '../src/ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import {OSM, Vector as VectorSource} from '../src/ol/source.js'; -import {shiftKeyOnly} from '../src/ol/events/condition.js'; -import Collection from '../src/ol/Collection.js'; -import Style from '../src/ol/style/Style.js'; -import Fill from '../src/ol/style/Fill.js'; -import Stroke from '../src/ol/style/Stroke.js'; const raster = new TileLayer({ @@ -23,34 +18,16 @@ const vector = new VectorLayer({ }) }); -const features = new Collection(); - -// style features in collection - -const highlightStyle = new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.7)' - }), - stroke: new Stroke({ - color: 'rgb(51,153,204)', - width: 3 - }) -}); - -features.on('add', function(e) { - e.element.setStyle(highlightStyle); -}); - -features.on('remove', function(e) { - e.element.setStyle(undefined); +const select = new Select({ + wrapX: false }); const modify = new Modify({ - features: features + features: select.getFeatures() }); const map = new Map({ - interactions: defaultInteractions().extend([modify]), + interactions: defaultInteractions().extend([select, modify]), layers: [raster, vector], target: 'map', view: new View({ @@ -58,12 +35,3 @@ const map = new Map({ zoom: 2 }) }); - -map.on('singleclick', function(e) { - if (!shiftKeyOnly(e)) { - features.clear(); - } - map.forEachFeatureAtPixel(e.pixel, function(f) { - features.push(f); - }); -}); diff --git a/examples/modify-test.js b/examples/modify-test.js index f982afb667..f399546a03 100644 --- a/examples/modify-test.js +++ b/examples/modify-test.js @@ -1,11 +1,10 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import GeoJSON from '../src/ol/format/GeoJSON.js'; -import {defaults as defaultInteractions, Modify} from '../src/ol/interaction.js'; +import {defaults as defaultInteractions, Modify, Select} from '../src/ol/interaction.js'; import VectorLayer from '../src/ol/layer/Vector.js'; import VectorSource from '../src/ol/source/Vector.js'; import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js'; -import Collection from '../src/ol/Collection.js'; const styleFunction = (function() { @@ -212,29 +211,23 @@ const overlayStyle = (function() { }; })(); -const collection = new Collection(); - -collection.on('add', function(e) { - e.element.setStyle(overlayStyle); -}); - -collection.on('remove', function(e) { - e.element.setStyle(undefined); +const select = new Select({ + style: overlayStyle }); const modify = new Modify({ - features: collection, + features: select.getFeatures(), style: overlayStyle, insertVertexCondition: function() { // prevent new vertices to be added to the polygons - return !collection.getArray().every(function(feature) { + return !select.getFeatures().getArray().every(function(feature) { return feature.getGeometry().getType().match(/Polygon/); }); } }); const map = new Map({ - interactions: defaultInteractions().extend([modify]), + interactions: defaultInteractions().extend([select, modify]), layers: [layer], target: 'map', view: new View({ @@ -242,12 +235,3 @@ const map = new Map({ zoom: 2 }) }); - -map.on('singleclick', function(e) { - collection.clear(); - map.forEachFeatureAtPixel(e.pixel, function(f) { - collection.push(f); - }); -}); - - diff --git a/examples/select-features.js b/examples/select-features.js new file mode 100644 index 0000000000..0303c58896 --- /dev/null +++ b/examples/select-features.js @@ -0,0 +1,85 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import {click, pointerMove, altKeyOnly} from '../src/ol/events/condition.js'; +import GeoJSON from '../src/ol/format/GeoJSON.js'; +import Select from '../src/ol/interaction/Select.js'; +import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; +import OSM from '../src/ol/source/OSM.js'; +import VectorSource from '../src/ol/source/Vector.js'; + +const raster = new TileLayer({ + source: new OSM() +}); + +const vector = new VectorLayer({ + source: new VectorSource({ + url: 'data/geojson/countries.geojson', + format: new GeoJSON() + }) +}); + +const map = new Map({ + layers: [raster, vector], + target: 'map', + view: new View({ + center: [0, 0], + zoom: 2 + }) +}); + +let select = null; // ref to currently selected interaction + +// select interaction working on "singleclick" +const selectSingleClick = new Select(); + +// select interaction working on "click" +const selectClick = new Select({ + condition: click +}); + +// select interaction working on "pointermove" +const selectPointerMove = new Select({ + condition: pointerMove +}); + +const selectAltClick = new Select({ + condition: function(mapBrowserEvent) { + return click(mapBrowserEvent) && altKeyOnly(mapBrowserEvent); + } +}); + +const selectElement = document.getElementById('type'); + +const changeInteraction = function() { + if (select !== null) { + map.removeInteraction(select); + } + const value = selectElement.value; + if (value == 'singleclick') { + select = selectSingleClick; + } else if (value == 'click') { + select = selectClick; + } else if (value == 'pointermove') { + select = selectPointerMove; + } else if (value == 'altclick') { + select = selectAltClick; + } else { + select = null; + } + if (select !== null) { + map.addInteraction(select); + select.on('select', function(e) { + document.getElementById('status').innerHTML = ' ' + + e.target.getFeatures().getLength() + + ' selected features (last operation selected ' + e.selected.length + + ' and deselected ' + e.deselected.length + ' features)'; + }); + } +}; + + +/** + * onchange callback on the select element. + */ +selectElement.onchange = changeInteraction; +changeInteraction(); diff --git a/examples/snap.js b/examples/snap.js index fbd4ee4b42..ab5d16d24c 100644 --- a/examples/snap.js +++ b/examples/snap.js @@ -1,11 +1,9 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; -import {Draw, Modify, Snap} from '../src/ol/interaction.js'; +import {Draw, Modify, Select, Snap} from '../src/ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import {OSM, Vector as VectorSource} from '../src/ol/source.js'; import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js'; -import Collection from '../src/ol/Collection.js'; -import {shiftKeyOnly} from '../src/ol/events/condition.js'; const raster = new TileLayer({ source: new OSM() @@ -39,53 +37,29 @@ const map = new Map({ }) }); -const highlightStyle = new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.7)' - }), - stroke: new Stroke({ - color: 'rgb(51,153,204)', - width: 3 - }) -}); - const ExampleModify = { init: function() { - this.features = new Collection(); - - this.features.on('add', function(e) { - e.element.setStyle(highlightStyle); - }); - - this.features.on('remove', function(e) { - e.element.setStyle(undefined); - }); - - this.select = function(e) { - if (!shiftKeyOnly(e)) { - this.features.clear(); - } - map.forEachFeatureAtPixel(e.pixel, function(f) { - this.features.push(f); - }.bind(this)); - }.bind(this); + this.select = new Select(); + map.addInteraction(this.select); this.modify = new Modify({ - features: this.features + features: this.select.getFeatures() }); map.addInteraction(this.modify); this.setEvents(); }, setEvents: function() { + const selectedFeatures = this.select.getFeatures(); + + this.select.on('change:active', function() { + selectedFeatures.forEach(function(each) { + selectedFeatures.remove(each); + }); + }); }, setActive: function(active) { - if (active) { - this.features.clear(); - map.on('singleclick', this.select); - } else { - map.un('singleclick', this.select); - } + this.select.setActive(active); this.modify.setActive(active); } }; diff --git a/examples/translate-features.js b/examples/translate-features.js index f9919aae2d..fa2aa4d57a 100644 --- a/examples/translate-features.js +++ b/examples/translate-features.js @@ -1,15 +1,10 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import GeoJSON from '../src/ol/format/GeoJSON.js'; -import {defaults as defaultInteractions, Translate} from '../src/ol/interaction.js'; +import {defaults as defaultInteractions, Select, Translate} from '../src/ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import OSM from '../src/ol/source/OSM.js'; import VectorSource from '../src/ol/source/Vector.js'; -import Collection from '../src/ol/Collection.js'; -import Style from '../src/ol/style/Style.js'; -import Fill from '../src/ol/style/Fill.js'; -import Stroke from '../src/ol/style/Stroke.js'; -import {shiftKeyOnly} from '../src/ol/events/condition.js'; const raster = new TileLayer({ @@ -23,32 +18,14 @@ const vector = new VectorLayer({ }) }); -const features = new Collection(); - -const highlightStyle = new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.7)' - }), - stroke: new Stroke({ - color: 'rgb(51,153,204)', - width: 3 - }) -}); - -features.on('add', function(e) { - e.element.setStyle(highlightStyle); -}); - -features.on('remove', function(e) { - e.element.setStyle(undefined); -}); +const select = new Select(); const translate = new Translate({ - features: features + features: select.getFeatures() }); const map = new Map({ - interactions: defaultInteractions().extend([translate]), + interactions: defaultInteractions().extend([select, translate]), layers: [raster, vector], target: 'map', view: new View({ @@ -56,12 +33,3 @@ const map = new Map({ zoom: 2 }) }); - -map.on('singleclick', function(e) { - if (!shiftKeyOnly(e)) { - features.clear(); - } - map.forEachFeatureAtPixel(e.pixel, function(f) { - features.push(f); - }); -}); diff --git a/examples/vector-esri-edit.js b/examples/vector-esri-edit.js index 768aea110b..6fa31162f4 100644 --- a/examples/vector-esri-edit.js +++ b/examples/vector-esri-edit.js @@ -1,18 +1,13 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import EsriJSON from '../src/ol/format/EsriJSON.js'; -import {defaults as defaultInteractions, Draw, Modify} from '../src/ol/interaction.js'; +import {defaults as defaultInteractions, Draw, Modify, Select} from '../src/ol/interaction.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; import {tile as tileStrategy} from '../src/ol/loadingstrategy.js'; import {fromLonLat} from '../src/ol/proj.js'; import VectorSource from '../src/ol/source/Vector.js'; import XYZ from '../src/ol/source/XYZ.js'; import {createXYZ} from '../src/ol/tilegrid.js'; -import Collection from '../src/ol/Collection.js'; -import Style from '../src/ol/style/Style.js'; -import Fill from '../src/ol/style/Fill.js'; -import Stroke from '../src/ol/style/Stroke.js'; -import {shiftKeyOnly} from '../src/ol/events/condition.js'; const serviceUrl = 'https://services.arcgis.com/rOo16HdIMeOBI4Mb/arcgis/rest/' + @@ -68,32 +63,17 @@ const draw = new Draw({ type: 'Polygon' }); -const selected = new Collection(); - -const highlightStyle = new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.7)' - }), - stroke: new Stroke({ - color: 'rgb(51,153,204)', - width: 3 - }) -}); - -selected.on('add', function(e) { - e.element.setStyle(highlightStyle); -}); - -selected.on('remove', function(e) { - e.element.setStyle(undefined); -}); +const select = new Select(); +select.setActive(false); +const selected = select.getFeatures(); const modify = new Modify({ features: selected }); +modify.setActive(false); const map = new Map({ - interactions: defaultInteractions().extend([draw, modify]), + interactions: defaultInteractions().extend([draw, select, modify]), layers: [raster, vector], target: document.getElementById('map'), view: new View({ @@ -102,15 +82,6 @@ const map = new Map({ }) }); -function select(e) { - if (!shiftKeyOnly(e)) { - selected.clear(); - } - map.forEachFeatureAtPixel(e.pixel, function(f) { - selected.push(f); - }); -} - const typeSelect = document.getElementById('type'); @@ -121,11 +92,6 @@ typeSelect.onchange = function() { draw.setActive(typeSelect.value === 'DRAW'); select.setActive(typeSelect.value === 'MODIFY'); modify.setActive(typeSelect.value === 'MODIFY'); - if (typeSelect.value === 'MODIFY') { - map.on('singleclick', select); - } else { - map.un('singleclick', select); - } }; const dirty = {}; diff --git a/src/ol/interaction.js b/src/ol/interaction.js index 3f5baf7c41..ce91f62bd5 100644 --- a/src/ol/interaction.js +++ b/src/ol/interaction.js @@ -31,6 +31,7 @@ export {default as MouseWheelZoom} from './interaction/MouseWheelZoom.js'; export {default as PinchRotate} from './interaction/PinchRotate.js'; export {default as PinchZoom} from './interaction/PinchZoom.js'; export {default as Pointer} from './interaction/Pointer.js'; +export {default as Select} from './interaction/Select.js'; export {default as Snap} from './interaction/Snap.js'; export {default as Translate} from './interaction/Translate.js'; diff --git a/src/ol/interaction/Select.js b/src/ol/interaction/Select.js new file mode 100644 index 0000000000..497d6f0212 --- /dev/null +++ b/src/ol/interaction/Select.js @@ -0,0 +1,483 @@ +/** + * @module ol/interaction/Select + */ +import {getUid} from '../util.js'; +import CollectionEventType from '../CollectionEventType.js'; +import {extend, includes} from '../array.js'; +import Event from '../events/Event.js'; +import {singleClick, never, shiftKeyOnly, pointerMove} from '../events/condition.js'; +import {TRUE} from '../functions.js'; +import GeometryType from '../geom/GeometryType.js'; +import Interaction from './Interaction.js'; +import VectorLayer from '../layer/Vector.js'; +import {clear} from '../obj.js'; +import VectorSource from '../source/Vector.js'; +import {createEditingStyle} from '../style/Style.js'; + + +/** + * @enum {string} + */ +const SelectEventType = { + /** + * Triggered when feature(s) has been (de)selected. + * @event SelectEvent#select + * @api + */ + SELECT: 'select' +}; + + +/** + * A function that takes an {@link module:ol/Feature} or + * {@link module:ol/render/Feature} and an + * {@link module:ol/layer/Layer} and returns `true` if the feature may be + * selected or `false` otherwise. + * @typedef {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default):boolean} FilterFunction + */ + + +/** + * @typedef {Object} Options + * @property {import("../events/condition.js").Condition} [addCondition] A function + * that takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a + * boolean to indicate whether that event should be handled. + * By default, this is {@link module:ol/events/condition~never}. Use this if you + * want to use different events for add and remove instead of `toggle`. + * @property {import("../events/condition.js").Condition} [condition] A function that + * takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a + * boolean to indicate whether that event should be handled. This is the event + * for the selected features as a whole. By default, this is + * {@link module:ol/events/condition~singleClick}. Clicking on a feature selects that + * feature and removes any that were in the selection. Clicking outside any + * feature removes all from the selection. + * See `toggle`, `add`, `remove` options for adding/removing extra features to/ + * from the selection. + * @property {Array|function(import("../layer/Layer.js").default): boolean} [layers] + * A list of layers from which features should be selected. Alternatively, a + * filter function can be provided. The function will be called for each layer + * in the map and should return `true` for layers that you want to be + * selectable. If the option is absent, all visible layers will be considered + * selectable. + * @property {import("../style/Style.js").StyleLike} [style] + * Style for the selected features. By default the default edit style is used + * (see {@link module:ol/style}). + * @property {import("../events/condition.js").Condition} [removeCondition] A function + * that takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a + * boolean to indicate whether that event should be handled. + * By default, this is {@link module:ol/events/condition~never}. Use this if you + * want to use different events for add and remove instead of `toggle`. + * @property {import("../events/condition.js").Condition} [toggleCondition] A function + * that takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a + * boolean to indicate whether that event should be handled. This is in addition + * to the `condition` event. By default, + * {@link module:ol/events/condition~shiftKeyOnly}, i.e. pressing `shift` as + * well as the `condition` event, adds that feature to the current selection if + * it is not currently selected, and removes it if it is. See `add` and `remove` + * if you want to use different events instead of a toggle. + * @property {boolean} [multi=false] A boolean that determines if the default + * behaviour should select only single features or all (overlapping) features at + * the clicked map position. The default of `false` means single select. + * @property {import("../Collection.js").default} [features] + * Collection where the interaction will place selected features. Optional. If + * not set the interaction will create a collection. In any case the collection + * used by the interaction is returned by + * {@link module:ol/interaction/Select~Select#getFeatures}. + * @property {FilterFunction} [filter] A function + * that takes an {@link module:ol/Feature} and an + * {@link module:ol/layer/Layer} and returns `true` if the feature may be + * selected or `false` otherwise. + * @property {boolean} [wrapX=true] Wrap the world horizontally on the selection + * overlay. + * @property {number} [hitTolerance=0] Hit-detection tolerance. Pixels inside + * the radius around the given position will be checked for features. + */ + + +/** + * @classdesc + * Events emitted by {@link module:ol/interaction/Select~Select} instances are instances of + * this type. + */ +class SelectEvent extends Event { + /** + * @param {SelectEventType} type The event type. + * @param {Array} selected Selected features. + * @param {Array} deselected Deselected features. + * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Associated + * {@link module:ol/MapBrowserEvent}. + */ + constructor(type, selected, deselected, mapBrowserEvent) { + super(type); + + /** + * Selected features array. + * @type {Array} + * @api + */ + this.selected = selected; + + /** + * Deselected features array. + * @type {Array} + * @api + */ + this.deselected = deselected; + + /** + * Associated {@link module:ol/MapBrowserEvent}. + * @type {import("../MapBrowserEvent.js").default} + * @api + */ + this.mapBrowserEvent = mapBrowserEvent; + + } + +} + + +/** + * @classdesc + * Interaction for selecting vector features. By default, selected features are + * styled differently, so this interaction can be used for visual highlighting, + * as well as selecting features for other actions, such as modification or + * output. There are three ways of controlling which features are selected: + * using the browser event as defined by the `condition` and optionally the + * `toggle`, `add`/`remove`, and `multi` options; a `layers` filter; and a + * further feature filter using the `filter` option. + * + * Selected features are added to an internal unmanaged layer. + * + * @fires SelectEvent + * @api + */ +class Select extends Interaction { + /** + * @param {Options=} opt_options Options. + */ + constructor(opt_options) { + + super({ + handleEvent: handleEvent + }); + + const options = opt_options ? opt_options : {}; + + /** + * @private + * @type {import("../events/condition.js").Condition} + */ + this.condition_ = options.condition ? options.condition : singleClick; + + /** + * @private + * @type {import("../events/condition.js").Condition} + */ + this.addCondition_ = options.addCondition ? options.addCondition : never; + + /** + * @private + * @type {import("../events/condition.js").Condition} + */ + this.removeCondition_ = options.removeCondition ? options.removeCondition : never; + + /** + * @private + * @type {import("../events/condition.js").Condition} + */ + this.toggleCondition_ = options.toggleCondition ? options.toggleCondition : shiftKeyOnly; + + /** + * @private + * @type {boolean} + */ + this.multi_ = options.multi ? options.multi : false; + + /** + * @private + * @type {FilterFunction} + */ + this.filter_ = options.filter ? options.filter : TRUE; + + /** + * @private + * @type {number} + */ + this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0; + + const featureOverlay = new VectorLayer({ + source: new VectorSource({ + useSpatialIndex: false, + features: options.features, + wrapX: options.wrapX + }), + style: options.style ? options.style : + getDefaultStyleFunction(), + updateWhileAnimating: true, + updateWhileInteracting: true + }); + + /** + * @private + * @type {VectorLayer} + */ + this.featureOverlay_ = featureOverlay; + + /** @type {function(import("../layer/Layer.js").default): boolean} */ + let layerFilter; + if (options.layers) { + if (typeof options.layers === 'function') { + layerFilter = options.layers; + } else { + const layers = options.layers; + layerFilter = function(layer) { + return includes(layers, layer); + }; + } + } else { + layerFilter = TRUE; + } + + /** + * @private + * @type {function(import("../layer/Layer.js").default): boolean} + */ + this.layerFilter_ = layerFilter; + + /** + * An association between selected feature (key) + * and layer (value) + * @private + * @type {Object} + */ + this.featureLayerAssociation_ = {}; + + const features = this.getFeatures(); + features.addEventListener(CollectionEventType.ADD, this.addFeature_.bind(this)); + features.addEventListener(CollectionEventType.REMOVE, this.removeFeature_.bind(this)); + } + + /** + * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../layer/Layer.js").default} layer Layer. + * @private + */ + addFeatureLayerAssociation_(feature, layer) { + this.featureLayerAssociation_[getUid(feature)] = layer; + } + + /** + * Get the selected features. + * @return {import("../Collection.js").default} Features collection. + * @api + */ + getFeatures() { + return this.featureOverlay_.getSource().getFeaturesCollection(); + } + + /** + * Returns the Hit-detection tolerance. + * @returns {number} Hit tolerance in pixels. + * @api + */ + getHitTolerance() { + return this.hitTolerance_; + } + + /** + * Returns the associated {@link module:ol/layer/Vector~Vector vectorlayer} of + * the (last) selected feature. Note that this will not work with any + * programmatic method like pushing features to + * {@link module:ol/interaction/Select~Select#getFeatures collection}. + * @param {import("../Feature.js").FeatureLike} feature Feature + * @return {VectorLayer} Layer. + * @api + */ + getLayer(feature) { + return ( + /** @type {VectorLayer} */ (this.featureLayerAssociation_[getUid(feature)]) + ); + } + + /** + * Get the overlay layer that this interaction renders selected features to. + * @return {VectorLayer} Overlay layer. + * @api + */ + getOverlay() { + return this.featureOverlay_; + } + + /** + * Hit-detection tolerance. Pixels inside the radius around the given position + * will be checked for features. + * @param {number} hitTolerance Hit tolerance in pixels. + * @api + */ + setHitTolerance(hitTolerance) { + this.hitTolerance_ = hitTolerance; + } + + /** + * Remove the interaction from its current map, if any, and attach it to a new + * map, if any. Pass `null` to just remove the interaction from the current map. + * @param {import("../PluggableMap.js").default} map Map. + * @override + * @api + */ + setMap(map) { + const currentMap = this.getMap(); + const selectedFeatures = this.getFeatures(); + if (currentMap) { + selectedFeatures.forEach(currentMap.unskipFeature.bind(currentMap)); + } + super.setMap(map); + this.featureOverlay_.setMap(map); + if (map) { + selectedFeatures.forEach(map.skipFeature.bind(map)); + } + } + + /** + * @param {import("../Collection.js").CollectionEvent} evt Event. + * @private + */ + addFeature_(evt) { + const map = this.getMap(); + if (map) { + map.skipFeature(/** @type {import("../Feature.js").default} */ (evt.element)); + } + } + + /** + * @param {import("../Collection.js").CollectionEvent} evt Event. + * @private + */ + removeFeature_(evt) { + const map = this.getMap(); + if (map) { + map.unskipFeature(/** @type {import("../Feature.js").default} */ (evt.element)); + } + } + + /** + * @param {import("../Feature.js").FeatureLike} feature Feature. + * @private + */ + removeFeatureLayerAssociation_(feature) { + delete this.featureLayerAssociation_[getUid(feature)]; + } +} + + +/** + * Handles the {@link module:ol/MapBrowserEvent map browser event} and may change the + * selected state of features. + * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event. + * @return {boolean} `false` to stop event propagation. + * @this {Select} + */ +function handleEvent(mapBrowserEvent) { + if (!this.condition_(mapBrowserEvent)) { + return true; + } + const add = this.addCondition_(mapBrowserEvent); + const remove = this.removeCondition_(mapBrowserEvent); + const toggle = this.toggleCondition_(mapBrowserEvent); + const set = !add && !remove && !toggle; + const map = mapBrowserEvent.map; + const features = this.getFeatures(); + const deselected = []; + const selected = []; + if (set) { + // Replace the currently selected feature(s) with the feature(s) at the + // pixel, or clear the selected feature(s) if there is no feature at + // the pixel. + clear(this.featureLayerAssociation_); + map.forEachFeatureAtPixel(mapBrowserEvent.pixel, + ( + /** + * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../layer/Layer.js").default} layer Layer. + * @return {boolean|undefined} Continue to iterate over the features. + */ + function(feature, layer) { + if (this.filter_(feature, layer)) { + selected.push(feature); + this.addFeatureLayerAssociation_(feature, layer); + return !this.multi_; + } + }).bind(this), { + layerFilter: this.layerFilter_, + hitTolerance: this.hitTolerance_ + }); + for (let i = features.getLength() - 1; i >= 0; --i) { + const feature = features.item(i); + const index = selected.indexOf(feature); + if (index > -1) { + // feature is already selected + selected.splice(index, 1); + } else { + features.remove(feature); + deselected.push(feature); + } + } + if (selected.length !== 0) { + features.extend(selected); + } + } else { + // Modify the currently selected feature(s). + map.forEachFeatureAtPixel(mapBrowserEvent.pixel, + ( + /** + * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../layer/Layer.js").default} layer Layer. + * @return {boolean|undefined} Continue to iterate over the features. + */ + function(feature, layer) { + if (this.filter_(feature, layer)) { + if ((add || toggle) && !includes(features.getArray(), feature)) { + selected.push(feature); + this.addFeatureLayerAssociation_(feature, layer); + } else if ((remove || toggle) && includes(features.getArray(), feature)) { + deselected.push(feature); + this.removeFeatureLayerAssociation_(feature); + } + return !this.multi_; + } + }).bind(this), { + layerFilter: this.layerFilter_, + hitTolerance: this.hitTolerance_ + }); + for (let j = deselected.length - 1; j >= 0; --j) { + features.remove(deselected[j]); + } + features.extend(selected); + } + if (selected.length > 0 || deselected.length > 0) { + this.dispatchEvent( + new SelectEvent(SelectEventType.SELECT, + selected, deselected, mapBrowserEvent)); + } + return pointerMove(mapBrowserEvent); +} + + +/** + * @return {import("../style/Style.js").StyleFunction} Styles. + */ +function getDefaultStyleFunction() { + const styles = createEditingStyle(); + extend(styles[GeometryType.POLYGON], styles[GeometryType.LINE_STRING]); + extend(styles[GeometryType.GEOMETRY_COLLECTION], styles[GeometryType.LINE_STRING]); + + return function(feature, resolution) { + if (!feature.getGeometry()) { + return null; + } + return styles[feature.getGeometry().getType()]; + }; +} + + +export default Select; diff --git a/test/spec/ol/interaction/select.test.js b/test/spec/ol/interaction/select.test.js new file mode 100644 index 0000000000..9dc9e562e1 --- /dev/null +++ b/test/spec/ol/interaction/select.test.js @@ -0,0 +1,451 @@ +import Collection from '../../../../src/ol/Collection.js'; +import Feature from '../../../../src/ol/Feature.js'; +import Map from '../../../../src/ol/Map.js'; +import MapBrowserEventType from '../../../../src/ol/MapBrowserEventType.js'; +import MapBrowserPointerEvent from '../../../../src/ol/MapBrowserPointerEvent.js'; +import View from '../../../../src/ol/View.js'; +import Polygon from '../../../../src/ol/geom/Polygon.js'; +import Interaction from '../../../../src/ol/interaction/Interaction.js'; +import Select from '../../../../src/ol/interaction/Select.js'; +import VectorLayer from '../../../../src/ol/layer/Vector.js'; +import VectorSource from '../../../../src/ol/source/Vector.js'; + + +describe('ol.interaction.Select', function() { + let target, map, layer, source; + + const width = 360; + const height = 180; + + beforeEach(function(done) { + target = document.createElement('div'); + + const style = target.style; + style.position = 'absolute'; + style.left = '-1000px'; + style.top = '-1000px'; + style.width = width + 'px'; + style.height = height + 'px'; + document.body.appendChild(target); + + const geometry = new Polygon([[[0, 0], [0, 40], [40, 40], [40, 0]]]); + + // Four overlapping features, two features of type "foo" and two features + // of type "bar". The rendering order is, from top to bottom, foo -> bar + // -> foo -> bar. + const features = []; + features.push( + new Feature({ + geometry: geometry, + type: 'bar' + }), + new Feature({ + geometry: geometry, + type: 'foo' + }), + new Feature({ + geometry: geometry, + type: 'bar' + }), + new Feature({ + geometry: geometry, + type: 'foo' + })); + + source = new VectorSource({ + features: features + }); + + layer = new VectorLayer({source: source}); + + map = new Map({ + target: target, + layers: [layer], + view: new View({ + projection: 'EPSG:4326', + center: [0, 0], + resolution: 1 + }) + }); + + map.once('postrender', function() { + done(); + }); + }); + + afterEach(function() { + map.dispose(); + document.body.removeChild(target); + }); + + /** + * Simulates a browser event on the map viewport. The client x/y location + * will be adjusted as if the map were centered at 0,0. + * @param {string} type Event type. + * @param {number} x Horizontal offset from map center. + * @param {number} y Vertical offset from map center. + * @param {boolean=} opt_shiftKey Shift key is pressed. + */ + function simulateEvent(type, x, y, opt_shiftKey) { + const viewport = map.getViewport(); + // calculated in case body has top < 0 (test runner with small window) + const position = viewport.getBoundingClientRect(); + const shiftKey = opt_shiftKey !== undefined ? opt_shiftKey : false; + const event = new PointerEvent(type, { + clientX: position.left + x + width / 2, + clientY: position.top + y + height / 2, + shiftKey: shiftKey + }); + map.handleMapBrowserEvent(new MapBrowserPointerEvent(type, map, event)); + } + + describe('constructor', function() { + + it('creates a new interaction', function() { + const select = new Select(); + expect(select).to.be.a(Select); + expect(select).to.be.a(Interaction); + }); + + describe('user-provided collection', function() { + + it('uses the user-provided collection', function() { + const features = new Collection(); + const select = new Select({features: features}); + expect(select.getFeatures()).to.be(features); + }); + + }); + + }); + + describe('selecting a polygon', function() { + let select; + + beforeEach(function() { + select = new Select(); + map.addInteraction(select); + }); + + it('select with single-click', function() { + const listenerSpy = sinon.spy(function(e) { + expect(e.selected).to.have.length(1); + }); + select.on('select', listenerSpy); + + simulateEvent('singleclick', 10, -20); + + expect(listenerSpy.callCount).to.be(1); + + const features = select.getFeatures(); + expect(features.getLength()).to.equal(1); + }); + + it('single-click outside the geometry', function() { + const listenerSpy = sinon.spy(function(e) { + expect(e.selected).to.have.length(1); + }); + select.on('select', listenerSpy); + + simulateEvent(MapBrowserEventType.SINGLECLICK, -10, -10); + + expect(listenerSpy.callCount).to.be(0); + + const features = select.getFeatures(); + expect(features.getLength()).to.equal(0); + }); + + it('select twice with single-click', function() { + const listenerSpy = sinon.spy(function(e) { + expect(e.selected).to.have.length(1); + }); + select.on('select', listenerSpy); + + simulateEvent(MapBrowserEventType.SINGLECLICK, 10, -20); + simulateEvent(MapBrowserEventType.SINGLECLICK, 9, -21); + + expect(listenerSpy.callCount).to.be(1); + + const features = select.getFeatures(); + expect(features.getLength()).to.equal(1); + }); + + it('select with shift single-click', function() { + const listenerSpy = sinon.spy(function(e) { + expect(e.selected).to.have.length(1); + }); + select.on('select', listenerSpy); + + simulateEvent('singleclick', 10, -20, true); + + expect(listenerSpy.callCount).to.be(1); + + const features = select.getFeatures(); + expect(features.getLength()).to.equal(1); + }); + }); + + describe('multiselecting polygons', function() { + let select; + + beforeEach(function() { + select = new Select({ + multi: true + }); + map.addInteraction(select); + }); + + it('select with single-click', function() { + const listenerSpy = sinon.spy(function(e) { + expect(e.selected).to.have.length(4); + }); + select.on('select', listenerSpy); + + simulateEvent('singleclick', 10, -20); + + expect(listenerSpy.callCount).to.be(1); + + const features = select.getFeatures(); + expect(features.getLength()).to.equal(4); + }); + + it('select with shift single-click', function() { + const listenerSpy = sinon.spy(function(e) { + expect(e.selected).to.have.length(4); + }); + select.on('select', listenerSpy); + + simulateEvent('singleclick', 10, -20, true); + + expect(listenerSpy.callCount).to.be(1); + + let features = select.getFeatures(); + expect(features.getLength()).to.equal(4); + expect(select.getLayer(features.item(0))).to.equal(layer); + + // Select again to make sure the internal layer isn't reported + simulateEvent('singleclick', 10, -20); + + expect(listenerSpy.callCount).to.be(1); + + features = select.getFeatures(); + expect(features.getLength()).to.equal(4); + expect(select.getLayer(features.item(0))).to.equal(layer); + }); + }); + + describe('toggle selecting polygons', function() { + let select; + + beforeEach(function() { + select = new Select({ + multi: true + }); + map.addInteraction(select); + }); + + it('with SHIFT + single-click', function() { + const listenerSpy = sinon.spy(); + select.on('select', listenerSpy); + + simulateEvent('singleclick', 10, -20, true); + + expect(listenerSpy.callCount).to.be(1); + + let features = select.getFeatures(); + expect(features.getLength()).to.equal(4); + + map.renderSync(); + + simulateEvent('singleclick', 10, -20, true); + + expect(listenerSpy.callCount).to.be(2); + + features = select.getFeatures(); + expect(features.getLength()).to.equal(0); + }); + }); + + describe('filter features using the filter option', function() { + + describe('with multi set to true', function() { + + it('only selects features that pass the filter', function() { + const select = new Select({ + multi: true, + filter: function(feature, layer) { + return feature.get('type') === 'bar'; + } + }); + map.addInteraction(select); + + simulateEvent('singleclick', 10, -20); + const features = select.getFeatures(); + expect(features.getLength()).to.equal(2); + expect(features.item(0).get('type')).to.be('bar'); + expect(features.item(1).get('type')).to.be('bar'); + }); + + it('only selects features that pass the filter ' + + 'using shift single-click', function() { + const select = new Select({ + multi: true, + filter: function(feature, layer) { + return feature.get('type') === 'bar'; + } + }); + map.addInteraction(select); + + simulateEvent('singleclick', 10, -20, + true); + const features = select.getFeatures(); + expect(features.getLength()).to.equal(2); + expect(features.item(0).get('type')).to.be('bar'); + expect(features.item(1).get('type')).to.be('bar'); + }); + }); + + describe('with multi set to false', function() { + + it('only selects the first feature that passes the filter', function() { + const select = new Select({ + multi: false, + filter: function(feature, layer) { + return feature.get('type') === 'bar'; + } + }); + map.addInteraction(select); + simulateEvent('singleclick', 10, -20); + const features = select.getFeatures(); + expect(features.getLength()).to.equal(1); + expect(features.item(0).get('type')).to.be('bar'); + }); + + it('only selects the first feature that passes the filter ' + + 'using shift single-click', function() { + const select = new Select({ + multi: false, + filter: function(feature, layer) { + return feature.get('type') === 'bar'; + } + }); + map.addInteraction(select); + simulateEvent('singleclick', 10, -20, + true); + const features = select.getFeatures(); + expect(features.getLength()).to.equal(1); + expect(features.item(0).get('type')).to.be('bar'); + }); + }); + }); + + describe('#getLayer(feature)', function() { + let interaction; + + beforeEach(function() { + interaction = new Select(); + map.addInteraction(interaction); + }); + afterEach(function() { + map.removeInteraction(interaction); + }); + + it('returns a layer from a selected feature', function() { + const listenerSpy = sinon.spy(function(e) { + const feature = e.selected[0]; + const layer_ = interaction.getLayer(feature); + expect(e.selected).to.have.length(1); + expect(feature).to.be.a(Feature); + expect(layer_).to.be.a(VectorLayer); + expect(layer_).to.equal(layer); + }); + interaction.on('select', listenerSpy); + + simulateEvent('singleclick', 10, -20); + // Select again to make sure that the internal layer doesn't get reported. + simulateEvent('singleclick', 10, -20); + }); + }); + + describe('#setActive()', function() { + let interaction; + + beforeEach(function() { + interaction = new Select(); + + expect(interaction.getActive()).to.be(true); + + map.addInteraction(interaction); + + expect(interaction.featureOverlay_).not.to.be(null); + + simulateEvent('singleclick', 10, -20); + }); + + afterEach(function() { + map.removeInteraction(interaction); + }); + + describe('#setActive(false)', function() { + it('keeps the the selection', function() { + interaction.setActive(false); + expect(interaction.getFeatures().getLength()).to.equal(1); + }); + }); + + describe('#setActive(true)', function() { + beforeEach(function() { + interaction.setActive(false); + }); + it('fires change:active', function() { + const listenerSpy = sinon.spy(); + interaction.on('change:active', listenerSpy); + interaction.setActive(true); + expect(listenerSpy.callCount).to.be(1); + }); + }); + + }); + + describe('#setMap()', function() { + let interaction; + + beforeEach(function() { + interaction = new Select(); + expect(interaction.getActive()).to.be(true); + }); + + describe('#setMap(null)', function() { + beforeEach(function() { + map.addInteraction(interaction); + }); + afterEach(function() { + map.removeInteraction(interaction); + }); + describe('#setMap(null) when interaction is active', function() { + it('unsets the map from the feature overlay', function() { + const spy = sinon.spy(interaction.featureOverlay_, 'setMap'); + interaction.setMap(null); + expect(spy.getCall(0).args[0]).to.be(null); + }); + }); + }); + + describe('#setMap(map)', function() { + describe('#setMap(map) when interaction is active', function() { + it('sets the map into the feature overlay', function() { + const spy = sinon.spy(interaction.featureOverlay_, 'setMap'); + interaction.setMap(map); + expect(spy.getCall(0).args[0]).to.be(map); + }); + }); + }); + }); + + describe('#getOverlay', function() { + it('returns the feature overlay layer', function() { + const select = new Select(); + expect (select.getOverlay()).to.eql(select.featureOverlay_); + }); + }); +});