# -*- coding: utf-8 -*- """ Classes for drawing maps. """ from collections import OrderedDict import warnings from branca.element import Element, Figure, Html, MacroElement from folium.utilities import validate_location, camelize, parse_options from jinja2 import Template class Layer(MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. Parameters ---------- name : string, default None The name of the Layer, as it will appear in LayerControls overlay : bool, default False Adds the layer as an optional overlay (True) or the base layer (False). control : bool, default True Whether the Layer will be included in LayerControls. show: bool, default True Whether the layer will be shown on opening (only for overlays). """ def __init__(self, name=None, overlay=False, control=True, show=True): super(Layer, self).__init__() self.layer_name = name if name is not None else self.get_name() self.overlay = overlay self.control = control self.show = show class FeatureGroup(Layer): """ Create a FeatureGroup layer ; you can put things in it and handle them as a single layer. For example, you can add a LayerControl to tick/untick the whole group. Parameters ---------- name : str, default None The name of the featureGroup layer. It will be displayed in the LayerControl. If None get_name() will be called to get the technical (ugly) name. overlay : bool, default True Whether your layer will be an overlay (ticked with a check box in LayerControls) or a base layer (ticked with a radio button). control: bool, default True Whether the layer will be included in LayerControls. show: bool, default True Whether the layer will be shown on opening (only for overlays). **kwargs Additional (possibly inherited) options. See https://leafletjs.com/reference-1.6.0.html#featuregroup """ _template = Template(u""" {% macro script(this, kwargs) %} var {{ this.get_name() }} = L.featureGroup( {{ this.options|tojson }} ).addTo({{ this._parent.get_name() }}); {% endmacro %} """) def __init__(self, name=None, overlay=True, control=True, show=True, **kwargs): super(FeatureGroup, self).__init__(name=name, overlay=overlay, control=control, show=show) self._name = 'FeatureGroup' self.tile_name = name if name is not None else self.get_name() self.options = parse_options(**kwargs) class LayerControl(MacroElement): """ Creates a LayerControl object to be added on a folium map. This object should be added to a Map object. Only Layer children of Map are included in the layer control. Parameters ---------- position : str The position of the control (one of the map corners), can be 'topleft', 'topright', 'bottomleft' or 'bottomright' default: 'topright' collapsed : bool, default True If true the control will be collapsed into an icon and expanded on mouse hover or touch. autoZIndex : bool, default True If true the control assigns zIndexes in increasing order to all of its layers so that the order is preserved when switching them on/off. **kwargs Additional (possibly inherited) options. See https://leafletjs.com/reference-1.6.0.html#control-layers """ _template = Template(""" {% macro script(this,kwargs) %} var {{ this.get_name() }} = { base_layers : { {%- for key, val in this.base_layers.items() %} {{ key|tojson }} : {{val}}, {%- endfor %} }, overlays : { {%- for key, val in this.overlays.items() %} {{ key|tojson }} : {{val}}, {%- endfor %} }, }; L.control.layers( {{ this.get_name() }}.base_layers, {{ this.get_name() }}.overlays, {{ this.options|tojson }} ).addTo({{this._parent.get_name()}}); {%- for val in this.layers_untoggle.values() %} {{ val }}.remove(); {%- endfor %} {% endmacro %} """) def __init__(self, position='topright', collapsed=True, autoZIndex=True, **kwargs): super(LayerControl, self).__init__() self._name = 'LayerControl' self.options = parse_options( position=position, collapsed=collapsed, autoZIndex=autoZIndex, **kwargs ) self.base_layers = OrderedDict() self.overlays = OrderedDict() self.layers_untoggle = OrderedDict() def reset(self): self.base_layers = OrderedDict() self.overlays = OrderedDict() self.layers_untoggle = OrderedDict() def render(self, **kwargs): """Renders the HTML representation of the element.""" for item in self._parent._children.values(): if not isinstance(item, Layer) or not item.control: continue key = item.layer_name if not item.overlay: self.base_layers[key] = item.get_name() if len(self.base_layers) > 1: self.layers_untoggle[key] = item.get_name() else: self.overlays[key] = item.get_name() if not item.show: self.layers_untoggle[key] = item.get_name() super(LayerControl, self).render() class Icon(MacroElement): """ Creates an Icon object that will be rendered using Leaflet.awesome-markers. Parameters ---------- color : str, default 'blue' The color of the marker. You can use: ['red', 'blue', 'green', 'purple', 'orange', 'darkred', 'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue', 'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen', 'gray', 'black', 'lightgray'] icon_color : str, default 'white' The color of the drawing on the marker. You can use colors above, or an html color code. icon : str, default 'info-sign' The name of the marker sign. See Font-Awesome website to choose yours. Warning : depending on the icon you choose you may need to adapt the `prefix` as well. angle : int, default 0 The icon will be rotated by this amount of degrees. prefix : str, default 'glyphicon' The prefix states the source of the icon. 'fa' for font-awesome or 'glyphicon' for bootstrap 3. https://github.com/lvoogdt/Leaflet.awesome-markers """ _template = Template(u""" {% macro script(this, kwargs) %} var {{ this.get_name() }} = L.AwesomeMarkers.icon( {{ this.options|tojson }} ); {{ this._parent.get_name() }}.setIcon({{ this.get_name() }}); {% endmacro %} """) color_options = {'red', 'darkred', 'lightred', 'orange', 'beige', 'green', 'darkgreen', 'lightgreen', 'blue', 'darkblue', 'cadetblue', 'lightblue', 'purple', 'darkpurple', 'pink', 'white', 'gray', 'lightgray', 'black'} def __init__(self, color='blue', icon_color='white', icon='info-sign', angle=0, prefix='glyphicon', **kwargs): super(Icon, self).__init__() self._name = 'Icon' if color not in self.color_options: warnings.warn('color argument of Icon should be one of: {}.' .format(self.color_options), stacklevel=2) self.options = parse_options( marker_color=color, icon_color=icon_color, icon=icon, prefix=prefix, extra_classes='fa-rotate-{}'.format(angle), **kwargs ) class Marker(MacroElement): """ Create a simple stock Leaflet marker on the map, with optional popup text or Vincent visualization. Parameters ---------- location: tuple or list Latitude and Longitude of Marker (Northing, Easting) popup: string or folium.Popup, default None Label for the Marker; either an escaped HTML string to initialize folium.Popup or a folium.Popup instance. tooltip: str or folium.Tooltip, default None Display a text when hovering over the object. icon: Icon plugin the Icon plugin to use to render the marker. draggable: bool, default False Set to True to be able to drag the marker around the map. Returns ------- Marker names and HTML in obj.template_vars Examples -------- >>> Marker(location=[45.5, -122.3], popup='Portland, OR') >>> Marker(location=[45.5, -122.3], popup=Popup('Portland, OR')) # If the popup label has characters that need to be escaped in HTML >>> Marker(location=[45.5, -122.3], ... popup=Popup('Mom & Pop Arrow Shop >>', parse_html=True)) """ _template = Template(u""" {% macro script(this, kwargs) %} var {{ this.get_name() }} = L.marker( {{ this.location|tojson }}, {{ this.options|tojson }} ).addTo({{ this._parent.get_name() }}); {% endmacro %} """) def __init__(self, location=None, popup=None, tooltip=None, icon=None, draggable=False, **kwargs): super(Marker, self).__init__() self._name = 'Marker' self.location = validate_location(location) if location else None self.options = parse_options( draggable=draggable or None, autoPan=draggable or None, **kwargs ) if icon is not None: self.add_child(icon) self.icon = icon if popup is not None: self.add_child(popup if isinstance(popup, Popup) else Popup(str(popup))) if tooltip is not None: self.add_child(tooltip if isinstance(tooltip, Tooltip) else Tooltip(str(tooltip))) def _get_self_bounds(self): """Computes the bounds of the object itself. Because a marker has only single coordinates, we repeat them. """ return [self.location, self.location] def render(self): if self.location is None: raise ValueError("{} location must be assigned when added directly to map.".format(self._name)) super(Marker, self).render() class Popup(Element): """Create a Popup instance that can be linked to a Layer. Parameters ---------- html: string or Element Content of the Popup. parse_html: bool, default False True if the popup is a template that needs to the rendered first. max_width: int for pixels or text for percentages, default '100%' The maximal width of the popup. show: bool, default False True renders the popup open on page load. sticky: bool, default False True prevents map and other popup clicks from closing. """ _template = Template(u""" var {{this.get_name()}} = L.popup({{ this.options|tojson }}); {% for name, element in this.html._children.items() %} var {{ name }} = $(`{{ element.render(**kwargs).replace('\\n',' ') }}`)[0]; {{ this.get_name() }}.setContent({{ name }}); {% endfor %} {{ this._parent.get_name() }}.bindPopup({{ this.get_name() }}) {% if this.show %}.openPopup(){% endif %}; {% for name, element in this.script._children.items() %} {{element.render()}} {% endfor %} """) # noqa def __init__(self, html=None, parse_html=False, max_width='100%', show=False, sticky=False, **kwargs): super(Popup, self).__init__() self._name = 'Popup' self.header = Element() self.html = Element() self.script = Element() self.header._parent = self self.html._parent = self self.script._parent = self script = not parse_html if isinstance(html, Element): self.html.add_child(html) elif isinstance(html, str): self.html.add_child(Html(html, script=script)) self.show = show self.options = parse_options( max_width=max_width, autoClose=False if show or sticky else None, closeOnClick=False if sticky else None, **kwargs ) def render(self, **kwargs): """Renders the HTML representation of the element.""" for name, child in self._children.items(): child.render(**kwargs) figure = self.get_root() assert isinstance(figure, Figure), ('You cannot render this Element ' 'if it is not in a Figure.') figure.script.add_child(Element( self._template.render(this=self, kwargs=kwargs)), name=self.get_name()) class Tooltip(MacroElement): """ Create a tooltip that shows text when hovering over its parent object. Parameters ---------- text: str String to display as a tooltip on the object. If the argument is of a different type it will be converted to str. style: str, default None. HTML inline style properties like font and colors. Will be applied to a div with the text in it. sticky: bool, default True Whether the tooltip should follow the mouse. **kwargs These values will map directly to the Leaflet Options. More info available here: https://leafletjs.com/reference-1.6.0#tooltip """ _template = Template(u""" {% macro script(this, kwargs) %} {{ this._parent.get_name() }}.bindTooltip( `