Skip to content

Commit b8f1373

Browse files
hansthenConengmo
andauthored
Add leaflet-realtime plugin (#1848)
* 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]>
1 parent b4982e4 commit b8f1373

File tree

7 files changed

+250
-0
lines changed

7 files changed

+250
-0
lines changed

docs/reference.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ Other map features
3131
.. automodule:: folium.features
3232

3333

34+
Utilities
35+
---------------------
36+
37+
.. autoclass:: folium.utilities.JsCode
38+
39+
3440
Plugins
3541
--------------------
3642
.. automodule:: folium.plugins

docs/user_guide/plugins.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Plugins
2323
plugins/pattern
2424
plugins/polyline_offset
2525
plugins/polyline_textpath_and_antpath
26+
plugins/realtime
2627
plugins/scroll_zoom_toggler
2728
plugins/search
2829
plugins/semi_circle
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
```{code-cell} ipython3
2+
---
3+
nbsphinx: hidden
4+
---
5+
import folium
6+
import folium.plugins
7+
```
8+
9+
# Realtime plugin
10+
11+
Put realtime data on a Leaflet map: live tracking GPS units,
12+
sensor data or just about anything.
13+
14+
Based on: https://github.com/perliedman/leaflet-realtime
15+
16+
This plugin functions much like an `L.GeoJson` layer, for
17+
which the geojson data is periodically polled from a url.
18+
19+
20+
## Simple example
21+
22+
In this example we use a static geojson, whereas normally you would have a
23+
url that actually updates in real time.
24+
25+
```{code-cell} ipython3
26+
from folium import JsCode
27+
m = folium.Map(location=[40.73, -73.94], zoom_start=12)
28+
rt = folium.plugins.Realtime(
29+
"https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson",
30+
get_feature_id=JsCode("(f) => { return f.properties.objectid; }"),
31+
interval=10000,
32+
)
33+
rt.add_to(m)
34+
m
35+
```
36+
37+
38+
## Javascript function as source
39+
40+
For more complicated scenarios, such as when the underlying data source does not return geojson, you can
41+
write a javascript function for the `source` parameter. In this example we track the location of the
42+
International Space Station using a public API.
43+
44+
45+
```{code-cell} ipython3
46+
import folium
47+
from folium.plugins import Realtime
48+
49+
m = folium.Map()
50+
51+
source = folium.JsCode("""
52+
function(responseHandler, errorHandler) {
53+
var url = 'https://api.wheretheiss.at/v1/satellites/25544';
54+
55+
fetch(url)
56+
.then((response) => {
57+
return response.json().then((data) => {
58+
var { id, longitude, latitude } = data;
59+
60+
return {
61+
'type': 'FeatureCollection',
62+
'features': [{
63+
'type': 'Feature',
64+
'geometry': {
65+
'type': 'Point',
66+
'coordinates': [longitude, latitude]
67+
},
68+
'properties': {
69+
'id': id
70+
}
71+
}]
72+
};
73+
})
74+
})
75+
.then(responseHandler)
76+
.catch(errorHandler);
77+
}
78+
""")
79+
80+
rt = Realtime(source, interval=10000)
81+
rt.add_to(m)
82+
83+
m
84+
```
85+
86+
87+
## Customizing the layer
88+
89+
The leaflet-realtime plugin typically uses an `L.GeoJson` layer to show the data. This
90+
means that you can also pass parameters which you would typically pass to an
91+
`L.GeoJson` layer. With this knowledge we can change the first example to display
92+
`L.CircleMarker` objects.
93+
94+
```{code-cell} ipython3
95+
import folium
96+
from folium import JsCode
97+
from folium.plugins import Realtime
98+
99+
m = folium.Map(location=[40.73, -73.94], zoom_start=12)
100+
source = "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson"
101+
102+
Realtime(
103+
source,
104+
get_feature_id=JsCode("(f) => { return f.properties.objectid }"),
105+
point_to_layer=JsCode("(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}"),
106+
interval=10000,
107+
).add_to(m)
108+
109+
m
110+
```

folium/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
Tooltip,
4141
)
4242
from folium.raster_layers import TileLayer, WmsTileLayer
43+
from folium.utilities import JsCode
4344
from folium.vector_layers import Circle, CircleMarker, Polygon, PolyLine, Rectangle
4445

4546
try:
@@ -79,6 +80,7 @@
7980
"IFrame",
8081
"Icon",
8182
"JavascriptLink",
83+
"JsCode",
8284
"LatLngPopup",
8385
"LayerControl",
8486
"LinearColormap",

folium/plugins/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from folium.plugins.pattern import CirclePattern, StripePattern
2222
from folium.plugins.polyline_offset import PolyLineOffset
2323
from folium.plugins.polyline_text_path import PolyLineTextPath
24+
from folium.plugins.realtime import Realtime
2425
from folium.plugins.scroll_zoom_toggler import ScrollZoomToggler
2526
from folium.plugins.search import Search
2627
from folium.plugins.semicircle import SemiCircle
@@ -54,6 +55,7 @@
5455
"MousePosition",
5556
"PolyLineTextPath",
5657
"PolyLineOffset",
58+
"Realtime",
5759
"ScrollZoomToggler",
5860
"Search",
5961
"SemiCircle",

folium/plugins/realtime.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from typing import Optional, Union
2+
3+
from branca.element import MacroElement
4+
from jinja2 import Template
5+
6+
from folium.elements import JSCSSMixin
7+
from folium.utilities import JsCode, camelize, parse_options
8+
9+
10+
class Realtime(JSCSSMixin, MacroElement):
11+
"""Put realtime data on a Leaflet map: live tracking GPS units,
12+
sensor data or just about anything.
13+
14+
Based on: https://github.com/perliedman/leaflet-realtime
15+
16+
Parameters
17+
----------
18+
source: str, dict, JsCode
19+
The source can be one of:
20+
21+
* a string with the URL to get data from
22+
* a dict that is passed to javascript's `fetch` function
23+
for fetching the data
24+
* a `folium.JsCode` object in case you need more freedom.
25+
start: bool, default True
26+
Should automatic updates be enabled when layer is added
27+
on the map and stopped when layer is removed from the map
28+
interval: int, default 60000
29+
Automatic update interval, in milliseconds
30+
get_feature_id: JsCode, optional
31+
A JS function with a geojson `feature` as parameter
32+
default returns `feature.properties.id`
33+
Function to get an identifier to uniquely identify a feature over time
34+
update_feature: JsCode, optional
35+
A JS function with a geojson `feature` as parameter
36+
Used to update an existing feature's layer;
37+
by default, points (markers) are updated, other layers are discarded
38+
and replaced with a new, updated layer.
39+
Allows to create more complex transitions,
40+
for example, when a feature is updated
41+
remove_missing: bool, default False
42+
Should missing features between updates been automatically
43+
removed from the layer
44+
45+
46+
Other keyword arguments are passed to the GeoJson layer, so you can pass
47+
`style`, `point_to_layer` and/or `on_each_feature`.
48+
49+
Examples
50+
--------
51+
>>> from folium import JsCode
52+
>>> m = folium.Map(location=[40.73, -73.94], zoom_start=12)
53+
>>> rt = Realtime(
54+
... "https://raw.githubusercontent.com/python-visualization/folium-example-data/main/subway_stations.geojson",
55+
... get_feature_id=JsCode("(f) => { return f.properties.objectid; }"),
56+
... point_to_layer=JsCode(
57+
... "(f, latlng) => { return L.circleMarker(latlng, {radius: 8, fillOpacity: 0.2})}"
58+
... ),
59+
... interval=10000,
60+
... )
61+
>>> rt.add_to(m)
62+
"""
63+
64+
_template = Template(
65+
"""
66+
{% macro script(this, kwargs) %}
67+
var {{ this.get_name() }}_options = {{ this.options|tojson }};
68+
{% for key, value in this.functions.items() %}
69+
{{ this.get_name() }}_options["{{key}}"] = {{ value }};
70+
{% endfor %}
71+
72+
var {{ this.get_name() }} = new L.realtime(
73+
{% if this.src is string or this.src is mapping -%}
74+
{{ this.src|tojson }},
75+
{% else -%}
76+
{{ this.src.js_code }},
77+
{% endif -%}
78+
{{ this.get_name() }}_options
79+
);
80+
{{ this._parent.get_name() }}.addLayer(
81+
{{ this.get_name() }}._container);
82+
{% endmacro %}
83+
"""
84+
)
85+
86+
default_js = [
87+
(
88+
"Leaflet_Realtime_js",
89+
"https://cdnjs.cloudflare.com/ajax/libs/leaflet-realtime/2.2.0/leaflet-realtime.js",
90+
)
91+
]
92+
93+
def __init__(
94+
self,
95+
source: Union[str, dict, JsCode],
96+
start: bool = True,
97+
interval: int = 60000,
98+
get_feature_id: Optional[JsCode] = None,
99+
update_feature: Optional[JsCode] = None,
100+
remove_missing: bool = False,
101+
**kwargs
102+
):
103+
super().__init__()
104+
self._name = "Realtime"
105+
self.src = source
106+
107+
kwargs["start"] = start
108+
kwargs["interval"] = interval
109+
if get_feature_id is not None:
110+
kwargs["get_feature_id"] = get_feature_id
111+
if update_feature is not None:
112+
kwargs["update_feature"] = update_feature
113+
kwargs["remove_missing"] = remove_missing
114+
115+
# extract JsCode objects
116+
self.functions = {}
117+
for key, value in list(kwargs.items()):
118+
if isinstance(value, JsCode):
119+
self.functions[camelize(key)] = value.js_code
120+
kwargs.pop(key)
121+
122+
self.options = parse_options(**kwargs)

folium/utilities.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,10 @@ def get_and_assert_figure_root(obj: Element) -> Figure:
410410
figure, Figure
411411
), "You cannot render this Element if it is not in a Figure."
412412
return figure
413+
414+
415+
class JsCode:
416+
"""Wrapper around Javascript code."""
417+
418+
def __init__(self, js_code: str):
419+
self.js_code = js_code

0 commit comments

Comments
 (0)