Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

removeFeatureState #7761

Merged
merged 1 commit into from
Jan 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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