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