Skip to content

Commit

Permalink
Add leaflet-realtime plugin (python-visualization#1848)
Browse files Browse the repository at this point in the history
* Implemented the leaflet-realtime plugin

Based on: https://github.com/perliedman/leaflet-realtime

* Fix for failing pre-commit hooks in origin

* Updated after review comments

* Add documentation for the realtime plugin

* Also update TOC

* Fix layout

* remove noqa

* don't use `options` var name

* use default arguments, add typing

* Update JsCode docstring for in docs

* Add JsCode to docs

* remove parameters from docs

* slight tweaks to docs

* import JsCode in init

---------

Co-authored-by: Frank <[email protected]>
  • Loading branch information
hansthen and Conengmo committed Jan 24, 2024
1 parent 7c084f1 commit 32624ce
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ Other map features
.. automodule:: folium.features


Utilities
---------------------

.. autoclass:: folium.utilities.JsCode


Plugins
--------------------
.. automodule:: folium.plugins
1 change: 1 addition & 0 deletions docs/user_guide/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 deletions docs/user_guide/plugins/realtime.md
Original file line number Diff line number Diff line change
@@ -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
```
2 changes: 2 additions & 0 deletions folium/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -79,6 +80,7 @@
"IFrame",
"Icon",
"JavascriptLink",
"JsCode",
"LatLngPopup",
"LayerControl",
"LinearColormap",
Expand Down
2 changes: 2 additions & 0 deletions folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,6 +55,7 @@
"MousePosition",
"PolyLineTextPath",
"PolyLineOffset",
"Realtime",
"ScrollZoomToggler",
"Search",
"SemiCircle",
Expand Down
122 changes: 122 additions & 0 deletions folium/plugins/realtime.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions folium/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 32624ce

Please sign in to comment.