diff --git a/docs/reference.rst b/docs/reference.rst index 0b3c7d9c7..5ed5f7c32 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -31,6 +31,12 @@ Other map features .. automodule:: folium.features +Utilities +--------------------- + +.. autoclass:: folium.utilities.JsCode + + Plugins -------------------- .. automodule:: folium.plugins diff --git a/docs/user_guide/plugins.rst b/docs/user_guide/plugins.rst index 00c3bd246..e8b372608 100644 --- a/docs/user_guide/plugins.rst +++ b/docs/user_guide/plugins.rst @@ -23,6 +23,7 @@ Plugins plugins/pattern plugins/polyline_offset plugins/polyline_textpath_and_antpath + plugins/realtime plugins/scroll_zoom_toggler plugins/search plugins/semi_circle diff --git a/docs/user_guide/plugins/realtime.md b/docs/user_guide/plugins/realtime.md new file mode 100644 index 000000000..7e5d315c5 --- /dev/null +++ b/docs/user_guide/plugins/realtime.md @@ -0,0 +1,110 @@ +```{code-cell} ipython3 +--- +nbsphinx: hidden +--- +import folium +import folium.plugins +``` + +# Realtime plugin + +Put realtime data on a Leaflet map: live tracking GPS units, +sensor data or just about anything. + +Based on: https://github.com/perliedman/leaflet-realtime + +This plugin functions much like an `L.GeoJson` layer, for +which the geojson data is periodically polled from a url. + + +## Simple example + +In this example we use a static geojson, whereas normally you would have a +url that actually updates in real time. + +```{code-cell} ipython3 +from folium import JsCode +m = folium.Map(location=[40.73, -73.94], zoom_start=12) +rt = folium.plugins.Realtime( + "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson", + get_feature_id=JsCode("(f) => { return f.properties.objectid; }"), + interval=10000, +) +rt.add_to(m) +m +``` + + +## Javascript function as source + +For more complicated scenarios, such as when the underlying data source does not return geojson, you can +write a javascript function for the `source` parameter. In this example we track the location of the +International Space Station using a public API. + + +```{code-cell} ipython3 +import folium +from folium.plugins import Realtime + +m = folium.Map() + +source = folium.JsCode(""" + function(responseHandler, errorHandler) { + var url = 'https://api.wheretheiss.at/v1/satellites/25544'; + + fetch(url) + .then((response) => { + return response.json().then((data) => { + var { id, longitude, latitude } = data; + + return { + 'type': 'FeatureCollection', + 'features': [{ + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [longitude, latitude] + }, + 'properties': { + 'id': id + } + }] + }; + }) + }) + .then(responseHandler) + .catch(errorHandler); + } +""") + +rt = Realtime(source, interval=10000) +rt.add_to(m) + +m +``` + + +## Customizing the layer + +The leaflet-realtime plugin typically uses an `L.GeoJson` layer to show the data. This +means that you can also pass parameters which you would typically pass to an +`L.GeoJson` layer. With this knowledge we can change the first example to display +`L.CircleMarker` objects. + +```{code-cell} ipython3 +import folium +from folium import JsCode +from folium.plugins import 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" + +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})}"), + interval=10000, +).add_to(m) + +m +``` diff --git a/folium/__init__.py b/folium/__init__.py index 305e9e98b..01a0b6607 100644 --- a/folium/__init__.py +++ b/folium/__init__.py @@ -40,6 +40,7 @@ Tooltip, ) from folium.raster_layers import TileLayer, WmsTileLayer +from folium.utilities import JsCode from folium.vector_layers import Circle, CircleMarker, Polygon, PolyLine, Rectangle try: @@ -79,6 +80,7 @@ "IFrame", "Icon", "JavascriptLink", + "JsCode", "LatLngPopup", "LayerControl", "LinearColormap", diff --git a/folium/plugins/__init__.py b/folium/plugins/__init__.py index f4f168e2b..93d2bfc14 100644 --- a/folium/plugins/__init__.py +++ b/folium/plugins/__init__.py @@ -21,6 +21,7 @@ from folium.plugins.pattern import CirclePattern, StripePattern from folium.plugins.polyline_offset import PolyLineOffset from folium.plugins.polyline_text_path import PolyLineTextPath +from folium.plugins.realtime import Realtime from folium.plugins.scroll_zoom_toggler import ScrollZoomToggler from folium.plugins.search import Search from folium.plugins.semicircle import SemiCircle @@ -54,6 +55,7 @@ "MousePosition", "PolyLineTextPath", "PolyLineOffset", + "Realtime", "ScrollZoomToggler", "Search", "SemiCircle", diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py new file mode 100644 index 000000000..429a40955 --- /dev/null +++ b/folium/plugins/realtime.py @@ -0,0 +1,122 @@ +from typing import Optional, Union + +from branca.element import MacroElement +from jinja2 import Template + +from folium.elements import JSCSSMixin +from folium.utilities import JsCode, camelize, parse_options + + +class Realtime(JSCSSMixin, MacroElement): + """Put realtime data on a Leaflet map: live tracking GPS units, + sensor data or just about anything. + + Based on: https://github.com/perliedman/leaflet-realtime + + Parameters + ---------- + source: str, dict, JsCode + The source can be one of: + + * a string with the URL to get data from + * a dict that is passed to javascript's `fetch` function + for fetching the data + * a `folium.JsCode` object in case you need more freedom. + start: bool, default True + Should automatic updates be enabled when layer is added + on the map and stopped when layer is removed from the map + interval: int, default 60000 + Automatic update interval, in milliseconds + get_feature_id: JsCode, optional + A JS function with a geojson `feature` as parameter + default returns `feature.properties.id` + Function to get an identifier to uniquely identify a feature over time + update_feature: JsCode, optional + A JS function with a geojson `feature` as parameter + Used to update an existing feature's layer; + by default, points (markers) are updated, other layers are discarded + and replaced with a new, updated layer. + Allows to create more complex transitions, + for example, when a feature is updated + remove_missing: bool, default False + Should missing features between updates been automatically + removed from the layer + + + Other keyword arguments are passed to the GeoJson layer, so you can pass + `style`, `point_to_layer` and/or `on_each_feature`. + + Examples + -------- + >>> from folium import JsCode + >>> m = folium.Map(location=[40.73, -73.94], zoom_start=12) + >>> rt = Realtime( + ... "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson", + ... get_feature_id=JsCode("(f) => { return f.properties.objectid; }"), + ... point_to_layer=JsCode( + ... "(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}" + ... ), + ... interval=10000, + ... ) + >>> rt.add_to(m) + """ + + _template = 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 %} + + 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 %} + """ + ) + + default_js = [ + ( + "Leaflet_Realtime_js", + "https://cdnjs.cloudflare.com/ajax/libs/leaflet-realtime/2.2.0/leaflet-realtime.js", + ) + ] + + def __init__( + self, + source: Union[str, dict, JsCode], + start: bool = True, + interval: int = 60000, + get_feature_id: Optional[JsCode] = None, + update_feature: Optional[JsCode] = None, + remove_missing: bool = False, + **kwargs + ): + super().__init__() + self._name = "Realtime" + self.src = source + + kwargs["start"] = start + kwargs["interval"] = interval + if get_feature_id is not None: + kwargs["get_feature_id"] = get_feature_id + if update_feature is not None: + kwargs["update_feature"] = update_feature + kwargs["remove_missing"] = remove_missing + + # extract JsCode objects + self.functions = {} + for key, value in list(kwargs.items()): + if isinstance(value, JsCode): + self.functions[camelize(key)] = value.js_code + kwargs.pop(key) + + self.options = parse_options(**kwargs) diff --git a/folium/utilities.py b/folium/utilities.py index 942206930..afc4fc531 100644 --- a/folium/utilities.py +++ b/folium/utilities.py @@ -410,3 +410,10 @@ def get_and_assert_figure_root(obj: Element) -> Figure: figure, Figure ), "You cannot render this Element if it is not in a Figure." return figure + + +class JsCode: + """Wrapper around Javascript code.""" + + def __init__(self, js_code: str): + self.js_code = js_code