From 391be53e1313070cd576f6f757fd641ea9116f9a Mon Sep 17 00:00:00 2001 From: peterqliu Date: Thu, 10 Jan 2019 13:44:09 -0800 Subject: [PATCH] removeFeatureState --- bench/benchmarks/remove_paint_state.js | 117 ++++++++ bench/versions/benchmarks.js | 2 + debug/highlightpoints.html | 23 +- src/source/source_cache.js | 9 + src/source/source_state.js | 118 +++++++- src/style/style.js | 36 +++ src/ui/map.js | 21 +- .../default/expected.json | 92 ++++++ .../remove-feature-state/default/style.json | 112 ++++++++ .../composite-expression/expected.png | Bin 0 -> 221 bytes .../composite-expression/style.json | 78 +++++ .../data-expression/expected.png | Bin 0 -> 221 bytes .../data-expression/style.json | 70 +++++ .../vector-source/expected.png | Bin 0 -> 39701 bytes .../vector-source/style.json | 76 +++++ test/unit/ui/map.test.js | 272 ++++++++++++++++++ 16 files changed, 998 insertions(+), 28 deletions(-) create mode 100644 bench/benchmarks/remove_paint_state.js create mode 100644 test/integration/query-tests/remove-feature-state/default/expected.json create mode 100644 test/integration/query-tests/remove-feature-state/default/style.json create mode 100644 test/integration/render-tests/remove-feature-state/composite-expression/expected.png create mode 100644 test/integration/render-tests/remove-feature-state/composite-expression/style.json create mode 100644 test/integration/render-tests/remove-feature-state/data-expression/expected.png create mode 100644 test/integration/render-tests/remove-feature-state/data-expression/style.json create mode 100644 test/integration/render-tests/remove-feature-state/vector-source/expected.png create mode 100644 test/integration/render-tests/remove-feature-state/vector-source/style.json diff --git a/bench/benchmarks/remove_paint_state.js b/bench/benchmarks/remove_paint_state.js new file mode 100644 index 00000000000..d4a04394f1c --- /dev/null +++ b/bench/benchmarks/remove_paint_state.js @@ -0,0 +1,117 @@ + +import style from '../data/empty.json'; +import Benchmark from '../lib/benchmark'; +import createMap from '../lib/create_map'; + +function generateLayers(layer) { + const generated = []; + for (let i = 0; i < 50; i++) { + const id = layer.id + i; + generated.push(Object.assign({}, layer, {id})); + } + return generated; +} + +const width = 1024; +const height = 768; +const zoom = 4; + +class RemovePaintState extends Benchmark { + constructor(center) { + super(); + this.center = center; + } + + setup() { + return fetch('/bench/data/naturalearth-land.json') + .then(response => response.json()) + .then(data => { + this.numFeatures = data.features.length; + return Object.assign({}, style, { + sources: {'land': {'type': 'geojson', data, 'maxzoom': 23}}, + layers: generateLayers({ + 'id': 'layer', + 'type': 'fill', + 'source': 'land', + 'paint': { + 'fill-color': [ + 'case', + ['boolean', ['feature-state', 'bench'], false], + ['rgb', 21, 210, 210], + ['rgb', 233, 233, 233] + ] + } + }) + }); + }) + .then((style) => { + return createMap({ + zoom, + width, + height, + center: this.center, + style + }).then(map => { + this.map = map; + }); + }); + } + + bench() { + this.map._styleDirty = true; + this.map._sourcesDirty = true; + this.map._render(); + } + + teardown() { + this.map.remove(); + } +} + +class propertyLevelRemove extends RemovePaintState { + bench() { + + for (let i = 0; i < this.numFeatures; i += 50) { + this.map.setFeatureState({ source: 'land', id: i }, { bench: true }); + } + for (let i = 0; i < this.numFeatures; i += 50) { + this.map.removeFeatureState({ source: 'land', id: i }, 'bench'); + } + this.map._render(); + + } +} + +class featureLevelRemove extends RemovePaintState { + bench() { + + for (let i = 0; i < this.numFeatures; i += 50) { + this.map.setFeatureState({ source: 'land', id: i }, { bench: true }); + } + for (let i = 0; i < this.numFeatures; i += 50) { + this.map.removeFeatureState({ source: 'land', id: i }); + } + this.map._render(); + + } +} + +class sourceLevelRemove extends RemovePaintState { + bench() { + + for (let i = 0; i < this.numFeatures; i += 50) { + this.map.setFeatureState({ source: 'land', id: i }, { bench: true }); + } + for (let i = 0; i < this.numFeatures; i += 50) { + this.map.removeFeatureState({ source: 'land', id: i }); + } + this.map._render(); + + } +} + +export default [ + propertyLevelRemove, + featureLevelRemove, + sourceLevelRemove +]; diff --git a/bench/versions/benchmarks.js b/bench/versions/benchmarks.js index 87e160e09ab..5729ee2eae3 100644 --- a/bench/versions/benchmarks.js +++ b/bench/versions/benchmarks.js @@ -22,6 +22,7 @@ import SymbolLayout from '../benchmarks/symbol_layout'; import WorkerTransfer from '../benchmarks/worker_transfer'; import Paint from '../benchmarks/paint'; import PaintStates from '../benchmarks/paint_states'; +import RemovePaintState from '../benchmarks/remove_paint_state'; import LayerBenchmarks from '../benchmarks/layers'; import Load from '../benchmarks/map_load'; import Validate from '../benchmarks/style_validate'; @@ -46,6 +47,7 @@ register(new StyleLayerCreate(style)); ExpressionBenchmarks.forEach((Bench) => register(new Bench(style))); register(new WorkerTransfer(style)); register(new PaintStates(center)); +register(new RemovePaintState(center)); LayerBenchmarks.forEach((Bench) => register(new Bench())); register(new Load()); register(new LayoutDDS()); diff --git a/debug/highlightpoints.html b/debug/highlightpoints.html index 6a1aee227e3..94dc7aab4c0 100644 --- a/debug/highlightpoints.html +++ b/debug/highlightpoints.html @@ -38,8 +38,8 @@ paint: { "circle-radius": ["case", ["boolean", ["feature-state", "hover"], false], - ["number", ["*", ["get", "scalerank"], 2]], - ["number", ["*", ["get", "scalerank"], 1.5]] + ["number", 20], + ["number", 10] ], "circle-color": ["case", ["boolean", ["feature-state", "hover"], false], @@ -49,16 +49,15 @@ } }); let hoveredFeature; - map.on('mousemove', 'places', function(e) { - if (e.features.length) { - const f = e.features[0]; - if (!f.state.hover) { - map.setFeatureState(f, {'hover': true}); - if (hoveredFeature) { - map.setFeatureState(hoveredFeature, {'hover': false}); - } - hoveredFeature = f; - } + map.on('mousemove', function(e) { + var f = map.queryRenderedFeatures(e.point, {layers:['places']})[0]; + if (f) { + map.setFeatureState(f, {'hover': true}); + if (hoveredFeature && f.id !== hoveredFeature.id) map.removeFeatureState(hoveredFeature); + hoveredFeature = f; + } else if (hoveredFeature) { + map.removeFeatureState(hoveredFeature); + hoveredFeature = null; } }); }); diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 0255f204781..76a5cb2660d 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -830,6 +830,15 @@ class SourceCache extends Evented { this._state.updateState(sourceLayer, feature, state); } + /** + * Resets the value of a particular state key for a feature + * @private + */ + removeFeatureState(sourceLayer?: string, feature?: number, key?: string) { + sourceLayer = sourceLayer || '_geojsonTileLayer'; + this._state.removeFeatureState(sourceLayer, feature, key); + } + /** * Get the entire state object for a feature * @private diff --git a/src/source/source_state.js b/src/source/source_state.js index 68ba65e277c..2881432c5a7 100644 --- a/src/source/source_state.js +++ b/src/source/source_state.js @@ -8,32 +8,95 @@ export type FeatureStates = {[feature_id: string]: FeatureState}; export type LayerFeatureStates = {[layer: string]: FeatureStates}; /** - * SourceFeatureState manages the state and state changes + * SourceFeatureState manages the state and pending changes * to features in a source, separated by source layer. - * + * stateChanges and deletedStates batch all changes to the tile (updates and removes, respectively) + * between coalesce() events. addFeatureState() and removeFeatureState() also update their counterpart's + * list of changes, such that coalesce() can apply the proper state changes while agnostic to the order of operations. + * In deletedStates, all null's denote complete removal of state at that scope * @private */ class SourceFeatureState { state: LayerFeatureStates; stateChanges: LayerFeatureStates; + deletedStates: {}; constructor() { this.state = {}; this.stateChanges = {}; + this.deletedStates = {}; } - updateState(sourceLayer: string, featureId: number, state: Object) { + updateState(sourceLayer: string, featureId: number, newState: Object) { const feature = String(featureId); this.stateChanges[sourceLayer] = this.stateChanges[sourceLayer] || {}; this.stateChanges[sourceLayer][feature] = this.stateChanges[sourceLayer][feature] || {}; - extend(this.stateChanges[sourceLayer][feature], state); + extend(this.stateChanges[sourceLayer][feature], newState); + + if (this.deletedStates[sourceLayer] === null) { + this.deletedStates[sourceLayer] = {}; + for (const ft in this.state[sourceLayer]) { + if (ft !== feature) this.deletedStates[sourceLayer][ft] = null; + } + } else { + const featureDeletionQueued = this.deletedStates[sourceLayer] && this.deletedStates[sourceLayer][feature] === null; + if (featureDeletionQueued) { + this.deletedStates[sourceLayer][feature] = {}; + for (const prop in this.state[sourceLayer][feature]) { + if (!newState[prop]) this.deletedStates[sourceLayer][feature][prop] = null; + } + } else { + for (const key in newState) { + const deletionInQueue = this.deletedStates[sourceLayer] && this.deletedStates[sourceLayer][feature] && this.deletedStates[sourceLayer][feature][key] === null; + if (deletionInQueue) delete this.deletedStates[sourceLayer][feature][key]; + } + } + } + } + + removeFeatureState(sourceLayer: string, featureId?: number, key?: string) { + const sourceLayerDeleted = this.deletedStates[sourceLayer] === null; + if (sourceLayerDeleted) return; + + const feature = String(featureId); + + this.deletedStates[sourceLayer] = this.deletedStates[sourceLayer] || {}; + + if (key && featureId) { + if (this.deletedStates[sourceLayer][feature] !== null) { + this.deletedStates[sourceLayer][feature] = this.deletedStates[sourceLayer][feature] || {}; + this.deletedStates[sourceLayer][feature][key] = null; + } + } else if (featureId) { + const updateInQueue = this.stateChanges[sourceLayer] && this.stateChanges[sourceLayer][feature]; + if (updateInQueue) { + this.deletedStates[sourceLayer][feature] = {}; + for (key in this.stateChanges[sourceLayer][feature]) this.deletedStates[sourceLayer][feature][key] = null; + + } else { + this.deletedStates[sourceLayer][feature] = null; + } + } else { + this.deletedStates[sourceLayer] = null; + } + } getState(sourceLayer: string, featureId: number) { const feature = String(featureId); const base = this.state[sourceLayer] || {}; const changes = this.stateChanges[sourceLayer] || {}; - return extend({}, base[feature], changes[feature]); + + const reconciledState = extend({}, base[feature], changes[feature]); + + //return empty object if the whole source layer is awaiting deletion + if (this.deletedStates[sourceLayer] === null) return {}; + else if (this.deletedStates[sourceLayer]) { + const featureDeletions = this.deletedStates[sourceLayer][featureId]; + if (featureDeletions === null) return {}; + for (const prop in featureDeletions) delete reconciledState[prop]; + } + return reconciledState; } initializeTileState(tile: Tile, painter: any) { @@ -41,25 +104,52 @@ class SourceFeatureState { } coalesceChanges(tiles: {[any]: Tile}, painter: any) { - const changes: LayerFeatureStates = {}; + //track changes with full state objects, but only for features that got modified + const featuresChanged: LayerFeatureStates = {}; + for (const sourceLayer in this.stateChanges) { this.state[sourceLayer] = this.state[sourceLayer] || {}; const layerStates = {}; - for (const id in this.stateChanges[sourceLayer]) { - if (!this.state[sourceLayer][id]) { - this.state[sourceLayer][id] = {}; + for (const feature in this.stateChanges[sourceLayer]) { + if (!this.state[sourceLayer][feature]) this.state[sourceLayer][feature] = {}; + extend(this.state[sourceLayer][feature], this.stateChanges[sourceLayer][feature]); + layerStates[feature] = this.state[sourceLayer][feature]; + } + featuresChanged[sourceLayer] = layerStates; + } + + for (const sourceLayer in this.deletedStates) { + this.state[sourceLayer] = this.state[sourceLayer] || {}; + const layerStates = {}; + + if (this.deletedStates[sourceLayer] === null) { + for (const ft in this.state[sourceLayer]) layerStates[ft] = {}; + this.state[sourceLayer] = {}; + } else { + for (const feature in this.deletedStates[sourceLayer]) { + const deleteWholeFeatureState = this.deletedStates[sourceLayer][feature] === null; + if (deleteWholeFeatureState) this.state[sourceLayer][feature] = {}; + else { + for (const key of Object.keys(this.deletedStates[sourceLayer][feature])) { + delete this.state[sourceLayer][feature][key]; + } + } + layerStates[feature] = this.state[sourceLayer][feature]; } - extend(this.state[sourceLayer][id], this.stateChanges[sourceLayer][id]); - layerStates[id] = this.state[sourceLayer][id]; } - changes[sourceLayer] = layerStates; + + featuresChanged[sourceLayer] = featuresChanged[sourceLayer] || {}; + extend(featuresChanged[sourceLayer], layerStates); } + this.stateChanges = {}; - if (Object.keys(changes).length === 0) return; + this.deletedStates = {}; + + if (Object.keys(featuresChanged).length === 0) return; for (const id in tiles) { const tile = tiles[id]; - tile.setFeatureState(changes, painter); + tile.setFeatureState(featuresChanged, painter); } } } diff --git a/src/style/style.js b/src/style/style.js index 8dbfaf7a78b..a9c07ce169f 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -837,6 +837,10 @@ class Style extends Evented { return; } const sourceType = sourceCache.getSource().type; + if (sourceType === 'geojson' && sourceLayer) { + this.fire(new ErrorEvent(new Error(`GeoJSON sources cannot have a sourceLayer parameter.`))); + return; + } if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); return; @@ -849,6 +853,38 @@ class Style extends Evented { sourceCache.setFeatureState(sourceLayer, featureId, state); } + removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { + this._checkLoaded(); + const sourceId = target.source; + const sourceCache = this.sourceCaches[sourceId]; + + if (sourceCache === undefined) { + this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); + return; + } + + const sourceType = sourceCache.getSource().type; + const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; + const featureId = parseInt(target.id, 10); + + if (sourceType === 'vector' && !sourceLayer) { + this.fire(new ErrorEvent(new Error(`The sourceLayer parameter must be provided for vector source types.`))); + return; + } + + if (target.id && isNaN(featureId) || featureId < 0) { + this.fire(new ErrorEvent(new Error(`The feature id parameter must be non-negative.`))); + return; + } + + if (key && !target.id) { + this.fire(new ErrorEvent(new Error(`A feature id is requred to remove its specific state property.`))); + return; + } + + sourceCache.removeFeatureState(sourceLayer, featureId, key); + } + getFeatureState(feature: { source: string; sourceLayer?: string; id: string | number; }) { this._checkLoaded(); const sourceId = feature.source; diff --git a/src/ui/map.js b/src/ui/map.js index 501bd28c8a6..dc77d872cbe 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -1409,6 +1409,25 @@ class Map extends Camera { return this._update(); } + /** + * Removes feature state, setting it back to the default behavior. If only + * source is specified, removes all states of that source. If + * target.id is also specified, removes all keys for that feature's state. + * If key is also specified, removes that key from that feature's state. + * + * @param {Object} target Identifier of where to set state: can be a source, a feature, or a specific key of feature. + * Feature objects returned from {@link Map#queryRenderedFeatures} or event handlers can be used as feature identifiers. + * @param {string | number} target.id (optional) Unique id of the feature. Optional if key is not specified. + * @param {string} target.source The Id of the vector source or GeoJSON source for the feature. + * @param {string} [target.sourceLayer] (optional) *For vector tile sources, the sourceLayer is + * required.* + * @param {string} key (optional) The key in the feature state to reset. + */ + removeFeatureState(target: { source: string; sourceLayer?: string; id?: string | number; }, key?: string) { + this.style.removeFeatureState(target, key); + return this._update(); + } + /** * Gets the state of a feature. * @@ -1701,7 +1720,6 @@ class Map extends Camera { } else if (!this.isMoving() && this.loaded()) { this.fire(new Event('idle')); } - return this; } @@ -1840,7 +1858,6 @@ class Map extends Camera { this.triggerRepaint(); } } - // show vertices get vertices(): boolean { return !!this._vertices; } set vertices(value: boolean) { this._vertices = value; this._update(); } diff --git a/test/integration/query-tests/remove-feature-state/default/expected.json b/test/integration/query-tests/remove-feature-state/default/expected.json new file mode 100644 index 00000000000..51de80fe451 --- /dev/null +++ b/test/integration/query-tests/remove-feature-state/default/expected.json @@ -0,0 +1,92 @@ +[ + { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 13.406662344932556, + 52.49845542419487 + ], + [ + 13.406715989112854, + 52.49853706825692 + ], + [ + 13.407037854194641, + 52.499007335102704 + ], + [ + 13.40782642364502, + 52.50002296369735 + ], + [ + 13.409215807914734, + 52.50175045812034 + ] + ] + }, + "type": "Feature", + "properties": { + "class": "main", + "oneway": 0, + "osm_id": 4612696, + "type": "secondary" + }, + "id": 4612696, + "source": "mapbox", + "sourceLayer": "road", + "state": {} + }, + { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 13.404956459999084, + 52.50075446300568 + ], + [ + 13.405857682228088, + 52.500525870779285 + ], + [ + 13.40782642364502, + 52.50002296369735 + ], + [ + 13.41029942035675, + 52.49939268890719 + ], + [ + 13.410347700119019, + 52.49937962612168 + ], + [ + 13.410476446151733, + 52.49934370344147 + ], + [ + 13.410674929618835, + 52.499291452217875 + ], + [ + 13.4122896194458, + 52.498840782836766 + ] + ] + }, + "type": "Feature", + "properties": { + "class": "street", + "oneway": 0, + "osm_id": 4612752, + "type": "residential" + }, + "id": 4612752, + "source": "mapbox", + "sourceLayer": "road", + "state": { + "stateA": false + } + } +] \ No newline at end of file diff --git a/test/integration/query-tests/remove-feature-state/default/style.json b/test/integration/query-tests/remove-feature-state/default/style.json new file mode 100644 index 00000000000..2e0da0bd8eb --- /dev/null +++ b/test/integration/query-tests/remove-feature-state/default/style.json @@ -0,0 +1,112 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "operations": [ + [ + "setFeatureState", + { + "source": "mapbox", + "sourceLayer": "road", + "id": 4612696 + }, + { + "stateA": 1, + "stateB": true + } + ], + [ + "setFeatureState", + { + "source": "mapbox", + "sourceLayer": "road", + "id": 4612752 + }, + { + "stateA": 1, + "stateB": false + } + ], + [ + "removeFeatureState", + { + "source": "mapbox", + "sourceLayer": "road" + } + ], + [ + "setFeatureState", + { + "source": "mapbox", + "sourceLayer": "road", + "id": 4612696 + }, + { + "stateA": true, + "stateB": true + } + ], + [ + "setFeatureState", + { + "source": "mapbox", + "sourceLayer": "road", + "id": 4612752 + }, + { + "stateA": false, + "stateB": false + } + ], + [ + "removeFeatureState", + { + "source": "mapbox", + "sourceLayer": "road", + "id": 4612696 + } + ], + [ + "removeFeatureState", + { + "source": "mapbox", + "sourceLayer": "road", + "id": 4612752 + }, + "stateB" + ] + ], + "queryGeometry": [ + 10, + 100 + ] + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "layers": [ + { + "id": "road", + "type": "circle", + "source": "mapbox", + "source-layer": "road", + "paint": { + "circle-radius": 10 + }, + "interactive": true + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/remove-feature-state/composite-expression/expected.png b/test/integration/render-tests/remove-feature-state/composite-expression/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb77303dde1fec9c3c4b58a002e13b1dde589be GIT binary patch literal 221 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 nbZLdW-b(|22JU { t.end(); }); + t.test('#removeFeatureState', (t) => { + t.test('remove specific state property', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.setFeatureState({ source: 'geojson', id: 12345}, {'hover': true}); + map.removeFeatureState({ source: 'geojson', id: 12345}, 'hover'); + const fState = map.getFeatureState({ source: 'geojson', id: 12345}); + t.equal(fState.hover, undefined); + t.end(); + }); + }); + t.test('remove all state properties of one feature', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.setFeatureState({ source: 'geojson', id: 1}, {'hover': true, 'foo': true}); + map.removeFeatureState({ source: 'geojson', id: 1}); + + const fState = map.getFeatureState({ source: 'geojson', id: 1}); + t.equal(fState.hover, undefined); + t.equal(fState.foo, undefined); + + t.end(); + }); + }); + t.test('other properties persist when removing specific property', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.setFeatureState({ source: 'geojson', id: 1}, {'hover': true, 'foo': true}); + map.removeFeatureState({ source: 'geojson', id: 1}, 'hover'); + + const fState = map.getFeatureState({ source: 'geojson', id: 1}); + t.equal(fState.foo, true); + + t.end(); + }); + }); + t.test('remove all state properties of all features in source', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.setFeatureState({ source: 'geojson', id: 1}, {'hover': true, 'foo': true}); + map.setFeatureState({ source: 'geojson', id: 2}, {'hover': true, 'foo': true}); + + map.removeFeatureState({ source: 'geojson'}); + + const fState1 = map.getFeatureState({ source: 'geojson', id: 1}); + t.equal(fState1.hover, undefined); + t.equal(fState1.foo, undefined); + + const fState2 = map.getFeatureState({ source: 'geojson', id: 2}); + t.equal(fState2.hover, undefined); + t.equal(fState2.foo, undefined); + + t.end(); + }); + }); + t.test('specific state deletion should not interfere with broader state deletion', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.setFeatureState({ source: 'geojson', id: 1}, {'hover': true, 'foo': true}); + map.setFeatureState({ source: 'geojson', id: 2}, {'hover': true, 'foo': true}); + + map.removeFeatureState({ source: 'geojson', id: 1}); + map.removeFeatureState({ source: 'geojson', id: 1}, 'foo'); + + const fState1 = map.getFeatureState({ source: 'geojson', id: 1}); + t.equal(fState1.hover, undefined); + + map.setFeatureState({ source: 'geojson', id: 1}, {'hover': true, 'foo': true}); + map.removeFeatureState({ source: 'geojson'}); + map.removeFeatureState({ source: 'geojson', id: 1}, 'foo'); + + const fState2 = map.getFeatureState({ source: 'geojson', id: 2}); + t.equal(fState2.hover, undefined); + + map.setFeatureState({ source: 'geojson', id: 2}, {'hover': true, 'foo': true}); + map.removeFeatureState({ source: 'geojson'}); + map.removeFeatureState({ source: 'geojson', id: 2}, 'foo'); + + const fState3 = map.getFeatureState({ source: 'geojson', id: 2}); + t.equal(fState3.hover, undefined); + + t.end(); + }); + }); + t.test('add/remove and remove/add state', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.setFeatureState({ source: 'geojson', id: 12345}, {'hover': true}); + + map.removeFeatureState({ source: 'geojson', id: 12345}); + map.setFeatureState({ source: 'geojson', id: 12345}, {'hover': true}); + + const fState1 = map.getFeatureState({ source: 'geojson', id: 12345}); + t.equal(fState1.hover, true); + + map.removeFeatureState({ source: 'geojson', id: 12345}); + + const fState2 = map.getFeatureState({ source: 'geojson', id: 12345}); + t.equal(fState2.hover, undefined); + + t.end(); + }); + }); + t.test('throw before loaded', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + t.throws(() => { + map.removeFeatureState({ source: 'geojson', id: 12345}, {'hover': true}); + }, Error, /load/i); + + t.end(); + }); + t.test('fires an error if source not found', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "geojson": createStyleSource() + }, + "layers": [] + } + }); + map.on('load', () => { + map.on('error', ({ error }) => { + t.match(error.message, /source/); + t.end(); + }); + map.removeFeatureState({ source: 'vector', id: 12345}, {'hover': true}); + }); + }); + t.test('fires an error if sourceLayer not provided for a vector source', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "vector": { + "type": "vector", + "tiles": ["http://example.com/{z}/{x}/{y}.png"] + } + }, + "layers": [] + } + }); + map.on('load', () => { + map.on('error', ({ error }) => { + t.match(error.message, /sourceLayer/); + t.end(); + }); + map.removeFeatureState({ source: 'vector', sourceLayer: 0, id: 12345}, {'hover': true}); + }); + }); + t.test('fires an error if state property is provided without a feature id', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "vector": { + "type": "vector", + "tiles": ["http://example.com/{z}/{x}/{y}.png"] + } + }, + "layers": [] + } + }); + map.on('load', () => { + map.on('error', ({ error }) => { + t.match(error.message, /id/); + t.end(); + }); + map.removeFeatureState({ source: 'vector', sourceLayer: "1"}, {'hover': true}); + }); + }); + t.test('removeFeatureState fires an error if id is less than zero', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "vector": { + "type": "vector", + "tiles": ["http://example.com/{z}/{x}/{y}.png"] + } + }, + "layers": [] + } + }); + map.on('load', () => { + map.on('error', ({ error }) => { + t.match(error.message, /id/); + t.end(); + }); + map.removeFeatureState({ source: 'vector', sourceLayer: "1", id: -1}, {'hover': true}); + }); + }); + t.test('fires an error if id cannot be parsed as an int', (t) => { + const map = createMap(t, { + style: { + "version": 8, + "sources": { + "vector": { + "type": "vector", + "tiles": ["http://example.com/{z}/{x}/{y}.png"] + } + }, + "layers": [] + } + }); + map.on('load', () => { + map.on('error', ({ error }) => { + t.match(error.message, /id/); + t.end(); + }); + map.removeFeatureState({ source: 'vector', sourceLayer: "1", id: 'abc'}, {'hover': true}); + }); + }); + t.end(); + }); + t.test('error event', (t) => { t.test('logs errors to console when it has NO listeners', (t) => { const map = createMap(t);