From ebc34c17cae611df33f7dc3eb4139346b6bf3a93 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 14 Mar 2024 11:39:55 +0100 Subject: [PATCH 1/9] Add EventHandler to layer object In combination with JsCode this makes it easier for users to add `on` method calls for event handling without extending Folium itself. The functionality was inspired by PR #1866 by @yschopfer19. The PR was not accepted yet, because of concerns with code duplication. In the approach taken in the current PR, #1866 would not be necessary anymore, as the requested changes could be added completely in client code space. --- folium/elements.py | 83 ++++++++++++++++++++++++++++++++++++++++++ folium/map.py | 4 +- tests/test_features.py | 18 +++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/folium/elements.py b/folium/elements.py index 9c41e66fa..56965b449 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -3,6 +3,8 @@ from branca.element import CssLink, Element, Figure, JavascriptLink, MacroElement from jinja2 import Template +from folium.utilities import JsCode + class JSCSSMixin(Element): """Render links to external Javascript and CSS resources.""" @@ -46,6 +48,87 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): default_list.append((name, url)) +class EventTargetMixin(Element): + '''Add Event Handlers to an element. + + Examples + -------- + >>> import folium + >>> from folium.utilities import JsCode + + >>> m = folium.Map() + + >>> geo_json_data = { + ... "type": "FeatureCollection", + ... "features": [ + ... { + ... "type": "Feature", + ... "geometry": { + ... "type": "Polygon", + ... "coordinates": [ + ... [ + ... [100.0, 0.0], + ... [101.0, 0.0], + ... [101.0, 1.0], + ... [100.0, 1.0], + ... [100.0, 0.0], + ... ] + ... ], + ... }, + ... "properties": {"prop1": {"title": "Somewhere on Sumatra"}}, + ... } + ... ], + ... } + + >>> g = folium.GeoJson(geo_json_data).add_to(m) + >>> highlight = JsCode( + ... """ + ... function highlight(e) { + ... e.target.original_color = e.layer.options.color; + ... e.target.setStyle({ color: "green" }); + ... } + ... """ + ... ) + >>> reset = JsCode( + ... """ + ... function reset(e) { + ... e.target.setStyle({ color: e.target.original_color }); + ... } + ... """ + ... ) + >>> g.on(mouseover=highlight, mouseout=reset) + ''' + + def on(self, **kwargs: JsCode): + for event, handler in kwargs.items(): + self.add_child(EventHandler(event, handler)) + return self + + def render(self, **kwargs) -> None: + super().render(**kwargs) + + +class EventHandler(MacroElement): + """Render Event Handlers.""" + + _template = Template( + """ + {% macro script(this, kwargs) %} + {{ this._parent.get_name()}}.on( + {{ this.event|tojson}}, + {{ this.handler.js_code }} + ); + {% endmacro %} + """ + ) + + def __init__(self, event: str, handler: JsCode): + super().__init__() + self._name = "EventHandler" + self.event = event + self.handler = handler + + class ElementAddToElement(MacroElement): """Abstract class to add an element to another element.""" diff --git a/folium/map.py b/folium/map.py index 01ac7d2ed..fb00ee72d 100644 --- a/folium/map.py +++ b/folium/map.py @@ -10,7 +10,7 @@ from branca.element import Element, Figure, Html, MacroElement from jinja2 import Template -from folium.elements import ElementAddToElement +from folium.elements import ElementAddToElement, EventTargetMixin from folium.utilities import ( TypeBounds, TypeJsonValue, @@ -21,7 +21,7 @@ ) -class Layer(MacroElement): +class Layer(EventTargetMixin, MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. diff --git a/tests/test_features.py b/tests/test_features.py index c879ad119..8dd51d61b 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -13,6 +13,7 @@ import folium from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup +from folium.utilities import JsCode @pytest.fixture @@ -283,6 +284,23 @@ def test_geojson_empty_features_with_styling(): m.get_root().render() +def test_geojson_event_handler(): + """Test that event handlers are properly generated""" + m = Map() + data = {"type": "FeatureCollection", "features": []} + geojson = GeoJson(data, style_function=lambda x: {}).add_to(m) + fn = JsCode( + """ + function f(e) { + console.log("only for testing") + } + """ + ) + geojson.on(mouseover=fn) + rendered = m.get_root().render() + assert fn.js_code in rendered + + def test_geometry_collection_get_bounds(): """Assert #1599 is fixed""" geojson_data = { From 934ab68a50833c4d544bdf5816615db0957400b1 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 25 Apr 2024 22:41:00 +0200 Subject: [PATCH 2/9] Make realtime inherit from Layer --- docs/reference.rst | 1 + folium/elements.py | 9 ++++++--- folium/plugins/realtime.py | 3 +-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 5ed5f7c32..bb4740d20 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -35,6 +35,7 @@ Utilities --------------------- .. autoclass:: folium.utilities.JsCode +.. autoclass:: folium.elements.EventTargetMixin Plugins diff --git a/folium/elements.py b/folium/elements.py index 56965b449..d6f04dfe3 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -55,9 +55,9 @@ class EventTargetMixin(Element): -------- >>> import folium >>> from folium.utilities import JsCode - + >>> >>> m = folium.Map() - + >>> >>> geo_json_data = { ... "type": "FeatureCollection", ... "features": [ @@ -79,8 +79,9 @@ class EventTargetMixin(Element): ... } ... ], ... } - + >>> >>> g = folium.GeoJson(geo_json_data).add_to(m) + >>> >>> highlight = JsCode( ... """ ... function highlight(e) { @@ -89,6 +90,7 @@ class EventTargetMixin(Element): ... } ... """ ... ) + >>> >>> reset = JsCode( ... """ ... function reset(e) { @@ -96,6 +98,7 @@ class EventTargetMixin(Element): ... } ... """ ... ) + >>> >>> g.on(mouseover=highlight, mouseout=reset) ''' diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py index d7f99594d..145623973 100644 --- a/folium/plugins/realtime.py +++ b/folium/plugins/realtime.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin @@ -8,7 +7,7 @@ from folium.utilities import JsCode, camelize, parse_options -class Realtime(JSCSSMixin, MacroElement): +class Realtime(JSCSSMixin, Layer): """Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything. From 1d1511b6983b5e068b73782fd869840232383db4 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 May 2024 16:37:40 +0200 Subject: [PATCH 3/9] Changes after review comments by conengmo --- docs/reference.rst | 2 +- folium/elements.py | 32 ++++++++++---------------------- folium/map.py | 4 ++-- tests/test_features.py | 3 ++- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index bb4740d20..da20b3377 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -35,7 +35,7 @@ Utilities --------------------- .. autoclass:: folium.utilities.JsCode -.. autoclass:: folium.elements.EventTargetMixin +.. autoclass:: folium.elements.EventHandler Plugins diff --git a/folium/elements.py b/folium/elements.py index d6f04dfe3..d017f7589 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -48,9 +48,8 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): default_list.append((name, url)) -class EventTargetMixin(Element): - '''Add Event Handlers to an element. - +class EventHandler(MacroElement): + ''' Examples -------- >>> import folium @@ -85,35 +84,24 @@ class EventTargetMixin(Element): >>> highlight = JsCode( ... """ ... function highlight(e) { - ... e.target.original_color = e.layer.options.color; - ... e.target.setStyle({ color: "green" }); - ... } + ... e.target.original_color = e.layer.options.color; + ... e.target.setStyle({ color: "green" }); + ... } ... """ ... ) >>> >>> reset = JsCode( ... """ - ... function reset(e) { - ... e.target.setStyle({ color: e.target.original_color }); - ... } + ... function reset(e) { + ... e.target.setStyle({ color: e.target.original_color }); + ... } ... """ ... ) >>> - >>> g.on(mouseover=highlight, mouseout=reset) + >>> g.add_child(EventHandler("mouseover", highlight)) + >>> g.add_child(EventHandler("mouseout", reset)) ''' - def on(self, **kwargs: JsCode): - for event, handler in kwargs.items(): - self.add_child(EventHandler(event, handler)) - return self - - def render(self, **kwargs) -> None: - super().render(**kwargs) - - -class EventHandler(MacroElement): - """Render Event Handlers.""" - _template = Template( """ {% macro script(this, kwargs) %} diff --git a/folium/map.py b/folium/map.py index fb00ee72d..01ac7d2ed 100644 --- a/folium/map.py +++ b/folium/map.py @@ -10,7 +10,7 @@ from branca.element import Element, Figure, Html, MacroElement from jinja2 import Template -from folium.elements import ElementAddToElement, EventTargetMixin +from folium.elements import ElementAddToElement from folium.utilities import ( TypeBounds, TypeJsonValue, @@ -21,7 +21,7 @@ ) -class Layer(EventTargetMixin, MacroElement): +class Layer(MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. diff --git a/tests/test_features.py b/tests/test_features.py index 8dd51d61b..94d27d329 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -13,6 +13,7 @@ import folium from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup +from folium.elements import EventHandler from folium.utilities import JsCode @@ -296,7 +297,7 @@ def test_geojson_event_handler(): } """ ) - geojson.on(mouseover=fn) + geojson.add_child(EventHandler("mouseover", fn)) rendered = m.get_root().render() assert fn.js_code in rendered From 5425f6d4432ef582ee3336cc160ef881189a5dfa Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 May 2024 17:03:14 +0200 Subject: [PATCH 4/9] Updates after review comments --- folium/plugins/realtime.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py index 145623973..d7f99594d 100644 --- a/folium/plugins/realtime.py +++ b/folium/plugins/realtime.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin @@ -7,7 +8,7 @@ from folium.utilities import JsCode, camelize, parse_options -class Realtime(JSCSSMixin, Layer): +class Realtime(JSCSSMixin, MacroElement): """Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything. From 6690c5275b40b16c5eecc8f7f661b98f2ffb8df2 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Fri, 24 May 2024 18:42:52 +0200 Subject: [PATCH 5/9] Add extra docstring line --- folium/elements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/folium/elements.py b/folium/elements.py index d017f7589..3d4498517 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -50,6 +50,8 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): class EventHandler(MacroElement): ''' + Add javascript event handlers. + Examples -------- >>> import folium From ee70ad87115c7f4df4058922cbebab18b1ba1191 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 1 Jun 2024 17:35:41 +0200 Subject: [PATCH 6/9] Add Evented class In Leaflet Evented is the parent class of both `L.Map` and `L.Layer`. It adds the `on` method which can be used to add event handlers to a leaflet object. --- folium/folium.py | 6 +++--- folium/map.py | 23 +++++++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/folium/folium.py b/folium/folium.py index a8a0dc8f8..1f32c66db 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -7,11 +7,11 @@ import webbrowser from typing import Any, List, Optional, Sequence, Union -from branca.element import Element, Figure, MacroElement +from branca.element import Element, Figure from jinja2 import Template from folium.elements import JSCSSMixin -from folium.map import FitBounds, Layer +from folium.map import Evented, FitBounds, Layer from folium.raster_layers import TileLayer from folium.utilities import ( TypeBounds, @@ -79,7 +79,7 @@ def __init__(self, no_touch=False, disable_3d=False): self.disable_3d = disable_3d -class Map(JSCSSMixin, MacroElement): +class Map(JSCSSMixin, Evented): """Create a Map with Folium and Leaflet.js Generate a base map of given width and height with either default diff --git a/folium/map.py b/folium/map.py index 01ac7d2ed..e31a39e2d 100644 --- a/folium/map.py +++ b/folium/map.py @@ -10,7 +10,7 @@ from branca.element import Element, Figure, Html, MacroElement from jinja2 import Template -from folium.elements import ElementAddToElement +from folium.elements import ElementAddToElement, EventHandler from folium.utilities import ( TypeBounds, TypeJsonValue, @@ -21,7 +21,26 @@ ) -class Layer(MacroElement): +class Evented(MacroElement): + """The base class for Layer and Map + + Adds the `on` method for event handling capabilities. + + See https://leafletjs.com/reference.html#evented for + more in depth documentation. Please note that we have + only added the `on( eventMap)` variant of this + method using python keyword arguments. + """ + + def __init__(self): + super().__init__() + + def on(self, **event_map): + for event_type, handler in event_map.items(): + self.add_child(EventHandler(event_type, handler)) + + +class Layer(Evented): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. From aea0952ea4cf9712cfc4e21fc132c98e218550e2 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 2 Jun 2024 19:36:52 +0200 Subject: [PATCH 7/9] Update folium/map.py Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> --- folium/map.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/folium/map.py b/folium/map.py index e31a39e2d..4bf76b357 100644 --- a/folium/map.py +++ b/folium/map.py @@ -32,9 +32,6 @@ class Evented(MacroElement): method using python keyword arguments. """ - def __init__(self): - super().__init__() - def on(self, **event_map): for event_type, handler in event_map.items(): self.add_child(EventHandler(event_type, handler)) From d4388fcaa4d8ecee4099ea0851eafb4f3d080d8d Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 2 Jun 2024 19:36:59 +0200 Subject: [PATCH 8/9] Update folium/map.py Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> --- folium/map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folium/map.py b/folium/map.py index 4bf76b357..ea6c66131 100644 --- a/folium/map.py +++ b/folium/map.py @@ -32,7 +32,7 @@ class Evented(MacroElement): method using python keyword arguments. """ - def on(self, **event_map): + def on(self, **event_map: JsCode): for event_type, handler in event_map.items(): self.add_child(EventHandler(event_type, handler)) From a4f5c78aba7c2bf3f9c836d79e8b18edd4a048e0 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 2 Jun 2024 19:37:25 +0200 Subject: [PATCH 9/9] As requested in review comment --- folium/map.py | 1 + 1 file changed, 1 insertion(+) diff --git a/folium/map.py b/folium/map.py index ea6c66131..26a333ca1 100644 --- a/folium/map.py +++ b/folium/map.py @@ -12,6 +12,7 @@ from folium.elements import ElementAddToElement, EventHandler from folium.utilities import ( + JsCode, TypeBounds, TypeJsonValue, camelize,