Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add leaflet-realtime plugin #1848

Merged
merged 14 commits into from
Jan 2, 2024
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);
Conengmo marked this conversation as resolved.
Show resolved Hide resolved
{% 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