From 964cf97fb1959eec4562be6b8e72b0c1688d18f9 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Wed, 6 Mar 2024 09:16:40 +0100 Subject: [PATCH] Add a custom container to the realtime plugin (#1869) * Add cluster override * Add tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add container argument to documentation * Fix incorrect import * Update realtime.py No idea why this was gone in this branch. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/user_guide/plugins/realtime.md | 24 +++++++++++ folium/plugins/realtime.py | 16 ++++++- tests/plugins/test_realtime.py | 66 +++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 tests/plugins/test_realtime.py diff --git a/docs/user_guide/plugins/realtime.md b/docs/user_guide/plugins/realtime.md index 7e5d315c5..8658851ab 100644 --- a/docs/user_guide/plugins/realtime.md +++ b/docs/user_guide/plugins/realtime.md @@ -108,3 +108,27 @@ Realtime( m ``` + +## Using a MarkerCluster as a container + +The subway stations in the previous example are not easy to distinguish at lower zoom levels. +It is possible to use a custom container for the GeoJson. In this example we use a MarkerCluster. +```{code-cell} ipython3 +import folium +from folium import JsCode +from folium.plugins import Realtime, MarkerCluster + +m = folium.Map(location=[40.73, -73.94], zoom_start=12) +source = "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson" + +container = MarkerCluster().add_to(m) +Realtime( + source, + get_feature_id=JsCode("(f) => { return f.properties.objectid }"), + point_to_layer=JsCode("(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}"), + container=container, + interval=10000, +).add_to(m) + +m +``` diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py index fe56de7c6..b59f40af8 100644 --- a/folium/plugins/realtime.py +++ b/folium/plugins/realtime.py @@ -1,9 +1,10 @@ -from typing import Union +from typing import Optional, Union from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin +from folium.map import Layer from folium.utilities import JsCode, camelize, parse_options @@ -41,7 +42,11 @@ class Realtime(JSCSSMixin, MacroElement): remove_missing: bool, default False Should missing features between updates been automatically removed from the layer - + container: Layer, default GeoJson + The container will typically be a `FeatureGroup`, `MarkerCluster` or + `GeoJson`, but it can be anything that generates a javascript + L.LayerGroup object, i.e. something that has the methods + `addLayer` and `removeLayer`. Other keyword arguments are passed to the GeoJson layer, so you can pass `style`, `point_to_layer` and/or `on_each_feature`. Make sure to wrap @@ -70,6 +75,11 @@ class Realtime(JSCSSMixin, MacroElement): {{ this.get_name() }}_options["{{key}}"] = {{ value }}; {% endfor %} + {% if this.container -%} + {{ this.get_name() }}_options["container"] + = {{ this.container.get_name() }}; + {% endif -%} + var {{ this.get_name() }} = new L.realtime( {% if this.src is string or this.src is mapping -%} {{ this.src|tojson }}, @@ -99,11 +109,13 @@ def __init__( get_feature_id: Union[JsCode, str, None] = None, update_feature: Union[JsCode, str, None] = None, remove_missing: bool = False, + container: Optional[Layer] = None, **kwargs ): super().__init__() self._name = "Realtime" self.src = source + self.container = container kwargs["start"] = start kwargs["interval"] = interval diff --git a/tests/plugins/test_realtime.py b/tests/plugins/test_realtime.py new file mode 100644 index 000000000..03bfb7716 --- /dev/null +++ b/tests/plugins/test_realtime.py @@ -0,0 +1,66 @@ +""" +Test Realtime +------------------ +""" + +from jinja2 import Template + +import folium +from folium.plugins import MarkerCluster, Realtime +from folium.utilities import JsCode, normalize + + +def test_realtime(): + m = folium.Map(location=[40.73, -73.94], zoom_start=12) + source = "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson" + + container = MarkerCluster().add_to(m) + + rt = Realtime( + source, + get_feature_id=JsCode("(f) => { return f.properties.objectid }"), + point_to_layer=JsCode( + "(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}" + ), + container=container, + interval=10000, + ) + rt.add_to(m) + + tmpl_for_expected = Template( + """ + {% macro script(this, kwargs) %} + var {{ this.get_name() }}_options = {{ this.options|tojson }}; + {% for key, value in this.functions.items() %} + {{ this.get_name() }}_options["{{key}}"] = {{ value }}; + {% endfor %} + + {% if this.container -%} + {{ this.get_name() }}_options["container"] + = {{ this.container.get_name() }}; + {% endif -%} + + var {{ this.get_name() }} = new L.realtime( + {% if this.src is string or this.src is mapping -%} + {{ this.src|tojson }}, + {% else -%} + {{ this.src.js_code }}, + {% endif -%} + {{ this.get_name() }}_options + ); + {{ this._parent.get_name() }}.addLayer( + {{ this.get_name() }}._container); + {% endmacro %} + """ + ) + expected = normalize(tmpl_for_expected.render(this=rt)) + + out = normalize(m._parent.render()) + + # We verify that imports + assert ( + '' # noqa + in out + ) # noqa + + assert expected in out