# -*- 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( ` {{ this.text }} `, {{ this.options|tojson }} ); {% endmacro %} """) valid_options = { 'pane': (str, ), 'offset': (tuple, ), 'direction': (str, ), 'permanent': (bool, ), 'sticky': (bool, ), 'interactive': (bool, ), 'opacity': (float, int), 'attribution': (str, ), 'className': (str, ), } def __init__(self, text, style=None, sticky=True, **kwargs): super(Tooltip, self).__init__() self._name = 'Tooltip' self.text = str(text) kwargs.update({'sticky': sticky}) self.options = self.parse_options(kwargs) if style: assert isinstance(style, str), \ 'Pass a valid inline HTML style property string to style.' # noqa outside of type checking. self.style = style def parse_options(self, kwargs): """Validate the provided kwargs and return options as json string.""" kwargs = {camelize(key): value for key, value in kwargs.items()} for key in kwargs.keys(): assert key in self.valid_options, ( 'The option {} is not in the available options: {}.' .format(key, ', '.join(self.valid_options)) ) assert isinstance(kwargs[key], self.valid_options[key]), ( 'The option {} must be one of the following types: {}.' .format(key, self.valid_options[key]) ) return kwargs class FitBounds(MacroElement): """Fit the map to contain a bounding box with the maximum zoom level possible. Parameters ---------- bounds: list of (latitude, longitude) points Bounding box specified as two points [southwest, northeast] padding_top_left: (x, y) point, default None Padding in the top left corner. Useful if some elements in the corner, such as controls, might obscure objects you're zooming to. padding_bottom_right: (x, y) point, default None Padding in the bottom right corner. padding: (x, y) point, default None Equivalent to setting both top left and bottom right padding to the same value. max_zoom: int, default None Maximum zoom to be used. """ _template = Template(u""" {% macro script(this, kwargs) %} {{ this._parent.get_name() }}.fitBounds( {{ this.bounds|tojson }}, {{ this.options|tojson }} ); {% endmacro %} """) def __init__(self, bounds, padding_top_left=None, padding_bottom_right=None, padding=None, max_zoom=None): super(FitBounds, self).__init__() self._name = 'FitBounds' self.bounds = bounds self.options = parse_options( max_zoom=max_zoom, padding_top_left=padding_top_left, padding_bottom_right=padding_bottom_right, padding=padding, ) class CustomPane(MacroElement): """ Creates a custom pane to hold map elements. Behavior is as in https://leafletjs.com/examples/map-panes/ Parameters ---------- name: string Name of the custom pane. Other map elements can be added to the pane by specifying the 'pane' kwarg when constructing them. z_index: int or string, default 625 The z-index that will be associated with the pane, and will determine which map elements lie over/under it. The default (625) corresponds to between markers and tooltips. Default panes and z-indexes can be found at https://leafletjs.com/reference-1.6.0.html#map-pane pointer_events: bool, default False Whether or not layers in the pane should interact with the cursor. Setting to False will prevent interfering with pointer events associated with lower layers. """ _template = Template(u""" {% macro script(this, kwargs) %} var {{ this.get_name() }} = {{ this._parent.get_name() }}.createPane( {{ this.name|tojson }}); {{ this.get_name() }}.style.zIndex = {{ this.z_index|tojson }}; {% if not this.pointer_events %} {{ this.get_name() }}.style.pointerEvents = 'none'; {% endif %} {% endmacro %} """) def __init__(self, name, z_index=625, pointer_events=False): super(CustomPane, self).__init__() self._name = 'Pane' self.name = name self.z_index = z_index self.pointer_events = pointer_events