10
10
11
11
from __future__ import (absolute_import , division , print_function )
12
12
13
+ import os
14
+ import tempfile
15
+ import time
16
+
13
17
from branca .colormap import StepColormap
14
- from branca .utilities import color_brewer
18
+ from branca .element import CssLink , Element , Figure , JavascriptLink , MacroElement
19
+ from branca .utilities import _parse_size , color_brewer
15
20
16
21
from folium .features import GeoJson , TopoJson
17
- from folium .map import FitBounds , LegacyMap
18
-
19
-
20
- class Map (LegacyMap ):
22
+ from folium .map import FitBounds
23
+ from folium .raster_layers import TileLayer
24
+ from folium .utilities import _validate_location
25
+
26
+ from jinja2 import Environment , PackageLoader , Template
27
+
28
+ ENV = Environment (loader = PackageLoader ('folium' , 'templates' ))
29
+
30
+
31
+ _default_js = [
32
+ ('leaflet' ,
33
+ 'https://cdn.jsdelivr.net/npm/[email protected] /dist/leaflet.js' ),
34
+ ('jquery' ,
35
+ 'https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js' ),
36
+ ('bootstrap' ,
37
+ 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js' ),
38
+ ('awesome_markers' ,
39
+ 'https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js' ), # noqa
40
+ ]
41
+
42
+ _default_css = [
43
+ ('leaflet_css' ,
44
+ 'https://cdn.jsdelivr.net/npm/[email protected] /dist/leaflet.css' ),
45
+ ('bootstrap_css' ,
46
+ 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' ),
47
+ ('bootstrap_theme_css' ,
48
+ 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css' ), # noqa
49
+ ('awesome_markers_font_css' ,
50
+ 'https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css' ), # noqa
51
+ ('awesome_markers_css' ,
52
+ 'https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css' ), # noqa
53
+ ('awesome_rotate_css' ,
54
+ 'https://rawgit.com/python-visualization/folium/master/folium/templates/leaflet.awesome.rotate.css' ), # noqa
55
+ ]
56
+
57
+
58
+ class GlobalSwitches (Element ):
59
+ def __init__ (self , prefer_canvas = False , no_touch = False , disable_3d = False ):
60
+ super (GlobalSwitches , self ).__init__ ()
61
+ self ._name = 'GlobalSwitches'
62
+
63
+ self .prefer_canvas = prefer_canvas
64
+ self .no_touch = no_touch
65
+ self .disable_3d = disable_3d
66
+
67
+ self ._template = Template (
68
+ '<script>'
69
+ 'L_PREFER_CANVAS = {% if this.prefer_canvas %}true{% else %}false{% endif %}; '
70
+ 'L_NO_TOUCH = {% if this.no_touch %}true{% else %}false{% endif %}; '
71
+ 'L_DISABLE_3D = {% if this.disable_3d %}true{% else %}false{% endif %};'
72
+ '</script>'
73
+ )
74
+
75
+
76
+ class Map (MacroElement ):
21
77
"""Create a Map with Folium and Leaflet.js
22
78
23
79
Generate a base map of given width and height with either default
@@ -89,17 +145,17 @@ class Map(LegacyMap):
89
145
90
146
Returns
91
147
-------
92
- Folium LegacyMap Object
148
+ Folium Map Object
93
149
94
150
Examples
95
151
--------
96
- >>> map = folium.LegacyMap (location=[45.523, -122.675],
152
+ >>> map = folium.Map (location=[45.523, -122.675],
97
153
... width=750, height=500)
98
- >>> map = folium.LegacyMap (location=[45.523, -122.675],
154
+ >>> map = folium.Map (location=[45.523, -122.675],
99
155
tiles='Mapbox Control Room')
100
- >>> map = folium.LegacyMap (location=(45.523, -122.675), max_zoom=20,
156
+ >>> map = folium.Map (location=(45.523, -122.675), max_zoom=20,
101
157
tiles='Cloudmade', API_key='YourKey')
102
- >>> map = folium.LegacyMap (
158
+ >>> map = folium.Map (
103
159
... location=[45.523, -122.675],
104
160
... zoom_start=2,
105
161
... tiles='http://{s}.tiles.mapbox.com/v3/mapbox.control-room/{z}/{x}/{y}.png',
@@ -108,6 +164,198 @@ class Map(LegacyMap):
108
164
109
165
"""
110
166
167
+ def __init__ (self , location = None , width = '100%' , height = '100%' ,
168
+ left = '0%' , top = '0%' , position = 'relative' ,
169
+ tiles = 'OpenStreetMap' , API_key = None , max_zoom = 18 , min_zoom = 1 ,
170
+ zoom_start = 10 , world_copy_jump = False ,
171
+ no_wrap = False , attr = None , min_lat = - 90 , max_lat = 90 ,
172
+ min_lon = - 180 , max_lon = 180 , max_bounds = False ,
173
+ detect_retina = False , crs = 'EPSG3857' , control_scale = False ,
174
+ prefer_canvas = False , no_touch = False , disable_3d = False ,
175
+ subdomains = 'abc' , png_enabled = False ):
176
+ super (Map , self ).__init__ ()
177
+ self ._name = 'Map'
178
+ self ._env = ENV
179
+ # Undocumented for now b/c this will be subject to a re-factor soon.
180
+ self ._png_image = None
181
+ self .png_enabled = png_enabled
182
+
183
+ if not location :
184
+ # If location is not passed we center and ignore zoom.
185
+ self .location = [0 , 0 ]
186
+ self .zoom_start = min_zoom
187
+ else :
188
+ self .location = _validate_location (location )
189
+ self .zoom_start = zoom_start
190
+
191
+ Figure ().add_child (self )
192
+
193
+ # Map Size Parameters.
194
+ self .width = _parse_size (width )
195
+ self .height = _parse_size (height )
196
+ self .left = _parse_size (left )
197
+ self .top = _parse_size (top )
198
+ self .position = position
199
+
200
+ self .min_lat = min_lat
201
+ self .max_lat = max_lat
202
+ self .min_lon = min_lon
203
+ self .max_lon = max_lon
204
+ self .max_bounds = max_bounds
205
+ self .no_wrap = no_wrap
206
+ self .world_copy_jump = world_copy_jump
207
+
208
+ self .crs = crs
209
+ self .control_scale = control_scale
210
+
211
+ self .global_switches = GlobalSwitches (
212
+ prefer_canvas ,
213
+ no_touch ,
214
+ disable_3d
215
+ )
216
+
217
+ if tiles :
218
+ self .add_tile_layer (
219
+ tiles = tiles , min_zoom = min_zoom , max_zoom = max_zoom ,
220
+ no_wrap = no_wrap , attr = attr ,
221
+ API_key = API_key , detect_retina = detect_retina ,
222
+ subdomains = subdomains
223
+ )
224
+
225
+ self ._template = Template (u"""
226
+ {% macro header(this, kwargs) %}
227
+ <style> #{{this.get_name()}} {
228
+ position : {{this.position}};
229
+ width : {{this.width[0]}}{{this.width[1]}};
230
+ height: {{this.height[0]}}{{this.height[1]}};
231
+ left: {{this.left[0]}}{{this.left[1]}};
232
+ top: {{this.top[0]}}{{this.top[1]}};
233
+ }
234
+ </style>
235
+ {% endmacro %}
236
+ {% macro html(this, kwargs) %}
237
+ <div class="folium-map" id="{{this.get_name()}}" ></div>
238
+ {% endmacro %}
239
+
240
+ {% macro script(this, kwargs) %}
241
+
242
+ {% if this.max_bounds %}
243
+ var southWest = L.latLng({{ this.min_lat }}, {{ this.min_lon }});
244
+ var northEast = L.latLng({{ this.max_lat }}, {{ this.max_lon }});
245
+ var bounds = L.latLngBounds(southWest, northEast);
246
+ {% else %}
247
+ var bounds = null;
248
+ {% endif %}
249
+
250
+ var {{this.get_name()}} = L.map(
251
+ '{{this.get_name()}}',
252
+ {center: [{{this.location[0]}},{{this.location[1]}}],
253
+ zoom: {{this.zoom_start}},
254
+ maxBounds: bounds,
255
+ layers: [],
256
+ worldCopyJump: {{this.world_copy_jump.__str__().lower()}},
257
+ crs: L.CRS.{{this.crs}}
258
+ });
259
+ {% if this.control_scale %}L.control.scale().addTo({{this.get_name()}});{% endif %}
260
+ {% endmacro %}
261
+ """ ) # noqa
262
+
263
+ def _repr_html_ (self , ** kwargs ):
264
+ """Displays the HTML Map in a Jupyter notebook."""
265
+ if self ._parent is None :
266
+ self .add_to (Figure ())
267
+ out = self ._parent ._repr_html_ (** kwargs )
268
+ self ._parent = None
269
+ else :
270
+ out = self ._parent ._repr_html_ (** kwargs )
271
+ return out
272
+
273
+ def _to_png (self ):
274
+ """Export the HTML to byte representation of a PNG image."""
275
+ if self ._png_image is None :
276
+ import selenium .webdriver
277
+
278
+ with tempfile .NamedTemporaryFile (suffix = '.html' ) as f :
279
+ fname = f .name
280
+ self .save (fname , close_file = False )
281
+ driver = selenium .webdriver .PhantomJS (
282
+ service_log_path = os .path .devnull
283
+ )
284
+ driver .get ('file://{}' .format (fname ))
285
+ driver .maximize_window ()
286
+ # Ignore user map size.
287
+ driver .execute_script ("document.body.style.width = '100%';" ) # noqa
288
+ # We should probably monitor if some element is present,
289
+ # but this is OK for now.
290
+ time .sleep (3 )
291
+ png = driver .get_screenshot_as_png ()
292
+ driver .quit ()
293
+ self ._png_image = png
294
+ return self ._png_image
295
+
296
+ def _repr_png_ (self ):
297
+ """Displays the PNG Map in a Jupyter notebook."""
298
+ # The notebook calls all _repr_*_ by default.
299
+ # We don't want that here b/c this one is quite slow.
300
+ if not self .png_enabled :
301
+ return None
302
+ return self ._to_png ()
303
+
304
+ def add_tile_layer (self , tiles = 'OpenStreetMap' , name = None ,
305
+ API_key = None , max_zoom = 18 , min_zoom = 1 ,
306
+ attr = None , active = False ,
307
+ detect_retina = False , no_wrap = False , subdomains = 'abc' ,
308
+ ** kwargs ):
309
+ """
310
+ Add a tile layer to the map. See TileLayer for options.
311
+
312
+ """
313
+ tile_layer = TileLayer (tiles = tiles , name = name ,
314
+ min_zoom = min_zoom , max_zoom = max_zoom ,
315
+ attr = attr , API_key = API_key ,
316
+ detect_retina = detect_retina ,
317
+ subdomains = subdomains ,
318
+ no_wrap = no_wrap )
319
+ self .add_child (tile_layer , name = tile_layer .tile_name )
320
+
321
+ def render (self , ** kwargs ):
322
+ """Renders the HTML representation of the element."""
323
+ figure = self .get_root ()
324
+ assert isinstance (figure , Figure ), ('You cannot render this Element '
325
+ 'if it is not in a Figure.' )
326
+
327
+ # Set global switches
328
+ figure .header .add_child (self .global_switches , name = 'global_switches' )
329
+
330
+ # Import Javascripts
331
+ for name , url in _default_js :
332
+ figure .header .add_child (JavascriptLink (url ), name = name )
333
+
334
+ # Import Css
335
+ for name , url in _default_css :
336
+ figure .header .add_child (CssLink (url ), name = name )
337
+
338
+ figure .header .add_child (Element (
339
+ '<style>html, body {'
340
+ 'width: 100%;'
341
+ 'height: 100%;'
342
+ 'margin: 0;'
343
+ 'padding: 0;'
344
+ '}'
345
+ '</style>' ), name = 'css_style' )
346
+
347
+ figure .header .add_child (Element (
348
+ '<style>#map {'
349
+ 'position:absolute;'
350
+ 'top:0;'
351
+ 'bottom:0;'
352
+ 'right:0;'
353
+ 'left:0;'
354
+ '}'
355
+ '</style>' ), name = 'map_style' )
356
+
357
+ super (Map , self ).render (** kwargs )
358
+
111
359
def fit_bounds (self , bounds , padding_top_left = None ,
112
360
padding_bottom_right = None , padding = None , max_zoom = None ):
113
361
"""Fit the map to contain a bounding box with the
0 commit comments