Skip to content

Leaflet.Timedimension ; a first shot #156

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

Merged
merged 1 commit into from
Aug 1, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion folium/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
from .terminator import Terminator
from .boat_marker import BoatMarker
from .layer import Layer, LayerControl
from .geo_json import GeoJson
from .geo_json import GeoJson
from .timestamped_geo_json import TimestampedGeoJson
4 changes: 4 additions & 0 deletions folium/plugins/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
"""
from uuid import uuid4

from jinja2 import Environment, PackageLoader
ENV = Environment(loader=PackageLoader('folium', 'plugins'))

class Plugin(object):
"""Basic plugin object that does nothing.
Other plugins may inherit from this one."""
def __init__(self):
"""Creates a plugin to append into a map with Map.add_plugin. """
self.plugin_name = 'Plugin'
self.object_name = uuid4().hex
self.env = ENV

def add_to_map(self, map):
"""Adds the plugin on a folium.map object."""
Expand Down
93 changes: 93 additions & 0 deletions folium/plugins/timestamped_geo_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
TimestampedGeoJson plugin
--------------

Add a timestamped geojson feature collection on a folium map.
This is based on Leaflet.TimeDimension (see https://github.com/socib/Leaflet.TimeDimension).

A geo-json is timestamped if :
* it contains only features of types LineString, MultiPoint, MultiLineString and MultiPolygon.
* each feature has a "times" property with the same length as the coordinates array.
* each element of each "times" property is a timestamp in ms since epoch, or in ISO string.
Eventually, you may have Point features with a "times" property being an array of length 1.
"""
import json

from .plugin import Plugin

class TimestampedGeoJson(Plugin):
"""Adds a TimestampedGeoJson layer on the map."""
def __init__(self, data, transition_time=200, loop=True, auto_play=True):
"""Creates a TimestampedGeoJson plugin to append into a map with
Map.add_plugin.

Parameters
----------
data: file, dict or str.
The timestamped geo-json data you want to plot.

If file, then data will be read in the file and fully embeded in Leaflet's javascript.
If dict, then data will be converted to json and embeded in the javascript.
If str, then data will be passed to the javascript as-is.

A geo-json is timestamped if :
* it contains only features of types LineString, MultiPoint, MultiLineString and MultiPolygon.
* each feature has a "times" property with the same length as the coordinates array.
* each element of each "times" property is a timestamp in ms since epoch, or in ISO string.
Eventually, you may have Point features with a "times" property being an array of length 1.

examples :
# providing file
TimestampedGeoJson(open('foo.json'))

# providing dict
TimestampedGeoJson({
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-70,-25],[-70,35],[70,35]],
},
"properties": {
"times": [1435708800000, 1435795200000, 1435881600000]
}
}
]
})

# providing string
TimestampedGeoJson(open('foo.json').read())
transition_time : int, default 200.
The duration in ms of a transition from one timestamp to another.
loop : bool, default True
Whether the animation shall loop.
auto_play : bool, default True
Whether the animation shall start automatically at startup.

"""
super(TimestampedGeoJson, self).__init__()
self.plugin_name = 'TimestampedGeoJson'
self.template = self.env.get_template('timestamped_geo_json.tpl')
if 'read' in dir(data):
self.data = data.read()
elif type(data) is dict:
self.data = json.dumps(data)
else:
self.data = data
self.transition_time = int(transition_time)
self.loop = bool(loop)
self.auto_play = bool(auto_play)

def render_header(self, nb):
"""Generates the header part of the plugin."""
header = self.template.module.__dict__.get('header',None)
assert header is not None, "This template must have a 'header' macro."
return header(nb)
def render_js(self, nb):
"""Generates the Javascript part of the plugin."""
js = self.template.module.__dict__.get('js',None)
assert js is not None, "This template must have a 'js' macro."
return js(nb,self)
31 changes: 31 additions & 0 deletions folium/plugins/timestamped_geo_json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% macro header(nb) %}
{% if nb==0 %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"></script>

<!-- iso8601 -->
<script type="text/javascript" src="https://raw.githubusercontent.com/nezasa/iso8601-js-period/master/iso8601.min.js"></script>

<!-- leaflet.timedimension.min.js -->
<script type="text/javascript" src="https://raw.githubusercontent.com/socib/Leaflet.TimeDimension/master/dist/leaflet.timedimension.min.js"></script>

<!-- leaflet.timedimension.control.min.css -->
<link rel="stylesheet" href="http://apps.socib.es/Leaflet.TimeDimension/dist/leaflet.timedimension.control.min.css" />
{% endif %}
{% endmacro %}

{% macro js(nb,self) %}
{% if nb==0 %}
map.timeDimension = L.timeDimension();
map.timeDimensionControl = L.control.timeDimension({
position: 'bottomleft',
autoPlay: {{'true' if self.auto_play else 'false'}},
playerOptions: {transitionTime: {{self.transition_time}},loop: {{'true' if self.loop else 'false'}}}
});
map.addControl(map.timeDimensionControl);
{% endif %}

var tsgeojson_{{nb}} = L.timeDimension.layer.geoJson(L.geoJson({{self.data}}),
{updateTimeDimension: true,addlastPoint: true}).addTo(map);
{% endmacro %}
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def walk_subpkg(name):

pkg_data = {'': ['*.js',
'plugins/*.js',
'plugins/*.html',
'plugins/*.css',
'plugins/*.tpl',
'templates/*.html',
'templates/*.js',
'templates/*.txt'] + walk_subpkg('templates/tiles')}
Expand Down
67 changes: 67 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,70 @@ def test_geo_json(self):
mapd.add_plugin(plugins.GeoJson(open('geojson_plugin_test2.json')))
mapd._build_map()

def test_timestamped_geo_json(self):
data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [0,0],
},
"properties": {
"times": [1435708800000+12*86400000]
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPoint",
"coordinates": [[lon,-25] for lon in np.linspace(-150,150,49)],
},
"properties": {
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,49)]
}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[lon,25] for lon in np.linspace(-150,150,25)],
},
"properties": {
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,25)]
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiLineString",
"coordinates": [[[lon-4*np.sin(theta),47+3*np.cos(theta)]\
for theta in np.linspace(0,2*np.pi,25)]\
for lon in np.linspace(-150,150,13)],
},
"properties": {
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,13)]
}
},
{
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[[lon-8*np.sin(theta),-47+6*np.cos(theta)]\
for theta in np.linspace(0,2*np.pi,25)],
[[lon-4*np.sin(theta),-47+3*np.cos(theta)]\
for theta in np.linspace(0,2*np.pi,25)]]\
for lon in np.linspace(-150,150,7)],
},
"properties": {
"times": [1435708800000+i*86400000 for i in np.linspace(0,25,7)]
}
},
],
}

mape = folium.Map([47,3], zoom_start=1)
mape.add_plugin(plugins.TimestampedGeoJson(data))
mape._build_map()