From d5035534d94d295941cc8f9c07769cc9b60da32c Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 7 Jun 2025 13:28:19 +0200 Subject: [PATCH 1/4] 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. --- folium/elements.py | 35 ++++++++++++++++++++++++++++++++++- folium/plugins/geoman.py | 10 ++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/folium/elements.py b/folium/elements.py index c99e35474b..8344abc3a8 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -1,3 +1,4 @@ +from functools import wraps from typing import List, Tuple from branca.element import ( @@ -9,7 +10,15 @@ ) from folium.template import Template -from folium.utilities import JsCode +from folium.utilities import JsCode, camelize + + +def leaflet_method(fn): + @wraps(fn) + def inner(self, *args, **kwargs): + self.add_child(MethodCall(self, fn.__name__, *args, **kwargs)) + + return inner class JSCSSMixin(MacroElement): @@ -148,3 +157,27 @@ def __init__(self, element_name: str, element_parent_name: str): super().__init__() self.element_name = element_name self.element_parent_name = element_parent_name + + +class MethodCall(MacroElement): + """Abstract class to add an element to another element.""" + + _template = Template( + """ + {% macro script(this, kwargs) %} + {{ this.target }}.{{ this.method }}( + {% for arg in this.args %} + {{ arg | tojavascript }}, + {% endfor %} + {{ this.kwargs | tojavascript }} + ); + {% endmacro %} + """ + ) + + def __init__(self, target: MacroElement, method: str, *args, **kwargs): + super().__init__() + self.target = target.get_name() + self.method = camelize(method) + self.args = args + self.kwargs = kwargs diff --git a/folium/plugins/geoman.py b/folium/plugins/geoman.py index c975d98777..8d6d78623b 100644 --- a/folium/plugins/geoman.py +++ b/folium/plugins/geoman.py @@ -1,6 +1,6 @@ from branca.element import MacroElement -from folium.elements import JSCSSMixin +from folium.elements import JSCSSMixin, leaflet_method from folium.template import Template from folium.utilities import remove_empty @@ -22,6 +22,8 @@ class GeoMan(JSCSSMixin, MacroElement): _template = Template( """ {% macro script(this, kwargs) %} + /* ensure the name is usable */ + var {{this.get_name()}} = {{this._parent.get_name()}}.pm; {%- if this.feature_group %} var drawnItems_{{ this.get_name() }} = {{ this.feature_group.get_name() }}; @@ -37,7 +39,7 @@ class GeoMan(JSCSSMixin, MacroElement): */ var drawnItems = drawnItems_{{ this.get_name() }}; - {{this._parent.get_name()}}.pm.addControls( + {{this.get_name()}}.addControls( {{this.options|tojavascript}} ) drawnItems_{{ this.get_name() }}.eachLayer(function(layer){ @@ -99,3 +101,7 @@ def __init__( self.options = remove_empty( position=position, layer_group=feature_group, **kwargs ) + + @leaflet_method + def set_global_options(self, **kwargs): + pass From bf1365076d37a1af48604137524a9ee775236983 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 7 Jun 2025 15:18:07 +0200 Subject: [PATCH 2/4] Fix tests --- tests/plugins/test_geoman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/test_geoman.py b/tests/plugins/test_geoman.py index fe6f5d30fc..09f5cdd416 100644 --- a/tests/plugins/test_geoman.py +++ b/tests/plugins/test_geoman.py @@ -20,7 +20,7 @@ def test_geoman(): # the map tmpl = Template( """ - {{this._parent.get_name()}}.pm.addControls( + {{this.get_name()}}.addControls( {{this.options|tojavascript}} ) """ From c97c9197dea42d57d1791c6981ba07cecb2b149c Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 8 Jun 2025 09:05:05 +0200 Subject: [PATCH 3/4] Implement all useful leaflet methods on geoman Only the free modules for now --- folium/plugins/geoman.py | 72 ++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/folium/plugins/geoman.py b/folium/plugins/geoman.py index 8d6d78623b..dc4c05be4c 100644 --- a/folium/plugins/geoman.py +++ b/folium/plugins/geoman.py @@ -34,7 +34,7 @@ class GeoMan(JSCSSMixin, MacroElement): {{ this._parent.get_name() }} ); {%- endif %} - /* The global varianble below is needed to prevent streamlit-folium + /* The global variable below is needed to prevent streamlit-folium from barfing :-( */ var drawnItems = drawnItems_{{ this.get_name() }}; @@ -62,12 +62,6 @@ class GeoMan(JSCSSMixin, MacroElement): {{handler}} ); {%- endfor %} - drawnItems_{{ this.get_name() }}.addLayer(layer); - }); - {{ this._parent.get_name() }}.on("pm:remove", function(e) { - var layer = e.layer, - type = e.layerType; - drawnItems_{{ this.get_name() }}.removeLayer(layer); }); {% endmacro %} @@ -87,21 +81,65 @@ class GeoMan(JSCSSMixin, MacroElement): ) ] - def __init__( - self, - position="topleft", - feature_group=None, - on=None, - **kwargs, - ): + def __init__(self, position="topleft", feature_group=None, on=None, **kwargs): super().__init__() self._name = "GeoMan" self.feature_group = feature_group self.on = on or {} - self.options = remove_empty( - position=position, layer_group=feature_group, **kwargs - ) + self.options = remove_empty(position=position, **kwargs) @leaflet_method def set_global_options(self, **kwargs): pass + + @leaflet_method + def enable_draw(self, shape, /, **kwargs): + pass + + @leaflet_method + def disable_draw(self): + pass + + @leaflet_method + def set_path_options(self, *, options_modifier, **options): + pass + + @leaflet_method + def enable_global_edit_mode(self, **options): + pass + + @leaflet_method + def disable_global_edit_mode(self): + pass + + @leaflet_method + def enable_global_drag_mode(self): + pass + + @leaflet_method + def disable_global_drag_mode(self): + pass + + @leaflet_method + def enable_global_removal_mode(self): + pass + + @leaflet_method + def disable_global_removal_mode(self): + pass + + @leaflet_method + def enable_global_cut_mode(self): + pass + + @leaflet_method + def disable_global_cut_mode(self): + pass + + @leaflet_method + def enable_global_rotation_mode(self): + pass + + @leaflet_method + def disable_global_rotation_mode(self): + pass From a1c6db0cf8ae7edd2fd895989cced01e37e8e08a Mon Sep 17 00:00:00 2001 From: Hans Then Date: Mon, 9 Jun 2025 21:59:20 +0200 Subject: [PATCH 4/4] Add a snapshot test --- .../modules/geoman_customizations.py | 62 ++++++++++++++++++ .../screenshot_geoman_customizations.png | Bin 0 -> 11849 bytes 2 files changed, 62 insertions(+) create mode 100644 tests/snapshots/modules/geoman_customizations.py create mode 100644 tests/snapshots/screenshots/screenshot_geoman_customizations.png diff --git a/tests/snapshots/modules/geoman_customizations.py b/tests/snapshots/modules/geoman_customizations.py new file mode 100644 index 0000000000..9a6c4e5a78 --- /dev/null +++ b/tests/snapshots/modules/geoman_customizations.py @@ -0,0 +1,62 @@ +import folium +from folium import JsCode +from folium.plugins import GeoMan, MousePosition + +m = folium.Map(tiles=None, location=[39.949610, -75.150282], zoom_start=5) +MousePosition().add_to(m) + +# This can be used to test the connection to streamlit +# by returning the resulting GeoJson +handler = JsCode( + """ + (e) => { + var map = %(map)s; + var layers = L.PM.Utils.findLayers(map); + var lg = L.layerGroup(layers); + console.log(lg.toGeoJSON()); + } + """ # noqa: UP031 + % dict(map=m.get_name()) +) + +# For manual testing +click = JsCode( + """ + (e) => { + console.log(e.target); + console.log(e.target.toGeoJSON()); + } + """ +) + +# Just a few customizations for the snapshot tests +# The test succeeds if the position is to the right +# and if the buttons for markers and circles are not +# shown. +gm = GeoMan( + position="topright", draw_marker=False, draw_circle=False, on={"click": click} +).add_to(m) + +# For manual testing of the global options +gm.set_global_options( + { + "snappable": True, + "snapDistance": 20, + } +) + +# Make rectangles green +gm.enable_draw("Rectangle", path_options={"color": "green"}) +gm.disable_draw() + +# On any event that updates the layers, we trigger the handler +event_handlers = { + "pm:create": handler, + "pm:remove": handler, + "pm:update": handler, + "pm:rotateend": handler, + "pm:cut": handler, + "pm:undoremove": handler, +} + +m.on(**event_handlers) diff --git a/tests/snapshots/screenshots/screenshot_geoman_customizations.png b/tests/snapshots/screenshots/screenshot_geoman_customizations.png new file mode 100644 index 0000000000000000000000000000000000000000..569a129bbe75e6fc9b00baf2730665e28935b79c GIT binary patch literal 11849 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 literal 0 HcmV?d00001