-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Arbitrary states for feature interactivity #6263
Conversation
src/data/bucket.js
Outdated
@@ -89,6 +93,7 @@ module.exports = { | |||
// look up StyleLayer objects from layer ids (since we don't | |||
// want to waste time serializing/copying them from the worker) | |||
(bucket: any).layers = layers; | |||
(bucket: any).stateDependentLayers = layers.filter((l) => l.isStateDependent()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pre-compute the layers with state-dependent expressions during deserialize. These are needed to re-compute the expressions in ProgramConfiguration
until expression evaluators can be re-created when deserialized (#6255)
After that change, layer ids will be sufficient
f10a553
to
d1cd559
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asheemmamoowala this is looking good! I think the biggest question left for me is whether this is the best way to store and mutate the individual features' states.
I wonder if we could use the _idMap
on each bucket as the canonical state store. Keeping state/state changes separate from the feature ids and indices seems like it may add in some unnecessary complexity.
src/data/program_configuration.js
Outdated
updatePaintArrays(featureStates: FeatureStates, vtLayer: VectorTileLayer, layers: $ReadOnlyArray<TypedStyleLayer>): boolean { | ||
let changed: boolean = false; | ||
layers.forEach((layer) => { | ||
changed = changed || this.programConfigurations[layer.id].updatePaintArrays(featureStates, vtLayer, layer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in general we avoid using forEach
– looks like this would be better suited for a for (let i = 0; i <layers.length;...
loop with a break
statement if changed
is ever true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 or even for(const layer of layers) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I think we can't break early from the loop, though, since we always need to call updatePaintArrays
on each layer's ProgramConfiguration)
src/data/bucket/circle_bucket.js
Outdated
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); | ||
} | ||
} | ||
} | ||
|
||
update(states: FeatureStates, vtLayer: VectorTileLayer) { | ||
if (!this.stateDependentLayers.length) return; | ||
this.changed = this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just use uploaded
for this state as well instead of adding changed
state? in other words, a dirty = true
return value from updatePaintArrays
would set uploaded to false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like uploaded
and changed
may be distinct in order to avoid re-uploading the layout and index buffers when only the paint buffers need to change.
Alternative simplification: could we move the changed
tracking into ProgramConfigurationSet
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anandthakker - If we follow that idea, the correct place to track changes would be in the individual ProgramConfiguration
's. That would require that Bucket.uploadPending()
check the state of each program.
It's not clear to me that the result is simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we follow that idea, the correct place to track changes would be in the individual ProgramConfiguration's
We can put a dirty
property on ProgramConfigurationSet
, which is set during updatePaintArrays
, checked in upload()
to decide whether to actually do work (and then cleared). I suppose it might not be simpler per se, but it has the advantage of localizing and centralizing this state tracking in program_configuration.js
, rather than having it replicated in each bucket.
@@ -52,6 +54,7 @@ class SourceCache extends Evented { | |||
transform: Transform; | |||
_isIdRenderable: (id: number) => boolean; | |||
used: boolean; | |||
_state: SourceFeatureState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are there any alternatives to storing state for all features on all loaded tiles in the SourceCache
? seems like this is a muddling of concerns 💭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it does make sense to store a SourceFeatureState
here on the SourceCache
, since it belongs to a source and should be discarded precisely when a source is discarded.
But I agree that the state-aware logic in below feels a bit out of place here. What if we:
- move the responsibility for calling
Tile#updateFeatureState
duringprepare()
directly intoSourceFeatureState#coalesceChanges()
(i.e., take a set of tiles as a parameter tocoalesceChanges
)? - Add a
SourceFeatureState#initializeTileState(Tile)
method and replace the other two cases oftile.updateFeatureState(this._state.state);
withthis._state.initializeTileState(tile)
?
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since SourceCache is responsible for creating sources and managing their tiles, it is the appropriate place to manage the feature state of the source as well
I disagree. SourceCache
is already very difficult to reason about, in part because it's doing too much. (See also #2291.) Ideally, it would not manipulate the "content" of its Tile
s at all (except perhaps setting tile.state
, since that's directly related to its job of caching tiles).
src/style/style.js
Outdated
@@ -780,6 +780,23 @@ class Style extends Evented { | |||
return this.getLayer(layer).getPaintProperty(name); | |||
} | |||
|
|||
setFeatureState(source: string, feature: string, key: string, value: any, sourceLayer?: string) { | |||
if (this.sourceCaches[source] === undefined) { | |||
throw new Error('There is no source with this ID'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for a more helpful error message, maybe print the passed source
id?
src/style/style.js
Outdated
|
||
getFeatureState(source: string, feature: string, key?: string, sourceLayer?: string) { | ||
if (this.sourceCaches[source] === undefined) { | ||
throw new Error('There is no source with this ID'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment about error as above
src/data/program_configuration.js
Outdated
}); | ||
} | ||
|
||
this._bufferPos = length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 should this be +=
instead of =
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first, I thought that too. The length
input parameter is confusing - its the end
index of the paint array after adding new elements. Its usage in the binder popluatePaintArray
makes it more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yeah that's confusing! Maybe we should rename the length
parameter to targetLength
or something?
src/data/program_configuration.js
Outdated
constructor() { | ||
this.binders = {}; | ||
this.cacheKey = ''; | ||
|
||
this._buffers = []; | ||
this._idMap = {}; | ||
this._bufferPos = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: name this a little more descriptively: maybe _populatedVertexCount
? _paintBufferLength
? 🤷♂️
src/data/program_configuration.js
Outdated
for (const id in featureStates) { | ||
const posArray = this._idMap[id]; | ||
|
||
if (posArray) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: if (!posArray) continue;
src/data/program_configuration.js
Outdated
|
||
if (posArray) { | ||
for (let i = 0; i < posArray.length; i++) { | ||
const pos = posArray[i]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: for (const pos of posArray) {
src/data/program_configuration.js
Outdated
updatePaintArrays(featureStates: FeatureStates, vtLayer: VectorTileLayer, layers: $ReadOnlyArray<TypedStyleLayer>): boolean { | ||
let changed: boolean = false; | ||
layers.forEach((layer) => { | ||
changed = changed || this.programConfigurations[layer.id].updatePaintArrays(featureStates, vtLayer, layer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 or even for(const layer of layers) {
@@ -52,6 +54,7 @@ class SourceCache extends Evented { | |||
transform: Transform; | |||
_isIdRenderable: (id: number) => boolean; | |||
used: boolean; | |||
_state: SourceFeatureState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it does make sense to store a SourceFeatureState
here on the SourceCache
, since it belongs to a source and should be discarded precisely when a source is discarded.
But I agree that the state-aware logic in below feels a bit out of place here. What if we:
- move the responsibility for calling
Tile#updateFeatureState
duringprepare()
directly intoSourceFeatureState#coalesceChanges()
(i.e., take a set of tiles as a parameter tocoalesceChanges
)? - Add a
SourceFeatureState#initializeTileState(Tile)
method and replace the other two cases oftile.updateFeatureState(this._state.state);
withthis._state.initializeTileState(tile)
?
(ctx, [key]) => get(key.evaluate(ctx), ctx.state()) | ||
] | ||
] | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there's only one implementation, use:
ValueType,
[StringType],
(ctx, [key]) => get(key.evaluate(ctx), ctx.state())
@@ -8,6 +8,8 @@ function isFeatureConstant(e: Expression) { | |||
if (e instanceof CompoundExpression) { | |||
if (e.name === 'get' && e.args.length === 1) { | |||
return false; | |||
} else if (e.name === 'state' && e.args.length === 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just e.name === 'state'
, no need to check argument count
@@ -28,6 +30,19 @@ function isFeatureConstant(e: Expression) { | |||
return result; | |||
} | |||
|
|||
function isStateConstant(e: Expression) { | |||
if (e instanceof CompoundExpression) { | |||
if (e.name === 'state' && e.args.length === 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to check e.args.length
src/ui/map.js
Outdated
*/ | ||
setFeatureState(source: string, feature: string, key: string, value: any, sourceLayer?: string) { | ||
this.style.setFeatureState(source, feature, key, value, sourceLayer); | ||
this._rerender(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use this._update()
src/data/bucket/circle_bucket.js
Outdated
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); | ||
} | ||
} | ||
} | ||
|
||
update(states: FeatureStates, vtLayer: VectorTileLayer) { | ||
if (!this.stateDependentLayers.length) return; | ||
this.changed = this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anandthakker - If we follow that idea, the correct place to track changes would be in the individual ProgramConfiguration
's. That would require that Bucket.uploadPending()
check the state of each program.
It's not clear to me that the result is simpler.
src/data/program_configuration.js
Outdated
const binder: Binder<any> = this.binders[property]; | ||
if (binder instanceof ConstantBinder) continue; | ||
if ((binder: any).expression.isStateDependent === true) { | ||
//AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it OK to leave this comment in here? Or should it be separated into a ticket?
src/data/program_configuration.js
Outdated
}); | ||
} | ||
|
||
this._bufferPos = length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first, I thought that too. The length
input parameter is confusing - its the end
index of the paint array after adding new elements. Its usage in the binder popluatePaintArray
makes it more clear.
@@ -52,6 +54,7 @@ class SourceCache extends Evented { | |||
transform: Transform; | |||
_isIdRenderable: (id: number) => boolean; | |||
used: boolean; | |||
_state: SourceFeatureState |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
src/ui/map.js
Outdated
* @param {string | number | boolean} value The value to be set. | ||
* @param {string} sourceLayer (optional) The source-layer identifier for Vector Tile sources. | ||
*/ | ||
setFeatureState(source: string, feature: string, key: string, value: any, sourceLayer?: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calls to setFeatureState
with a sourceLayer
parameter may be more clear if we pass the sourceLayer
as an object rather than an optional parameter: source: string | { id: string; layerId: string; }
. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a good improvement! I'd also like to make the API error out if a source layer is not provided for Vector sources. It would mean checking the type of the source id, but it would prevent erroneous use of the API.
src/source/source_state.js
Outdated
const util = require('../util/util'); | ||
const Tile = require('./tile'); | ||
|
||
export type FeatureStates = {[feature_id: string]: {[key: string]: string | number | boolean }}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we intend to only allow string | number | boolean
in feature states? Aren't GeoJSON properties allowed to include null
, arrays, and objects now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think everything but object
is OK to use. Nested objects are not supported in expressions, so it wouldn't be usable for runtime styling, and possibly cause confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anandthakker Are nested objects supported in GeoJSON feature properties? I suspect the least confusing thing will be parity between GeoJSON feature properties and feature state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, they're supported. E.g., the following works:
map.on('load', () => {
map.addLayer({
id: 'test',
source: {
type: 'geojson',
data: {
type: 'Feature',
properties: {
x: { y: 'hello' }
},
geometry: {
type: 'Point',
coordinates: [0, 0]
}
}
},
type: 'symbol',
layout: {
'text-field': ["get", "y", ["get", "x"]]
}
})
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although note that in queryRenderedFeatures
, array or object property values currently get stringified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the correction @anandthakker! In that case, feature states should accept all JSON types.
For queryRenderedFeatures
, this means that arrays and objects would be stringified in properties
but not in state
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's fine -- qRF shouldn't stringify them, it's just a limitation of a current implementation detail: #2434
@@ -414,16 +418,28 @@ class SymbolBucket implements Bucket { | |||
} | |||
} | |||
|
|||
update(states: FeatureStates, vtLayer: VectorTileLayer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would assume that a method called Bucket#update
did something more general than this (like update all the data in the bucket). Would a name like Bucket#updateFeatureState
be more clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bucket#update
updates the dynamic buffers in the bucket with the passed in states
. The layout and index buffers are static, and so cannot be updated. The more specific name is more descriptive, but it doesn't distinguish itself from any other type of update
.
this.opacityVertexBuffer = context.createVertexBuffer(this.opacityVertexArray, shaderOpacityAttributes, true); | ||
// This is a performance hack so that we can write to opacityVertexArray with uint32s | ||
// even though the shaders read uint8s | ||
this.opacityVertexBuffer.itemSize = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain how this works in more detail? Is this prone to break in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe @ChrisLoer can help with its futureproof-ness. I havent changed this code, except move it so it runs only once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is a relatively brittle performance hack. We might be able to make it more future proof by pushing the hack into StructArray
so the external interface was always clearly "this is an array of uint8s" even if uint32s were used internally. But it's a weird special case and it's not clear to me how we'd define that for StructArray
.
Background: we do lots of opacity vertex updates for symbols: for each symbol, we have one packed opacity value that we need to update on all four vertices for every glyph. Profiling in Chrome, the actual cost of building the array showed up as a significant part of rendering time. Pushing n/4 uint32s turned out to be significantly faster than pushing n uint8s (see #5150 (comment), there's an additional wrinkle that the same change included packing "target opacity" into the same value). At some point it will probably make sense to re-test this optimization, since testing this kind of thing is so squirrelly and changes in the javascript engine could render it useless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might be able to make it more future proof by pushing the hack into
StructArray
I think that's a good approach 👍
e65553b
to
bd5a653
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks great! I commented on a couple things that raised some questions for me and a couple things that looked like possible bugs. The changes I think might be necessary are:
- a fix for the
this.needsUpload
/||
short-circuiting if that is not intentional queryPadding
possibly being invalid if thestatistics.max
value changed
paintArray.emplace(i, value); | ||
} | ||
|
||
this.statistics.max = Math.max(this.statistics.max, value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't handle the case where the maximum value is replaced with a smaller value and the max should decrease. I think this is fine though, because having a bigger max won't cause bugs the way we use it and maintaining the real max value would be more complicated and expensive. This should be fine and safe the way it is, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping track of the max
value correctly may not be worth it. When a property value is reduced, the update logic would need to filter the previously populated values to check if there is a new lower max
.
Leaving this as is won't return incorrect results, but it will increase the number of features that are matched in the initial query (with queryPadding
) and then have to be filtered out.
If there are use cases that significantly affect the performance of queryRenderedFeatures
, this can be revisited.
src/ui/map.js
Outdated
* | ||
* @returns {any} The value of the specified specified state or all states. | ||
*/ | ||
getFeatureState(source: string | { sourceId: string; sourceLayer: string; }, feature: string, key?: string): any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The optional key
param changes the return type. Should returning the full state object be split into a separate method? If it's useful to return the entire state as an object, should it be possible to set state with an object instead of individual calls for each key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The optional key
parameter is only for internal use and can be removed from the external interface.
I'm not entirely sure that getting and setting the entire state as an object is a good idea. Setting state as a single object can create confusion - Does it extend the current object or replace it? Updating a single value would require fetching the entire object first and setting it again.
src/data/program_configuration.js
Outdated
|
||
updatePaintArrays(featureStates: FeatureStates, vtLayer: VectorTileLayer, layers: $ReadOnlyArray<TypedStyleLayer>) { | ||
for (const layer of layers) { | ||
this.needsUpload = this.needsUpload || this.programConfigurations[layer.id].updatePaintArrays(featureStates, vtLayer, layer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this.needsUpload
is true
before is gets called then the ||
will short-circuit and this.programConfigurations[layer.id].updatePaintArrays(...)
will not get called. It should always get called, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great catch! I didn't run into the short-circuiting, but it is possible.
src/source/source_state.js
Outdated
const layerStates = {}; | ||
for (const id in this.stateChanges[sourceLayer]) { | ||
this.state[sourceLayer][id] = extend( | ||
{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to create and extend a new object? or could it just extend the existing this.state[sourceLayer][id]
?
paintArray.emplace(i, value); | ||
} | ||
|
||
this.statistics.max = Math.max(this.statistics.max, value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the max changing means the cached queryPadding
value might be invalid:
mapbox-gl-js/src/source/tile.js
Lines 170 to 174 in bd5a653
this.queryPadding = 0; | |
for (const id in this.buckets) { | |
const bucket = this.buckets[id]; | |
this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(bucket.layerIds[0]).queryRadius(bucket)); | |
} |
I think we need some way of either invalidating it, or if it is cheap enough, recalculating it every time we need to use it instead of caching it.
8e79e95
to
25d23f9
Compare
I haven't been this excited about a new feature in a loooong time! |
This looks good to me. The only remaining questions I have are details about the external interface. The current interface seems fine but I think it's worth looking at this extra carefully since it will be public. The current public signature is:
What if the method signature was:
The advantages I'm seeing:
@lucaswoj @ChrisLoer @anandthakker @mollymerp @jfirebaugh did any of you want to review this any further? |
I agree with @ansis's suggestion for public API. In addition, I think the expression operator should be spelled |
Should there be a method that allows updating multiple state values at once? E.g.,
This is appealing. Is there any chance it would come across as though this method actually makes changes to |
25d23f9
to
6a88bce
Compare
@anandthakker Were you thinking of this as a replacement for |
I'm concerned that this would make the API less convenient to use when not directly piping the result from an event handler or call to A
|
I was thinking of it as an alternative, mainly because it seems to me like it covers the |
@anandthakker
|
I'm okay with breaking with that convention in this case: 1) there are ideas about changing it anyway, and 2) I think it's different in a meaningful way: |
4b0aedb
to
adcc01e
Compare
The original API design, I don't have a strong opinion about whether or not we also allow #2741 is about internal architecture not naming conventions. |
I see this not as a new naming convention, but instead a different behavior for which there isn't a precedent in our existing APIs. There's a broader convention that |
React's |
Another inconsistency: our own |
This looks like nice technology, but I'm trying to understand exactly when it should be used. It's generally been encouraged to have a separate layer for features that change rapidly (eg a layer that contains only a copy of the hovered item but with some different styling.) That approach is pretty performant, and as long as the hovered item obscures the original it works well. So when should the separate hover-layer approach be used and when should one use a single layer with |
The only case in which you may want to use a second layer is if the geometry of a feature needs to change rapidly (i.e. for a drawing plugin) as |
I'm not sure if there is a bug in this feature or I'm using it wrong
The idea is to select all "related" features to the selected one and color them as red (regardless of zoom level).
EDIT: So it seems as setting the same feature id to those features work, is that how it's suppose to work? Since the documentation says the feature id should be unique but it doesn't say anything across zoom levels. |
@shayke your version using |
@lucaswoj Does setFeatureState always require feature.id? Can it work on feature.properties.name? |
@Amit-Gore Currently |
Based on @ansis' work in
data-update-proof-of-concept
Implements
map.setFeatureState()
, a new method to setkey: value
pairs on specific features in a source. The source must provide unique feature id's per source-layer for this to work correctly.Also implemented
['state', '<stateName>']
expression syntax. Similar toget
, this allows accessing a feature's state instead of source data property.todo
Improve ExpressionCreate benchmarkdynamicDraw
on vertex and index buffersLaunch Checklist
cc @ansis @mollymerp