Skip to content

Commit 5ae326e

Browse files
author
Martin Journois
committed
Leaflet.Timedimension ; a first shot
1 parent 521c0c5 commit 5ae326e

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-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
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
if 'read' in dir(data):
74+
self.data = data.read()
75+
elif type(data) is dict:
76+
self.data = json.dumps(data)
77+
else:
78+
self.data = data
79+
self.transition_time = int(transition_time)
80+
self.loop = bool(loop)
81+
self.auto_play = bool(auto_play)
82+
83+
def render_header(self, nb):
84+
"""Generates the header part of the plugin."""
85+
return """
86+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css">
87+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
88+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"></script>
89+
90+
<!-- iso8601 -->
91+
<script type="text/javascript" src="https://raw.githubusercontent.com/nezasa/iso8601-js-period/master/iso8601.min.js"></script>
92+
93+
<!-- leaflet.timedimension.min.js -->
94+
<script type="text/javascript" src="https://raw.githubusercontent.com/socib/Leaflet.TimeDimension/master/dist/leaflet.timedimension.min.js"></script>
95+
96+
<!-- leaflet.timedimension.control.min.css -->
97+
<link rel="stylesheet" href="http://apps.socib.es/Leaflet.TimeDimension/dist/leaflet.timedimension.control.min.css" />
98+
""" if nb==0 else ""
99+
def render_js(self, nb):
100+
"""Generates the Javascript part of the plugin."""
101+
out = """
102+
map.timeDimension = L.timeDimension();
103+
map.timeDimensionControl = L.control.timeDimension({
104+
position: 'bottomleft',
105+
autoPlay: %s,
106+
playerOptions: {transitionTime: %i,loop: %s}
107+
});
108+
map.addControl(map.timeDimensionControl);
109+
"""%(str(self.auto_play).lower(),self.transition_time,str(self.loop).lower()) if nb==0 else ""
110+
111+
out += """
112+
var tsgeojson_%i = L.timeDimension.layer.geoJson(L.geoJson(%s),
113+
{updateTimeDimension: true,addlastPoint: true}).addTo(map);
114+
""" % (nb, self.data)
115+
return out

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)