1# -*- coding: utf-8 -*- 2 3""" 4Make beautiful, interactive maps with Python and Leaflet.js 5 6""" 7 8import time 9import warnings 10 11from branca.element import Element, Figure, MacroElement 12 13from folium.elements import JSCSSMixin 14from folium.map import FitBounds 15from folium.raster_layers import TileLayer 16from folium.utilities import ( 17 _parse_size, 18 temp_html_filepath, 19 validate_location, 20 parse_options, 21) 22 23from jinja2 import Environment, PackageLoader, Template 24 25ENV = Environment(loader=PackageLoader('folium', 'templates')) 26 27 28_default_js = [ 29 ('leaflet', 30 'https://cdn.jsdelivr.net/npm/leaflet@1.6.0/dist/leaflet.js'), 31 ('jquery', 32 'https://code.jquery.com/jquery-1.12.4.min.js'), 33 ('bootstrap', 34 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js'), 35 ('awesome_markers', 36 'https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js'), # noqa 37 ] 38 39_default_css = [ 40 ('leaflet_css', 41 'https://cdn.jsdelivr.net/npm/leaflet@1.6.0/dist/leaflet.css'), 42 ('bootstrap_css', 43 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css'), 44 ('bootstrap_theme_css', 45 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css'), # noqa 46 ('awesome_markers_font_css', 47 'https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css'), # noqa 48 ('awesome_markers_css', 49 'https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css'), # noqa 50 ('awesome_rotate_css', 51 'https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/leaflet.awesome.rotate.min.css'), # noqa 52 ] 53 54 55class GlobalSwitches(Element): 56 57 _template = Template(""" 58 <script> 59 L_NO_TOUCH = {{ this.no_touch |tojson}}; 60 L_DISABLE_3D = {{ this.disable_3d|tojson }}; 61 </script> 62 """) 63 64 def __init__(self, no_touch=False, disable_3d=False): 65 super(GlobalSwitches, self).__init__() 66 self._name = 'GlobalSwitches' 67 self.no_touch = no_touch 68 self.disable_3d = disable_3d 69 70 71class Map(JSCSSMixin, MacroElement): 72 """Create a Map with Folium and Leaflet.js 73 74 Generate a base map of given width and height with either default 75 tilesets or a custom tileset URL. The following tilesets are built-in 76 to Folium. Pass any of the following to the "tiles" keyword: 77 78 - "OpenStreetMap" 79 - "Mapbox Bright" (Limited levels of zoom for free tiles) 80 - "Mapbox Control Room" (Limited levels of zoom for free tiles) 81 - "Stamen" (Terrain, Toner, and Watercolor) 82 - "Cloudmade" (Must pass API key) 83 - "Mapbox" (Must pass API key) 84 - "CartoDB" (positron and dark_matter) 85 86 You can pass a custom tileset to Folium by passing a Leaflet-style 87 URL to the tiles parameter: ``http://{s}.yourtiles.com/{z}/{x}/{y}.png``. 88 89 You can find a list of free tile providers here: 90 ``http://leaflet-extras.github.io/leaflet-providers/preview/``. 91 Be sure to check their terms and conditions and to provide attribution 92 with the `attr` keyword. 93 94 Parameters 95 ---------- 96 location: tuple or list, default None 97 Latitude and Longitude of Map (Northing, Easting). 98 width: pixel int or percentage string (default: '100%') 99 Width of the map. 100 height: pixel int or percentage string (default: '100%') 101 Height of the map. 102 tiles: str, default 'OpenStreetMap' 103 Map tileset to use. Can choose from a list of built-in tiles, 104 pass a custom URL or pass `None` to create a map without tiles. 105 For more advanced tile layer options, use the `TileLayer` class. 106 min_zoom: int, default 0 107 Minimum allowed zoom level for the tile layer that is created. 108 max_zoom: int, default 18 109 Maximum allowed zoom level for the tile layer that is created. 110 zoom_start: int, default 10 111 Initial zoom level for the map. 112 attr: string, default None 113 Map tile attribution; only required if passing custom tile URL. 114 crs : str, default 'EPSG3857' 115 Defines coordinate reference systems for projecting geographical points 116 into pixel (screen) coordinates and back. 117 You can use Leaflet's values : 118 * EPSG3857 : The most common CRS for online maps, used by almost all 119 free and commercial tile providers. Uses Spherical Mercator projection. 120 Set in by default in Map's crs option. 121 * EPSG4326 : A common CRS among GIS enthusiasts. 122 Uses simple Equirectangular projection. 123 * EPSG3395 : Rarely used by some commercial tile providers. 124 Uses Elliptical Mercator projection. 125 * Simple : A simple CRS that maps longitude and latitude into 126 x and y directly. May be used for maps of flat surfaces 127 (e.g. game maps). Note that the y axis should still be inverted 128 (going from bottom to top). 129 control_scale : bool, default False 130 Whether to add a control scale on the map. 131 prefer_canvas : bool, default False 132 Forces Leaflet to use the Canvas back-end (if available) for 133 vector layers instead of SVG. This can increase performance 134 considerably in some cases (e.g. many thousands of circle 135 markers on the map). 136 no_touch : bool, default False 137 Forces Leaflet to not use touch events even if it detects them. 138 disable_3d : bool, default False 139 Forces Leaflet to not use hardware-accelerated CSS 3D 140 transforms for positioning (which may cause glitches in some 141 rare environments) even if they're supported. 142 zoom_control : bool, default True 143 Display zoom controls on the map. 144 **kwargs 145 Additional keyword arguments are passed to Leaflets Map class: 146 https://leafletjs.com/reference-1.6.0.html#map 147 148 Returns 149 ------- 150 Folium Map Object 151 152 Examples 153 -------- 154 >>> m = folium.Map(location=[45.523, -122.675], width=750, height=500) 155 >>> m = folium.Map(location=[45.523, -122.675], tiles='cartodb positron') 156 >>> m = folium.Map( 157 ... location=[45.523, -122.675], 158 ... zoom_start=2, 159 ... tiles='https://api.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token=mytoken', 160 ... attr='Mapbox attribution' 161 ...) 162 163 """ # noqa 164 _template = Template(u""" 165 {% macro header(this, kwargs) %} 166 <meta name="viewport" content="width=device-width, 167 initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 168 <style> 169 #{{ this.get_name() }} { 170 position: {{this.position}}; 171 width: {{this.width[0]}}{{this.width[1]}}; 172 height: {{this.height[0]}}{{this.height[1]}}; 173 left: {{this.left[0]}}{{this.left[1]}}; 174 top: {{this.top[0]}}{{this.top[1]}}; 175 } 176 </style> 177 {% endmacro %} 178 179 {% macro html(this, kwargs) %} 180 <div class="folium-map" id={{ this.get_name()|tojson }} ></div> 181 {% endmacro %} 182 183 {% macro script(this, kwargs) %} 184 var {{ this.get_name() }} = L.map( 185 {{ this.get_name()|tojson }}, 186 { 187 center: {{ this.location|tojson }}, 188 crs: L.CRS.{{ this.crs }}, 189 {%- for key, value in this.options.items() %} 190 {{ key }}: {{ value|tojson }}, 191 {%- endfor %} 192 } 193 ); 194 195 {%- if this.control_scale %} 196 L.control.scale().addTo({{ this.get_name() }}); 197 {%- endif %} 198 199 {% if this.objects_to_stay_in_front %} 200 function objects_in_front() { 201 {%- for obj in this.objects_to_stay_in_front %} 202 {{ obj.get_name() }}.bringToFront(); 203 {%- endfor %} 204 }; 205 {{ this.get_name() }}.on("overlayadd", objects_in_front); 206 $(document).ready(objects_in_front); 207 {%- endif %} 208 209 {% endmacro %} 210 """) 211 212 # use the module variables for backwards compatibility 213 default_js = _default_js 214 default_css = _default_css 215 216 def __init__( 217 self, 218 location=None, 219 width='100%', 220 height='100%', 221 left='0%', 222 top='0%', 223 position='relative', 224 tiles='OpenStreetMap', 225 attr=None, 226 min_zoom=0, 227 max_zoom=18, 228 zoom_start=10, 229 min_lat=-90, 230 max_lat=90, 231 min_lon=-180, 232 max_lon=180, 233 max_bounds=False, 234 crs='EPSG3857', 235 control_scale=False, 236 prefer_canvas=False, 237 no_touch=False, 238 disable_3d=False, 239 png_enabled=False, 240 zoom_control=True, 241 **kwargs 242 ): 243 super(Map, self).__init__() 244 self._name = 'Map' 245 self._env = ENV 246 # Undocumented for now b/c this will be subject to a re-factor soon. 247 self._png_image = None 248 self.png_enabled = png_enabled 249 250 if location is None: 251 # If location is not passed we center and zoom out. 252 self.location = [0, 0] 253 zoom_start = 1 254 else: 255 self.location = validate_location(location) 256 257 Figure().add_child(self) 258 259 # Map Size Parameters. 260 self.width = _parse_size(width) 261 self.height = _parse_size(height) 262 self.left = _parse_size(left) 263 self.top = _parse_size(top) 264 self.position = position 265 266 max_bounds_array = [[min_lat, min_lon], [max_lat, max_lon]] \ 267 if max_bounds else None 268 269 self.crs = crs 270 self.control_scale = control_scale 271 272 self.options = parse_options( 273 max_bounds=max_bounds_array, 274 zoom=zoom_start, 275 zoom_control=zoom_control, 276 prefer_canvas=prefer_canvas, 277 **kwargs 278 ) 279 280 self.global_switches = GlobalSwitches( 281 no_touch, 282 disable_3d 283 ) 284 285 self.objects_to_stay_in_front = [] 286 287 if tiles: 288 tile_layer = TileLayer(tiles=tiles, attr=attr, 289 min_zoom=min_zoom, max_zoom=max_zoom) 290 self.add_child(tile_layer, name=tile_layer.tile_name) 291 292 def _repr_html_(self, **kwargs): 293 """Displays the HTML Map in a Jupyter notebook.""" 294 if self._parent is None: 295 self.add_to(Figure()) 296 out = self._parent._repr_html_(**kwargs) 297 self._parent = None 298 else: 299 out = self._parent._repr_html_(**kwargs) 300 return out 301 302 def _to_png(self, delay=3): 303 """Export the HTML to byte representation of a PNG image. 304 305 Uses selenium to render the HTML and record a PNG. You may need to 306 adjust the `delay` time keyword argument if maps render without data or tiles. 307 308 Examples 309 -------- 310 >>> m._to_png() 311 >>> m._to_png(time=10) # Wait 10 seconds between render and snapshot. 312 313 """ 314 if self._png_image is None: 315 from selenium import webdriver 316 317 options = webdriver.firefox.options.Options() 318 options.add_argument('--headless') 319 driver = webdriver.Firefox(options=options) 320 321 html = self.get_root().render() 322 with temp_html_filepath(html) as fname: 323 # We need the tempfile to avoid JS security issues. 324 driver.get('file:///{path}'.format(path=fname)) 325 driver.maximize_window() 326 time.sleep(delay) 327 png = driver.get_screenshot_as_png() 328 driver.quit() 329 self._png_image = png 330 return self._png_image 331 332 def _repr_png_(self): 333 """Displays the PNG Map in a Jupyter notebook.""" 334 # The notebook calls all _repr_*_ by default. 335 # We don't want that here b/c this one is quite slow. 336 if not self.png_enabled: 337 return None 338 return self._to_png() 339 340 def render(self, **kwargs): 341 """Renders the HTML representation of the element.""" 342 figure = self.get_root() 343 assert isinstance(figure, Figure), ('You cannot render this Element ' 344 'if it is not in a Figure.') 345 346 # Set global switches 347 figure.header.add_child(self.global_switches, name='global_switches') 348 349 figure.header.add_child(Element( 350 '<style>html, body {' 351 'width: 100%;' 352 'height: 100%;' 353 'margin: 0;' 354 'padding: 0;' 355 '}' 356 '</style>'), name='css_style') 357 358 figure.header.add_child(Element( 359 '<style>#map {' 360 'position:absolute;' 361 'top:0;' 362 'bottom:0;' 363 'right:0;' 364 'left:0;' 365 '}' 366 '</style>'), name='map_style') 367 368 super(Map, self).render(**kwargs) 369 370 def fit_bounds(self, bounds, padding_top_left=None, 371 padding_bottom_right=None, padding=None, max_zoom=None): 372 """Fit the map to contain a bounding box with the 373 maximum zoom level possible. 374 375 Parameters 376 ---------- 377 bounds: list of (latitude, longitude) points 378 Bounding box specified as two points [southwest, northeast] 379 padding_top_left: (x, y) point, default None 380 Padding in the top left corner. Useful if some elements in 381 the corner, such as controls, might obscure objects you're zooming 382 to. 383 padding_bottom_right: (x, y) point, default None 384 Padding in the bottom right corner. 385 padding: (x, y) point, default None 386 Equivalent to setting both top left and bottom right padding to 387 the same value. 388 max_zoom: int, default None 389 Maximum zoom to be used. 390 391 Examples 392 -------- 393 >>> m.fit_bounds([[52.193636, -2.221575], [52.636878, -1.139759]]) 394 395 """ 396 self.add_child(FitBounds(bounds, 397 padding_top_left=padding_top_left, 398 padding_bottom_right=padding_bottom_right, 399 padding=padding, 400 max_zoom=max_zoom, 401 ) 402 ) 403 404 def choropleth(self, *args, **kwargs): 405 """Call the Choropleth class with the same arguments. 406 407 This method may be deleted after a year from now (Nov 2018). 408 """ 409 warnings.warn( 410 'The choropleth method has been deprecated. Instead use the new ' 411 'Choropleth class, which has the same arguments. See the example ' 412 'notebook \'GeoJSON_and_choropleth\' for how to do this.', 413 FutureWarning 414 ) 415 from folium.features import Choropleth 416 self.add_child(Choropleth(*args, **kwargs)) 417 418 def keep_in_front(self, *args): 419 """Pass one or multiple layers that must stay in front. 420 421 The ordering matters, the last one is put on top. 422 423 Parameters 424 ---------- 425 *args : 426 Variable length argument list. Any folium object that counts as an 427 overlay. For example FeatureGroup or TileLayer. 428 Does not work with markers, for those use z_index_offset. 429 """ 430 for obj in args: 431 self.objects_to_stay_in_front.append(obj) 432