Skip to content

Commit 3922b31

Browse files
authored
Issue 1520: Propagate feature to child layers of multi-geometries (#2263)
* Propagate feature to child layers of multi-geometries Leaflet's geometryToLayer returns a FeatureGroup for MultiPoint, and addData assigns `feature` to that group only. Tooltips resolve their source to the layer that fired the event (Tooltip.js:418, v1.9.3), which is a child marker with no `feature`, so GeoJsonTooltip and GeoJsonPopup throw and never render. Copy the feature onto child layers in onEachFeature, before any user-supplied callback, so both see the same data. Fixes #1520 * Descend into nested groups when propagating features A GeometryCollection containing a MultiPoint produces a FeatureGroup inside a FeatureGroup, placing the hovered marker two levels below the layer that owns `feature`. eachLayer only iterates one level, so the propagation now recurses. * Add regression test for MultiPoint tooltips
1 parent e0d7128 commit 3922b31

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

folium/features.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,13 @@ class GeoJson(Layer):
586586
{%- endif %}
587587
588588
function {{this.get_name()}}_onEachFeature(feature, layer) {
589+
(function propagate(parent){
590+
if (typeof parent.eachLayer !== "function") {return;}
591+
parent.eachLayer(function (child) {
592+
if (child.feature === undefined) { child.feature = feature; }
593+
propagate(child);
594+
})
595+
})(layer)
589596
{%- if this.on_each_feature %}
590597
({{this.on_each_feature}})(feature, layer);
591598
{%- endif %}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import folium
2+
from folium import GeoJson, GeoJsonTooltip, Map
3+
from folium.utilities import temp_html_filepath
4+
5+
# Selenium's pointer actions do not reliably reach Leaflet's SVG paths in
6+
# headless Chrome, so the hover is dispatched as a DOM event instead. It
7+
# bubbles through Map._handleDOMEvent exactly as a real pointer hover does.
8+
HOVER = """
9+
const el = arguments[0];
10+
const rect = el.getBoundingClientRect();
11+
const opts = {
12+
bubbles: true,
13+
clientX: rect.x + rect.width / 2,
14+
clientY: rect.y + rect.height / 2,
15+
};
16+
el.dispatchEvent(new MouseEvent('mouseover', opts));
17+
el.dispatchEvent(new MouseEvent('mousemove', opts));
18+
"""
19+
20+
21+
def build() -> Map:
22+
data = {
23+
"type": "FeatureCollection",
24+
"features": [
25+
{
26+
"type": "Feature",
27+
"properties": {"name": "multipoint"},
28+
"geometry": {"type": "MultiPoint", "coordinates": [[0.0, 0.0]]},
29+
}
30+
],
31+
}
32+
m = Map((0, 0), zoom_start=10)
33+
GeoJson(
34+
data,
35+
marker=folium.CircleMarker(radius=20),
36+
tooltip=GeoJsonTooltip(fields=["name"], labels=False),
37+
).add_to(m)
38+
return m
39+
40+
41+
def test_geojson_multipoint_tooltip(driver):
42+
"""A GeoJsonTooltip must render for MultiPoint geometry.
43+
44+
Leaflet returns a FeatureGroup for MultiPoint and assigns `feature` to
45+
that group only, while the tooltip resolves its source to the child
46+
layer that fired the event. Without the feature on the children, the
47+
tooltip's content function throws and nothing renders.
48+
49+
https://github.com/python-visualization/folium/issues/1520
50+
"""
51+
html = build().get_root().render()
52+
with temp_html_filepath(html) as filepath:
53+
driver.get_file(filepath)
54+
driver.wait_until(".folium-map")
55+
56+
marker = driver.wait_until("path.leaflet-interactive")
57+
driver.execute_script(HOVER, marker)
58+
59+
tooltip = driver.wait_until(".leaflet-tooltip.foliumtooltip")
60+
assert "multipoint" in tooltip.text
61+
62+
# No verify_js_logs() here: Leaflet 1.9.3 (our pin) throws
63+
# "t.getElement is not a function" from _addFocusListenersOnLayer for any
64+
# GeoJSON layer containing a nested FeatureGroup, independent of this fix.
65+
# Guarded upstream in 1.9.4.

0 commit comments

Comments
 (0)