|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | + |
| 3 | +from branca.element import JavascriptLink, Figure, CssLink, Element |
| 4 | +from branca.utilities import none_min, none_max |
| 5 | + |
| 6 | +from folium.map import TileLayer |
| 7 | + |
| 8 | +from jinja2 import Template |
| 9 | + |
| 10 | + |
| 11 | +class HeatMapWithTime(TileLayer): |
| 12 | + """ |
| 13 | + Create a HeatMapWithTime layer |
| 14 | +
|
| 15 | + Parameters |
| 16 | + ---------- |
| 17 | + data: list of list of points of the form [lat, lng] or [lat, lng, weight] |
| 18 | + The points you want to plot. The outer list corresponds to the various time |
| 19 | + steps in sequential order. (weight defaults to 1 if not specified for a point) |
| 20 | + index: Index giving the label (or timestamp) of the elements of data. Should have |
| 21 | + the same length as data, or is replaced by a simple count if not specified. |
| 22 | + name: str |
| 23 | + The name of the layer that will be created. |
| 24 | + radius: default 15. |
| 25 | + The radius used around points for the heatmap. |
| 26 | + min_opacity: default 0 |
| 27 | + The minimum opacity for the heatmap. |
| 28 | + max_opacity: default 0.6 |
| 29 | + The maximum opacity for the heatmap. |
| 30 | + scale_radius: default False |
| 31 | + Scale the radius of the points based on the zoom level. |
| 32 | + use_local_extrema: default False |
| 33 | + Defines whether the heatmap uses a global extrema set found from the input data |
| 34 | + OR a local extrema (the maximum and minimum of the currently displayed view). |
| 35 | + auto_play: default False |
| 36 | + Automatically play the animation across time. |
| 37 | + display_index: default True |
| 38 | + Display the index (usually time) in the time control. |
| 39 | + index_steps: default 1 |
| 40 | + Steps to take in the index dimension between aimation steps. |
| 41 | + min_speed: default 0.1 |
| 42 | + Minimum fps speed for animation. |
| 43 | + max_speed: default 10 |
| 44 | + Maximum fps speed for animation. |
| 45 | + speed_step: default 0.1 |
| 46 | + Step between different fps speeds on the speed slider. |
| 47 | + position: default 'bottomleft' |
| 48 | + Position string for the time slider. Format: 'bottom/top'+'left/right'. |
| 49 | +
|
| 50 | + """ |
| 51 | + def __init__(self, data, index=None, name=None, radius=15, min_opacity=0, max_opacity=0.6, |
| 52 | + scale_radius=False, use_local_extrema=False, auto_play=False, display_index=True, |
| 53 | + index_steps=1, min_speed=0.1, max_speed=10, speed_step=0.1, position='bottomleft' |
| 54 | + ): |
| 55 | + super(TileLayer, self).__init__(name=name) |
| 56 | + self._name = 'HeatMap' |
| 57 | + self._control_name = self.get_name() + 'Control' |
| 58 | + self.tile_name = name if name is not None else self.get_name() |
| 59 | + |
| 60 | + # Input data. |
| 61 | + self.data = data |
| 62 | + self.index = index if index is not None else [str(i) for i in range(1, len(data)+1)] |
| 63 | + if len(self.data) != len(self.index): |
| 64 | + raise ValueError('Input data and index are not of compatible lengths.') |
| 65 | + self.times = range(1, len(data)+1) |
| 66 | + |
| 67 | + # Heatmap settings. |
| 68 | + self.radius = radius |
| 69 | + self.min_opacity = min_opacity |
| 70 | + self.max_opacity = max_opacity |
| 71 | + self.scale_radius = 'true' if scale_radius else 'false' |
| 72 | + self.use_local_extrema = 'true' if use_local_extrema else 'false' |
| 73 | + |
| 74 | + # Time dimension settings. |
| 75 | + self.auto_play = 'true' if auto_play else 'false' |
| 76 | + self.display_index = 'true' if display_index else 'false' |
| 77 | + self.min_speed = min_speed |
| 78 | + self.max_speed = max_speed |
| 79 | + self.position = position |
| 80 | + self.speed_step = speed_step |
| 81 | + self.index_steps = index_steps |
| 82 | + |
| 83 | + # Hard coded defaults for simplicity. |
| 84 | + self.backward_button = 'true' |
| 85 | + self.forward_button = 'true' |
| 86 | + self.limit_sliders = 'true' |
| 87 | + self.limit_minimum_range = 5 |
| 88 | + self.loop_button = 'true' |
| 89 | + self.speed_slider = 'true' |
| 90 | + self.time_slider = 'true' |
| 91 | + self.play_button = 'true' |
| 92 | + self.play_reverse_button = 'true' |
| 93 | + self.time_slider_drap_update = 'false' |
| 94 | + self.style_NS = 'leaflet-control-timecontrol' |
| 95 | + |
| 96 | + self._template = Template(u""" |
| 97 | + {% macro script(this, kwargs) %} |
| 98 | +
|
| 99 | + var times = {{this.times}}; |
| 100 | +
|
| 101 | + {{this._parent.get_name()}}.timeDimension = L.timeDimension( |
| 102 | + {times : times, currentTime: new Date(1)} |
| 103 | + ); |
| 104 | +
|
| 105 | + var {{this._control_name}} = new L.Control.TimeDimensionCustom({{this.index}}, { |
| 106 | + autoPlay: {{this.auto_play}}, |
| 107 | + backwardButton: {{this.backward_button}}, |
| 108 | + displayDate: {{this.display_index}}, |
| 109 | + forwardButton: {{this.forward_button}}, |
| 110 | + limitMinimumRange: {{this.limit_minimum_range}}, |
| 111 | + limitSliders: {{this.limit_sliders}}, |
| 112 | + loopButton: {{this.loop_button}}, |
| 113 | + maxSpeed: {{this.max_speed}}, |
| 114 | + minSpeed: {{this.min_speed}}, |
| 115 | + playButton: {{this.play_button}}, |
| 116 | + playReverseButton: {{this.play_reverse_button}}, |
| 117 | + position: "{{this.position}}", |
| 118 | + speedSlider: {{this.speed_slider}}, |
| 119 | + speedStep: {{this.speed_step}}, |
| 120 | + styleNS: "{{this.style_NS}}", |
| 121 | + timeSlider: {{this.time_slider}}, |
| 122 | + timeSliderDrapUpdate: {{this.time_slider_drap_update}}, |
| 123 | + timeSteps: {{this.index_steps}} |
| 124 | + }) |
| 125 | + .addTo({{this._parent.get_name()}}); |
| 126 | +
|
| 127 | + var {{this.get_name()}} = new TDHeatmap({{this.data}}, |
| 128 | + {heatmapOptions: { |
| 129 | + radius: {{this.radius}}, |
| 130 | + minOpacity: {{this.min_opacity}}, |
| 131 | + maxOpacity: {{this.max_opacity}}, |
| 132 | + scaleRadius: {{this.scale_radius}}, |
| 133 | + useLocalExtrema: {{this.use_local_extrema}}, |
| 134 | + defaultWeight: 1 , |
| 135 | + } |
| 136 | + }) |
| 137 | + .addTo({{this._parent.get_name()}}); |
| 138 | +
|
| 139 | + {% endmacro %} |
| 140 | + """) |
| 141 | + |
| 142 | + def render(self, **kwargs): |
| 143 | + super(TileLayer, self).render() |
| 144 | + |
| 145 | + figure = self.get_root() |
| 146 | + assert isinstance(figure, Figure), ('You cannot render this Element ' |
| 147 | + 'if it is not in a Figure.') |
| 148 | + |
| 149 | + figure.header.add_child( |
| 150 | + JavascriptLink('https://rawgit.com/socib/Leaflet.TimeDimension/master/dist/leaflet.timedimension.min.js'), # noqa |
| 151 | + name='leaflet.timedimension.min.js') |
| 152 | + |
| 153 | + figure.header.add_child( |
| 154 | + JavascriptLink( |
| 155 | + 'https://cdnjs.cloudflare.com/ajax/libs/heatmap.js/2.0.2/heatmap.min.js'), |
| 156 | + name='heatmap.min.js') |
| 157 | + |
| 158 | + figure.header.add_child( |
| 159 | + JavascriptLink('https://rawgit.com/pa7/heatmap.js/develop/plugins/leaflet-heatmap/leaflet-heatmap.js'), # noqa |
| 160 | + name='leaflet-heatmap.js') |
| 161 | + |
| 162 | + figure.header.add_child( |
| 163 | + CssLink('http://apps.socib.es/Leaflet.TimeDimension/dist/leaflet.timedimension.control.min.css'), # noqa |
| 164 | + name='leaflet.timedimension.control.min.css') |
| 165 | + |
| 166 | + figure.header.add_child( |
| 167 | + Element( |
| 168 | + """ |
| 169 | + <script> |
| 170 | + var TDHeatmap = L.TimeDimension.Layer.extend({ |
| 171 | +
|
| 172 | + initialize: function(data, options) { |
| 173 | + var heatmapCfg = { |
| 174 | + radius: 15, |
| 175 | + maxOpacity: 1., |
| 176 | + scaleRadius: false, |
| 177 | + useLocalExtrema: false, |
| 178 | + latField: 'lat', |
| 179 | + lngField: 'lng', |
| 180 | + valueField: 'count', |
| 181 | + defaultWeight : 1, |
| 182 | + }; |
| 183 | + heatmapCfg = $.extend({}, heatmapCfg, options.heatmapOptions || {}); |
| 184 | + var layer = new HeatmapOverlay(heatmapCfg); |
| 185 | + L.TimeDimension.Layer.prototype.initialize.call(this, layer, options); |
| 186 | + this._currentLoadedTime = 0; |
| 187 | + this._currentTimeData = { |
| 188 | + data: [] |
| 189 | + }; |
| 190 | + this.data= data; |
| 191 | + this.defaultWeight = heatmapCfg.defaultWeight || 1; |
| 192 | + }, |
| 193 | + onAdd: function(map) { |
| 194 | + L.TimeDimension.Layer.prototype.onAdd.call(this, map); |
| 195 | + map.addLayer(this._baseLayer); |
| 196 | + if (this._timeDimension) { |
| 197 | + this._getDataForTime(this._timeDimension.getCurrentTime()); |
| 198 | + } |
| 199 | + }, |
| 200 | + _onNewTimeLoading: function(ev) { |
| 201 | + this._getDataForTime(ev.time); |
| 202 | + return; |
| 203 | + }, |
| 204 | + isReady: function(time) { |
| 205 | + return (this._currentLoadedTime == time); |
| 206 | + }, |
| 207 | + _update: function() { |
| 208 | + this._baseLayer.setData(this._currentTimeData); |
| 209 | + return true; |
| 210 | + }, |
| 211 | + _getDataForTime: function(time) { |
| 212 | + delete this._currentTimeData.data; |
| 213 | + this._currentTimeData.data = []; |
| 214 | + var data = this.data[time-1]; |
| 215 | + for (var i = 0; i < data.length; i++) { |
| 216 | + this._currentTimeData.data.push({ |
| 217 | + lat: data[i][0], |
| 218 | + lng: data[i][1], |
| 219 | + count: data[i].length>2 ? data[i][2] : this.defaultWeight |
| 220 | + }); |
| 221 | + } |
| 222 | + this._currentLoadedTime = time; |
| 223 | + if (this._timeDimension && time == this._timeDimension.getCurrentTime() && !this._timeDimension.isLoading()) { |
| 224 | + this._update(); |
| 225 | + } |
| 226 | + this.fire('timeload', { |
| 227 | + time: time |
| 228 | + }); |
| 229 | + } |
| 230 | + }); |
| 231 | +
|
| 232 | + L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({ |
| 233 | + initialize: function(index, options) { |
| 234 | + var playerOptions = { |
| 235 | + buffer: 1, |
| 236 | + minBufferReady: -1 |
| 237 | + }; |
| 238 | + options.playerOptions = $.extend({}, playerOptions, options.playerOptions || {}); |
| 239 | + L.Control.TimeDimension.prototype.initialize.call(this, options); |
| 240 | + this.index = index; |
| 241 | + }, |
| 242 | + _getDisplayDateFormat: function(date){ |
| 243 | + return this.index[date.getTime()-1]; |
| 244 | + } |
| 245 | + }); |
| 246 | + </script> |
| 247 | + """, |
| 248 | + template_name="timeControlScript" |
| 249 | + ) |
| 250 | + ) |
| 251 | + |
| 252 | + def _get_self_bounds(self): |
| 253 | + """ |
| 254 | + Computes the bounds of the object itself (not including it's children) |
| 255 | + in the form [[lat_min, lon_min], [lat_max, lon_max]]. |
| 256 | +
|
| 257 | + """ |
| 258 | + bounds = [[None, None], [None, None]] |
| 259 | + for point in self.data: |
| 260 | + bounds = [ |
| 261 | + [ |
| 262 | + none_min(bounds[0][0], point[0]), |
| 263 | + none_min(bounds[0][1], point[1]), |
| 264 | + ], |
| 265 | + [ |
| 266 | + none_max(bounds[1][0], point[0]), |
| 267 | + none_max(bounds[1][1], point[1]), |
| 268 | + ], |
| 269 | + ] |
| 270 | + return bounds |
0 commit comments