280 lines
9.2 KiB
Python
280 lines
9.2 KiB
Python
from typing import List, Optional, TextIO, Union
|
|
|
|
from branca.element import MacroElement
|
|
from jinja2 import Template
|
|
|
|
from folium.elements import JSCSSMixin
|
|
from folium.features import GeoJson
|
|
from folium.folium import Map
|
|
from folium.utilities import JsCode, camelize, get_bounds, parse_options
|
|
|
|
|
|
class Timeline(GeoJson):
|
|
"""
|
|
Create a layer from GeoJSON with time data to add to a map.
|
|
|
|
To add time data, you need to do one of the following:
|
|
|
|
* Add a 'start' and 'end' property to each feature. The start and end
|
|
can be any comparable item.
|
|
|
|
Alternatively, you can provide a `get_interval` function.
|
|
|
|
* This function should be a JsCode object and take as parameter
|
|
a GeoJson feature and return a dict containing values for
|
|
'start', 'end', 'startExclusive' and 'endExcusive' (or false if no
|
|
data could be extracted from the feature).
|
|
* 'start' and 'end' can be any comparable items
|
|
* 'startExclusive' and 'endExclusive' should be boolean values.
|
|
|
|
Parameters
|
|
----------
|
|
data: file, dict or str.
|
|
The geojson data you want to plot.
|
|
|
|
get_interval: JsCode, optional
|
|
Called for each feature, and should return either a time range for the
|
|
feature or `false`, indicating that it should not be included in the
|
|
timeline. The time range object should have 'start' and 'end' properties.
|
|
Optionally, the boolean keys 'startExclusive' and 'endExclusive' allow the
|
|
interval to be considered exclusive.
|
|
|
|
If `get_interval` is not provided, 'start' and 'end' properties are
|
|
assumed to be present on each feature.
|
|
|
|
Examples
|
|
--------
|
|
>>> from folium.plugins import Timeline, TimelineSlider
|
|
>>> m = folium.Map()
|
|
|
|
>>> data = requests.get(
|
|
... "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson"
|
|
... ).json()
|
|
|
|
>>> timeline = Timeline(
|
|
... data,
|
|
... get_interval=JsCode(
|
|
... '''
|
|
... function (quake) {
|
|
... // earthquake data only has a time, so we\'ll use that as a "start"
|
|
... // and the "end" will be that + some value based on magnitude
|
|
... // 18000000 = 30 minutes, so a quake of magnitude 5 would show on the
|
|
... // map for 150 minutes or 2.5 hours
|
|
... return {
|
|
... start: quake.properties.time,
|
|
... end: quake.properties.time + quake.properties.mag * 1800000,
|
|
... };
|
|
... };
|
|
... '''
|
|
... ),
|
|
... ).add_to(m)
|
|
>>> TimelineSlider(
|
|
... auto_play=False,
|
|
... show_ticks=True,
|
|
... enable_keyboard_controls=True,
|
|
... playback_duration=30000,
|
|
... ).add_timelines(timeline).add_to(m)
|
|
|
|
Other keyword arguments are passed to the GeoJson layer, so you can pass
|
|
`style`, `point_to_layer` and/or `on_each_feature`.
|
|
|
|
"""
|
|
|
|
_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() }} = L.timeline(
|
|
{{ this.data|tojson }},
|
|
{{ this.get_name() }}_options
|
|
);
|
|
{{ this.get_name() }}.addTo({{ this._parent.get_name() }});
|
|
{% endmacro %}
|
|
"""
|
|
)
|
|
|
|
default_js = [
|
|
(
|
|
"timeline",
|
|
"https://cdn.jsdelivr.net/npm/leaflet.timeline@1.6.0/dist/leaflet.timeline.min.js",
|
|
),
|
|
(
|
|
"moment",
|
|
"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js",
|
|
),
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
data: Union[dict, str, TextIO],
|
|
get_interval: Optional[JsCode] = None,
|
|
**kwargs
|
|
):
|
|
super().__init__(data)
|
|
self._name = "Timeline"
|
|
|
|
if get_interval is not None:
|
|
kwargs["get_interval"] = get_interval
|
|
|
|
# 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)
|
|
|
|
def _get_self_bounds(self):
|
|
"""
|
|
Computes the bounds of the object itself (not including it's children)
|
|
in the form [[lat_min, lon_min], [lat_max, lon_max]].
|
|
|
|
"""
|
|
return get_bounds(self.data, lonlat=True)
|
|
|
|
|
|
class TimelineSlider(JSCSSMixin, MacroElement):
|
|
"""
|
|
Creates a timeline slider for timeline layers.
|
|
|
|
Parameters
|
|
----------
|
|
auto_play: bool, default True
|
|
Whether the animation shall start automatically at startup.
|
|
start: str, int or float, default earliest 'start' in GeoJson
|
|
The beginning/minimum value of the timeline.
|
|
end: str, int or float, default latest 'end' in GeoJSON
|
|
The end/maximum value of the timeline.
|
|
date_options: str, default "YYYY-MM-DD HH:mm:ss"
|
|
A format string to render the currently active time in the control.
|
|
enable_playback: bool, default True
|
|
Show playback controls (i.e. prev/play/pause/next).
|
|
enable_keyboard_controls: bool, default False
|
|
Allow playback to be controlled using the spacebar (play/pause) and
|
|
right/left arrow keys (next/previous).
|
|
show_ticks: bool, default True
|
|
Show tick marks on the slider
|
|
steps: int, default 1000
|
|
How many steps to break the timeline into.
|
|
Each step will then be (end-start) / steps. Only affects playback.
|
|
playback_duration: int, default 10000
|
|
Minimum time, in ms, for the playback to take. Will almost certainly
|
|
actually take at least a bit longer -- after each frame, the next
|
|
one displays in playback_duration/steps ms, so each frame really
|
|
takes frame processing time PLUS step time.
|
|
|
|
Examples
|
|
--------
|
|
See the documentation for Timeline
|
|
|
|
"""
|
|
|
|
_template = Template(
|
|
"""
|
|
{% macro header(this,kwargs) %}
|
|
<style>
|
|
.leaflet-bottom.leaflet-left {
|
|
width: 100%;
|
|
}
|
|
.leaflet-control-container .leaflet-timeline-controls {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
margin: 0;
|
|
margin-bottom: 15px;
|
|
}
|
|
</style>
|
|
{% endmacro %}
|
|
|
|
{% 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() }} = L.timelineSliderControl(
|
|
{{ this.get_name() }}_options
|
|
);
|
|
{{ this.get_name() }}.addTo({{ this._parent.get_name() }});
|
|
|
|
{% for timeline in this.timelines %}
|
|
{{ this.get_name() }}.addTimelines({{ timeline.get_name() }});
|
|
{% endfor %}
|
|
|
|
{% endmacro %}
|
|
"""
|
|
)
|
|
|
|
default_js = [
|
|
(
|
|
"timeline",
|
|
"https://cdn.jsdelivr.net/npm/leaflet.timeline@1.6.0/dist/leaflet.timeline.min.js",
|
|
),
|
|
(
|
|
"moment",
|
|
"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js",
|
|
),
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
# arguments relevant to both interval and timestamp mode
|
|
auto_play: bool = True,
|
|
date_options: str = "YYYY-MM-DD HH:mm:ss",
|
|
start: Optional[Union[str, int, float]] = None,
|
|
end: Optional[Union[str, int, float]] = None,
|
|
enable_playback: bool = True,
|
|
enable_keyboard_controls: bool = False,
|
|
show_ticks: bool = True,
|
|
steps: int = 1000,
|
|
playback_duration: int = 10000,
|
|
**kwargs
|
|
):
|
|
super().__init__()
|
|
self._name = "TimelineSlider"
|
|
|
|
kwargs["auto_play"] = auto_play
|
|
kwargs["start"] = start
|
|
kwargs["end"] = end
|
|
kwargs["enable_playback"] = enable_playback
|
|
kwargs["enable_keyboard_controls"] = enable_keyboard_controls
|
|
kwargs["show_ticks"] = show_ticks
|
|
kwargs["steps"] = steps
|
|
kwargs["duration"] = playback_duration
|
|
|
|
kwargs["format_output"] = JsCode(
|
|
"""
|
|
function(date) {
|
|
var newdate = new moment(date);
|
|
return newdate.format(\""""
|
|
+ date_options
|
|
+ """\");
|
|
}
|
|
"""
|
|
)
|
|
|
|
# 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.timelines: List[Timeline] = []
|
|
self.options = parse_options(**kwargs)
|
|
|
|
def add_timelines(self, *args):
|
|
"""Add timelines to the control"""
|
|
self.timelines += args # we do not check for duplicates
|
|
return self
|
|
|
|
def render(self, **kwargs):
|
|
assert isinstance(
|
|
self._parent, Map
|
|
), "TimelineSlider can only be added to a Map object."
|
|
super().render(**kwargs)
|