Skip to content

Commit 032537c

Browse files
author
Martin Journois
committed
timestampedGeoJson plugin based on Leaflet.Timedimension
1 parent 521c0c5 commit 032537c

File tree

6 files changed

+200
-1
lines changed

6 files changed

+200
-1
lines changed

folium/plugins/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
from .terminator import Terminator
1111
from .boat_marker import BoatMarker
1212
from .layer import Layer, LayerControl
13-
from .geo_json import GeoJson
13+
from .geo_json import GeoJson
14+
from .timestamped_geo_json import TimestampedGeoJson

folium/plugins/plugin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
"""
1010
from uuid import uuid4
1111

12+
from jinja2 import Environment, PackageLoader
13+
ENV = Environment(loader=PackageLoader('folium', 'plugins'))
14+
1215
class Plugin(object):
1316
"""Basic plugin object that does nothing.
1417
Other plugins may inherit from this one."""
1518
def __init__(self):
1619
"""Creates a plugin to append into a map with Map.add_plugin. """
1720
self.plugin_name = 'Plugin'
1821
self.object_name = uuid4().hex
22+
self.env = ENV
1923

2024
def add_to_map(self, map):
2125
"""Adds the plugin on a folium.map object."""
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
TimestampedGeoJson plugin
4+
--------------
5+
6+
Add a timestamped geojson feature collection on a folium map.
7+
This is based on Leaflet.TimeDimension (see https://github.com/socib/Leaflet.TimeDimension).
8+
9+
A geo-json is timestamped if :
10+
* it contains only features of types LineString, MultiPoint, MultiLineString and MultiPolygon.
11+
* each feature has a "times" property with the same length as the coordinates array.
12+
* each element of each "times" property is a timestamp in ms since epoch, or in ISO string.
13+
Eventually, you may have Point features with a "times" property being an array of length 1.
14+
"""
15+
import json
16+
17+
from .plugin import Plugin
18+
19+
class TimestampedGeoJson(Plugin):
20+
"""Adds a TimestampedGeoJson layer on the map."""
21+
def __init__(self, data, transition_time=200, loop=True, auto_play=True):
22+
"""Creates a TimestampedGeoJson plugin to append into a map with
23+
Map.add_plugin.
24+
25+
Parameters
26+
----------
27+
data: file, dict or str.
28+
The timestamped geo-json data you want to plot.
29+
30+
If file, then data will be read in the file and fully embeded in Leaflet's javascript.
31+
If dict, then data will be converted to json and embeded in the javascript.
32+
If str, then data will be passed to the javascript as-is.
33+
34+
A geo-json is timestamped if :
35+
* it contains only features of types LineString, MultiPoint, MultiLineString and MultiPolygon.
36+
* each feature has a "times" property with the same length as the coordinates array.
37+
* each element of each "times" property is a timestamp in ms since epoch, or in ISO string.
38+
Eventually, you may have Point features with a "times" property being an array of length 1.
39+
40+
examples :
41+
# providing file
42+
TimestampedGeoJson(open('foo.json'))
43+
44+
# providing dict
45+
TimestampedGeoJson({
46+
"type": "FeatureCollection",
47+
"features": [
48+
{
49+
"type": "Feature",
50+
"geometry": {
51+
"type": "LineString",
52+
"coordinates": [[-70,-25],[-70,35],[70,35]],
53+
},
54+
"properties": {
55+
"times": [1435708800000, 1435795200000, 1435881600000]
56+
}
57+
}
58+
]
59+
})
60+
61+
# providing string
62+
TimestampedGeoJson(open('foo.json').read())
63+
transition_time : int, default 200.
64+
The duration in ms of a transition from one timestamp to another.
65+
loop : bool, default True
66+
Whether the animation shall loop.
67+
auto_play : bool, default True
68+
Whether the animation shall start automatically at startup.
69+
70+
"""
71+
super(TimestampedGeoJson, self).__init__()
72+
self.plugin_name = 'TimestampedGeoJson'
73+
self.template = self.env.get_template('timestamped_geo_json.tpl')
74+
if 'read' in dir(data):
75+
self.data = data.read()
76+
elif type(data) is dict:
77+
self.data = json.dumps(data)
78+
else:
79+
self.data = data
80+
self.transition_time = int(transition_time)
81+
self.loop = bool(loop)
82+
self.auto_play = bool(auto_play)
83+
84+
def render_header(self, nb):
85+
"""Generates the header part of the plugin."""
86+
header = self.template.module.__dict__.get('header',None)
87+
assert header is not None, "This template must have a 'header' macro."
88+
return header(nb)
89+
def render_js(self, nb):
90+
"""Generates the Javascript part of the plugin."""
91+
js = self.template.module.__dict__.get('js',None)
92+
assert js is not None, "This template must have a 'js' macro."
93+
return js(nb,self)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% macro header(nb) %}
2+
{% if nb==0 %}
3+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css">
4+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
5+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"></script>
6+
7+
<!-- iso8601 -->
8+
<script type="text/javascript" src="https://raw.githubusercontent.com/nezasa/iso8601-js-period/master/iso8601.min.js"></script>
9+
10+
<!-- leaflet.timedimension.min.js -->
11+
<script type="text/javascript" src="https://raw.githubusercontent.com/socib/Leaflet.TimeDimension/master/dist/leaflet.timedimension.min.js"></script>
12+
13+
<!-- leaflet.timedimension.control.min.css -->
14+
<link rel="stylesheet" href="http://apps.socib.es/Leaflet.TimeDimension/dist/leaflet.timedimension.control.min.css" />
15+
{% endif %}
16+
{% endmacro %}
17+
18+
{% macro js(nb,self) %}
19+
{% if nb==0 %}
20+
map.timeDimension = L.timeDimension();
21+
map.timeDimensionControl = L.control.timeDimension({
22+
position: 'bottomleft',
23+
autoPlay: {{'true' if self.auto_play else 'false'}},
24+
playerOptions: {transitionTime: {{self.transition_time}},loop: {{'true' if self.loop else 'false'}}}
25+
});
26+
map.addControl(map.timeDimensionControl);
27+
{% endif %}
28+
29+
var tsgeojson_{{nb}} = L.timeDimension.layer.geoJson(L.geoJson({{self.data}}),
30+
{updateTimeDimension: true,addlastPoint: true}).addTo(map);
31+
{% endmacro %}

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def walk_subpkg(name):
4040

4141
pkg_data = {'': ['*.js',
4242
'plugins/*.js',
43+
'plugins/*.html',
44+
'plugins/*.css',
45+
'plugins/*.tpl',
4346
'templates/*.html',
4447
'templates/*.js',
4548
'templates/*.txt'] + walk_subpkg('templates/tiles')}

tests/test_plugins.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,70 @@ def test_geo_json(self):
104104
mapd.add_plugin(plugins.GeoJson(open('geojson_plugin_test2.json')))
105105
mapd._build_map()
106106

107+
def test_timestamped_geo_json(self):
108+
data = {
109+
"type": "FeatureCollection",
110+
"features": [
111+
{
112+
"type": "Feature",
113+
"geometry": {
114+
"type": "Point",
115+
"coordinates": [0,0],
116+
},
117+
"properties": {
118+
"times": [1435708800000+12*86400000]
119+
}
120+
},
121+
{
122+
"type": "Feature",
123+
"geometry": {
124+
"type": "MultiPoint",
125+
"coordinates": [[lon,-25] for lon in np.linspace(-150,150,49)],
126+
},
127+
"properties": {
128+
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,49)]
129+
}
130+
},
131+
{
132+
"type": "Feature",
133+
"geometry": {
134+
"type": "LineString",
135+
"coordinates": [[lon,25] for lon in np.linspace(-150,150,25)],
136+
},
137+
"properties": {
138+
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,25)]
139+
}
140+
},
141+
{
142+
"type": "Feature",
143+
"geometry": {
144+
"type": "MultiLineString",
145+
"coordinates": [[[lon-4*np.sin(theta),47+3*np.cos(theta)]\
146+
for theta in np.linspace(0,2*np.pi,25)]\
147+
for lon in np.linspace(-150,150,13)],
148+
},
149+
"properties": {
150+
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,13)]
151+
}
152+
},
153+
{
154+
"type": "Feature",
155+
"geometry": {
156+
"type": "MultiPolygon",
157+
"coordinates": [[[[lon-8*np.sin(theta),-47+6*np.cos(theta)]\
158+
for theta in np.linspace(0,2*np.pi,25)],
159+
[[lon-4*np.sin(theta),-47+3*np.cos(theta)]\
160+
for theta in np.linspace(0,2*np.pi,25)]]\
161+
for lon in np.linspace(-150,150,7)],
162+
},
163+
"properties": {
164+
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,7)]
165+
}
166+
},
167+
],
168+
}
169+
170+
mape = folium.Map([47,3], zoom_start=1)
171+
mape.add_plugin(plugins.TimestampedGeoJson(data))
172+
mape._build_map()
173+

0 commit comments

Comments
 (0)