1# -*- coding: utf-8 -*- 2 3""" 4Classes for drawing maps. 5 6""" 7 8from collections import OrderedDict 9 10import warnings 11 12from branca.element import Element, Figure, Html, MacroElement 13 14from folium.utilities import validate_location, camelize, parse_options 15 16from jinja2 import Template 17 18 19class Layer(MacroElement): 20 """An abstract class for everything that is a Layer on the map. 21 It will be used to define whether an object will be included in 22 LayerControls. 23 24 Parameters 25 ---------- 26 name : string, default None 27 The name of the Layer, as it will appear in LayerControls 28 overlay : bool, default False 29 Adds the layer as an optional overlay (True) or the base layer (False). 30 control : bool, default True 31 Whether the Layer will be included in LayerControls. 32 show: bool, default True 33 Whether the layer will be shown on opening (only for overlays). 34 """ 35 def __init__(self, name=None, overlay=False, control=True, show=True): 36 super(Layer, self).__init__() 37 self.layer_name = name if name is not None else self.get_name() 38 self.overlay = overlay 39 self.control = control 40 self.show = show 41 42 43class FeatureGroup(Layer): 44 """ 45 Create a FeatureGroup layer ; you can put things in it and handle them 46 as a single layer. For example, you can add a LayerControl to 47 tick/untick the whole group. 48 49 Parameters 50 ---------- 51 name : str, default None 52 The name of the featureGroup layer. 53 It will be displayed in the LayerControl. 54 If None get_name() will be called to get the technical (ugly) name. 55 overlay : bool, default True 56 Whether your layer will be an overlay (ticked with a check box in 57 LayerControls) or a base layer (ticked with a radio button). 58 control: bool, default True 59 Whether the layer will be included in LayerControls. 60 show: bool, default True 61 Whether the layer will be shown on opening (only for overlays). 62 **kwargs 63 Additional (possibly inherited) options. See 64 https://leafletjs.com/reference-1.6.0.html#featuregroup 65 66 """ 67 _template = Template(u""" 68 {% macro script(this, kwargs) %} 69 var {{ this.get_name() }} = L.featureGroup( 70 {{ this.options|tojson }} 71 ).addTo({{ this._parent.get_name() }}); 72 {% endmacro %} 73 """) 74 75 def __init__(self, name=None, overlay=True, control=True, show=True, 76 **kwargs): 77 super(FeatureGroup, self).__init__(name=name, overlay=overlay, 78 control=control, show=show) 79 self._name = 'FeatureGroup' 80 self.tile_name = name if name is not None else self.get_name() 81 self.options = parse_options(**kwargs) 82 83 84class LayerControl(MacroElement): 85 """ 86 Creates a LayerControl object to be added on a folium map. 87 88 This object should be added to a Map object. Only Layer children 89 of Map are included in the layer control. 90 91 Parameters 92 ---------- 93 position : str 94 The position of the control (one of the map corners), can be 95 'topleft', 'topright', 'bottomleft' or 'bottomright' 96 default: 'topright' 97 collapsed : bool, default True 98 If true the control will be collapsed into an icon and expanded on 99 mouse hover or touch. 100 autoZIndex : bool, default True 101 If true the control assigns zIndexes in increasing order to all of 102 its layers so that the order is preserved when switching them on/off. 103 **kwargs 104 Additional (possibly inherited) options. See 105 https://leafletjs.com/reference-1.6.0.html#control-layers 106 107 """ 108 _template = Template(""" 109 {% macro script(this,kwargs) %} 110 var {{ this.get_name() }} = { 111 base_layers : { 112 {%- for key, val in this.base_layers.items() %} 113 {{ key|tojson }} : {{val}}, 114 {%- endfor %} 115 }, 116 overlays : { 117 {%- for key, val in this.overlays.items() %} 118 {{ key|tojson }} : {{val}}, 119 {%- endfor %} 120 }, 121 }; 122 L.control.layers( 123 {{ this.get_name() }}.base_layers, 124 {{ this.get_name() }}.overlays, 125 {{ this.options|tojson }} 126 ).addTo({{this._parent.get_name()}}); 127 128 {%- for val in this.layers_untoggle.values() %} 129 {{ val }}.remove(); 130 {%- endfor %} 131 {% endmacro %} 132 """) 133 134 def __init__(self, position='topright', collapsed=True, autoZIndex=True, 135 **kwargs): 136 super(LayerControl, self).__init__() 137 self._name = 'LayerControl' 138 self.options = parse_options( 139 position=position, 140 collapsed=collapsed, 141 autoZIndex=autoZIndex, 142 **kwargs 143 ) 144 self.base_layers = OrderedDict() 145 self.overlays = OrderedDict() 146 self.layers_untoggle = OrderedDict() 147 148 def reset(self): 149 self.base_layers = OrderedDict() 150 self.overlays = OrderedDict() 151 self.layers_untoggle = OrderedDict() 152 153 def render(self, **kwargs): 154 """Renders the HTML representation of the element.""" 155 for item in self._parent._children.values(): 156 if not isinstance(item, Layer) or not item.control: 157 continue 158 key = item.layer_name 159 if not item.overlay: 160 self.base_layers[key] = item.get_name() 161 if len(self.base_layers) > 1: 162 self.layers_untoggle[key] = item.get_name() 163 else: 164 self.overlays[key] = item.get_name() 165 if not item.show: 166 self.layers_untoggle[key] = item.get_name() 167 super(LayerControl, self).render() 168 169 170class Icon(MacroElement): 171 """ 172 Creates an Icon object that will be rendered 173 using Leaflet.awesome-markers. 174 175 Parameters 176 ---------- 177 color : str, default 'blue' 178 The color of the marker. You can use: 179 180 ['red', 'blue', 'green', 'purple', 'orange', 'darkred', 181 'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue', 182 'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen', 183 'gray', 'black', 'lightgray'] 184 185 icon_color : str, default 'white' 186 The color of the drawing on the marker. You can use colors above, 187 or an html color code. 188 icon : str, default 'info-sign' 189 The name of the marker sign. 190 See Font-Awesome website to choose yours. 191 Warning : depending on the icon you choose you may need to adapt 192 the `prefix` as well. 193 angle : int, default 0 194 The icon will be rotated by this amount of degrees. 195 prefix : str, default 'glyphicon' 196 The prefix states the source of the icon. 'fa' for font-awesome or 197 'glyphicon' for bootstrap 3. 198 199 https://github.com/lvoogdt/Leaflet.awesome-markers 200 201 """ 202 _template = Template(u""" 203 {% macro script(this, kwargs) %} 204 var {{ this.get_name() }} = L.AwesomeMarkers.icon( 205 {{ this.options|tojson }} 206 ); 207 {{ this._parent.get_name() }}.setIcon({{ this.get_name() }}); 208 {% endmacro %} 209 """) 210 color_options = {'red', 'darkred', 'lightred', 'orange', 'beige', 211 'green', 'darkgreen', 'lightgreen', 212 'blue', 'darkblue', 'cadetblue', 'lightblue', 213 'purple', 'darkpurple', 'pink', 214 'white', 'gray', 'lightgray', 'black'} 215 216 def __init__(self, color='blue', icon_color='white', icon='info-sign', 217 angle=0, prefix='glyphicon', **kwargs): 218 super(Icon, self).__init__() 219 self._name = 'Icon' 220 if color not in self.color_options: 221 warnings.warn('color argument of Icon should be one of: {}.' 222 .format(self.color_options), stacklevel=2) 223 self.options = parse_options( 224 marker_color=color, 225 icon_color=icon_color, 226 icon=icon, 227 prefix=prefix, 228 extra_classes='fa-rotate-{}'.format(angle), 229 **kwargs 230 ) 231 232 233class Marker(MacroElement): 234 """ 235 Create a simple stock Leaflet marker on the map, with optional 236 popup text or Vincent visualization. 237 238 Parameters 239 ---------- 240 location: tuple or list 241 Latitude and Longitude of Marker (Northing, Easting) 242 popup: string or folium.Popup, default None 243 Label for the Marker; either an escaped HTML string to initialize 244 folium.Popup or a folium.Popup instance. 245 tooltip: str or folium.Tooltip, default None 246 Display a text when hovering over the object. 247 icon: Icon plugin 248 the Icon plugin to use to render the marker. 249 draggable: bool, default False 250 Set to True to be able to drag the marker around the map. 251 252 Returns 253 ------- 254 Marker names and HTML in obj.template_vars 255 256 Examples 257 -------- 258 >>> Marker(location=[45.5, -122.3], popup='Portland, OR') 259 >>> Marker(location=[45.5, -122.3], popup=Popup('Portland, OR')) 260 # If the popup label has characters that need to be escaped in HTML 261 >>> Marker(location=[45.5, -122.3], 262 ... popup=Popup('Mom & Pop Arrow Shop >>', parse_html=True)) 263 """ 264 _template = Template(u""" 265 {% macro script(this, kwargs) %} 266 var {{ this.get_name() }} = L.marker( 267 {{ this.location|tojson }}, 268 {{ this.options|tojson }} 269 ).addTo({{ this._parent.get_name() }}); 270 {% endmacro %} 271 """) 272 273 def __init__(self, location=None, popup=None, tooltip=None, icon=None, 274 draggable=False, **kwargs): 275 super(Marker, self).__init__() 276 self._name = 'Marker' 277 self.location = validate_location(location) if location else None 278 self.options = parse_options( 279 draggable=draggable or None, 280 autoPan=draggable or None, 281 **kwargs 282 ) 283 if icon is not None: 284 self.add_child(icon) 285 self.icon = icon 286 if popup is not None: 287 self.add_child(popup if isinstance(popup, Popup) 288 else Popup(str(popup))) 289 if tooltip is not None: 290 self.add_child(tooltip if isinstance(tooltip, Tooltip) 291 else Tooltip(str(tooltip))) 292 293 def _get_self_bounds(self): 294 """Computes the bounds of the object itself. 295 296 Because a marker has only single coordinates, we repeat them. 297 """ 298 return [self.location, self.location] 299 300 def render(self): 301 if self.location is None: 302 raise ValueError("{} location must be assigned when added directly to map.".format(self._name)) 303 super(Marker, self).render() 304 305class Popup(Element): 306 """Create a Popup instance that can be linked to a Layer. 307 308 Parameters 309 ---------- 310 html: string or Element 311 Content of the Popup. 312 parse_html: bool, default False 313 True if the popup is a template that needs to the rendered first. 314 max_width: int for pixels or text for percentages, default '100%' 315 The maximal width of the popup. 316 show: bool, default False 317 True renders the popup open on page load. 318 sticky: bool, default False 319 True prevents map and other popup clicks from closing. 320 """ 321 _template = Template(u""" 322 var {{this.get_name()}} = L.popup({{ this.options|tojson }}); 323 324 {% for name, element in this.html._children.items() %} 325 var {{ name }} = $(`{{ element.render(**kwargs).replace('\\n',' ') }}`)[0]; 326 {{ this.get_name() }}.setContent({{ name }}); 327 {% endfor %} 328 329 {{ this._parent.get_name() }}.bindPopup({{ this.get_name() }}) 330 {% if this.show %}.openPopup(){% endif %}; 331 332 {% for name, element in this.script._children.items() %} 333 {{element.render()}} 334 {% endfor %} 335 """) # noqa 336 337 def __init__(self, html=None, parse_html=False, max_width='100%', 338 show=False, sticky=False, **kwargs): 339 super(Popup, self).__init__() 340 self._name = 'Popup' 341 self.header = Element() 342 self.html = Element() 343 self.script = Element() 344 345 self.header._parent = self 346 self.html._parent = self 347 self.script._parent = self 348 349 script = not parse_html 350 351 if isinstance(html, Element): 352 self.html.add_child(html) 353 elif isinstance(html, str): 354 self.html.add_child(Html(html, script=script)) 355 356 self.show = show 357 self.options = parse_options( 358 max_width=max_width, 359 autoClose=False if show or sticky else None, 360 closeOnClick=False if sticky else None, 361 **kwargs 362 ) 363 364 def render(self, **kwargs): 365 """Renders the HTML representation of the element.""" 366 for name, child in self._children.items(): 367 child.render(**kwargs) 368 369 figure = self.get_root() 370 assert isinstance(figure, Figure), ('You cannot render this Element ' 371 'if it is not in a Figure.') 372 373 figure.script.add_child(Element( 374 self._template.render(this=self, kwargs=kwargs)), 375 name=self.get_name()) 376 377 378class Tooltip(MacroElement): 379 """ 380 Create a tooltip that shows text when hovering over its parent object. 381 382 Parameters 383 ---------- 384 text: str 385 String to display as a tooltip on the object. If the argument is of a 386 different type it will be converted to str. 387 style: str, default None. 388 HTML inline style properties like font and colors. Will be applied to 389 a div with the text in it. 390 sticky: bool, default True 391 Whether the tooltip should follow the mouse. 392 **kwargs 393 These values will map directly to the Leaflet Options. More info 394 available here: https://leafletjs.com/reference-1.6.0#tooltip 395 396 """ 397 _template = Template(u""" 398 {% macro script(this, kwargs) %} 399 {{ this._parent.get_name() }}.bindTooltip( 400 `<div{% if this.style %} style={{ this.style|tojson }}{% endif %}> 401 {{ this.text }} 402 </div>`, 403 {{ this.options|tojson }} 404 ); 405 {% endmacro %} 406 """) 407 valid_options = { 408 'pane': (str, ), 409 'offset': (tuple, ), 410 'direction': (str, ), 411 'permanent': (bool, ), 412 'sticky': (bool, ), 413 'interactive': (bool, ), 414 'opacity': (float, int), 415 'attribution': (str, ), 416 'className': (str, ), 417 } 418 419 def __init__(self, text, style=None, sticky=True, **kwargs): 420 super(Tooltip, self).__init__() 421 self._name = 'Tooltip' 422 423 self.text = str(text) 424 425 kwargs.update({'sticky': sticky}) 426 self.options = self.parse_options(kwargs) 427 428 if style: 429 assert isinstance(style, str), \ 430 'Pass a valid inline HTML style property string to style.' 431 # noqa outside of type checking. 432 self.style = style 433 434 def parse_options(self, kwargs): 435 """Validate the provided kwargs and return options as json string.""" 436 kwargs = {camelize(key): value for key, value in kwargs.items()} 437 for key in kwargs.keys(): 438 assert key in self.valid_options, ( 439 'The option {} is not in the available options: {}.' 440 .format(key, ', '.join(self.valid_options)) 441 ) 442 assert isinstance(kwargs[key], self.valid_options[key]), ( 443 'The option {} must be one of the following types: {}.' 444 .format(key, self.valid_options[key]) 445 ) 446 return kwargs 447 448 449class FitBounds(MacroElement): 450 """Fit the map to contain a bounding box with the 451 maximum zoom level possible. 452 453 Parameters 454 ---------- 455 bounds: list of (latitude, longitude) points 456 Bounding box specified as two points [southwest, northeast] 457 padding_top_left: (x, y) point, default None 458 Padding in the top left corner. Useful if some elements in 459 the corner, such as controls, might obscure objects you're zooming 460 to. 461 padding_bottom_right: (x, y) point, default None 462 Padding in the bottom right corner. 463 padding: (x, y) point, default None 464 Equivalent to setting both top left and bottom right padding to 465 the same value. 466 max_zoom: int, default None 467 Maximum zoom to be used. 468 """ 469 _template = Template(u""" 470 {% macro script(this, kwargs) %} 471 {{ this._parent.get_name() }}.fitBounds( 472 {{ this.bounds|tojson }}, 473 {{ this.options|tojson }} 474 ); 475 {% endmacro %} 476 """) 477 478 def __init__(self, bounds, padding_top_left=None, 479 padding_bottom_right=None, padding=None, max_zoom=None): 480 super(FitBounds, self).__init__() 481 self._name = 'FitBounds' 482 self.bounds = bounds 483 self.options = parse_options( 484 max_zoom=max_zoom, 485 padding_top_left=padding_top_left, 486 padding_bottom_right=padding_bottom_right, 487 padding=padding, 488 ) 489 490 491class CustomPane(MacroElement): 492 """ 493 Creates a custom pane to hold map elements. 494 495 Behavior is as in https://leafletjs.com/examples/map-panes/ 496 497 Parameters 498 ---------- 499 name: string 500 Name of the custom pane. Other map elements can be added 501 to the pane by specifying the 'pane' kwarg when constructing 502 them. 503 z_index: int or string, default 625 504 The z-index that will be associated with the pane, and will 505 determine which map elements lie over/under it. The default 506 (625) corresponds to between markers and tooltips. Default 507 panes and z-indexes can be found at 508 https://leafletjs.com/reference-1.6.0.html#map-pane 509 pointer_events: bool, default False 510 Whether or not layers in the pane should interact with the 511 cursor. Setting to False will prevent interfering with 512 pointer events associated with lower layers. 513 """ 514 _template = Template(u""" 515 {% macro script(this, kwargs) %} 516 var {{ this.get_name() }} = {{ this._parent.get_name() }}.createPane( 517 {{ this.name|tojson }}); 518 {{ this.get_name() }}.style.zIndex = {{ this.z_index|tojson }}; 519 {% if not this.pointer_events %} 520 {{ this.get_name() }}.style.pointerEvents = 'none'; 521 {% endif %} 522 {% endmacro %} 523 """) 524 525 def __init__(self, name, z_index=625, pointer_events=False): 526 super(CustomPane, self).__init__() 527 self._name = 'Pane' 528 self.name = name 529 self.z_index = z_index 530 self.pointer_events = pointer_events 531