From 47adef5852861e33e2509b96421d07fdda01efe7 Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Mon, 6 Nov 2023 23:02:01 +0100 Subject: [PATCH 1/5] use xyzservices instead of templates --- folium/folium.py | 13 ++-- folium/raster_layers.py | 68 ++++++++----------- .../tiles/cartodbdark_matter/attr.txt | 1 - .../tiles/cartodbdark_matter/tiles.txt | 1 - .../templates/tiles/cartodbpositron/attr.txt | 1 - .../templates/tiles/cartodbpositron/tiles.txt | 1 - .../tiles/cartodbpositronnolabels/attr.txt | 1 - .../tiles/cartodbpositronnolabels/tiles.txt | 1 - .../tiles/cartodbpositrononlylabels/attr.txt | 1 - .../tiles/cartodbpositrononlylabels/tiles.txt | 1 - folium/templates/tiles/openstreetmap/attr.txt | 1 - .../templates/tiles/openstreetmap/tiles.txt | 1 - requirements-dev.txt | 1 - requirements.txt | 1 + tests/test_folium.py | 26 +++---- tests/test_raster_layers.py | 7 +- 16 files changed, 51 insertions(+), 75 deletions(-) delete mode 100644 folium/templates/tiles/cartodbdark_matter/attr.txt delete mode 100644 folium/templates/tiles/cartodbdark_matter/tiles.txt delete mode 100644 folium/templates/tiles/cartodbpositron/attr.txt delete mode 100644 folium/templates/tiles/cartodbpositron/tiles.txt delete mode 100644 folium/templates/tiles/cartodbpositronnolabels/attr.txt delete mode 100644 folium/templates/tiles/cartodbpositronnolabels/tiles.txt delete mode 100644 folium/templates/tiles/cartodbpositrononlylabels/attr.txt delete mode 100644 folium/templates/tiles/cartodbpositrononlylabels/tiles.txt delete mode 100644 folium/templates/tiles/openstreetmap/attr.txt delete mode 100644 folium/templates/tiles/openstreetmap/tiles.txt diff --git a/folium/folium.py b/folium/folium.py index 4cf889244..60e4f905a 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -82,15 +82,14 @@ class Map(JSCSSMixin, MacroElement): """Create a Map with Folium and Leaflet.js Generate a base map of given width and height with either default - tilesets or a custom tileset URL. The following tilesets are built-in - to Folium. Pass any of the following to the "tiles" keyword: + tilesets or a custom tileset URL. Folium has built-in all tilesets + available in the ``xyzservices`` package. For example, you can pass + any of the following to the "tiles" keyword: - "OpenStreetMap" - - "Mapbox Bright" (Limited levels of zoom for free tiles) - - "Mapbox Control Room" (Limited levels of zoom for free tiles) - - "Cloudmade" (Must pass API key) - - "Mapbox" (Must pass API key) - - "CartoDB" (positron and dark_matter) + - "CartoDB Positron" + - "CartoBD Voyager" + - "NASAGIBS Blue Marble" You can pass a custom tileset to Folium by passing a :class:`xyzservices.TileProvider` or a Leaflet-style diff --git a/folium/raster_layers.py b/folium/raster_layers.py index fe3079b72..39ce0712a 100644 --- a/folium/raster_layers.py +++ b/folium/raster_layers.py @@ -2,8 +2,9 @@ Wraps leaflet TileLayer, WmsTileLayer (TileLayer.WMS), ImageOverlay, and VideoOverlay """ -from typing import TYPE_CHECKING, Any, Callable, Optional, Union +from typing import Any, Callable, Optional, Union +import xyzservices from branca.element import Element, Figure from jinja2 import Environment, PackageLoader, Template @@ -16,10 +17,6 @@ parse_options, ) -if TYPE_CHECKING: - import xyzservices - - ENV = Environment(loader=PackageLoader("folium", "templates")) @@ -30,9 +27,14 @@ class TileLayer(Layer): Parameters ---------- tiles: str or :class:`xyzservices.TileProvider`, default 'OpenStreetMap' - Map tileset to use. Can choose from this list of built-in tiles: + Map tileset to use. Folium has built-in all tilesets + available in the ``xyzservices`` package. For example, you can pass + any of the following to the "tiles" keyword: + - "OpenStreetMap" - - "CartoDB positron", "CartoDB dark_matter" + - "CartoDB Positron" + - "CartoBD Voyager" + - "NASAGIBS Blue Marble" You can pass a custom tileset to Folium by passing a :class:`xyzservices.TileProvider` or a Leaflet-style @@ -90,7 +92,7 @@ class TileLayer(Layer): def __init__( self, - tiles: Union[str, "xyzservices.TileProvider"] = "OpenStreetMap", + tiles: Union[str, xyzservices.TileProvider] = "OpenStreetMap", min_zoom: int = 0, max_zoom: int = 18, max_native_zoom: Optional[int] = None, @@ -104,45 +106,35 @@ def __init__( subdomains: str = "abc", tms: bool = False, opacity: float = 1, - **kwargs + **kwargs, ): - # check for xyzservices.TileProvider without importing it - if isinstance(tiles, dict): + if isinstance(tiles, str): + if tiles == "OpenStreetMap": + tiles = "OpenStreetMap Mapnik" + if name is None: + name = "openstreetmap" + try: + tiles = xyzservices.providers.query_name(tiles) + except ValueError: + # no match, likely a custom URL + pass + + if isinstance(tiles, xyzservices.TileProvider): attr = attr if attr else tiles.html_attribution # type: ignore min_zoom = tiles.get("min_zoom", min_zoom) max_zoom = tiles.get("max_zoom", max_zoom) subdomains = tiles.get("subdomains", subdomains) + if name is None: + name = tiles.name.replace(".", "").lower() tiles = tiles.build_url(fill_subdomain=False, scale_factor="{r}") # type: ignore - self.tile_name = ( - name if name is not None else "".join(tiles.lower().strip().split()) - ) - super().__init__( - name=self.tile_name, overlay=overlay, control=control, show=show - ) + self.tile_name = name if name is not None else "".join(tiles.lower().strip().split()) + super().__init__(name=self.tile_name, overlay=overlay, control=control, show=show) self._name = "TileLayer" - tiles_flat = "".join(tiles.lower().strip().split()) - if tiles_flat in {"cloudmade", "mapbox", "mapboxbright", "mapboxcontrolroom"}: - # added in May 2020 after v0.11.0, remove in a future release - raise ValueError( - "Built-in templates for Mapbox and Cloudmade have been removed. " - "You can still use these providers by passing a URL to the `tiles` " - "argument. See the documentation of the `TileLayer` class." - ) - templates = list( - ENV.list_templates(filter_func=lambda x: x.startswith("tiles/")) - ) - tile_template = "tiles/" + tiles_flat + "/tiles.txt" - attr_template = "tiles/" + tiles_flat + "/attr.txt" - - if tile_template in templates and attr_template in templates: - self.tiles = ENV.get_template(tile_template).render() - attr = ENV.get_template(attr_template).render() - else: - self.tiles = tiles - if not attr: - raise ValueError("Custom tiles must have an attribution.") + self.tiles = tiles + if not attr: + raise ValueError("Custom tiles must have an attribution.") self.options = parse_options( min_zoom=min_zoom, diff --git a/folium/templates/tiles/cartodbdark_matter/attr.txt b/folium/templates/tiles/cartodbdark_matter/attr.txt deleted file mode 100644 index 0892c0cc3..000000000 --- a/folium/templates/tiles/cartodbdark_matter/attr.txt +++ /dev/null @@ -1 +0,0 @@ -© OpenStreetMap contributors © CartoDB, CartoDB attributions diff --git a/folium/templates/tiles/cartodbdark_matter/tiles.txt b/folium/templates/tiles/cartodbdark_matter/tiles.txt deleted file mode 100644 index ebf805b3e..000000000 --- a/folium/templates/tiles/cartodbdark_matter/tiles.txt +++ /dev/null @@ -1 +0,0 @@ -https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png diff --git a/folium/templates/tiles/cartodbpositron/attr.txt b/folium/templates/tiles/cartodbpositron/attr.txt deleted file mode 100644 index 0892c0cc3..000000000 --- a/folium/templates/tiles/cartodbpositron/attr.txt +++ /dev/null @@ -1 +0,0 @@ -© OpenStreetMap contributors © CartoDB, CartoDB attributions diff --git a/folium/templates/tiles/cartodbpositron/tiles.txt b/folium/templates/tiles/cartodbpositron/tiles.txt deleted file mode 100644 index 316884cc5..000000000 --- a/folium/templates/tiles/cartodbpositron/tiles.txt +++ /dev/null @@ -1 +0,0 @@ -https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png diff --git a/folium/templates/tiles/cartodbpositronnolabels/attr.txt b/folium/templates/tiles/cartodbpositronnolabels/attr.txt deleted file mode 100644 index 43c55c25a..000000000 --- a/folium/templates/tiles/cartodbpositronnolabels/attr.txt +++ /dev/null @@ -1 +0,0 @@ -(c) OpenStreetMap contributors (c) CartoDB, CartoDB attributions diff --git a/folium/templates/tiles/cartodbpositronnolabels/tiles.txt b/folium/templates/tiles/cartodbpositronnolabels/tiles.txt deleted file mode 100644 index b92e71915..000000000 --- a/folium/templates/tiles/cartodbpositronnolabels/tiles.txt +++ /dev/null @@ -1 +0,0 @@ -https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png diff --git a/folium/templates/tiles/cartodbpositrononlylabels/attr.txt b/folium/templates/tiles/cartodbpositrononlylabels/attr.txt deleted file mode 100644 index 43c55c25a..000000000 --- a/folium/templates/tiles/cartodbpositrononlylabels/attr.txt +++ /dev/null @@ -1 +0,0 @@ -(c) OpenStreetMap contributors (c) CartoDB, CartoDB attributions diff --git a/folium/templates/tiles/cartodbpositrononlylabels/tiles.txt b/folium/templates/tiles/cartodbpositrononlylabels/tiles.txt deleted file mode 100644 index f5e0b0c85..000000000 --- a/folium/templates/tiles/cartodbpositrononlylabels/tiles.txt +++ /dev/null @@ -1 +0,0 @@ -https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png diff --git a/folium/templates/tiles/openstreetmap/attr.txt b/folium/templates/tiles/openstreetmap/attr.txt deleted file mode 100644 index bfabc61be..000000000 --- a/folium/templates/tiles/openstreetmap/attr.txt +++ /dev/null @@ -1 +0,0 @@ -Data by © OpenStreetMap, under ODbL. diff --git a/folium/templates/tiles/openstreetmap/tiles.txt b/folium/templates/tiles/openstreetmap/tiles.txt deleted file mode 100644 index 26261ed37..000000000 --- a/folium/templates/tiles/openstreetmap/tiles.txt +++ /dev/null @@ -1 +0,0 @@ -https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png diff --git a/requirements-dev.txt b/requirements-dev.txt index 61d92d317..c01cd6f49 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -36,4 +36,3 @@ types-requests vega_datasets vincent wheel -xyzservices diff --git a/requirements.txt b/requirements.txt index b06947b5f..dff8e41f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ branca>=0.6.0 jinja2>=2.9 numpy requests +xyzservices diff --git a/tests/test_folium.py b/tests/test_folium.py index 4d119176f..6419e2ab7 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -13,6 +13,7 @@ import numpy as np import pandas as pd import pytest +import xyzservices.providers as xyz from jinja2 import Environment, PackageLoader from jinja2.utils import htmlsafe_json_dumps @@ -106,24 +107,17 @@ def test_init(self): }, } - def test_builtin_tile(self): + @pytest.mark.parametrize("tiles,provider", [("OpenStreetMap", xyz.OpenStreetMap.Mapnik), ("CartoDB positron", xyz.CartoDB.Positron), ("CartoDB dark_matter", xyz.CartoDB.DarkMatter)]) + def test_builtin_tile(self, tiles, provider): """Test custom maptiles.""" - default_tiles = [ - "OpenStreetMap", - "CartoDB positron", - "CartoDB dark_matter", - ] - for tiles in default_tiles: - m = folium.Map(location=[45.5236, -122.6750], tiles=tiles) - tiles = "".join(tiles.lower().strip().split()) - url = "tiles/{}/tiles.txt".format - attr = "tiles/{}/attr.txt".format - url = ENV.get_template(url(tiles)).render() - attr = ENV.get_template(attr(tiles)).render() - - assert m._children[tiles].tiles == url - assert htmlsafe_json_dumps(attr) in m._parent.render() + m = folium.Map(location=[45.5236, -122.6750], tiles=tiles) + tiles = "".join(tiles.lower().strip().split()) + url = provider.build_url(fill_subdomain=False, scale_factor="{r}") + attr = provider.html_attribution + + assert m._children[tiles.replace("_", "")].tiles == url + assert htmlsafe_json_dumps(attr) in m._parent.render() bounds = m.get_bounds() assert bounds == [[None, None], [None, None]], bounds diff --git a/tests/test_raster_layers.py b/tests/test_raster_layers.py index c4cefec09..5377bd53d 100644 --- a/tests/test_raster_layers.py +++ b/tests/test_raster_layers.py @@ -4,6 +4,7 @@ """ import xyzservices +import pytest from jinja2 import Template import folium @@ -112,10 +113,10 @@ def test_image_overlay(): bounds = m.get_bounds() assert bounds == [[0, -180], [90, 180]], bounds - -def test_xyzservices(): +@pytest.mark.parametrize("tiles", ["CartoDB DarkMatter", xyzservices.providers.CartoDB.DarkMatter]) +def test_xyzservices(tiles): m = folium.Map( - [48.0, 5.0], tiles=xyzservices.providers.CartoDB.DarkMatter, zoom_start=6 + [48.0, 5.0], tiles=tiles, zoom_start=6 ) folium.raster_layers.TileLayer( From 2ab4be868c2490dbc95f4aea3a04fd15a8d045ff Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Mon, 6 Nov 2023 23:08:42 +0100 Subject: [PATCH 2/5] fix minimap test --- tests/plugins/test_minimap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/test_minimap.py b/tests/plugins/test_minimap.py index 5919c9cde..f80f31152 100644 --- a/tests/plugins/test_minimap.py +++ b/tests/plugins/test_minimap.py @@ -25,4 +25,4 @@ def test_minimap(): out = normalize(m._parent.render()) # verify that tiles are being used - assert r"https://{s}.tile.openstreetmap.org" in out + assert r"https://tile.openstreetmap.org" in out From 460daaa4bebbdf6e2ed4769969db645a7286f529 Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Mon, 6 Nov 2023 23:14:54 +0100 Subject: [PATCH 3/5] more robust check for openstreetmap --- folium/raster_layers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folium/raster_layers.py b/folium/raster_layers.py index 39ce0712a..ccc75a1fb 100644 --- a/folium/raster_layers.py +++ b/folium/raster_layers.py @@ -109,7 +109,7 @@ def __init__( **kwargs, ): if isinstance(tiles, str): - if tiles == "OpenStreetMap": + if tiles.lower() == "openstreetmap": tiles = "OpenStreetMap Mapnik" if name is None: name = "openstreetmap" From 2ab3edd91a2d3c8ea956982fb59d222760d6c200 Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Mon, 6 Nov 2023 23:17:51 +0100 Subject: [PATCH 4/5] lint --- folium/raster_layers.py | 18 +++++++++++------- tests/test_folium.py | 10 ++++++++-- tests/test_raster_layers.py | 11 ++++++----- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/folium/raster_layers.py b/folium/raster_layers.py index ccc75a1fb..84a65baaa 100644 --- a/folium/raster_layers.py +++ b/folium/raster_layers.py @@ -128,8 +128,12 @@ def __init__( name = tiles.name.replace(".", "").lower() tiles = tiles.build_url(fill_subdomain=False, scale_factor="{r}") # type: ignore - self.tile_name = name if name is not None else "".join(tiles.lower().strip().split()) - super().__init__(name=self.tile_name, overlay=overlay, control=control, show=show) + self.tile_name = ( + name if name is not None else "".join(tiles.lower().strip().split()) + ) + super().__init__( + name=self.tile_name, overlay=overlay, control=control, show=show + ) self._name = "TileLayer" self.tiles = tiles @@ -146,7 +150,7 @@ def __init__( detect_retina=detect_retina, tms=tms, opacity=opacity, - **kwargs + **kwargs, ) @@ -211,7 +215,7 @@ def __init__( overlay: bool = True, control: bool = True, show: bool = True, - **kwargs + **kwargs, ): super().__init__(name=name, overlay=overlay, control=control, show=show) self.url = url @@ -223,7 +227,7 @@ def __init__( transparent=transparent, version=version, attribution=attr, - **kwargs + **kwargs, ) if cql_filter: # special parameter that shouldn't be camelized @@ -301,7 +305,7 @@ def __init__( overlay: bool = True, control: bool = True, show: bool = True, - **kwargs + **kwargs, ): super().__init__(name=name, overlay=overlay, control=control, show=show) self._name = "ImageOverlay" @@ -398,7 +402,7 @@ def __init__( overlay: bool = True, control: bool = True, show: bool = True, - **kwargs: TypeJsonValue + **kwargs: TypeJsonValue, ): super().__init__(name=name, overlay=overlay, control=control, show=show) self._name = "VideoOverlay" diff --git a/tests/test_folium.py b/tests/test_folium.py index 6419e2ab7..36533e193 100644 --- a/tests/test_folium.py +++ b/tests/test_folium.py @@ -20,7 +20,6 @@ import folium from folium import TileLayer from folium.features import Choropleth, GeoJson -from folium.raster_layers import ENV rootpath = os.path.abspath(os.path.dirname(__file__)) @@ -107,7 +106,14 @@ def test_init(self): }, } - @pytest.mark.parametrize("tiles,provider", [("OpenStreetMap", xyz.OpenStreetMap.Mapnik), ("CartoDB positron", xyz.CartoDB.Positron), ("CartoDB dark_matter", xyz.CartoDB.DarkMatter)]) + @pytest.mark.parametrize( + "tiles,provider", + [ + ("OpenStreetMap", xyz.OpenStreetMap.Mapnik), + ("CartoDB positron", xyz.CartoDB.Positron), + ("CartoDB dark_matter", xyz.CartoDB.DarkMatter), + ], + ) def test_builtin_tile(self, tiles, provider): """Test custom maptiles.""" diff --git a/tests/test_raster_layers.py b/tests/test_raster_layers.py index 5377bd53d..600d87664 100644 --- a/tests/test_raster_layers.py +++ b/tests/test_raster_layers.py @@ -3,8 +3,8 @@ ------------------ """ -import xyzservices import pytest +import xyzservices from jinja2 import Template import folium @@ -113,11 +113,12 @@ def test_image_overlay(): bounds = m.get_bounds() assert bounds == [[0, -180], [90, 180]], bounds -@pytest.mark.parametrize("tiles", ["CartoDB DarkMatter", xyzservices.providers.CartoDB.DarkMatter]) + +@pytest.mark.parametrize( + "tiles", ["CartoDB DarkMatter", xyzservices.providers.CartoDB.DarkMatter] +) def test_xyzservices(tiles): - m = folium.Map( - [48.0, 5.0], tiles=tiles, zoom_start=6 - ) + m = folium.Map([48.0, 5.0], tiles=tiles, zoom_start=6) folium.raster_layers.TileLayer( tiles=xyzservices.providers.CartoDB.Positron, From 5f5fe0a5ccdd775dd141bed15c3cd60fd874a18e Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Tue, 7 Nov 2023 13:28:00 +0100 Subject: [PATCH 5/5] rm ENV --- folium/raster_layers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/folium/raster_layers.py b/folium/raster_layers.py index 84a65baaa..ea0e6d180 100644 --- a/folium/raster_layers.py +++ b/folium/raster_layers.py @@ -6,7 +6,7 @@ import xyzservices from branca.element import Element, Figure -from jinja2 import Environment, PackageLoader, Template +from jinja2 import Template from folium.map import Layer from folium.utilities import ( @@ -17,8 +17,6 @@ parse_options, ) -ENV = Environment(loader=PackageLoader("folium", "templates")) - class TileLayer(Layer): """