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