Skip to content

Commit

Permalink
removeFeatureState
Browse files Browse the repository at this point in the history
  • Loading branch information
peterqliu committed Jan 11, 2019
1 parent 47925a6 commit 391be53
Show file tree
Hide file tree
Showing 16 changed files with 998 additions and 28 deletions.
117 changes: 117 additions & 0 deletions bench/benchmarks/remove_paint_state.js
Original file line number Diff line number Diff line change
@@ -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
];
2 changes: 2 additions & 0 deletions bench/versions/benchmarks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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());
Expand Down
23 changes: 11 additions & 12 deletions debug/highlightpoints.html
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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;
}
});
});
Expand Down
9 changes: 9 additions & 0 deletions src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
118 changes: 104 additions & 14 deletions src/source/source_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,148 @@ 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) {
tile.setFeatureState(this.state, painter);
}

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);
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 391be53

Please sign in to comment.