Skip to content

Commit 0f04eeb

Browse files
authored
Add leaflet_method decorator (#2157)
* Add leaflet_method decorator This allows javascript method calls to be generated using an empty method body and a signature. Useful for writing plugins. As an example (and the driving motivation) I implemented this on the geoman plugin. * Fix tests * Implement all useful leaflet methods on geoman Only the free modules for now * Add a snapshot test
1 parent e3ca7ba commit 0f04eeb

File tree

5 files changed

+160
-21
lines changed

5 files changed

+160
-21
lines changed

folium/elements.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import wraps
12
from typing import List, Tuple
23

34
from branca.element import (
@@ -9,7 +10,15 @@
910
)
1011

1112
from folium.template import Template
12-
from folium.utilities import JsCode
13+
from folium.utilities import JsCode, camelize
14+
15+
16+
def leaflet_method(fn):
17+
@wraps(fn)
18+
def inner(self, *args, **kwargs):
19+
self.add_child(MethodCall(self, fn.__name__, *args, **kwargs))
20+
21+
return inner
1322

1423

1524
class JSCSSMixin(MacroElement):
@@ -148,3 +157,27 @@ def __init__(self, element_name: str, element_parent_name: str):
148157
super().__init__()
149158
self.element_name = element_name
150159
self.element_parent_name = element_parent_name
160+
161+
162+
class MethodCall(MacroElement):
163+
"""Abstract class to add an element to another element."""
164+
165+
_template = Template(
166+
"""
167+
{% macro script(this, kwargs) %}
168+
{{ this.target }}.{{ this.method }}(
169+
{% for arg in this.args %}
170+
{{ arg | tojavascript }},
171+
{% endfor %}
172+
{{ this.kwargs | tojavascript }}
173+
);
174+
{% endmacro %}
175+
"""
176+
)
177+
178+
def __init__(self, target: MacroElement, method: str, *args, **kwargs):
179+
super().__init__()
180+
self.target = target.get_name()
181+
self.method = camelize(method)
182+
self.args = args
183+
self.kwargs = kwargs

folium/plugins/geoman.py

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from branca.element import MacroElement
22

3-
from folium.elements import JSCSSMixin
3+
from folium.elements import JSCSSMixin, leaflet_method
44
from folium.template import Template
55
from folium.utilities import remove_empty
66

@@ -22,6 +22,8 @@ class GeoMan(JSCSSMixin, MacroElement):
2222
_template = Template(
2323
"""
2424
{% macro script(this, kwargs) %}
25+
/* ensure the name is usable */
26+
var {{this.get_name()}} = {{this._parent.get_name()}}.pm;
2527
{%- if this.feature_group %}
2628
var drawnItems_{{ this.get_name() }} =
2729
{{ this.feature_group.get_name() }};
@@ -32,12 +34,12 @@ class GeoMan(JSCSSMixin, MacroElement):
3234
{{ this._parent.get_name() }}
3335
);
3436
{%- endif %}
35-
/* The global varianble below is needed to prevent streamlit-folium
37+
/* The global variable below is needed to prevent streamlit-folium
3638
from barfing :-(
3739
*/
3840
var drawnItems = drawnItems_{{ this.get_name() }};
3941
40-
{{this._parent.get_name()}}.pm.addControls(
42+
{{this.get_name()}}.addControls(
4143
{{this.options|tojavascript}}
4244
)
4345
drawnItems_{{ this.get_name() }}.eachLayer(function(layer){
@@ -60,12 +62,6 @@ class GeoMan(JSCSSMixin, MacroElement):
6062
{{handler}}
6163
);
6264
{%- endfor %}
63-
drawnItems_{{ this.get_name() }}.addLayer(layer);
64-
});
65-
{{ this._parent.get_name() }}.on("pm:remove", function(e) {
66-
var layer = e.layer,
67-
type = e.layerType;
68-
drawnItems_{{ this.get_name() }}.removeLayer(layer);
6965
});
7066
7167
{% endmacro %}
@@ -85,17 +81,65 @@ class GeoMan(JSCSSMixin, MacroElement):
8581
)
8682
]
8783

88-
def __init__(
89-
self,
90-
position="topleft",
91-
feature_group=None,
92-
on=None,
93-
**kwargs,
94-
):
84+
def __init__(self, position="topleft", feature_group=None, on=None, **kwargs):
9585
super().__init__()
9686
self._name = "GeoMan"
9787
self.feature_group = feature_group
9888
self.on = on or {}
99-
self.options = remove_empty(
100-
position=position, layer_group=feature_group, **kwargs
101-
)
89+
self.options = remove_empty(position=position, **kwargs)
90+
91+
@leaflet_method
92+
def set_global_options(self, **kwargs):
93+
pass
94+
95+
@leaflet_method
96+
def enable_draw(self, shape, /, **kwargs):
97+
pass
98+
99+
@leaflet_method
100+
def disable_draw(self):
101+
pass
102+
103+
@leaflet_method
104+
def set_path_options(self, *, options_modifier, **options):
105+
pass
106+
107+
@leaflet_method
108+
def enable_global_edit_mode(self, **options):
109+
pass
110+
111+
@leaflet_method
112+
def disable_global_edit_mode(self):
113+
pass
114+
115+
@leaflet_method
116+
def enable_global_drag_mode(self):
117+
pass
118+
119+
@leaflet_method
120+
def disable_global_drag_mode(self):
121+
pass
122+
123+
@leaflet_method
124+
def enable_global_removal_mode(self):
125+
pass
126+
127+
@leaflet_method
128+
def disable_global_removal_mode(self):
129+
pass
130+
131+
@leaflet_method
132+
def enable_global_cut_mode(self):
133+
pass
134+
135+
@leaflet_method
136+
def disable_global_cut_mode(self):
137+
pass
138+
139+
@leaflet_method
140+
def enable_global_rotation_mode(self):
141+
pass
142+
143+
@leaflet_method
144+
def disable_global_rotation_mode(self):
145+
pass

tests/plugins/test_geoman.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def test_geoman():
2020
# the map
2121
tmpl = Template(
2222
"""
23-
{{this._parent.get_name()}}.pm.addControls(
23+
{{this.get_name()}}.addControls(
2424
{{this.options|tojavascript}}
2525
)
2626
"""
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import folium
2+
from folium import JsCode
3+
from folium.plugins import GeoMan, MousePosition
4+
5+
m = folium.Map(tiles=None, location=[39.949610, -75.150282], zoom_start=5)
6+
MousePosition().add_to(m)
7+
8+
# This can be used to test the connection to streamlit
9+
# by returning the resulting GeoJson
10+
handler = JsCode(
11+
"""
12+
(e) => {
13+
var map = %(map)s;
14+
var layers = L.PM.Utils.findLayers(map);
15+
var lg = L.layerGroup(layers);
16+
console.log(lg.toGeoJSON());
17+
}
18+
""" # noqa: UP031
19+
% dict(map=m.get_name())
20+
)
21+
22+
# For manual testing
23+
click = JsCode(
24+
"""
25+
(e) => {
26+
console.log(e.target);
27+
console.log(e.target.toGeoJSON());
28+
}
29+
"""
30+
)
31+
32+
# Just a few customizations for the snapshot tests
33+
# The test succeeds if the position is to the right
34+
# and if the buttons for markers and circles are not
35+
# shown.
36+
gm = GeoMan(
37+
position="topright", draw_marker=False, draw_circle=False, on={"click": click}
38+
).add_to(m)
39+
40+
# For manual testing of the global options
41+
gm.set_global_options(
42+
{
43+
"snappable": True,
44+
"snapDistance": 20,
45+
}
46+
)
47+
48+
# Make rectangles green
49+
gm.enable_draw("Rectangle", path_options={"color": "green"})
50+
gm.disable_draw()
51+
52+
# On any event that updates the layers, we trigger the handler
53+
event_handlers = {
54+
"pm:create": handler,
55+
"pm:remove": handler,
56+
"pm:update": handler,
57+
"pm:rotateend": handler,
58+
"pm:cut": handler,
59+
"pm:undoremove": handler,
60+
}
61+
62+
m.on(**event_handlers)
Loading

0 commit comments

Comments
 (0)