on hover functionality for features
alexandru-m-g opened this issue · 7 comments
Is there a way ( or is it in the plan to be implemented ) a way to have onhover events on a layer ( to show for example a districts name on hover ) ?
I saw something somewhat related to this here: #10 (comment) Does it mean that it would be too slow/resource intensive to implement ?
This is something we've experimented with, but have experienced such poor performance that I've removed it for now. I think implementing this will take some serious adjustments to the way we're storing the per-tile features. Something like Rbush or another (smarter) way of indexing/storing/fetching features to do hit tests on. This is especially challenging on tiles that contain thousands of point features (for example) - to do a hit test on all of those onMouseMove is crazy... but desirable!
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/addHitRegion
Finally, frontend UTF grids. It's so close...
This doesn't fix the query part, but I added #35 to improve hover rendering performance (with hover coming from another source, e.g. a UTFGrid). Basically, I'm binding a second MVTLayer to the same MVT data, as a second render pass. The style for this layer can return null for all features except the hovered one, making performance quite snappy even at high scale.
@henryptung I've tried creating an onhover functionality by taking the latest code from your fork. Basically I did the following:
- Added a mousemove event handler on the leaflet layer
- Found the hovered feature via
mvtSource._featureAt()
- Toggled it's state ( if it wasn't the same feature as when the event previously triggered )
It worked quite well ( even with about 35 000 feature, the lag was under 1s ) so it seems that rbush is quite efficient.
Question: is there a way of doing this even more efficiently ? I saw your comment about binding several layers to the same MVT data and styling them differently but couldn't figure out how to use this ( especially in conjuction with featureAt() functionality ). Could you point me to some example perhaps. Thanks.
Hey,
Sure, the setup is relatively simple (though it makes some assumptions about the contents of the MVTs, which is my main reservation; in particular, you want to know which layers are available to render, by name).
Originally, the visibleLayers
parameter to the MVTSource
was an array specifying the names of the layers to display; all other layers would be hidden and not rendered. I changed it so that it is instead a map of layerKey: layerNameInMVT
. So, if you had a layer named layer
in the tile data, you could specify visibleLayers: { "base": "layer", "overlay": "layer" }
and get two render passes.
For the purposes of the MVTSource API, you would have two independently-styled base
and overlay
layers to work with. To accommodate this, the style
parameter of MVTSource
also accepts a map of layerKey: StyleFunction
in place of a direct style function.
As such, the way I do it is
...
visibleLayers: {
base: "layer",
overlay: "layer"
},
style: {
base: /* normal style here */,
overlay: function () { return null; }
}
Then, when you get a hover (or other) event, you can run
feature.style = /* overlay style here */;
feature.redraw();
to get just an overlay of the one feature with a non-null style. It might be necessary to cross-reference using mvtSource.layers["overlay"].features[feature.id]
to make sure you're redrawing the right feature (the overlay instance, not the original). When hovering off the feature, set the overlay style back to null
and redraw again to "undraw" the overlay.
Please let me know if you run into any issues with the above; I get near instant-hover with that setup.
Unfortunately, using an object for visibleLayers
was actually a terrible idea from the standpoint of controlled z-indexing, but I can try fiddling with it a bit more and see if I can get a saner API for it all. Currently thinking:
visibleLayers: { layerNameInMVT: ["base", "overlay"] }
.- Pros: Controls Z-indexing of the aliases.
- Cons: Still confusing API; multiple feature instances corresponding to base and overlay.
- Add an overlay canvas layer sandwiched underneath the debug layer but above all the other MVT layers. API:
MVTSource.clearOverlay()
clears full overlay canvas,MVTFeature.drawOverlay(Style)
draws overlays for that feature.- Pros: Each feature gets exactly one instance, overlay guaranteed on top (but underneath debug grid if on).
- Cons: Overlays are not persistent: to keep an overlay alive on 99 features but remove an overlay on 1, you would need to clear the overlay layer and redraw the 99 remaining overlays manually.
- Same as (2), but with persistent overlay style API.
MVTFeature.clearOverlay()
removes that feature from the overlay canvas,MVTFeature.setOverlay(Style)
sets that feature to be drawn by the overlay layer with that overlay style.- Pros: Easier to manage overlays for selection-like handling (or for anything else involving persistent state).
- Cons: Mutations of overlay styles is n^2 in number of features affected, since relevant overlay tile(s) will be redrawn for every feature inside.
API struggles...
@henryptung , thanks for guiding me through binding several layers to the same MVT data. Just got to testing this and it worked quite nicely.
I'm still confused about some aspects related to this:
- Is this supposed to work for point layers too ? As far as I tested the
_featureAt()
seems to always return null in my scenarios and I couldn't get the hover effect - After dragging the map up, the features that get highlighted on hover are no longer the ones under the cursor but some that are further down ( presumably with exactly the distance that I dragged the map up ). I'm doing this to get the hovered feature:
mvtSource._featureAt(evt.layerPoint, layers);
where evt comes from the leaflet mousemove event. What am I missing ? - What is actually the benefit of having 2 layers (base & overlay) compared to having just the base and finding and redrawing the feature on the base layer ? Does
feature.redraw();
redraw more than just the tiles corresponding to the current feature (in which case redrawing a mostly empty overlay layer would indeed be faster) ?
Would definitely be nice to have a way of controlling the z-indexing of the layers :)
- Points are a little tricky because the collision algorithm uses the point radius to determine cursor-point collision, but the point radius is a style parameter and thus mutable at runtime (and also unbounded). The general strategy would be to add a buffer radius to bounding boxes for point and line geometries on spatial index insertion, but the radius would need to be at least as large as the largest point radius drawn.
- The better strategy here would be responding to style changes and mutating the index accordingly, but I'm not well-versed enough with the MVTFeature.style/setStyle API to be comfortable with changing it.
- You might want to use
featureAtContainerPoint
orfeatureAtLatlng
instead of the internal_featureAt
method. Useevt.containerPoint
to get thecontainerPoint
of the event, instead of thelayerPoint
.- Honestly they're more or less equivalent, but to my understanding
layerPoint
is really just a strange shorthand forcontainerPoint
as it would be ignoring subsequent user panning, which corresponds to the behavior you've observed. I'd expectlayerPoint
instead correspond to pixel-coordinate-relative-to-top-left-of-layer (i.e. the raw output of the map projection in pixel coords) but unfortunately it is not so; of the two,containerPoint
is not dependent on initial map state.
- Honestly they're more or less equivalent, but to my understanding
- A separate overlay layer is mostly useful for fast redraw. The library draws directly onto canvas, but what is drawn cannot be undrawn. As such, when hovering over features, an overlay drawn for one feature can only be undrawn by clearing the canvas containing the feature and redrawing everything in the canvas. This can be very expensive at high feature scale, and leads to additional drawing lag (you probably observed this by using
.toggle()
to draw the hover, since each such.toggle()
clears and redraws the canvas for that entire tile).