22 | {% set nav = get_nav_object(maxdepth=3, collapse=True) %}
23 |
24 |
25 |
26 |
33 |
34 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | # Dependencies.
4 | with open("requirements.txt") as f:
5 | tests_require = f.readlines()
6 | install_requires = [t.strip() for t in tests_require]
7 |
8 | with open("README.md") as f:
9 | long_description = f.read()
10 |
11 | setup(
12 | name="contextily",
13 | version="1.0.0",
14 | description="Context geo-tiles in Python",
15 | long_description=long_description,
16 | long_description_content_type="text/markdown",
17 | url="https://github.com/darribas/contextily",
18 | author="Dani Arribas-Bel",
19 | author_email="daniel.arribas.bel@gmail.com",
20 | license="3-Clause BSD",
21 | packages=["contextily"],
22 | package_data={"": ["requirements.txt"]},
23 | classifiers=[
24 | "License :: OSI Approved :: BSD License",
25 | "Programming Language :: Python :: 3",
26 | "Programming Language :: Python :: 3.5",
27 | "Programming Language :: Python :: 3.6",
28 | "Programming Language :: Python :: 3.7",
29 | "Programming Language :: Python :: 3 :: Only",
30 | "Programming Language :: Python :: Implementation :: CPython",
31 | ],
32 | python_requires=">=3.6",
33 | install_requires=install_requires,
34 | zip_safe=False,
35 | )
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Dani Arribas-Bel
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | * Neither the name of Dani Arribas-Bel nor the names of other contributors
15 | may be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
19 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
20 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
21 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
23 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 | POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/examples/plot_map.py:
--------------------------------------------------------------------------------
1 | """
2 | Downloading and Plotting Maps
3 | -----------------------------
4 |
5 | Plotting maps with Contextily.
6 |
7 | Contextily is designed to pull map tile information from the web. In many
8 | cases we want to go from a location to a map of that location as quickly
9 | as possible. There are two main ways to do this with Contextily.
10 |
11 | Searching for places with text
12 | ==============================
13 |
14 | The simplest approach is to search for a location with text. You can do
15 | this with the ``Place`` class. This will return an object that contains
16 | metadata about the place, such as its bounding box. It will also contain an
17 | image of the place.
18 | """
19 | import numpy as np
20 | import matplotlib.pyplot as plt
21 | import contextily as ctx
22 |
23 | loc = ctx.Place("boulder", zoom_adjust=0) # zoom_adjust modifies the auto-zoom
24 |
25 | # Print some metadata
26 | for attr in ["w", "s", "e", "n", "place", "zoom", "n_tiles"]:
27 | print("{}: {}".format(attr, getattr(loc, attr)))
28 |
29 | # Show the map
30 | im1 = loc.im
31 |
32 | fig, axs = plt.subplots(1, 3, figsize=(15, 5))
33 | ctx.plot_map(loc, ax=axs[0])
34 |
35 | ###############################################################################
36 | # The zoom level will be chosen for you by default, though you can specify
37 | # this manually as well:
38 |
39 | loc2 = ctx.Place("boulder", zoom=11)
40 | ctx.plot_map(loc2, ax=axs[1])
41 |
42 | ###############################################################################
43 | # Downloading tiles from bounds
44 | # =============================
45 | #
46 | # You can also grab tile information directly from a bounding box + zoom level.
47 | # This is demoed below:
48 |
49 | im2, bbox = ctx.bounds2img(loc.w, loc.s, loc.e, loc.n, zoom=loc.zoom, ll=True)
50 | ctx.plot_map(im2, bbox, ax=axs[2], title="Boulder, CO")
51 |
52 | plt.show()
53 |
--------------------------------------------------------------------------------
/contextily/tile_providers.py:
--------------------------------------------------------------------------------
1 | """Common tile provider URLs."""
2 | import warnings
3 | import sys
4 |
5 | ### Tile provider sources ###
6 |
7 | _ST_TONER = "http://tile.stamen.com/toner/{z}/{x}/{y}.png"
8 | _ST_TONER_HYBRID = "http://tile.stamen.com/toner-hybrid/{z}/{x}/{y}.png"
9 | _ST_TONER_LABELS = "http://tile.stamen.com/toner-labels/{z}/{x}/{y}.png"
10 | _ST_TONER_LINES = "http://tile.stamen.com/toner-lines/{z}/{x}/{y}.png"
11 | _ST_TONER_BACKGROUND = "http://tile.stamen.com/toner-background/{z}/{x}/{y}.png"
12 | _ST_TONER_LITE = "http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png"
13 |
14 | _ST_TERRAIN = "http://tile.stamen.com/terrain/{z}/{x}/{y}.png"
15 | _ST_TERRAIN_LABELS = "http://tile.stamen.com/terrain-labels/{z}/{x}/{y}.png"
16 | _ST_TERRAIN_LINES = "http://tile.stamen.com/terrain-lines/{z}/{x}/{y}.png"
17 | _ST_TERRAIN_BACKGROUND = "http://tile.stamen.com/terrain-background/{z}/{x}/{y}.png"
18 |
19 | _T_WATERCOLOR = "http://tile.stamen.com/watercolor/{z}/{x}/{y}.png"
20 |
21 | # OpenStreetMap as an alternative
22 | _OSM_A = "http://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
23 | _OSM_B = "http://b.tile.openstreetmap.org/{z}/{x}/{y}.png"
24 | _OSM_C = "http://c.tile.openstreetmap.org/{z}/{x}/{y}.png"
25 |
26 | deprecated_sources = {k.lstrip('_') for k, v in locals().items()
27 | if (False if not isinstance(v, str)
28 | else (v.startswith('http')))
29 | }
30 |
31 |
32 | def __getattr__(name):
33 | if name in deprecated_sources:
34 | warnings.warn('The "contextily.tile_providers" module is deprecated and will be removed in '
35 | 'contextily v1.1. Please use "contextily.providers" instead.',
36 | FutureWarning, stacklevel=2)
37 | return globals()[f'_{name}']
38 | raise AttributeError(f'module {__name__} has no attribute {name}')
39 |
40 |
41 | if (sys.version_info.major == 3 and sys.version_info.minor < 7):
42 | globals().update({k: globals().get('_'+k) for k in deprecated_sources})
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `contextily`: context geo tiles in Python
2 |
3 | `contextily` is a small Python 3 (3.6 and above) package to retrieve tile maps from the
4 | internet. It can add those tiles as basemap to matplotlib figures or write tile
5 | maps to disk into geospatial raster files. Bounding boxes can be passed in both
6 | WGS84 (`EPSG:4326`) and Spheric Mercator (`EPSG:3857`). See the notebook
7 | `contextily_guide.ipynb` for usage.
8 |
9 | [](https://travis-ci.org/geopandas/contextily)
10 | [](https://coveralls.io/github/darribas/contextily?branch=master)
11 | [](https://mybinder.org/v2/gh/geopandas/contextily/master?urlpath=lab/tree/notebooks/intro_guide.ipynb)
12 |
13 | 
14 |
15 | The current tile providers that are available in contextily are the providers
16 | defined in the [leaflet-providers](https://github.com/leaflet-extras/leaflet-providers)
17 | package. This includes some popular tile maps, such as:
18 |
19 | * The standard [OpenStreetMap](http://openstreetmap.org) map tiles
20 | * Toner, Terrain and Watercolor map tiles by [Stamen Design](http://stamen.com)
21 |
22 | ## Dependencies
23 |
24 | * `mercantile`
25 | * `numpy`
26 | * `matplotlib`
27 | * `pillow`
28 | * `rasterio`
29 | * `requests`
30 | * `geopy`
31 | * `joblib`
32 |
33 | ## Installation
34 |
35 | **Python 3 only** (3.6 and above)
36 |
37 | [Latest released version](https://github.com/geopandas/contextily/releases/), using pip:
38 |
39 | ```sh
40 | pip3 install contextily
41 | ```
42 |
43 | or conda:
44 |
45 | ```sh
46 | conda install contextily
47 | ```
48 |
49 |
50 | ## Contributors
51 |
52 | `contextily` is developed by a community of enthusiastic volunteers. You can see a full list [here](https://github.com/geopandas/contextily/graphs/contributors).
53 |
54 | If you would like to contribute to the project, have a look at the list of [open issues](https://github.com/geopandas/contextily/issues), particularly those labeled as [good first contributions](https://github.com/geopandas/contextily/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-contribution).
55 |
56 | ## License
57 |
58 | BSD compatible. See `LICENSE.txt`
59 |
--------------------------------------------------------------------------------
/tests/test_providers.py:
--------------------------------------------------------------------------------
1 | import contextily as ctx
2 | import contextily.tile_providers as tilers
3 |
4 | import pytest
5 | from numpy.testing import assert_allclose
6 |
7 |
8 | def test_sources():
9 | # NOTE: only tests they download, does not check pixel values
10 | w, s, e, n = (
11 | -106.6495132446289,
12 | 25.845197677612305,
13 | -93.50721740722656,
14 | 36.49387741088867,
15 | )
16 | sources = tilers.deprecated_sources
17 | for src in sources:
18 | img, ext = ctx.bounds2img(w, s, e, n, 4, source=getattr(tilers, src), ll=True)
19 |
20 |
21 | def test_deprecated_url_format():
22 | old_url = "http://a.tile.openstreetmap.org/tileZ/tileX/tileY.png"
23 | new_url = "http://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
24 |
25 | w, s, e, n = (
26 | -106.6495132446289,
27 | 25.845197677612305,
28 | -93.50721740722656,
29 | 36.49387741088867,
30 | )
31 |
32 | with pytest.warns(FutureWarning, match="The url format using 'tileX'"):
33 | img1, ext1 = ctx.bounds2img(w, s, e, n, 4, source=old_url, ll=True)
34 |
35 | img2, ext2 = ctx.bounds2img(w, s, e, n, 4, source=new_url, ll=True)
36 | assert_allclose(img1, img2)
37 | assert_allclose(ext1, ext2)
38 |
39 |
40 | def test_providers():
41 | # NOTE: only tests they download, does not check pixel values
42 | w, s, e, n = (
43 | -106.6495132446289,
44 | 25.845197677612305,
45 | -93.50721740722656,
46 | 36.49387741088867,
47 | )
48 | for provider in [
49 | ctx.providers.OpenStreetMap.Mapnik,
50 | ctx.providers.Stamen.Toner,
51 | ctx.providers.NASAGIBS.ViirsEarthAtNight2012,
52 | ]:
53 | ctx.bounds2img(w, s, e, n, 4, source=provider, ll=True)
54 |
55 |
56 | def test_providers_callable():
57 | # only testing the callable functionality to override a keyword, as we
58 | # cannot test the actual providers that need an API key
59 | updated_provider = ctx.providers.GeoportailFrance.maps(apikey="mykey")
60 | assert isinstance(updated_provider, ctx._providers.TileProvider)
61 | assert "url" in updated_provider
62 | assert updated_provider["apikey"] == "mykey"
63 | # check that original provider dict is not modified
64 | assert ctx.providers.GeoportailFrance.maps["apikey"] == "choisirgeoportail"
65 |
66 |
67 | def test_invalid_provider():
68 | w, s, e, n = (-106.649, 25.845, -93.507, 36.494)
69 | with pytest.raises(ValueError, match="The 'url' dict should at least contain"):
70 | ctx.bounds2img(w, s, e, n, 4, source={"missing": "url"}, ll=True)
71 |
72 |
73 | def test_provider_attribute_access():
74 | provider = ctx.providers.OpenStreetMap.Mapnik
75 | assert provider.name == "OpenStreetMap.Mapnik"
76 | with pytest.raises(AttributeError):
77 | provider.non_existing_key
78 |
79 |
80 | def test_url():
81 | # NOTE: only tests they download, does not check pixel values
82 | w, s, e, n = (
83 | -106.6495132446289,
84 | 25.845197677612305,
85 | -93.50721740722656,
86 | 36.49387741088867,
87 | )
88 | ctx.bounds2img(w, s, e, n, 4, url=ctx.providers.OpenStreetMap.Mapnik, ll=True)
89 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | import os
8 | import pathlib
9 | import shutil
10 | import subprocess
11 |
12 |
13 | # -- Path setup --------------------------------------------------------------
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | # import os
20 | # import sys
21 | # sys.path.insert(0, os.path.abspath('.'))
22 |
23 |
24 | # -- Project information -----------------------------------------------------
25 |
26 | project = 'contextily'
27 | copyright = '2020, Dani Arribas-Bel & Contexily Contributors'
28 | author = 'Dani Arribas-Bel & Contexily Contributors'
29 |
30 | # The full version, including alpha/beta/rc tags
31 | release = '1.0.0'
32 |
33 |
34 | # -- General configuration ---------------------------------------------------
35 |
36 | # Add any Sphinx extension module names here, as strings. They can be
37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
38 | # ones.
39 | extensions = [
40 | "sphinx.ext.autodoc",
41 | "numpydoc",
42 | "nbsphinx"
43 | ]
44 |
45 | # nbsphinx do not use requirejs (breaks bootstrap)
46 | nbsphinx_requirejs_path = ""
47 |
48 | # Add any paths that contain templates here, relative to this directory.
49 | templates_path = ['_templates']
50 |
51 | # List of patterns, relative to source directory, that match files and
52 | # directories to ignore when looking for source files.
53 | # This pattern also affects html_static_path and html_extra_path.
54 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
55 |
56 |
57 | # -- Options for HTML output -------------------------------------------------
58 |
59 | # The theme to use for HTML and HTML Help pages. See the documentation for
60 | # a list of builtin themes.
61 | #
62 | html_theme = 'pydata_sphinx_theme'
63 |
64 | # Add any paths that contain custom static files (such as style sheets) here,
65 | # relative to this directory. They are copied after the builtin static files,
66 | # so a file named "default.css" will overwrite the builtin "default.css".
67 | html_static_path = ['_static']
68 |
69 | html_css_files = [
70 | 'css/custom.css',
71 | ]
72 |
73 |
74 | # ---------------------------------------------------------------------------
75 |
76 | # Copy notebooks into the docs/ directory so sphinx sees them
77 |
78 | HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
79 |
80 |
81 | files_to_copy = [
82 | "notebooks/add_basemap_deepdive.ipynb",
83 | "notebooks/intro_guide.ipynb",
84 | "notebooks/places_guide.ipynb",
85 | "notebooks/providers_deepdive.ipynb",
86 | "notebooks/warping_guide.ipynb",
87 | "notebooks/working_with_local_files.ipynb",
88 | "notebooks/friends_gee.ipynb",
89 | "tiles.png"
90 | ]
91 |
92 |
93 | for filename in files_to_copy:
94 | shutil.copy(HERE / ".." / filename, HERE)
95 |
96 |
97 | # convert README to rst
98 |
99 | subprocess.check_output(['pandoc','--to', 'rst', '-o', 'README.rst', '../README.md'])
100 |
--------------------------------------------------------------------------------
/scripts/parse_leaflet_providers.py:
--------------------------------------------------------------------------------
1 | """
2 | Script to parse the tile providers defined by the leaflet-providers.js
3 | extension to Leaflet (https://github.com/leaflet-extras/leaflet-providers).
4 |
5 | It accesses the defined TileLayer.Providers objects through javascript
6 | using Selenium as JSON, and then processes this a fully specified
7 | javascript-independent dictionary and saves that final result as a JSON file.
8 |
9 | """
10 | import datetime
11 | import json
12 | import os
13 | import tempfile
14 | import textwrap
15 |
16 | import selenium.webdriver
17 | import git
18 | import html2text
19 |
20 |
21 | GIT_URL = "https://github.com/leaflet-extras/leaflet-providers.git"
22 |
23 |
24 | # -----------------------------------------------------------------------------
25 | # Downloading and processing the json data
26 |
27 |
28 | def get_json_data():
29 | with tempfile.TemporaryDirectory() as tmpdirname:
30 | repo = git.Repo.clone_from(GIT_URL, tmpdirname)
31 | commit_hexsha = repo.head.object.hexsha
32 | commit_message = repo.head.object.message
33 |
34 | index_path = "file://" + os.path.join(tmpdirname, "index.html")
35 |
36 | driver = selenium.webdriver.Firefox()
37 | driver.get(index_path)
38 | data = driver.execute_script(
39 | "return JSON.stringify(L.TileLayer.Provider.providers)"
40 | )
41 | driver.close()
42 |
43 | data = json.loads(data)
44 | description = "commit {0} ({1})".format(commit_hexsha, commit_message.strip())
45 |
46 | return data, description
47 |
48 |
49 | def process_data(data):
50 | # extract attributions from rawa data that later need to be substituted
51 | global ATTRIBUTIONS
52 | ATTRIBUTIONS = {
53 | "{attribution.OpenStreetMap}": data["OpenStreetMap"]["options"]["attribution"],
54 | "{attribution.Esri}": data["Esri"]["options"]["attribution"],
55 | "{attribution.OpenMapSurfer}": data["OpenMapSurfer"]["options"]["attribution"],
56 | }
57 |
58 | result = {}
59 | for provider in data:
60 | result[provider] = process_provider(data, provider)
61 | return result
62 |
63 |
64 | def process_provider(data, name="OpenStreetMap"):
65 | provider = data[name].copy()
66 | variants = provider.pop("variants", None)
67 | options = provider.pop("options")
68 | provider_keys = {**provider, **options}
69 |
70 | if variants is None:
71 | provider_keys["name"] = name
72 | provider_keys = pythonize_data(provider_keys)
73 | return provider_keys
74 |
75 | result = {}
76 |
77 | for variant in variants:
78 | var = variants[variant]
79 | if isinstance(var, str):
80 | variant_keys = {"variant": var}
81 | else:
82 | variant_keys = var.copy()
83 | variant_options = variant_keys.pop("options", {})
84 | variant_keys = {**variant_keys, **variant_options}
85 | variant_keys = {**provider_keys, **variant_keys}
86 | variant_keys["name"] = "{provider}.{variant}".format(
87 | provider=name, variant=variant
88 | )
89 | variant_keys = pythonize_data(variant_keys)
90 | result[variant] = variant_keys
91 |
92 | return result
93 |
94 |
95 | def pythonize_data(data):
96 | """
97 | Clean-up the javascript based dictionary:
98 | - rename mixedCase keys
99 | - substitute the attribution placeholders
100 | - convert html attribution to plain text
101 |
102 | """
103 | rename_keys = {"maxZoom": "max_zoom", "minZoom": "min_zoom"}
104 | attributions = ATTRIBUTIONS
105 |
106 | items = data.items()
107 |
108 | new_data = []
109 | for key, value in items:
110 | if key == "attribution":
111 | if "{attribution." in value:
112 | for placeholder, attr in attributions.items():
113 | if placeholder in value:
114 | value = value.replace(placeholder, attr)
115 | if "{attribution." not in value:
116 | # replaced last attribution
117 | break
118 | else:
119 | raise ValueError("Attribution not known: {}".format(value))
120 | # convert html text to plain text
121 | converter = html2text.HTML2Text(bodywidth=1000)
122 | converter.ignore_links = True
123 | value = converter.handle(value).strip()
124 | elif key in rename_keys:
125 | key = rename_keys[key]
126 | elif key == "url" and any(k in value for k in rename_keys):
127 | # NASAGIBS providers have {maxZoom} in the url
128 | for old, new in rename_keys.items():
129 | value = value.replace("{" + old + "}", "{" + new + "}")
130 | new_data.append((key, value))
131 |
132 | return dict(new_data)
133 |
134 |
135 | # -----------------------------------------------------------------------------
136 | # Generating a python file from the json
137 |
138 | template = '''\
139 | """
140 | Tile providers.
141 |
142 | This file is autogenerated! It is a python representation of the leaflet
143 | providers defined by the leaflet-providers.js extension to Leaflet
144 | (https://github.com/leaflet-extras/leaflet-providers).
145 | Credit to the leaflet-providers.js project (BSD 2-Clause "Simplified" License)
146 | and the Leaflet Providers contributors.
147 |
148 | Generated by parse_leaflet_providers.py at {timestamp} from leaflet-providers
149 | at {description}.
150 |
151 | """
152 |
153 |
154 | class Bunch(dict):
155 | """A dict with attribute-access"""
156 |
157 | def __getattr__(self, key):
158 | try:
159 | return self.__getitem__(key)
160 | except KeyError:
161 | raise AttributeError(key)
162 |
163 | def __dir__(self):
164 | return self.keys()
165 |
166 |
167 | class TileProvider(Bunch):
168 | """
169 | A dict with attribute-access and that
170 | can be called to update keys
171 | """
172 |
173 | def __call__(self, **kwargs):
174 | new = TileProvider(self) # takes a copy preserving the class
175 | new.update(kwargs)
176 | return new
177 |
178 |
179 | providers = Bunch(
180 | {providers}
181 | )
182 |
183 | '''
184 |
185 |
186 | def format_provider(data, name):
187 | formatted_keys = ",\n ".join(
188 | [
189 | "{key} = {value!r}".format(key=key, value=value)
190 | for key, value in data.items()
191 | ]
192 | )
193 | provider_template = """\
194 | {name} = TileProvider(
195 | {formatted_keys}
196 | )"""
197 | return provider_template.format(name=name, formatted_keys=formatted_keys)
198 |
199 |
200 | def format_bunch(data, name):
201 | bunch_template = """\
202 | {name} = Bunch(
203 | {variants}
204 | )"""
205 | return bunch_template.format(name=name, variants=textwrap.indent(data, " "))
206 |
207 |
208 | def generate_file(data, description):
209 | providers = []
210 |
211 | for provider_name in data.keys():
212 | provider = data[provider_name]
213 | if "url" in provider.keys():
214 | res = format_provider(provider, provider_name)
215 | else:
216 | variants = []
217 |
218 | for variant in provider:
219 | formatted = format_provider(provider[variant], variant)
220 | variants.append(formatted)
221 |
222 | variants = ",\n".join(variants)
223 | res = format_bunch(variants, provider_name)
224 |
225 | providers.append(res)
226 |
227 | providers = ",\n".join(providers)
228 | content = template.format(
229 | providers=textwrap.indent(providers, " "),
230 | description=description,
231 | timestamp=datetime.date.today(),
232 | )
233 | return content
234 |
235 |
236 | if __name__ == "__main__":
237 | data, description = get_json_data()
238 | with open("leaflet-providers-raw.json", "w") as f:
239 | json.dump(data, f)
240 |
241 | # with open("leaflet-providers-raw.json", "r") as f:
242 | # data = json.load(f)
243 | # description = ''
244 |
245 | result = process_data(data)
246 | with open("leaflet-providers-parsed.json", "w") as f:
247 | # wanted to add this as header to the file, but JSON does not support
248 | # comments
249 | print(
250 | "JSON representation of the leaflet providers defined by the "
251 | "leaflet-providers.js extension to Leaflet "
252 | "(https://github.com/leaflet-extras/leaflet-providers)"
253 | )
254 | print("This file is automatically generated from {}".format(description))
255 | json.dump(result, f)
256 |
257 | content = generate_file(result, description)
258 | with open("_providers.py", "w") as f:
259 | f.write(content)
260 |
--------------------------------------------------------------------------------
/contextily/plotting.py:
--------------------------------------------------------------------------------
1 | """Tools to plot basemaps"""
2 |
3 | import warnings
4 | import numpy as np
5 | from . import tile_providers as sources
6 | from . import providers
7 | from ._providers import TileProvider
8 | from .tile import bounds2img, _sm2ll, warp_tiles, _warper
9 | from rasterio.enums import Resampling
10 | from rasterio.warp import transform_bounds
11 | from matplotlib import patheffects
12 | from matplotlib.pyplot import draw
13 |
14 | INTERPOLATION = "bilinear"
15 | ZOOM = "auto"
16 | ATTRIBUTION_SIZE = 8
17 |
18 |
19 | def add_basemap(
20 | ax,
21 | zoom=ZOOM,
22 | source=None,
23 | interpolation=INTERPOLATION,
24 | attribution=None,
25 | attribution_size=ATTRIBUTION_SIZE,
26 | reset_extent=True,
27 | crs=None,
28 | resampling=Resampling.bilinear,
29 | url=None,
30 | **extra_imshow_args
31 | ):
32 | """
33 | Add a (web/local) basemap to `ax`.
34 |
35 | Parameters
36 | ----------
37 | ax : AxesSubplot
38 | Matplotlib axes object on which to add the basemap. The extent of the
39 | axes is assumed to be in Spherical Mercator (EPSG:3857), unless the `crs`
40 | keyword is specified.
41 | zoom : int or 'auto'
42 | [Optional. Default='auto'] Level of detail for the basemap. If 'auto',
43 | it is calculated automatically. Ignored if `source` is a local file.
44 | source : contextily.providers object or str
45 | [Optional. Default: Stamen Terrain web tiles]
46 | The tile source: web tile provider or path to local file. The web tile
47 | provider can be in the form of a `contextily.providers` object or a
48 | URL. The placeholders for the XYZ in the URL need to be `{x}`, `{y}`,
49 | `{z}`, respectively. For local file paths, the file is read with
50 | `rasterio` and all bands are loaded into the basemap.
51 | IMPORTANT: tiles are assumed to be in the Spherical Mercator
52 | projection (EPSG:3857), unless the `crs` keyword is specified.
53 | interpolation : str
54 | [Optional. Default='bilinear'] Interpolation algorithm to be passed
55 | to `imshow`. See `matplotlib.pyplot.imshow` for further details.
56 | attribution : str
57 | [Optional. Defaults to attribution specified by the source]
58 | Text to be added at the bottom of the axis. This
59 | defaults to the attribution of the provider specified
60 | in `source` if available. Specify False to not
61 | automatically add an attribution, or a string to pass
62 | a custom attribution.
63 | attribution_size : int
64 | [Optional. Defaults to `ATTRIBUTION_SIZE`].
65 | Font size to render attribution text with.
66 | reset_extent : bool
67 | [Optional. Default=True] If True, the extent of the
68 | basemap added is reset to the original extent (xlim,
69 | ylim) of `ax`
70 | crs : None or str or CRS
71 | [Optional. Default=None] coordinate reference system (CRS),
72 | expressed in any format permitted by rasterio, to use for the
73 | resulting basemap. If None (default), no warping is performed
74 | and the original Spherical Mercator (EPSG:3857) is used.
75 | resampling :
76 | [Optional. Default=Resampling.bilinear] Resampling
77 | method for executing warping, expressed as a
78 | `rasterio.enums.Resampling` method
79 | url : str [DEPRECATED]
80 | [Optional. Default: 'http://tile.stamen.com/terrain/{z}/{x}/{y}.png']
81 | Source url for web tiles, or path to local file. If
82 | local, the file is read with `rasterio` and all
83 | bands are loaded into the basemap.
84 | **extra_imshow_args :
85 | Other parameters to be passed to `imshow`.
86 |
87 | Examples
88 | --------
89 |
90 | >>> import geopandas
91 | >>> import contextily as ctx
92 | >>> db = geopandas.read_file(ps.examples.get_path('virginia.shp'))
93 |
94 | Ensure the data is in Spherical Mercator:
95 |
96 | >>> db = db.to_crs(epsg=3857)
97 |
98 | Add a web basemap:
99 |
100 | >>> ax = db.plot(alpha=0.5, color='k', figsize=(6, 6))
101 | >>> ctx.add_basemap(ax, source=url)
102 | >>> plt.show()
103 |
104 | Or download a basemap to a local file and then plot it:
105 |
106 | >>> source = 'virginia.tiff'
107 | >>> _ = ctx.bounds2raster(*db.total_bounds, zoom=6, source=source)
108 | >>> ax = db.plot(alpha=0.5, color='k', figsize=(6, 6))
109 | >>> ctx.add_basemap(ax, source=source)
110 | >>> plt.show()
111 |
112 | """
113 | xmin, xmax, ymin, ymax = ax.axis()
114 | if url is not None and source is None:
115 | warnings.warn(
116 | 'The "url" option is deprecated. Please use the "source"'
117 | " argument instead.",
118 | FutureWarning,
119 | stacklevel=2,
120 | )
121 | source = url
122 | elif url is not None and source is not None:
123 | warnings.warn(
124 | 'The "url" argument is deprecated. Please use the "source"'
125 | ' argument. Do not supply a "url" argument. It will be ignored.',
126 | FutureWarning,
127 | stacklevel=2,
128 | )
129 | # If web source
130 | if (
131 | source is None
132 | or isinstance(source, (dict, TileProvider))
133 | or (isinstance(source, str) and source[:4] == "http")
134 | ):
135 | # Extent
136 | left, right, bottom, top = xmin, xmax, ymin, ymax
137 | # Convert extent from `crs` into WM for tile query
138 | if crs is not None:
139 | left, right, bottom, top = _reproj_bb(
140 | left, right, bottom, top, crs, {"init": "epsg:3857"}
141 | )
142 | # Download image
143 | image, extent = bounds2img(
144 | left, bottom, right, top, zoom=zoom, source=source, ll=False
145 | )
146 | # Warping
147 | if crs is not None:
148 | image, extent = warp_tiles(image, extent, t_crs=crs, resampling=resampling)
149 | # If local source
150 | else:
151 | import rasterio as rio
152 |
153 | # Read file
154 | with rio.open(source) as raster:
155 | if reset_extent:
156 | from rasterio.mask import mask as riomask
157 |
158 | # Read window
159 | if crs:
160 | left, bottom, right, top = rio.warp.transform_bounds(
161 | crs, raster.crs, xmin, ymin, xmax, ymax
162 | )
163 | else:
164 | left, bottom, right, top = xmin, ymin, xmax, ymax
165 | window = [
166 | {
167 | "type": "Polygon",
168 | "coordinates": (
169 | (
170 | (left, bottom),
171 | (right, bottom),
172 | (right, top),
173 | (left, top),
174 | (left, bottom),
175 | ),
176 | ),
177 | }
178 | ]
179 | image, img_transform = riomask(raster, window, crop=True)
180 | else:
181 | # Read full
182 | image = np.array([band for band in raster.read()])
183 | img_transform = raster.transform
184 | # Warp
185 | if (crs is not None) and (raster.crs != crs):
186 | image, raster = _warper(
187 | image, img_transform, raster.crs, crs, resampling
188 | )
189 | image = image.transpose(1, 2, 0)
190 | bb = raster.bounds
191 | extent = bb.left, bb.right, bb.bottom, bb.top
192 | # Plotting
193 | if image.shape[2] == 1:
194 | image = image[:, :, 0]
195 | img = ax.imshow(
196 | image, extent=extent, interpolation=interpolation, **extra_imshow_args
197 | )
198 |
199 | if reset_extent:
200 | ax.axis((xmin, xmax, ymin, ymax))
201 | else:
202 | max_bounds = (
203 | min(xmin, extent[0]),
204 | max(xmax, extent[1]),
205 | min(ymin, extent[2]),
206 | max(ymax, extent[3]),
207 | )
208 | ax.axis(max_bounds)
209 |
210 | # Add attribution text
211 | if source is None:
212 | source = providers.Stamen.Terrain
213 | if isinstance(source, (dict, TileProvider)) and attribution is None:
214 | attribution = source.get("attribution")
215 | if attribution:
216 | add_attribution(ax, attribution, font_size=attribution_size)
217 |
218 | return
219 |
220 |
221 | def _reproj_bb(left, right, bottom, top, s_crs, t_crs):
222 | n_l, n_b, n_r, n_t = transform_bounds(s_crs, t_crs, left, bottom, right, top)
223 | return n_l, n_r, n_b, n_t
224 |
225 |
226 | def add_attribution(ax, text, font_size=ATTRIBUTION_SIZE, **kwargs):
227 | """
228 | Utility to add attribution text.
229 |
230 | Parameters
231 | ----------
232 | ax : AxesSubplot
233 | Matplotlib axes object on which to add the attribution text.
234 | text : str
235 | Text to be added at the bottom of the axis.
236 | font_size : int
237 | [Optional. Defaults to 8] Font size in which to render
238 | the attribution text.
239 | **kwargs : Additional keywords to pass to the matplotlib `text` method.
240 |
241 | Returns
242 | -------
243 | matplotlib.text.Text
244 | Matplotlib Text object added to the plot.
245 | """
246 | # Add draw() as it resizes the axis and allows the wrapping to work as
247 | # expected. See https://github.com/darribas/contextily/issues/95 for some
248 | # details on the issue
249 | draw()
250 |
251 | text_artist = ax.text(
252 | 0.005,
253 | 0.005,
254 | text,
255 | transform=ax.transAxes,
256 | size=font_size,
257 | path_effects=[patheffects.withStroke(linewidth=2, foreground="w")],
258 | wrap=True,
259 | **kwargs,
260 | )
261 | # hack to have the text wrapped in the ax extent, for some explanation see
262 | # https://stackoverflow.com/questions/48079364/wrapping-text-not-working-in-matplotlib
263 | wrap_width = ax.get_window_extent().width * 0.99
264 | text_artist._get_wrap_line_width = lambda: wrap_width
265 | return text_artist
266 |
--------------------------------------------------------------------------------
/contextily/place.py:
--------------------------------------------------------------------------------
1 | """Tools for generating maps from a text search."""
2 | import geopy as gp
3 | import numpy as np
4 | import matplotlib.pyplot as plt
5 | from warnings import warn
6 | from .tile import howmany, bounds2raster, bounds2img, _sm2ll, _calculate_zoom
7 | from .plotting import INTERPOLATION, ZOOM, add_attribution
8 | from . import providers
9 | from ._providers import TileProvider
10 |
11 |
12 | class Place(object):
13 | """Geocode a place by name and get its map.
14 |
15 | This allows you to search for a name (e.g., city, street, country) and
16 | grab map and location data from the internet.
17 |
18 | Parameters
19 | ----------
20 | search : string
21 | The location to be searched.
22 | zoom : int or None
23 | [Optional. Default: None]
24 | The level of detail to include in the map. Higher levels mean more
25 | tiles and thus longer download time. If None, the zoom level will be
26 | automatically determined.
27 | path : str or None
28 | [Optional. Default: None]
29 | Path to a raster file that will be created after getting the place map.
30 | If None, no raster file will be downloaded.
31 | zoom_adjust : int or None
32 | [Optional. Default: None]
33 | The amount to adjust a chosen zoom level if it is chosen automatically.
34 | source : contextily.providers object or str
35 | [Optional. Default: Stamen Terrain web tiles]
36 | The tile source: web tile provider or path to local file. The web tile
37 | provider can be in the form of a `contextily.providers` object or a
38 | URL. The placeholders for the XYZ in the URL need to be `{x}`, `{y}`,
39 | `{z}`, respectively. For local file paths, the file is read with
40 | `rasterio` and all bands are loaded into the basemap.
41 | IMPORTANT: tiles are assumed to be in the Spherical Mercator
42 | projection (EPSG:3857), unless the `crs` keyword is specified.
43 | url : str [DEPRECATED]
44 | [Optional. Default: 'http://tile.stamen.com/terrain/{z}/{x}/{y}.png']
45 | Source url for web tiles, or path to local file. If
46 | local, the file is read with `rasterio` and all
47 | bands are loaded into the basemap.
48 |
49 | Attributes
50 | ----------
51 | geocode : geopy object
52 | The result of calling ``geopy.geocoders.Nominatim`` with ``search`` as input.
53 | s : float
54 | The southern bbox edge.
55 | n : float
56 | The northern bbox edge.
57 | e : float
58 | The eastern bbox edge.
59 | w : float
60 | The western bbox edge.
61 | im : ndarray
62 | The image corresponding to the map of ``search``.
63 | bbox : list
64 | The bounding box of the returned image, expressed in lon/lat, with the
65 | following order: [minX, minY, maxX, maxY]
66 | bbox_map : tuple
67 | The bounding box of the returned image, expressed in Web Mercator, with the
68 | following order: [minX, minY, maxX, maxY]
69 | """
70 |
71 | def __init__(
72 | self, search, zoom=None, path=None, zoom_adjust=None, source=None, url=None
73 | ):
74 | self.path = path
75 | if url is not None and source is None:
76 | warnings.warn(
77 | 'The "url" option is deprecated. Please use the "source"'
78 | " argument instead.",
79 | FutureWarning,
80 | stacklevel=2,
81 | )
82 | source = url
83 | elif url is not None and source is not None:
84 | warnings.warn(
85 | 'The "url" argument is deprecated. Please use the "source"'
86 | ' argument. Do not supply a "url" argument. It will be ignored.',
87 | FutureWarning,
88 | stacklevel=2,
89 | )
90 | if source is None:
91 | source = providers.Stamen.Terrain
92 | self.source = source
93 | self.zoom_adjust = zoom_adjust
94 |
95 | # Get geocoded values
96 | resp = gp.geocoders.Nominatim().geocode(search)
97 | bbox = np.array([float(ii) for ii in resp.raw["boundingbox"]])
98 |
99 | if "display_name" in resp.raw.keys():
100 | place = resp.raw["display_name"]
101 | elif "address" in resp.raw.keys():
102 | place = resp.raw["address"]
103 | else:
104 | place = search
105 | self.place = place
106 | self.search = search
107 | self.s, self.n, self.w, self.e = bbox
108 | self.bbox = [self.w, self.s, self.e, self.n] # So bbox is standard
109 | self.latitude = resp.latitude
110 | self.longitude = resp.longitude
111 | self.geocode = resp
112 |
113 | # Get map params
114 | self.zoom = (
115 | _calculate_zoom(self.w, self.s, self.e, self.n) if zoom is None else zoom
116 | )
117 | self.zoom = int(self.zoom)
118 | if self.zoom_adjust is not None:
119 | self.zoom += zoom_adjust
120 | self.n_tiles = howmany(self.w, self.s, self.e, self.n, self.zoom, verbose=False)
121 |
122 | # Get the map
123 | self._get_map()
124 |
125 | def _get_map(self):
126 | kwargs = {"ll": True}
127 | if self.source is not None:
128 | kwargs["source"] = self.source
129 |
130 | try:
131 | if isinstance(self.path, str):
132 | im, bbox = bounds2raster(
133 | self.w, self.s, self.e, self.n, self.path, zoom=self.zoom, **kwargs
134 | )
135 | else:
136 | im, bbox = bounds2img(
137 | self.w, self.s, self.e, self.n, self.zoom, **kwargs
138 | )
139 | except Exception as err:
140 | raise ValueError(
141 | "Could not retrieve map with parameters: {}, {}, {}, {}, zoom={}\n{}\nError: {}".format(
142 | self.w, self.s, self.e, self.n, self.zoom, kwargs, err
143 | )
144 | )
145 |
146 | self.im = im
147 | self.bbox_map = bbox
148 | return im, bbox
149 |
150 | def plot(self, ax=None, zoom=ZOOM, interpolation=INTERPOLATION, attribution=None):
151 | """
152 | Plot a `Place` object
153 | ...
154 |
155 | Parameters
156 | ----------
157 | ax : AxesSubplot
158 | Matplotlib axis with `x_lim` and `y_lim` set in Web
159 | Mercator (EPSG=3857). If not provided, a new
160 | 12x12 figure will be set and the name of the place
161 | will be added as title
162 | zoom : int/'auto'
163 | [Optional. Default='auto'] Level of detail for the
164 | basemap. If 'auto', if calculates it automatically.
165 | Ignored if `source` is a local file.
166 | interpolation : str
167 | [Optional. Default='bilinear'] Interpolation
168 | algorithm to be passed to `imshow`. See
169 | `matplotlib.pyplot.imshow` for further details.
170 | attribution : str
171 | [Optional. Defaults to attribution specified by the source of the map tiles]
172 | Text to be added at the bottom of the axis. This
173 | defaults to the attribution of the provider specified
174 | in `source` if available. Specify False to not
175 | automatically add an attribution, or a string to pass
176 | a custom attribution.
177 |
178 | Returns
179 | -------
180 | ax : AxesSubplot
181 | Matplotlib axis with `x_lim` and `y_lim` set in Web
182 | Mercator (EPSG=3857) containing the basemap
183 |
184 | Examples
185 | --------
186 |
187 | >>> lvl = ctx.Place('Liverpool')
188 | >>> lvl.plot()
189 |
190 | """
191 | im = self.im
192 | bbox = self.bbox_map
193 |
194 | title = None
195 | axisoff = False
196 | if ax is None:
197 | fig, ax = plt.subplots(figsize=(12, 12))
198 | title = self.place
199 | axisoff = True
200 | ax.imshow(im, extent=bbox, interpolation=interpolation)
201 | ax.set(xlabel="X", ylabel="Y")
202 | if isinstance(self.source, (dict, TileProvider)) and attribution is None:
203 | attribution = self.source.get("attribution")
204 | if attribution:
205 | add_attribution(ax, attribution)
206 | if title is not None:
207 | ax.set(title=title)
208 | if axisoff:
209 | ax.set_axis_off()
210 | return ax
211 |
212 | def __repr__(self):
213 | s = "Place : {} | n_tiles: {} | zoom : {} | im : {}".format(
214 | self.place, self.n_tiles, self.zoom, self.im.shape[:2]
215 | )
216 | return s
217 |
218 |
219 | def plot_map(
220 | place, bbox=None, title=None, ax=None, axis_off=True, latlon=True, attribution=None
221 | ):
222 | """Plot a map of the given place.
223 |
224 | Parameters
225 | ----------
226 | place : instance of Place or ndarray
227 | The map to plot. If an ndarray, this must be an image corresponding
228 | to a map. If an instance of ``Place``, the extent of the image and name
229 | will be inferred from the bounding box.
230 | ax : instance of matplotlib Axes object or None
231 | The axis on which to plot. If None, one will be created.
232 | axis_off : bool
233 | Whether to turn off the axis border and ticks before plotting.
234 | attribution : str
235 | [Optional. Default to standard `ATTRIBUTION`] Text to be added at the
236 | bottom of the axis.
237 |
238 | Returns
239 | -------
240 | ax : instance of matplotlib Axes object or None
241 | The axis on the map is plotted.
242 | """
243 | warn(
244 | (
245 | "The method `plot_map` is deprecated and will be removed from the"
246 | " library in future versions. Please use either `add_basemap` or"
247 | " the internal method `Place.plot`"
248 | ),
249 | DeprecationWarning,
250 | )
251 | if not isinstance(place, Place):
252 | im = place
253 | bbox = bbox
254 | title = title
255 | else:
256 | im = place.im
257 | if bbox is None:
258 | bbox = place.bbox_map
259 | if latlon is True:
260 | # Convert w, s, e, n into lon/lat
261 | w, e, s, n = bbox
262 | w, s = _sm2ll(w, s)
263 | e, n = _sm2ll(e, n)
264 | bbox = [w, e, s, n]
265 |
266 | title = place.place if title is None else title
267 |
268 | if ax is None:
269 | fig, ax = plt.subplots(figsize=(15, 15))
270 | ax.imshow(im, extent=bbox)
271 | ax.set(xlabel="X", ylabel="Y")
272 | if title is not None:
273 | ax.set(title=title)
274 | if attribution:
275 | add_attribution(ax, attribution)
276 | if axis_off is True:
277 | ax.set_axis_off()
278 | return ax
279 |
--------------------------------------------------------------------------------
/tests/test_ctx.py:
--------------------------------------------------------------------------------
1 | import matplotlib
2 |
3 | matplotlib.use("agg") # To prevent plots from using display
4 | import contextily as ctx
5 | import os
6 | import numpy as np
7 | import mercantile as mt
8 | import rasterio as rio
9 | from contextily.tile import _calculate_zoom
10 | from numpy.testing import assert_array_almost_equal
11 | import pytest
12 |
13 | TOL = 7
14 | SEARCH = "boulder"
15 | ADJUST = -3 # To save download size / time
16 |
17 | # Tile
18 |
19 |
20 | def test_bounds2raster():
21 | w, s, e, n = (
22 | -106.6495132446289,
23 | 25.845197677612305,
24 | -93.50721740722656,
25 | 36.49387741088867,
26 | )
27 | _ = ctx.bounds2raster(w, s, e, n, "test.tif", zoom=4, ll=True)
28 | rtr = rio.open("test.tif")
29 | img = np.array([band for band in rtr.read()]).transpose(1, 2, 0)
30 | solu = (
31 | -12528334.684053527,
32 | 2509580.5126589066,
33 | -10023646.141204873,
34 | 5014269.05550756,
35 | )
36 | for i, j in zip(rtr.bounds, solu):
37 | assert round(i - j, TOL) == 0
38 | assert img[100, 100, :].tolist() == [230, 229, 188]
39 | assert img[100, 200, :].tolist() == [156, 180, 131]
40 | assert img[200, 100, :].tolist() == [230, 225, 189]
41 | assert img.sum() == 36926856
42 | assert_array_almost_equal(img.mean(), 187.8197021484375)
43 |
44 | # multiple tiles for which result is not square
45 | w, s, e, n = (
46 | 2.5135730322461427,
47 | 49.529483547557504,
48 | 6.15665815595878,
49 | 51.47502370869813,
50 | )
51 | img, ext = ctx.bounds2raster(w, s, e, n, "test2.tif", zoom=7, ll=True)
52 | rtr = rio.open("test2.tif")
53 | rimg = np.array([band for band in rtr.read()]).transpose(1, 2, 0)
54 | assert rimg.shape == img.shape
55 | assert rimg.sum() == img.sum()
56 | assert_array_almost_equal(rimg.mean(), img.mean())
57 | assert_array_almost_equal(
58 | ext, (0.0, 939258.2035682457, 6261721.35712164, 6887893.492833804)
59 | )
60 | rtr_bounds = [
61 | -611.49622628141,
62 | 6262332.853347922,
63 | 938646.7073419644,
64 | 6888504.989060086,
65 | ]
66 | assert_array_almost_equal(list(rtr.bounds), rtr_bounds)
67 |
68 |
69 | def test_bounds2img():
70 | w, s, e, n = (
71 | -106.6495132446289,
72 | 25.845197677612305,
73 | -93.50721740722656,
74 | 36.49387741088867,
75 | )
76 | img, ext = ctx.bounds2img(w, s, e, n, zoom=4, ll=True)
77 | solu = (
78 | -12523442.714243276,
79 | -10018754.171394622,
80 | 2504688.5428486555,
81 | 5009377.085697309,
82 | )
83 | for i, j in zip(ext, solu):
84 | assert round(i - j, TOL) == 0
85 | assert img[100, 100, :].tolist() == [230, 229, 188]
86 | assert img[100, 200, :].tolist() == [156, 180, 131]
87 | assert img[200, 100, :].tolist() == [230, 225, 189]
88 |
89 |
90 | def test_warp_tiles():
91 | w, s, e, n = (
92 | -106.6495132446289,
93 | 25.845197677612305,
94 | -93.50721740722656,
95 | 36.49387741088867,
96 | )
97 | img, ext = ctx.bounds2img(w, s, e, n, zoom=4, ll=True)
98 | wimg, wext = ctx.warp_tiles(img, ext)
99 | assert_array_almost_equal(
100 | np.array(wext),
101 | np.array(
102 | [
103 | -112.54394531249996,
104 | -90.07903186397023,
105 | 21.966726124122374,
106 | 41.013065787006276,
107 | ]
108 | ),
109 | )
110 | assert wimg[100, 100, :].tolist() == [228, 221, 184]
111 | assert wimg[100, 200, :].tolist() == [213, 219, 177]
112 | assert wimg[200, 100, :].tolist() == [133, 130, 109]
113 |
114 |
115 | def test_warp_img_transform():
116 | w, s, e, n = ext = (
117 | -106.6495132446289,
118 | 25.845197677612305,
119 | -93.50721740722656,
120 | 36.49387741088867,
121 | )
122 | _ = ctx.bounds2raster(w, s, e, n, "test.tif", zoom=4, ll=True)
123 | rtr = rio.open("test.tif")
124 | img = np.array([band for band in rtr.read()])
125 | wimg, wext = ctx.warp_img_transform(
126 | img, rtr.transform, rtr.crs, {"init": "epsg:4326"}
127 | )
128 | assert wimg[:, 100, 100].tolist() == [228, 221, 184]
129 | assert wimg[:, 100, 200].tolist() == [213, 219, 177]
130 | assert wimg[:, 200, 100].tolist() == [133, 130, 109]
131 |
132 |
133 | def test_howmany():
134 | w, s, e, n = (
135 | -106.6495132446289,
136 | 25.845197677612305,
137 | -93.50721740722656,
138 | 36.49387741088867,
139 | )
140 | zoom = 7
141 | expected = 25
142 | got = ctx.howmany(w, s, e, n, zoom=zoom, verbose=False, ll=True)
143 | assert got == expected
144 |
145 |
146 | def test_ll2wdw():
147 | w, s, e, n = (
148 | -106.6495132446289,
149 | 25.845197677612305,
150 | -93.50721740722656,
151 | 36.49387741088867,
152 | )
153 | hou = (-10676650.69219051, 3441477.046670125, -10576977.7804825, 3523606.146650609)
154 | _ = ctx.bounds2raster(w, s, e, n, "test.tif", zoom=4, ll=True)
155 | rtr = rio.open("test.tif")
156 | wdw = ctx.tile.bb2wdw(hou, rtr)
157 | assert wdw == ((152, 161), (189, 199))
158 |
159 |
160 | def test__sm2ll():
161 | w, s, e, n = (
162 | -106.6495132446289,
163 | 25.845197677612305,
164 | -93.50721740722656,
165 | 36.49387741088867,
166 | )
167 | minX, minY = ctx.tile._sm2ll(w, s)
168 | maxX, maxY = ctx.tile._sm2ll(e, n)
169 | nw, ns = mt.xy(minX, minY)
170 | ne, nn = mt.xy(maxX, maxY)
171 | assert round(nw - w, TOL) == 0
172 | assert round(ns - s, TOL) == 0
173 | assert round(ne - e, TOL) == 0
174 | assert round(nn - n, TOL) == 0
175 |
176 |
177 | def test_autozoom():
178 | w, s, e, n = (-105.3014509, 39.9643513, -105.1780988, 40.094409)
179 | expected_zoom = 13
180 | zoom = _calculate_zoom(w, s, e, n)
181 | assert zoom == expected_zoom
182 |
183 |
184 | def test_validate_zoom():
185 | # tiny extent to trigger large calculated zoom
186 | w, s, e, n = (0, 0, 0.001, 0.001)
187 |
188 | # automatically inferred -> set to known max but warn
189 | with pytest.warns(UserWarning, match="inferred zoom level"):
190 | ctx.bounds2img(w, s, e, n)
191 |
192 | # specify manually -> raise an error
193 | with pytest.raises(ValueError):
194 | ctx.bounds2img(w, s, e, n, zoom=23)
195 |
196 | # with specific string url (not dict) -> error when specified
197 | url = "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
198 | with pytest.raises(ValueError):
199 | ctx.bounds2img(w, s, e, n, zoom=33, source=url)
200 |
201 | # but also when inferred (no max zoom know to set to)
202 | with pytest.raises(ValueError):
203 | ctx.bounds2img(w, s, e, n, source=url)
204 |
205 |
206 | # Place
207 |
208 |
209 | def test_place():
210 | expected_bbox = [-105.3014509, 39.9643513, -105.1780988, 40.094409]
211 | expected_bbox_map = [
212 | -11740727.544603072,
213 | -11701591.786121061,
214 | 4852834.0517692715,
215 | 4891969.810251278,
216 | ]
217 | expected_zoom = 10
218 | loc = ctx.Place(SEARCH, zoom_adjust=ADJUST)
219 | assert loc.im.shape == (256, 256, 3)
220 | loc # Make sure repr works
221 |
222 | # Check auto picks are correct
223 | assert loc.search == SEARCH
224 | assert_array_almost_equal([loc.w, loc.s, loc.e, loc.n], expected_bbox)
225 | assert_array_almost_equal(loc.bbox_map, expected_bbox_map)
226 | assert loc.zoom == expected_zoom
227 |
228 | loc = ctx.Place(SEARCH, path="./test2.tif", zoom_adjust=ADJUST)
229 | assert os.path.exists("./test2.tif")
230 |
231 | # .plot() method
232 | ax = loc.plot()
233 | assert_array_almost_equal(loc.bbox_map, ax.images[0].get_extent())
234 |
235 | f, ax = matplotlib.pyplot.subplots(1)
236 | ax = loc.plot(ax=ax)
237 | assert_array_almost_equal(loc.bbox_map, ax.images[0].get_extent())
238 |
239 |
240 | def test_plot_map():
241 | # Place as a search
242 | loc = ctx.Place(SEARCH, zoom_adjust=ADJUST)
243 | w, e, s, n = loc.bbox_map
244 | ax = ctx.plot_map(loc)
245 |
246 | assert ax.get_title() == loc.place
247 | ax = ctx.plot_map(loc.im, loc.bbox)
248 | assert_array_almost_equal(loc.bbox, ax.images[0].get_extent())
249 |
250 | # Place as an image
251 | img, ext = ctx.bounds2img(w, s, e, n, zoom=10)
252 | ax = ctx.plot_map(img, ext)
253 | assert_array_almost_equal(ext, ax.images[0].get_extent())
254 |
255 |
256 | # Plotting
257 |
258 |
259 | def test_add_basemap():
260 | # Plot boulder bbox as in test_place
261 | x1, x2, y1, y2 = [
262 | -11740727.544603072,
263 | -11701591.786121061,
264 | 4852834.0517692715,
265 | 4891969.810251278,
266 | ]
267 |
268 | # Test web basemap
269 | fig, ax = matplotlib.pyplot.subplots(1)
270 | ax.set_xlim(x1, x2)
271 | ax.set_ylim(y1, y2)
272 | ctx.add_basemap(ax, zoom=10)
273 |
274 | # ensure add_basemap did not change the axis limits of ax
275 | ax_extent = (x1, x2, y1, y2)
276 | assert ax.axis() == ax_extent
277 |
278 | assert ax.images[0].get_array().sum() == 34840247
279 | assert ax.images[0].get_array().shape == (256, 256, 3)
280 | assert_array_almost_equal(ax.images[0].get_array().mean(), 177.20665995279947)
281 |
282 | # Test local source
283 | ## Windowed read
284 | subset = (
285 | -11730803.981631357,
286 | -11711668.223149346,
287 | 4862910.488797557,
288 | 4882046.247279563,
289 | )
290 |
291 | f, ax = matplotlib.pyplot.subplots(1)
292 | ax.set_xlim(subset[0], subset[1])
293 | ax.set_ylim(subset[2], subset[3])
294 | loc = ctx.Place(SEARCH, path="./test2.tif", zoom_adjust=ADJUST)
295 | ctx.add_basemap(ax, url="./test2.tif", reset_extent=True)
296 |
297 | raster_extent = (
298 | -11740803.981631357,
299 | -11701668.223149346,
300 | 4852910.488797556,
301 | 4892046.247279563,
302 | )
303 | assert_array_almost_equal(raster_extent, ax.images[0].get_extent())
304 | assert ax.images[0].get_array().sum() == 8440966
305 | assert ax.images[0].get_array().shape == (126, 126, 3)
306 | assert_array_almost_equal(ax.images[0].get_array().mean(), 177.22696733014195)
307 | ## Full read
308 | f, ax = matplotlib.pyplot.subplots(1)
309 | ax.set_xlim(x1, x2)
310 | ax.set_ylim(y1, y2)
311 | loc = ctx.Place(SEARCH, path="./test2.tif", zoom_adjust=ADJUST)
312 | ctx.add_basemap(ax, source="./test2.tif", reset_extent=False)
313 |
314 | raster_extent = (
315 | -11740803.981631357,
316 | -11701668.223149346,
317 | 4852910.488797557,
318 | 4892046.247279563,
319 | )
320 | assert_array_almost_equal(raster_extent, ax.images[0].get_extent())
321 | assert ax.images[0].get_array().sum() == 34840247
322 | assert ax.images[0].get_array().shape == (256, 256, 3)
323 | assert_array_almost_equal(ax.images[0].get_array().mean(), 177.20665995279947)
324 |
325 | # Test with auto-zoom
326 | f, ax = matplotlib.pyplot.subplots(1)
327 | ax.set_xlim(x1, x2)
328 | ax.set_ylim(y1, y2)
329 | ctx.add_basemap(ax, zoom="auto")
330 |
331 | ax_extent = (
332 | -11740727.544603072,
333 | -11701591.786121061,
334 | 4852834.051769271,
335 | 4891969.810251278,
336 | )
337 | assert_array_almost_equal(ax_extent, ax.images[0].get_extent())
338 | assert ax.images[0].get_array().sum() == 563185119
339 | assert ax.images[0].get_array().shape == (1024, 1024, 3)
340 | assert_array_almost_equal(ax.images[0].get_array().mean(), 179.03172779083252)
341 |
342 | # Test on-th-fly warping
343 | x1, x2 = -105.5, -105.00
344 | y1, y2 = 39.56, 40.13
345 | f, ax = matplotlib.pyplot.subplots(1)
346 | ax.set_xlim(x1, x2)
347 | ax.set_ylim(y1, y2)
348 | ctx.add_basemap(ax, crs={"init": "epsg:4326"}, attribution=None)
349 | assert ax.get_xlim() == (x1, x2)
350 | assert ax.get_ylim() == (y1, y2)
351 | assert ax.images[0].get_array().sum() == 724238693
352 | assert ax.images[0].get_array().shape == (1135, 1183, 3)
353 | assert_array_almost_equal(ax.images[0].get_array().mean(), 179.79593258881636)
354 | # Test local source warping
355 | _ = ctx.bounds2raster(x1, y1, x2, y2, "./test2.tif", ll=True)
356 | f, ax = matplotlib.pyplot.subplots(1)
357 | ax.set_xlim(x1, x2)
358 | ax.set_ylim(y1, y2)
359 | ctx.add_basemap(
360 | ax, source="./test2.tif", crs={"init": "epsg:4326"}, attribution=None
361 | )
362 | assert ax.get_xlim() == (x1, x2)
363 | assert ax.get_ylim() == (y1, y2)
364 | assert ax.images[0].get_array().sum() == 464751694
365 | assert ax.images[0].get_array().shape == (980, 862, 3)
366 | assert_array_almost_equal(ax.images[0].get_array().mean(), 183.38608756727749)
367 |
368 |
369 | def test_basemap_attribution():
370 | extent = (-11945319, -10336026, 2910477, 4438236)
371 |
372 | def get_attr(ax):
373 | return [
374 | c
375 | for c in ax.get_children()
376 | if isinstance(c, matplotlib.text.Text) and c.get_text()
377 | ]
378 |
379 | # default provider and attribution
380 | fig, ax = matplotlib.pyplot.subplots()
381 | ax.axis(extent)
382 | ctx.add_basemap(ax)
383 | (txt,) = get_attr(ax)
384 | assert txt.get_text() == ctx.providers.Stamen.Terrain["attribution"]
385 |
386 | # override attribution
387 | fig, ax = matplotlib.pyplot.subplots()
388 | ax.axis(extent)
389 | ctx.add_basemap(ax, attribution="custom text")
390 | (txt,) = get_attr(ax)
391 | assert txt.get_text() == "custom text"
392 |
393 | # disable attribution
394 | fig, ax = matplotlib.pyplot.subplots()
395 | ax.axis(extent)
396 | ctx.add_basemap(ax, attribution=False)
397 | assert len(get_attr(ax)) == 0
398 |
399 | # specified provider
400 | fig, ax = matplotlib.pyplot.subplots()
401 | ax.axis(extent)
402 | ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik)
403 | (txt,) = get_attr(ax)
404 | assert txt.get_text() == ctx.providers.OpenStreetMap.Mapnik["attribution"]
405 |
406 |
407 | def test_attribution():
408 | fig, ax = matplotlib.pyplot.subplots(1)
409 | txt = ctx.add_attribution(ax, "Test")
410 | assert isinstance(txt, matplotlib.text.Text)
411 | assert txt.get_text() == "Test"
412 |
413 | # test passthrough font size and kwargs
414 | fig, ax = matplotlib.pyplot.subplots(1)
415 | txt = ctx.add_attribution(ax, "Test", font_size=15, fontfamily="monospace")
416 | assert txt.get_size() == 15
417 | assert txt.get_fontfamily() == ["monospace"]
418 |
419 |
420 | def test_set_cache_dir(tmpdir):
421 | # set cache directory manually
422 | path = str(tmpdir.mkdir("cache"))
423 | ctx.set_cache_dir(path)
424 |
425 | # then check that plotting still works
426 | extent = (-11945319, -10336026, 2910477, 4438236)
427 | fig, ax = matplotlib.pyplot.subplots()
428 | ax.axis(extent)
429 | ctx.add_basemap(ax)
430 |
--------------------------------------------------------------------------------
/contextily/tile.py:
--------------------------------------------------------------------------------
1 | """Tools for downloading map tiles from coordinates."""
2 | from __future__ import absolute_import, division, print_function
3 |
4 | import uuid
5 |
6 | import mercantile as mt
7 | import requests
8 | import atexit
9 | import io
10 | import os
11 | import shutil
12 | import tempfile
13 | import warnings
14 |
15 | import numpy as np
16 | import rasterio as rio
17 | from PIL import Image
18 | from joblib import Memory as _Memory
19 | from rasterio.transform import from_origin
20 | from rasterio.io import MemoryFile
21 | from rasterio.vrt import WarpedVRT
22 | from rasterio.enums import Resampling
23 | from . import tile_providers as sources
24 | from . import providers
25 | from ._providers import TileProvider
26 |
27 | __all__ = [
28 | "bounds2raster",
29 | "bounds2img",
30 | "warp_tiles",
31 | "warp_img_transform",
32 | "howmany",
33 | "set_cache_dir",
34 | ]
35 |
36 |
37 | USER_AGENT = "contextily-" + uuid.uuid4().hex
38 |
39 | tmpdir = tempfile.mkdtemp()
40 | memory = _Memory(tmpdir, verbose=0)
41 |
42 |
43 | def set_cache_dir(path):
44 | """
45 | Set a cache directory to use in the current python session.
46 |
47 | By default, contextily caches downloaded tiles per python session, but
48 | will afterwards delete the cache directory. By setting it to a custom
49 | path, you can avoid this, and re-use the same cache a next time by
50 | again setting the cache dir to that directory.
51 |
52 | Parameters
53 | ----------
54 | path : str
55 | Path to the cache directory.
56 | """
57 | memory.store_backend.location = path
58 |
59 |
60 | def _clear_cache():
61 | shutil.rmtree(tmpdir)
62 |
63 |
64 | atexit.register(_clear_cache)
65 |
66 |
67 | def bounds2raster(
68 | w,
69 | s,
70 | e,
71 | n,
72 | path,
73 | zoom="auto",
74 | source=None,
75 | ll=False,
76 | wait=0,
77 | max_retries=2,
78 | url=None,
79 | ):
80 | """
81 | Take bounding box and zoom, and write tiles into a raster file in
82 | the Spherical Mercator CRS (EPSG:3857)
83 |
84 | Parameters
85 | ----------
86 | w : float
87 | West edge
88 | s : float
89 | South edge
90 | e : float
91 | East edge
92 | n : float
93 | North edge
94 | zoom : int
95 | Level of detail
96 | path : str
97 | Path to raster file to be written
98 | source : contextily.providers object or str
99 | [Optional. Default: Stamen Terrain web tiles]
100 | The tile source: web tile provider or path to local file. The web tile
101 | provider can be in the form of a `contextily.providers` object or a
102 | URL. The placeholders for the XYZ in the URL need to be `{x}`, `{y}`,
103 | `{z}`, respectively. For local file paths, the file is read with
104 | `rasterio` and all bands are loaded into the basemap.
105 | IMPORTANT: tiles are assumed to be in the Spherical Mercator
106 | projection (EPSG:3857), unless the `crs` keyword is specified.
107 | ll : Boolean
108 | [Optional. Default: False] If True, `w`, `s`, `e`, `n` are
109 | assumed to be lon/lat as opposed to Spherical Mercator.
110 | wait : int
111 | [Optional. Default: 0]
112 | if the tile API is rate-limited, the number of seconds to wait
113 | between a failed request and the next try
114 | max_retries: int
115 | [Optional. Default: 2]
116 | total number of rejected requests allowed before contextily
117 | will stop trying to fetch more tiles from a rate-limited API.
118 | url : str [DEPRECATED]
119 | [Optional. Default:
120 | 'http://tile.stamen.com/terrain/{z}/{x}/{y}.png'] URL for
121 | tile provider. The placeholders for the XYZ need to be `{x}`,
122 | `{y}`, `{z}`, respectively. See `cx.sources`.
123 |
124 | Returns
125 | -------
126 | img : ndarray
127 | Image as a 3D array of RGB values
128 | extent : tuple
129 | Bounding box [minX, maxX, minY, maxY] of the returned image
130 | """
131 | if not ll:
132 | # Convert w, s, e, n into lon/lat
133 | w, s = _sm2ll(w, s)
134 | e, n = _sm2ll(e, n)
135 | # Download
136 | Z, ext = bounds2img(w, s, e, n, zoom=zoom, source=source, url=url, ll=True)
137 | # Write
138 | # ---
139 | h, w, b = Z.shape
140 | # --- https://mapbox.github.io/rasterio/quickstart.html#opening-a-dataset-in-writing-mode
141 | minX, maxX, minY, maxY = ext
142 | x = np.linspace(minX, maxX, w)
143 | y = np.linspace(minY, maxY, h)
144 | resX = (x[-1] - x[0]) / w
145 | resY = (y[-1] - y[0]) / h
146 | transform = from_origin(x[0] - resX / 2, y[-1] + resY / 2, resX, resY)
147 | # ---
148 | with rio.open(
149 | path,
150 | "w",
151 | driver="GTiff",
152 | height=h,
153 | width=w,
154 | count=b,
155 | dtype=str(Z.dtype.name),
156 | crs="epsg:3857",
157 | transform=transform,
158 | ) as raster:
159 | for band in range(b):
160 | raster.write(Z[:, :, band], band + 1)
161 | return Z, ext
162 |
163 |
164 | def bounds2img(
165 | w, s, e, n, zoom="auto", source=None, ll=False, wait=0, max_retries=2, url=None
166 | ):
167 | """
168 | Take bounding box and zoom and return an image with all the tiles
169 | that compose the map and its Spherical Mercator extent.
170 |
171 | Parameters
172 | ----------
173 | w : float
174 | West edge
175 | s : float
176 | South edge
177 | e : float
178 | East edge
179 | n : float
180 | North edge
181 | zoom : int
182 | Level of detail
183 | source : contextily.providers object or str
184 | [Optional. Default: Stamen Terrain web tiles]
185 | The tile source: web tile provider or path to local file. The web tile
186 | provider can be in the form of a `contextily.providers` object or a
187 | URL. The placeholders for the XYZ in the URL need to be `{x}`, `{y}`,
188 | `{z}`, respectively. For local file paths, the file is read with
189 | `rasterio` and all bands are loaded into the basemap.
190 | IMPORTANT: tiles are assumed to be in the Spherical Mercator
191 | projection (EPSG:3857), unless the `crs` keyword is specified.
192 | ll : Boolean
193 | [Optional. Default: False] If True, `w`, `s`, `e`, `n` are
194 | assumed to be lon/lat as opposed to Spherical Mercator.
195 | wait : int
196 | [Optional. Default: 0]
197 | if the tile API is rate-limited, the number of seconds to wait
198 | between a failed request and the next try
199 | max_retries: int
200 | [Optional. Default: 2]
201 | total number of rejected requests allowed before contextily
202 | will stop trying to fetch more tiles from a rate-limited API.
203 | url : str [DEPRECATED]
204 | [Optional. Default: 'http://tile.stamen.com/terrain/{z}/{x}/{y}.png']
205 | URL for tile provider. The placeholders for the XYZ need to be
206 | `{x}`, `{y}`, `{z}`, respectively. IMPORTANT: tiles are
207 | assumed to be in the Spherical Mercator projection (EPSG:3857).
208 |
209 | Returns
210 | -------
211 | img : ndarray
212 | Image as a 3D array of RGB values
213 | extent : tuple
214 | Bounding box [minX, maxX, minY, maxY] of the returned image
215 | """
216 | if not ll:
217 | # Convert w, s, e, n into lon/lat
218 | w, s = _sm2ll(w, s)
219 | e, n = _sm2ll(e, n)
220 | if url is not None and source is None:
221 | warnings.warn(
222 | 'The "url" option is deprecated. Please use the "source"'
223 | " argument instead.",
224 | FutureWarning,
225 | stacklevel=2,
226 | )
227 | source = url
228 | elif url is not None and source is not None:
229 | warnings.warn(
230 | 'The "url" argument is deprecated. Please use the "source"'
231 | ' argument. Do not supply a "url" argument. It will be ignored.',
232 | FutureWarning,
233 | stacklevel=2,
234 | )
235 | # get provider dict given the url
236 | provider = _process_source(source)
237 | # calculate and validate zoom level
238 | auto_zoom = zoom == "auto"
239 | if auto_zoom:
240 | zoom = _calculate_zoom(w, s, e, n)
241 | zoom = _validate_zoom(zoom, provider, auto=auto_zoom)
242 | # download and merge tiles
243 | tiles = []
244 | arrays = []
245 | for t in mt.tiles(w, s, e, n, [zoom]):
246 | x, y, z = t.x, t.y, t.z
247 | tile_url = _construct_tile_url(provider, x, y, z)
248 | image = _fetch_tile(tile_url, wait, max_retries)
249 | tiles.append(t)
250 | arrays.append(image)
251 | merged, extent = _merge_tiles(tiles, arrays)
252 | # lon/lat extent --> Spheric Mercator
253 | west, south, east, north = extent
254 | left, bottom = mt.xy(west, south)
255 | right, top = mt.xy(east, north)
256 | extent = left, right, bottom, top
257 | return merged, extent
258 |
259 |
260 | def _url_from_string(url):
261 | """
262 | Generate actual tile url from tile provider definition or template url.
263 | """
264 | if "tileX" in url and "tileY" in url:
265 | warnings.warn(
266 | "The url format using 'tileX', 'tileY', 'tileZ' as placeholders "
267 | "is deprecated. Please use '{x}', '{y}', '{z}' instead.",
268 | FutureWarning,
269 | )
270 | url = (
271 | url.replace("tileX", "{x}").replace("tileY", "{y}").replace("tileZ", "{z}")
272 | )
273 | return {"url": url}
274 |
275 |
276 | def _process_source(source):
277 | if source is None:
278 | provider = providers.Stamen.Terrain
279 | elif isinstance(source, str):
280 | provider = _url_from_string(source)
281 | elif not isinstance(source, (dict, TileProvider)):
282 | raise TypeError(
283 | "The 'url' needs to be a contextily.providers object, a dict, or string"
284 | )
285 | elif "url" not in source:
286 | raise ValueError("The 'url' dict should at least contain a 'url' key")
287 | else:
288 | provider = source
289 | return provider
290 |
291 |
292 | def _construct_tile_url(provider, x, y, z):
293 | provider = provider.copy()
294 | tile_url = provider.pop("url")
295 | subdomains = provider.pop("subdomains", "abc")
296 | r = provider.pop("r", "")
297 | tile_url = tile_url.format(x=x, y=y, z=z, s=subdomains[0], r=r, **provider)
298 | return tile_url
299 |
300 |
301 | @memory.cache
302 | def _fetch_tile(tile_url, wait, max_retries):
303 | request = _retryer(tile_url, wait, max_retries)
304 | with io.BytesIO(request.content) as image_stream:
305 | image = Image.open(image_stream).convert("RGB")
306 | array = np.asarray(image)
307 | image.close()
308 | return array
309 |
310 |
311 | def warp_tiles(img, extent, t_crs="EPSG:4326", resampling=Resampling.bilinear):
312 | """
313 | Reproject (warp) a Web Mercator basemap into any CRS on-the-fly
314 |
315 | NOTE: this method works well with contextily's `bounds2img` approach to
316 | raster dimensions (h, w, b)
317 |
318 | Parameters
319 | ----------
320 | img : ndarray
321 | Image as a 3D array (h, w, b) of RGB values (e.g. as
322 | returned from `contextily.bounds2img`)
323 | extent : tuple
324 | Bounding box [minX, maxX, minY, maxY] of the returned image,
325 | expressed in Web Mercator (`EPSG:3857`)
326 | t_crs : str/CRS
327 | [Optional. Default='EPSG:4326'] Target CRS, expressed in any
328 | format permitted by rasterio. Defaults to WGS84 (lon/lat)
329 | resampling :
330 | [Optional. Default=Resampling.bilinear] Resampling method for
331 | executing warping, expressed as a `rasterio.enums.Resampling`
332 | method
333 |
334 | Returns
335 | -------
336 | img : ndarray
337 | Image as a 3D array (h, w, b) of RGB values (e.g. as
338 | returned from `contextily.bounds2img`)
339 | ext : tuple
340 | Bounding box [minX, maxX, minY, maxY] of the returned (warped)
341 | image
342 | """
343 | h, w, b = img.shape
344 | # --- https://rasterio.readthedocs.io/en/latest/quickstart.html#opening-a-dataset-in-writing-mode
345 | minX, maxX, minY, maxY = extent
346 | x = np.linspace(minX, maxX, w)
347 | y = np.linspace(minY, maxY, h)
348 | resX = (x[-1] - x[0]) / w
349 | resY = (y[-1] - y[0]) / h
350 | transform = from_origin(x[0] - resX / 2, y[-1] + resY / 2, resX, resY)
351 | # ---
352 | w_img, vrt = _warper(
353 | img.transpose(2, 0, 1), transform, "EPSG:3857", t_crs, resampling
354 | )
355 | # ---
356 | extent = vrt.bounds.left, vrt.bounds.right, vrt.bounds.bottom, vrt.bounds.top
357 | return w_img.transpose(1, 2, 0), extent
358 |
359 |
360 | def warp_img_transform(img, transform, s_crs, t_crs, resampling=Resampling.bilinear):
361 | """
362 | Reproject (warp) an `img` with a given `transform` and `s_crs` into a
363 | different `t_crs`
364 |
365 | NOTE: this method works well with rasterio's `.read()` approach to
366 | raster's dimensions (b, h, w)
367 |
368 | Parameters
369 | ----------
370 | img : ndarray
371 | Image as a 3D array (b, h, w) of RGB values (e.g. as
372 | returned from rasterio's `.read()` method)
373 | transform : affine.Affine
374 | Transform of the input image as expressed by `rasterio` and
375 | the `affine` package
376 | s_crs : str/CRS
377 | Source CRS in which `img` is passed, expressed in any format
378 | permitted by rasterio.
379 | t_crs : str/CRS
380 | Target CRS, expressed in any format permitted by rasterio.
381 | resampling :
382 | [Optional. Default=Resampling.bilinear] Resampling method for
383 | executing warping, expressed as a `rasterio.enums.Resampling`
384 | method
385 |
386 | Returns
387 | -------
388 | w_img : ndarray
389 | Warped image as a 3D array (b, h, w) of RGB values (e.g. as
390 | returned from rasterio's `.read()` method)
391 | w_transform : affine.Affine
392 | Transform of the input image as expressed by `rasterio` and
393 | the `affine` package
394 | """
395 | w_img, vrt = _warper(img, transform, s_crs, t_crs, resampling)
396 | return w_img, vrt.transform
397 |
398 |
399 | def _warper(img, transform, s_crs, t_crs, resampling):
400 | """
401 | Warp an image returning it as a virtual file
402 | """
403 | b, h, w = img.shape
404 | with MemoryFile() as memfile:
405 | with memfile.open(
406 | driver="GTiff",
407 | height=h,
408 | width=w,
409 | count=b,
410 | dtype=str(img.dtype.name),
411 | crs=s_crs,
412 | transform=transform,
413 | ) as mraster:
414 | for band in range(b):
415 | mraster.write(img[band, :, :], band + 1)
416 | # --- Virtual Warp
417 | vrt = WarpedVRT(mraster, crs=t_crs, resampling=resampling)
418 | img = vrt.read()
419 | return img, vrt
420 |
421 |
422 | def _retryer(tile_url, wait, max_retries):
423 | """
424 | Retry a url many times in attempt to get a tile
425 |
426 | Arguments
427 | ---------
428 | tile_url : str
429 | string that is the target of the web request. Should be
430 | a properly-formatted url for a tile provider.
431 | wait : int
432 | if the tile API is rate-limited, the number of seconds to wait
433 | between a failed request and the next try
434 | max_retries : int
435 | total number of rejected requests allowed before contextily
436 | will stop trying to fetch more tiles from a rate-limited API.
437 |
438 | Returns
439 | -------
440 | request object containing the web response.
441 | """
442 | try:
443 | request = requests.get(tile_url, headers={"user-agent": USER_AGENT})
444 | request.raise_for_status()
445 | except requests.HTTPError:
446 | if request.status_code == 404:
447 | raise requests.HTTPError(
448 | "Tile URL resulted in a 404 error. "
449 | "Double-check your tile url:\n{}".format(tile_url)
450 | )
451 | elif request.status_code == 104:
452 | if max_retries > 0:
453 | os.wait(wait)
454 | max_retries -= 1
455 | request = _retryer(tile_url, wait, max_retries)
456 | else:
457 | raise requests.HTTPError("Connection reset by peer too many times.")
458 | return request
459 |
460 |
461 | def howmany(w, s, e, n, zoom, verbose=True, ll=False):
462 | """
463 | Number of tiles required for a given bounding box and a zoom level
464 |
465 | Parameters
466 | ----------
467 | w : float
468 | West edge
469 | s : float
470 | South edge
471 | e : float
472 | East edge
473 | n : float
474 | North edge
475 | zoom : int
476 | Level of detail
477 | verbose : Boolean
478 | [Optional. Default=True] If True, print short message with
479 | number of tiles and zoom.
480 | ll : Boolean
481 | [Optional. Default: False] If True, `w`, `s`, `e`, `n` are
482 | assumed to be lon/lat as opposed to Spherical Mercator.
483 | """
484 | if not ll:
485 | # Convert w, s, e, n into lon/lat
486 | w, s = _sm2ll(w, s)
487 | e, n = _sm2ll(e, n)
488 | if zoom == "auto":
489 | zoom = _calculate_zoom(w, s, e, n)
490 | tiles = len(list(mt.tiles(w, s, e, n, [zoom])))
491 | if verbose:
492 | print("Using zoom level %i, this will download %i tiles" % (zoom, tiles))
493 | return tiles
494 |
495 |
496 | def bb2wdw(bb, rtr):
497 | """
498 | Convert XY bounding box into the window of the tile raster
499 |
500 | Parameters
501 | ----------
502 | bb : tuple
503 | (left, bottom, right, top) in the CRS of `rtr`
504 | rtr : RasterReader
505 | Open rasterio raster from which the window will be extracted
506 |
507 | Returns
508 | -------
509 | window : tuple
510 | ((row_start, row_stop), (col_start, col_stop))
511 | """
512 | rbb = rtr.bounds
513 | xi = np.linspace(rbb.left, rbb.right, rtr.shape[1])
514 | yi = np.linspace(rbb.bottom, rbb.top, rtr.shape[0])
515 |
516 | window = (
517 | (rtr.shape[0] - yi.searchsorted(bb[3]), rtr.shape[0] - yi.searchsorted(bb[1])),
518 | (xi.searchsorted(bb[0]), xi.searchsorted(bb[2])),
519 | )
520 | return window
521 |
522 |
523 | def _sm2ll(x, y):
524 | """
525 | Transform Spherical Mercator coordinates point into lon/lat
526 |
527 | NOTE: Translated from the JS implementation in
528 | http://dotnetfollower.com/wordpress/2011/07/javascript-how-to-convert-mercator-sphere-coordinates-to-latitude-and-longitude/
529 | ...
530 |
531 | Arguments
532 | ---------
533 | x : float
534 | Easting
535 | y : float
536 | Northing
537 |
538 | Returns
539 | -------
540 | ll : tuple
541 | lon/lat coordinates
542 | """
543 | rMajor = 6378137.0 # Equatorial Radius, QGS84
544 | shift = np.pi * rMajor
545 | lon = x / shift * 180.0
546 | lat = y / shift * 180.0
547 | lat = 180.0 / np.pi * (2.0 * np.arctan(np.exp(lat * np.pi / 180.0)) - np.pi / 2.0)
548 | return lon, lat
549 |
550 |
551 | def _calculate_zoom(w, s, e, n):
552 | """Automatically choose a zoom level given a desired number of tiles.
553 |
554 | .. note:: all values are interpreted as latitude / longitutde.
555 |
556 | Parameters
557 | ----------
558 | w : float
559 | The western bbox edge.
560 | s : float
561 | The southern bbox edge.
562 | e : float
563 | The eastern bbox edge.
564 | n : float
565 | The northern bbox edge.
566 |
567 | Returns
568 | -------
569 | zoom : int
570 | The zoom level to use in order to download this number of tiles.
571 | """
572 | # Calculate bounds of the bbox
573 | lon_range = np.sort([e, w])[::-1]
574 | lat_range = np.sort([s, n])[::-1]
575 |
576 | lon_length = np.subtract(*lon_range)
577 | lat_length = np.subtract(*lat_range)
578 |
579 | # Calculate the zoom
580 | zoom_lon = np.ceil(np.log2(360 * 2.0 / lon_length))
581 | zoom_lat = np.ceil(np.log2(360 * 2.0 / lat_length))
582 | zoom = np.max([zoom_lon, zoom_lat])
583 | return int(zoom)
584 |
585 |
586 | def _validate_zoom(zoom, provider, auto=True):
587 | """
588 | Validate the zoom level and if needed raise informative error message.
589 | Returns the validated zoom.
590 |
591 | Parameters
592 | ----------
593 | zoom : int
594 | The specified or calculated zoom level
595 | provider : dict
596 | auto : bool
597 | Indicating if zoom was specified or calculated (to have specific
598 | error message for each case).
599 |
600 | Returns
601 | -------
602 | int
603 | Validated zoom level.
604 |
605 | """
606 | min_zoom = provider.get("min_zoom", 0)
607 | if "max_zoom" in provider:
608 | max_zoom = provider.get("max_zoom")
609 | max_zoom_known = True
610 | else:
611 | # 22 is known max in existing providers, taking some margin
612 | max_zoom = 30
613 | max_zoom_known = False
614 |
615 | if min_zoom <= zoom <= max_zoom:
616 | return zoom
617 |
618 | mode = "inferred" if auto else "specified"
619 | msg = "The {0} zoom level of {1} is not valid for the current tile provider".format(
620 | mode, zoom
621 | )
622 | if max_zoom_known:
623 | msg += " (valid zooms: {0} - {1}).".format(min_zoom, max_zoom)
624 | else:
625 | msg += "."
626 | if auto:
627 | # automatically inferred zoom: clip to max zoom if that is known ...
628 | if zoom > max_zoom and max_zoom_known:
629 | warnings.warn(msg)
630 | return max_zoom
631 | # ... otherwise extend the error message with possible reasons
632 | msg += (
633 | " This can indicate that the extent of your figure is wrong (e.g. too "
634 | "small extent, or in the wrong coordinate reference system)"
635 | )
636 | raise ValueError(msg)
637 |
638 |
639 | def _merge_tiles(tiles, arrays):
640 | """
641 | Merge a set of tiles into a single array.
642 |
643 | Parameters
644 | ---------
645 | tiles : list of mercantile.Tile objects
646 | The tiles to merge.
647 | arrays : list of numpy arrays
648 | The corresponding arrays (image pixels) of the tiles. This list
649 | has the same length and order as the `tiles` argument.
650 |
651 | Returns
652 | -------
653 | img : np.ndarray
654 | Merged arrays.
655 | extent : tuple
656 | Bounding box [west, south, east, north] of the returned image
657 | in long/lat.
658 | """
659 | # create (n_tiles x 2) array with column for x and y coordinates
660 | tile_xys = np.array([(t.x, t.y) for t in tiles])
661 |
662 | # get indices starting at zero
663 | indices = tile_xys - tile_xys.min(axis=0)
664 |
665 | # the shape of individual tile images
666 | h, w, d = arrays[0].shape
667 |
668 | # number of rows and columns in the merged tile
669 | n_x, n_y = (indices + 1).max(axis=0)
670 |
671 | # empty merged tiles array to be filled in
672 | img = np.zeros((h * n_y, w * n_x, d), dtype=np.uint8)
673 |
674 | for ind, arr in zip(indices, arrays):
675 | x, y = ind
676 | img[y * h : (y + 1) * h, x * w : (x + 1) * w, :] = arr
677 |
678 | bounds = np.array([mt.bounds(t) for t in tiles])
679 | west, south, east, north = (
680 | min(bounds[:, 0]),
681 | min(bounds[:, 1]),
682 | max(bounds[:, 2]),
683 | max(bounds[:, 3]),
684 | )
685 |
686 | return img, (west, south, east, north)
687 |
--------------------------------------------------------------------------------
/contextily/_providers.py:
--------------------------------------------------------------------------------
1 | """
2 | Tile providers.
3 |
4 | This file is autogenerated! It is a python representation of the leaflet
5 | providers defined by the leaflet-providers.js extension to Leaflet
6 | (https://github.com/leaflet-extras/leaflet-providers).
7 | Credit to the leaflet-providers.js project (BSD 2-Clause "Simplified" License)
8 | and the Leaflet Providers contributors.
9 |
10 | Generated by parse_leaflet_providers.py at 2019-08-01 from leaflet-providers
11 | at commit 9eb968f8442ea492626c9c8f0dac8ede484e6905 (Bumped version to 1.8.0).
12 |
13 | """
14 |
15 |
16 | class Bunch(dict):
17 | """A dict with attribute-access"""
18 |
19 | def __getattr__(self, key):
20 | try:
21 | return self.__getitem__(key)
22 | except KeyError:
23 | raise AttributeError(key)
24 |
25 | def __dir__(self):
26 | return self.keys()
27 |
28 |
29 | class TileProvider(Bunch):
30 | """
31 | A dict with attribute-access and that
32 | can be called to update keys
33 | """
34 |
35 | def __call__(self, **kwargs):
36 | new = TileProvider(self) # takes a copy preserving the class
37 | new.update(kwargs)
38 | return new
39 |
40 |
41 | providers = Bunch(
42 | OpenStreetMap = Bunch(
43 | Mapnik = TileProvider(
44 | url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
45 | max_zoom = 19,
46 | attribution = '(C) OpenStreetMap contributors',
47 | name = 'OpenStreetMap.Mapnik'
48 | ),
49 | DE = TileProvider(
50 | url = 'https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png',
51 | max_zoom = 18,
52 | attribution = '(C) OpenStreetMap contributors',
53 | name = 'OpenStreetMap.DE'
54 | ),
55 | CH = TileProvider(
56 | url = 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png',
57 | max_zoom = 18,
58 | attribution = '(C) OpenStreetMap contributors',
59 | bounds = [[45, 5], [48, 11]],
60 | name = 'OpenStreetMap.CH'
61 | ),
62 | France = TileProvider(
63 | url = 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
64 | max_zoom = 20,
65 | attribution = '(C) Openstreetmap France | (C) OpenStreetMap contributors',
66 | name = 'OpenStreetMap.France'
67 | ),
68 | HOT = TileProvider(
69 | url = 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
70 | max_zoom = 19,
71 | attribution = '(C) OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France',
72 | name = 'OpenStreetMap.HOT'
73 | ),
74 | BZH = TileProvider(
75 | url = 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png',
76 | max_zoom = 19,
77 | attribution = '(C) OpenStreetMap contributors, Tiles courtesy of Breton OpenStreetMap Team',
78 | bounds = [[46.2, -5.5], [50, 0.7]],
79 | name = 'OpenStreetMap.BZH'
80 | )
81 | ),
82 | OpenSeaMap = TileProvider(
83 | url = 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
84 | attribution = 'Map data: (C) OpenSeaMap contributors',
85 | name = 'OpenSeaMap'
86 | ),
87 | OpenPtMap = TileProvider(
88 | url = 'http://openptmap.org/tiles/{z}/{x}/{y}.png',
89 | max_zoom = 17,
90 | attribution = 'Map data: (C) OpenPtMap contributors',
91 | name = 'OpenPtMap'
92 | ),
93 | OpenTopoMap = TileProvider(
94 | url = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
95 | max_zoom = 17,
96 | attribution = 'Map data: (C) OpenStreetMap contributors, SRTM | Map style: (C) OpenTopoMap (CC-BY-SA)',
97 | name = 'OpenTopoMap'
98 | ),
99 | OpenRailwayMap = TileProvider(
100 | url = 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png',
101 | max_zoom = 19,
102 | attribution = 'Map data: (C) OpenStreetMap contributors | Map style: (C) OpenRailwayMap (CC-BY-SA)',
103 | name = 'OpenRailwayMap'
104 | ),
105 | OpenFireMap = TileProvider(
106 | url = 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png',
107 | max_zoom = 19,
108 | attribution = 'Map data: (C) OpenStreetMap contributors | Map style: (C) OpenFireMap (CC-BY-SA)',
109 | name = 'OpenFireMap'
110 | ),
111 | SafeCast = TileProvider(
112 | url = 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png',
113 | max_zoom = 16,
114 | attribution = 'Map data: (C) OpenStreetMap contributors | Map style: (C) SafeCast (CC-BY-SA)',
115 | name = 'SafeCast'
116 | ),
117 | Thunderforest = Bunch(
118 | OpenCycleMap = TileProvider(
119 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
120 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
121 | variant = 'cycle',
122 | apikey = '',
123 | max_zoom = 22,
124 | name = 'Thunderforest.OpenCycleMap'
125 | ),
126 | Transport = TileProvider(
127 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
128 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
129 | variant = 'transport',
130 | apikey = '',
131 | max_zoom = 22,
132 | name = 'Thunderforest.Transport'
133 | ),
134 | TransportDark = TileProvider(
135 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
136 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
137 | variant = 'transport-dark',
138 | apikey = '',
139 | max_zoom = 22,
140 | name = 'Thunderforest.TransportDark'
141 | ),
142 | SpinalMap = TileProvider(
143 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
144 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
145 | variant = 'spinal-map',
146 | apikey = '',
147 | max_zoom = 22,
148 | name = 'Thunderforest.SpinalMap'
149 | ),
150 | Landscape = TileProvider(
151 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
152 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
153 | variant = 'landscape',
154 | apikey = '',
155 | max_zoom = 22,
156 | name = 'Thunderforest.Landscape'
157 | ),
158 | Outdoors = TileProvider(
159 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
160 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
161 | variant = 'outdoors',
162 | apikey = '',
163 | max_zoom = 22,
164 | name = 'Thunderforest.Outdoors'
165 | ),
166 | Pioneer = TileProvider(
167 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
168 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
169 | variant = 'pioneer',
170 | apikey = '',
171 | max_zoom = 22,
172 | name = 'Thunderforest.Pioneer'
173 | ),
174 | MobileAtlas = TileProvider(
175 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
176 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
177 | variant = 'mobile-atlas',
178 | apikey = '',
179 | max_zoom = 22,
180 | name = 'Thunderforest.MobileAtlas'
181 | ),
182 | Neighbourhood = TileProvider(
183 | url = 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}',
184 | attribution = '(C) Thunderforest, (C) OpenStreetMap contributors',
185 | variant = 'neighbourhood',
186 | apikey = '',
187 | max_zoom = 22,
188 | name = 'Thunderforest.Neighbourhood'
189 | )
190 | ),
191 | OpenMapSurfer = Bunch(
192 | Roads = TileProvider(
193 | url = 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
194 | max_zoom = 19,
195 | variant = 'roads',
196 | attribution = 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
197 | name = 'OpenMapSurfer.Roads'
198 | ),
199 | Hybrid = TileProvider(
200 | url = 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
201 | max_zoom = 19,
202 | variant = 'hybrid',
203 | attribution = 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
204 | name = 'OpenMapSurfer.Hybrid'
205 | ),
206 | AdminBounds = TileProvider(
207 | url = 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
208 | max_zoom = 18,
209 | variant = 'adminb',
210 | attribution = 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
211 | name = 'OpenMapSurfer.AdminBounds'
212 | ),
213 | ContourLines = TileProvider(
214 | url = 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
215 | max_zoom = 18,
216 | variant = 'asterc',
217 | attribution = 'Imagery from GIScience Research Group @ University of Heidelberg | Map data ASTER GDEM',
218 | min_zoom = 13,
219 | name = 'OpenMapSurfer.ContourLines'
220 | ),
221 | Hillshade = TileProvider(
222 | url = 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
223 | max_zoom = 18,
224 | variant = 'asterh',
225 | attribution = 'Imagery from GIScience Research Group @ University of Heidelberg | Map data ASTER GDEM, SRTM',
226 | name = 'OpenMapSurfer.Hillshade'
227 | ),
228 | ElementsAtRisk = TileProvider(
229 | url = 'https://maps.heigit.org/openmapsurfer/tiles/{variant}/webmercator/{z}/{x}/{y}.png',
230 | max_zoom = 19,
231 | variant = 'elements_at_risk',
232 | attribution = 'Imagery from GIScience Research Group @ University of Heidelberg | Map data (C) OpenStreetMap contributors',
233 | name = 'OpenMapSurfer.ElementsAtRisk'
234 | )
235 | ),
236 | Hydda = Bunch(
237 | Full = TileProvider(
238 | url = 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
239 | max_zoom = 18,
240 | variant = 'full',
241 | attribution = 'Tiles courtesy of OpenStreetMap Sweden -- Map data (C) OpenStreetMap contributors',
242 | name = 'Hydda.Full'
243 | ),
244 | Base = TileProvider(
245 | url = 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
246 | max_zoom = 18,
247 | variant = 'base',
248 | attribution = 'Tiles courtesy of OpenStreetMap Sweden -- Map data (C) OpenStreetMap contributors',
249 | name = 'Hydda.Base'
250 | ),
251 | RoadsAndLabels = TileProvider(
252 | url = 'https://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png',
253 | max_zoom = 18,
254 | variant = 'roads_and_labels',
255 | attribution = 'Tiles courtesy of OpenStreetMap Sweden -- Map data (C) OpenStreetMap contributors',
256 | name = 'Hydda.RoadsAndLabels'
257 | )
258 | ),
259 | MapBox = TileProvider(
260 | url = 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}',
261 | attribution = '(C) Mapbox (C) OpenStreetMap contributors Improve this map',
262 | subdomains = 'abcd',
263 | id = 'mapbox.streets',
264 | accessToken = '',
265 | name = 'MapBox'
266 | ),
267 | Stamen = Bunch(
268 | Toner = TileProvider(
269 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
270 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
271 | subdomains = 'abcd',
272 | min_zoom = 0,
273 | max_zoom = 20,
274 | variant = 'toner',
275 | ext = 'png',
276 | name = 'Stamen.Toner'
277 | ),
278 | TonerBackground = TileProvider(
279 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
280 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
281 | subdomains = 'abcd',
282 | min_zoom = 0,
283 | max_zoom = 20,
284 | variant = 'toner-background',
285 | ext = 'png',
286 | name = 'Stamen.TonerBackground'
287 | ),
288 | TonerHybrid = TileProvider(
289 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
290 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
291 | subdomains = 'abcd',
292 | min_zoom = 0,
293 | max_zoom = 20,
294 | variant = 'toner-hybrid',
295 | ext = 'png',
296 | name = 'Stamen.TonerHybrid'
297 | ),
298 | TonerLines = TileProvider(
299 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
300 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
301 | subdomains = 'abcd',
302 | min_zoom = 0,
303 | max_zoom = 20,
304 | variant = 'toner-lines',
305 | ext = 'png',
306 | name = 'Stamen.TonerLines'
307 | ),
308 | TonerLabels = TileProvider(
309 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
310 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
311 | subdomains = 'abcd',
312 | min_zoom = 0,
313 | max_zoom = 20,
314 | variant = 'toner-labels',
315 | ext = 'png',
316 | name = 'Stamen.TonerLabels'
317 | ),
318 | TonerLite = TileProvider(
319 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
320 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
321 | subdomains = 'abcd',
322 | min_zoom = 0,
323 | max_zoom = 20,
324 | variant = 'toner-lite',
325 | ext = 'png',
326 | name = 'Stamen.TonerLite'
327 | ),
328 | Watercolor = TileProvider(
329 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}',
330 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
331 | subdomains = 'abcd',
332 | min_zoom = 1,
333 | max_zoom = 16,
334 | variant = 'watercolor',
335 | ext = 'jpg',
336 | name = 'Stamen.Watercolor'
337 | ),
338 | Terrain = TileProvider(
339 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
340 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
341 | subdomains = 'abcd',
342 | min_zoom = 0,
343 | max_zoom = 18,
344 | variant = 'terrain',
345 | ext = 'png',
346 | name = 'Stamen.Terrain'
347 | ),
348 | TerrainBackground = TileProvider(
349 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
350 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
351 | subdomains = 'abcd',
352 | min_zoom = 0,
353 | max_zoom = 18,
354 | variant = 'terrain-background',
355 | ext = 'png',
356 | name = 'Stamen.TerrainBackground'
357 | ),
358 | TopOSMRelief = TileProvider(
359 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}.{ext}',
360 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
361 | subdomains = 'abcd',
362 | min_zoom = 0,
363 | max_zoom = 20,
364 | variant = 'toposm-color-relief',
365 | ext = 'jpg',
366 | bounds = [[22, -132], [51, -56]],
367 | name = 'Stamen.TopOSMRelief'
368 | ),
369 | TopOSMFeatures = TileProvider(
370 | url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/{variant}/{z}/{x}/{y}{r}.{ext}',
371 | attribution = 'Map tiles by Stamen Design, CC BY 3.0 -- Map data (C) OpenStreetMap contributors',
372 | subdomains = 'abcd',
373 | min_zoom = 0,
374 | max_zoom = 20,
375 | variant = 'toposm-features',
376 | ext = 'png',
377 | bounds = [[22, -132], [51, -56]],
378 | opacity = 0.9,
379 | name = 'Stamen.TopOSMFeatures'
380 | )
381 | ),
382 | Esri = Bunch(
383 | WorldStreetMap = TileProvider(
384 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
385 | variant = 'World_Street_Map',
386 | attribution = 'Tiles (C) Esri -- Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012',
387 | name = 'Esri.WorldStreetMap'
388 | ),
389 | DeLorme = TileProvider(
390 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
391 | variant = 'Specialty/DeLorme_World_Base_Map',
392 | attribution = 'Tiles (C) Esri -- Copyright: (C)2012 DeLorme',
393 | min_zoom = 1,
394 | max_zoom = 11,
395 | name = 'Esri.DeLorme'
396 | ),
397 | WorldTopoMap = TileProvider(
398 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
399 | variant = 'World_Topo_Map',
400 | attribution = 'Tiles (C) Esri -- Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community',
401 | name = 'Esri.WorldTopoMap'
402 | ),
403 | WorldImagery = TileProvider(
404 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
405 | variant = 'World_Imagery',
406 | attribution = 'Tiles (C) Esri -- Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
407 | name = 'Esri.WorldImagery'
408 | ),
409 | WorldTerrain = TileProvider(
410 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
411 | variant = 'World_Terrain_Base',
412 | attribution = 'Tiles (C) Esri -- Source: USGS, Esri, TANA, DeLorme, and NPS',
413 | max_zoom = 13,
414 | name = 'Esri.WorldTerrain'
415 | ),
416 | WorldShadedRelief = TileProvider(
417 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
418 | variant = 'World_Shaded_Relief',
419 | attribution = 'Tiles (C) Esri -- Source: Esri',
420 | max_zoom = 13,
421 | name = 'Esri.WorldShadedRelief'
422 | ),
423 | WorldPhysical = TileProvider(
424 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
425 | variant = 'World_Physical_Map',
426 | attribution = 'Tiles (C) Esri -- Source: US National Park Service',
427 | max_zoom = 8,
428 | name = 'Esri.WorldPhysical'
429 | ),
430 | OceanBasemap = TileProvider(
431 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
432 | variant = 'Ocean_Basemap',
433 | attribution = 'Tiles (C) Esri -- Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri',
434 | max_zoom = 13,
435 | name = 'Esri.OceanBasemap'
436 | ),
437 | NatGeoWorldMap = TileProvider(
438 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
439 | variant = 'NatGeo_World_Map',
440 | attribution = 'Tiles (C) Esri -- National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC',
441 | max_zoom = 16,
442 | name = 'Esri.NatGeoWorldMap'
443 | ),
444 | WorldGrayCanvas = TileProvider(
445 | url = 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}',
446 | variant = 'Canvas/World_Light_Gray_Base',
447 | attribution = 'Tiles (C) Esri -- Esri, DeLorme, NAVTEQ',
448 | max_zoom = 16,
449 | name = 'Esri.WorldGrayCanvas'
450 | )
451 | ),
452 | OpenWeatherMap = Bunch(
453 | Clouds = TileProvider(
454 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
455 | max_zoom = 19,
456 | attribution = 'Map data (C) OpenWeatherMap',
457 | apiKey = '',
458 | opacity = 0.5,
459 | variant = 'clouds',
460 | name = 'OpenWeatherMap.Clouds'
461 | ),
462 | CloudsClassic = TileProvider(
463 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
464 | max_zoom = 19,
465 | attribution = 'Map data (C) OpenWeatherMap',
466 | apiKey = '',
467 | opacity = 0.5,
468 | variant = 'clouds_cls',
469 | name = 'OpenWeatherMap.CloudsClassic'
470 | ),
471 | Precipitation = TileProvider(
472 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
473 | max_zoom = 19,
474 | attribution = 'Map data (C) OpenWeatherMap',
475 | apiKey = '',
476 | opacity = 0.5,
477 | variant = 'precipitation',
478 | name = 'OpenWeatherMap.Precipitation'
479 | ),
480 | PrecipitationClassic = TileProvider(
481 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
482 | max_zoom = 19,
483 | attribution = 'Map data (C) OpenWeatherMap',
484 | apiKey = '',
485 | opacity = 0.5,
486 | variant = 'precipitation_cls',
487 | name = 'OpenWeatherMap.PrecipitationClassic'
488 | ),
489 | Rain = TileProvider(
490 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
491 | max_zoom = 19,
492 | attribution = 'Map data (C) OpenWeatherMap',
493 | apiKey = '',
494 | opacity = 0.5,
495 | variant = 'rain',
496 | name = 'OpenWeatherMap.Rain'
497 | ),
498 | RainClassic = TileProvider(
499 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
500 | max_zoom = 19,
501 | attribution = 'Map data (C) OpenWeatherMap',
502 | apiKey = '',
503 | opacity = 0.5,
504 | variant = 'rain_cls',
505 | name = 'OpenWeatherMap.RainClassic'
506 | ),
507 | Pressure = TileProvider(
508 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
509 | max_zoom = 19,
510 | attribution = 'Map data (C) OpenWeatherMap',
511 | apiKey = '',
512 | opacity = 0.5,
513 | variant = 'pressure',
514 | name = 'OpenWeatherMap.Pressure'
515 | ),
516 | PressureContour = TileProvider(
517 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
518 | max_zoom = 19,
519 | attribution = 'Map data (C) OpenWeatherMap',
520 | apiKey = '',
521 | opacity = 0.5,
522 | variant = 'pressure_cntr',
523 | name = 'OpenWeatherMap.PressureContour'
524 | ),
525 | Wind = TileProvider(
526 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
527 | max_zoom = 19,
528 | attribution = 'Map data (C) OpenWeatherMap',
529 | apiKey = '',
530 | opacity = 0.5,
531 | variant = 'wind',
532 | name = 'OpenWeatherMap.Wind'
533 | ),
534 | Temperature = TileProvider(
535 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
536 | max_zoom = 19,
537 | attribution = 'Map data (C) OpenWeatherMap',
538 | apiKey = '',
539 | opacity = 0.5,
540 | variant = 'temp',
541 | name = 'OpenWeatherMap.Temperature'
542 | ),
543 | Snow = TileProvider(
544 | url = 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}',
545 | max_zoom = 19,
546 | attribution = 'Map data (C) OpenWeatherMap',
547 | apiKey = '',
548 | opacity = 0.5,
549 | variant = 'snow',
550 | name = 'OpenWeatherMap.Snow'
551 | )
552 | ),
553 | HERE = Bunch(
554 | normalDay = TileProvider(
555 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
556 | attribution = 'Map (C) 1987-2019 HERE',
557 | subdomains = '1234',
558 | mapID = 'newest',
559 | app_id = '',
560 | app_code = '',
561 | base = 'base',
562 | variant = 'normal.day',
563 | max_zoom = 20,
564 | type = 'maptile',
565 | language = 'eng',
566 | format = 'png8',
567 | size = '256',
568 | name = 'HERE.normalDay'
569 | ),
570 | normalDayCustom = TileProvider(
571 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
572 | attribution = 'Map (C) 1987-2019 HERE',
573 | subdomains = '1234',
574 | mapID = 'newest',
575 | app_id = '',
576 | app_code = '',
577 | base = 'base',
578 | variant = 'normal.day.custom',
579 | max_zoom = 20,
580 | type = 'maptile',
581 | language = 'eng',
582 | format = 'png8',
583 | size = '256',
584 | name = 'HERE.normalDayCustom'
585 | ),
586 | normalDayGrey = TileProvider(
587 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
588 | attribution = 'Map (C) 1987-2019 HERE',
589 | subdomains = '1234',
590 | mapID = 'newest',
591 | app_id = '',
592 | app_code = '',
593 | base = 'base',
594 | variant = 'normal.day.grey',
595 | max_zoom = 20,
596 | type = 'maptile',
597 | language = 'eng',
598 | format = 'png8',
599 | size = '256',
600 | name = 'HERE.normalDayGrey'
601 | ),
602 | normalDayMobile = TileProvider(
603 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
604 | attribution = 'Map (C) 1987-2019 HERE',
605 | subdomains = '1234',
606 | mapID = 'newest',
607 | app_id = '',
608 | app_code = '',
609 | base = 'base',
610 | variant = 'normal.day.mobile',
611 | max_zoom = 20,
612 | type = 'maptile',
613 | language = 'eng',
614 | format = 'png8',
615 | size = '256',
616 | name = 'HERE.normalDayMobile'
617 | ),
618 | normalDayGreyMobile = TileProvider(
619 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
620 | attribution = 'Map (C) 1987-2019 HERE',
621 | subdomains = '1234',
622 | mapID = 'newest',
623 | app_id = '',
624 | app_code = '',
625 | base = 'base',
626 | variant = 'normal.day.grey.mobile',
627 | max_zoom = 20,
628 | type = 'maptile',
629 | language = 'eng',
630 | format = 'png8',
631 | size = '256',
632 | name = 'HERE.normalDayGreyMobile'
633 | ),
634 | normalDayTransit = TileProvider(
635 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
636 | attribution = 'Map (C) 1987-2019 HERE',
637 | subdomains = '1234',
638 | mapID = 'newest',
639 | app_id = '',
640 | app_code = '',
641 | base = 'base',
642 | variant = 'normal.day.transit',
643 | max_zoom = 20,
644 | type = 'maptile',
645 | language = 'eng',
646 | format = 'png8',
647 | size = '256',
648 | name = 'HERE.normalDayTransit'
649 | ),
650 | normalDayTransitMobile = TileProvider(
651 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
652 | attribution = 'Map (C) 1987-2019 HERE',
653 | subdomains = '1234',
654 | mapID = 'newest',
655 | app_id = '',
656 | app_code = '',
657 | base = 'base',
658 | variant = 'normal.day.transit.mobile',
659 | max_zoom = 20,
660 | type = 'maptile',
661 | language = 'eng',
662 | format = 'png8',
663 | size = '256',
664 | name = 'HERE.normalDayTransitMobile'
665 | ),
666 | normalNight = TileProvider(
667 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
668 | attribution = 'Map (C) 1987-2019 HERE',
669 | subdomains = '1234',
670 | mapID = 'newest',
671 | app_id = '',
672 | app_code = '',
673 | base = 'base',
674 | variant = 'normal.night',
675 | max_zoom = 20,
676 | type = 'maptile',
677 | language = 'eng',
678 | format = 'png8',
679 | size = '256',
680 | name = 'HERE.normalNight'
681 | ),
682 | normalNightMobile = TileProvider(
683 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
684 | attribution = 'Map (C) 1987-2019 HERE',
685 | subdomains = '1234',
686 | mapID = 'newest',
687 | app_id = '',
688 | app_code = '',
689 | base = 'base',
690 | variant = 'normal.night.mobile',
691 | max_zoom = 20,
692 | type = 'maptile',
693 | language = 'eng',
694 | format = 'png8',
695 | size = '256',
696 | name = 'HERE.normalNightMobile'
697 | ),
698 | normalNightGrey = TileProvider(
699 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
700 | attribution = 'Map (C) 1987-2019 HERE',
701 | subdomains = '1234',
702 | mapID = 'newest',
703 | app_id = '',
704 | app_code = '',
705 | base = 'base',
706 | variant = 'normal.night.grey',
707 | max_zoom = 20,
708 | type = 'maptile',
709 | language = 'eng',
710 | format = 'png8',
711 | size = '256',
712 | name = 'HERE.normalNightGrey'
713 | ),
714 | normalNightGreyMobile = TileProvider(
715 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
716 | attribution = 'Map (C) 1987-2019 HERE',
717 | subdomains = '1234',
718 | mapID = 'newest',
719 | app_id = '',
720 | app_code = '',
721 | base = 'base',
722 | variant = 'normal.night.grey.mobile',
723 | max_zoom = 20,
724 | type = 'maptile',
725 | language = 'eng',
726 | format = 'png8',
727 | size = '256',
728 | name = 'HERE.normalNightGreyMobile'
729 | ),
730 | normalNightTransit = TileProvider(
731 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
732 | attribution = 'Map (C) 1987-2019 HERE',
733 | subdomains = '1234',
734 | mapID = 'newest',
735 | app_id = '',
736 | app_code = '',
737 | base = 'base',
738 | variant = 'normal.night.transit',
739 | max_zoom = 20,
740 | type = 'maptile',
741 | language = 'eng',
742 | format = 'png8',
743 | size = '256',
744 | name = 'HERE.normalNightTransit'
745 | ),
746 | normalNightTransitMobile = TileProvider(
747 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
748 | attribution = 'Map (C) 1987-2019 HERE',
749 | subdomains = '1234',
750 | mapID = 'newest',
751 | app_id = '',
752 | app_code = '',
753 | base = 'base',
754 | variant = 'normal.night.transit.mobile',
755 | max_zoom = 20,
756 | type = 'maptile',
757 | language = 'eng',
758 | format = 'png8',
759 | size = '256',
760 | name = 'HERE.normalNightTransitMobile'
761 | ),
762 | reducedDay = TileProvider(
763 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
764 | attribution = 'Map (C) 1987-2019 HERE',
765 | subdomains = '1234',
766 | mapID = 'newest',
767 | app_id = '',
768 | app_code = '',
769 | base = 'base',
770 | variant = 'reduced.day',
771 | max_zoom = 20,
772 | type = 'maptile',
773 | language = 'eng',
774 | format = 'png8',
775 | size = '256',
776 | name = 'HERE.reducedDay'
777 | ),
778 | reducedNight = TileProvider(
779 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
780 | attribution = 'Map (C) 1987-2019 HERE',
781 | subdomains = '1234',
782 | mapID = 'newest',
783 | app_id = '',
784 | app_code = '',
785 | base = 'base',
786 | variant = 'reduced.night',
787 | max_zoom = 20,
788 | type = 'maptile',
789 | language = 'eng',
790 | format = 'png8',
791 | size = '256',
792 | name = 'HERE.reducedNight'
793 | ),
794 | basicMap = TileProvider(
795 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
796 | attribution = 'Map (C) 1987-2019 HERE',
797 | subdomains = '1234',
798 | mapID = 'newest',
799 | app_id = '',
800 | app_code = '',
801 | base = 'base',
802 | variant = 'normal.day',
803 | max_zoom = 20,
804 | type = 'basetile',
805 | language = 'eng',
806 | format = 'png8',
807 | size = '256',
808 | name = 'HERE.basicMap'
809 | ),
810 | mapLabels = TileProvider(
811 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
812 | attribution = 'Map (C) 1987-2019 HERE',
813 | subdomains = '1234',
814 | mapID = 'newest',
815 | app_id = '',
816 | app_code = '',
817 | base = 'base',
818 | variant = 'normal.day',
819 | max_zoom = 20,
820 | type = 'labeltile',
821 | language = 'eng',
822 | format = 'png',
823 | size = '256',
824 | name = 'HERE.mapLabels'
825 | ),
826 | trafficFlow = TileProvider(
827 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
828 | attribution = 'Map (C) 1987-2019 HERE',
829 | subdomains = '1234',
830 | mapID = 'newest',
831 | app_id = '',
832 | app_code = '',
833 | base = 'traffic',
834 | variant = 'normal.day',
835 | max_zoom = 20,
836 | type = 'flowtile',
837 | language = 'eng',
838 | format = 'png8',
839 | size = '256',
840 | name = 'HERE.trafficFlow'
841 | ),
842 | carnavDayGrey = TileProvider(
843 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
844 | attribution = 'Map (C) 1987-2019 HERE',
845 | subdomains = '1234',
846 | mapID = 'newest',
847 | app_id = '',
848 | app_code = '',
849 | base = 'base',
850 | variant = 'carnav.day.grey',
851 | max_zoom = 20,
852 | type = 'maptile',
853 | language = 'eng',
854 | format = 'png8',
855 | size = '256',
856 | name = 'HERE.carnavDayGrey'
857 | ),
858 | hybridDay = TileProvider(
859 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
860 | attribution = 'Map (C) 1987-2019 HERE',
861 | subdomains = '1234',
862 | mapID = 'newest',
863 | app_id = '',
864 | app_code = '',
865 | base = 'aerial',
866 | variant = 'hybrid.day',
867 | max_zoom = 20,
868 | type = 'maptile',
869 | language = 'eng',
870 | format = 'png8',
871 | size = '256',
872 | name = 'HERE.hybridDay'
873 | ),
874 | hybridDayMobile = TileProvider(
875 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
876 | attribution = 'Map (C) 1987-2019 HERE',
877 | subdomains = '1234',
878 | mapID = 'newest',
879 | app_id = '',
880 | app_code = '',
881 | base = 'aerial',
882 | variant = 'hybrid.day.mobile',
883 | max_zoom = 20,
884 | type = 'maptile',
885 | language = 'eng',
886 | format = 'png8',
887 | size = '256',
888 | name = 'HERE.hybridDayMobile'
889 | ),
890 | hybridDayTransit = TileProvider(
891 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
892 | attribution = 'Map (C) 1987-2019 HERE',
893 | subdomains = '1234',
894 | mapID = 'newest',
895 | app_id = '',
896 | app_code = '',
897 | base = 'aerial',
898 | variant = 'hybrid.day.transit',
899 | max_zoom = 20,
900 | type = 'maptile',
901 | language = 'eng',
902 | format = 'png8',
903 | size = '256',
904 | name = 'HERE.hybridDayTransit'
905 | ),
906 | hybridDayGrey = TileProvider(
907 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
908 | attribution = 'Map (C) 1987-2019 HERE',
909 | subdomains = '1234',
910 | mapID = 'newest',
911 | app_id = '',
912 | app_code = '',
913 | base = 'aerial',
914 | variant = 'hybrid.grey.day',
915 | max_zoom = 20,
916 | type = 'maptile',
917 | language = 'eng',
918 | format = 'png8',
919 | size = '256',
920 | name = 'HERE.hybridDayGrey'
921 | ),
922 | pedestrianDay = TileProvider(
923 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
924 | attribution = 'Map (C) 1987-2019 HERE',
925 | subdomains = '1234',
926 | mapID = 'newest',
927 | app_id = '',
928 | app_code = '',
929 | base = 'base',
930 | variant = 'pedestrian.day',
931 | max_zoom = 20,
932 | type = 'maptile',
933 | language = 'eng',
934 | format = 'png8',
935 | size = '256',
936 | name = 'HERE.pedestrianDay'
937 | ),
938 | pedestrianNight = TileProvider(
939 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
940 | attribution = 'Map (C) 1987-2019 HERE',
941 | subdomains = '1234',
942 | mapID = 'newest',
943 | app_id = '',
944 | app_code = '',
945 | base = 'base',
946 | variant = 'pedestrian.night',
947 | max_zoom = 20,
948 | type = 'maptile',
949 | language = 'eng',
950 | format = 'png8',
951 | size = '256',
952 | name = 'HERE.pedestrianNight'
953 | ),
954 | satelliteDay = TileProvider(
955 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
956 | attribution = 'Map (C) 1987-2019 HERE',
957 | subdomains = '1234',
958 | mapID = 'newest',
959 | app_id = '',
960 | app_code = '',
961 | base = 'aerial',
962 | variant = 'satellite.day',
963 | max_zoom = 20,
964 | type = 'maptile',
965 | language = 'eng',
966 | format = 'png8',
967 | size = '256',
968 | name = 'HERE.satelliteDay'
969 | ),
970 | terrainDay = TileProvider(
971 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
972 | attribution = 'Map (C) 1987-2019 HERE',
973 | subdomains = '1234',
974 | mapID = 'newest',
975 | app_id = '',
976 | app_code = '',
977 | base = 'aerial',
978 | variant = 'terrain.day',
979 | max_zoom = 20,
980 | type = 'maptile',
981 | language = 'eng',
982 | format = 'png8',
983 | size = '256',
984 | name = 'HERE.terrainDay'
985 | ),
986 | terrainDayMobile = TileProvider(
987 | url = 'https://{s}.{base}.maps.api.here.com/maptile/2.1/{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?app_id={app_id}&app_code={app_code}&lg={language}',
988 | attribution = 'Map (C) 1987-2019 HERE',
989 | subdomains = '1234',
990 | mapID = 'newest',
991 | app_id = '',
992 | app_code = '',
993 | base = 'aerial',
994 | variant = 'terrain.day.mobile',
995 | max_zoom = 20,
996 | type = 'maptile',
997 | language = 'eng',
998 | format = 'png8',
999 | size = '256',
1000 | name = 'HERE.terrainDayMobile'
1001 | )
1002 | ),
1003 | FreeMapSK = TileProvider(
1004 | url = 'http://t{s}.freemap.sk/T/{z}/{x}/{y}.jpeg',
1005 | min_zoom = 8,
1006 | max_zoom = 16,
1007 | subdomains = '1234',
1008 | bounds = [[47.204642, 15.996093], [49.830896, 22.576904]],
1009 | attribution = '(C) OpenStreetMap contributors, vizualization CC-By-SA 2.0 Freemap.sk',
1010 | name = 'FreeMapSK'
1011 | ),
1012 | MtbMap = TileProvider(
1013 | url = 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png',
1014 | attribution = '(C) OpenStreetMap contributors & USGS',
1015 | name = 'MtbMap'
1016 | ),
1017 | CartoDB = Bunch(
1018 | Positron = TileProvider(
1019 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1020 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1021 | subdomains = 'abcd',
1022 | max_zoom = 19,
1023 | variant = 'light_all',
1024 | name = 'CartoDB.Positron'
1025 | ),
1026 | PositronNoLabels = TileProvider(
1027 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1028 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1029 | subdomains = 'abcd',
1030 | max_zoom = 19,
1031 | variant = 'light_nolabels',
1032 | name = 'CartoDB.PositronNoLabels'
1033 | ),
1034 | PositronOnlyLabels = TileProvider(
1035 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1036 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1037 | subdomains = 'abcd',
1038 | max_zoom = 19,
1039 | variant = 'light_only_labels',
1040 | name = 'CartoDB.PositronOnlyLabels'
1041 | ),
1042 | DarkMatter = TileProvider(
1043 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1044 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1045 | subdomains = 'abcd',
1046 | max_zoom = 19,
1047 | variant = 'dark_all',
1048 | name = 'CartoDB.DarkMatter'
1049 | ),
1050 | DarkMatterNoLabels = TileProvider(
1051 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1052 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1053 | subdomains = 'abcd',
1054 | max_zoom = 19,
1055 | variant = 'dark_nolabels',
1056 | name = 'CartoDB.DarkMatterNoLabels'
1057 | ),
1058 | DarkMatterOnlyLabels = TileProvider(
1059 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1060 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1061 | subdomains = 'abcd',
1062 | max_zoom = 19,
1063 | variant = 'dark_only_labels',
1064 | name = 'CartoDB.DarkMatterOnlyLabels'
1065 | ),
1066 | Voyager = TileProvider(
1067 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1068 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1069 | subdomains = 'abcd',
1070 | max_zoom = 19,
1071 | variant = 'rastertiles/voyager',
1072 | name = 'CartoDB.Voyager'
1073 | ),
1074 | VoyagerNoLabels = TileProvider(
1075 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1076 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1077 | subdomains = 'abcd',
1078 | max_zoom = 19,
1079 | variant = 'rastertiles/voyager_nolabels',
1080 | name = 'CartoDB.VoyagerNoLabels'
1081 | ),
1082 | VoyagerOnlyLabels = TileProvider(
1083 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1084 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1085 | subdomains = 'abcd',
1086 | max_zoom = 19,
1087 | variant = 'rastertiles/voyager_only_labels',
1088 | name = 'CartoDB.VoyagerOnlyLabels'
1089 | ),
1090 | VoyagerLabelsUnder = TileProvider(
1091 | url = 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png',
1092 | attribution = '(C) OpenStreetMap contributors (C) CARTO',
1093 | subdomains = 'abcd',
1094 | max_zoom = 19,
1095 | variant = 'rastertiles/voyager_labels_under',
1096 | name = 'CartoDB.VoyagerLabelsUnder'
1097 | )
1098 | ),
1099 | HikeBike = Bunch(
1100 | HikeBike = TileProvider(
1101 | url = 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png',
1102 | max_zoom = 19,
1103 | attribution = '(C) OpenStreetMap contributors',
1104 | variant = 'hikebike',
1105 | name = 'HikeBike.HikeBike'
1106 | ),
1107 | HillShading = TileProvider(
1108 | url = 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png',
1109 | max_zoom = 15,
1110 | attribution = '(C) OpenStreetMap contributors',
1111 | variant = 'hillshading',
1112 | name = 'HikeBike.HillShading'
1113 | )
1114 | ),
1115 | BasemapAT = Bunch(
1116 | basemap = TileProvider(
1117 | url = 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
1118 | max_zoom = 20,
1119 | attribution = 'Datenquelle: basemap.at',
1120 | subdomains = ['', '1', '2', '3', '4'],
1121 | format = 'png',
1122 | bounds = [[46.35877, 8.782379], [49.037872, 17.189532]],
1123 | variant = 'geolandbasemap',
1124 | name = 'BasemapAT.basemap'
1125 | ),
1126 | grau = TileProvider(
1127 | url = 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
1128 | max_zoom = 19,
1129 | attribution = 'Datenquelle: basemap.at',
1130 | subdomains = ['', '1', '2', '3', '4'],
1131 | format = 'png',
1132 | bounds = [[46.35877, 8.782379], [49.037872, 17.189532]],
1133 | variant = 'bmapgrau',
1134 | name = 'BasemapAT.grau'
1135 | ),
1136 | overlay = TileProvider(
1137 | url = 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
1138 | max_zoom = 19,
1139 | attribution = 'Datenquelle: basemap.at',
1140 | subdomains = ['', '1', '2', '3', '4'],
1141 | format = 'png',
1142 | bounds = [[46.35877, 8.782379], [49.037872, 17.189532]],
1143 | variant = 'bmapoverlay',
1144 | name = 'BasemapAT.overlay'
1145 | ),
1146 | highdpi = TileProvider(
1147 | url = 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
1148 | max_zoom = 19,
1149 | attribution = 'Datenquelle: basemap.at',
1150 | subdomains = ['', '1', '2', '3', '4'],
1151 | format = 'jpeg',
1152 | bounds = [[46.35877, 8.782379], [49.037872, 17.189532]],
1153 | variant = 'bmaphidpi',
1154 | name = 'BasemapAT.highdpi'
1155 | ),
1156 | orthofoto = TileProvider(
1157 | url = 'https://maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}',
1158 | max_zoom = 20,
1159 | attribution = 'Datenquelle: basemap.at',
1160 | subdomains = ['', '1', '2', '3', '4'],
1161 | format = 'jpeg',
1162 | bounds = [[46.35877, 8.782379], [49.037872, 17.189532]],
1163 | variant = 'bmaporthofoto30cm',
1164 | name = 'BasemapAT.orthofoto'
1165 | )
1166 | ),
1167 | nlmaps = Bunch(
1168 | standaard = TileProvider(
1169 | url = 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
1170 | min_zoom = 6,
1171 | max_zoom = 19,
1172 | bounds = [[50.5, 3.25], [54, 7.6]],
1173 | attribution = 'Kaartgegevens (C) Kadaster',
1174 | variant = 'brtachtergrondkaart',
1175 | name = 'nlmaps.standaard'
1176 | ),
1177 | pastel = TileProvider(
1178 | url = 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
1179 | min_zoom = 6,
1180 | max_zoom = 19,
1181 | bounds = [[50.5, 3.25], [54, 7.6]],
1182 | attribution = 'Kaartgegevens (C) Kadaster',
1183 | variant = 'brtachtergrondkaartpastel',
1184 | name = 'nlmaps.pastel'
1185 | ),
1186 | grijs = TileProvider(
1187 | url = 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/{variant}/EPSG:3857/{z}/{x}/{y}.png',
1188 | min_zoom = 6,
1189 | max_zoom = 19,
1190 | bounds = [[50.5, 3.25], [54, 7.6]],
1191 | attribution = 'Kaartgegevens (C) Kadaster',
1192 | variant = 'brtachtergrondkaartgrijs',
1193 | name = 'nlmaps.grijs'
1194 | ),
1195 | luchtfoto = TileProvider(
1196 | url = 'https://geodata.nationaalgeoregister.nl/luchtfoto/rgb/wmts/1.0.0/2016_ortho25/EPSG:3857/{z}/{x}/{y}.png',
1197 | min_zoom = 6,
1198 | max_zoom = 19,
1199 | bounds = [[50.5, 3.25], [54, 7.6]],
1200 | attribution = 'Kaartgegevens (C) Kadaster',
1201 | name = 'nlmaps.luchtfoto'
1202 | )
1203 | ),
1204 | NASAGIBS = Bunch(
1205 | ModisTerraTrueColorCR = TileProvider(
1206 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1207 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1208 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1209 | min_zoom = 1,
1210 | max_zoom = 9,
1211 | format = 'jpg',
1212 | time = '',
1213 | tilematrixset = 'GoogleMapsCompatible_Level',
1214 | variant = 'MODIS_Terra_CorrectedReflectance_TrueColor',
1215 | name = 'NASAGIBS.ModisTerraTrueColorCR'
1216 | ),
1217 | ModisTerraBands367CR = TileProvider(
1218 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1219 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1220 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1221 | min_zoom = 1,
1222 | max_zoom = 9,
1223 | format = 'jpg',
1224 | time = '',
1225 | tilematrixset = 'GoogleMapsCompatible_Level',
1226 | variant = 'MODIS_Terra_CorrectedReflectance_Bands367',
1227 | name = 'NASAGIBS.ModisTerraBands367CR'
1228 | ),
1229 | ViirsEarthAtNight2012 = TileProvider(
1230 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1231 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1232 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1233 | min_zoom = 1,
1234 | max_zoom = 8,
1235 | format = 'jpg',
1236 | time = '',
1237 | tilematrixset = 'GoogleMapsCompatible_Level',
1238 | variant = 'VIIRS_CityLights_2012',
1239 | name = 'NASAGIBS.ViirsEarthAtNight2012'
1240 | ),
1241 | ModisTerraLSTDay = TileProvider(
1242 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1243 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1244 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1245 | min_zoom = 1,
1246 | max_zoom = 7,
1247 | format = 'png',
1248 | time = '',
1249 | tilematrixset = 'GoogleMapsCompatible_Level',
1250 | variant = 'MODIS_Terra_Land_Surface_Temp_Day',
1251 | opacity = 0.75,
1252 | name = 'NASAGIBS.ModisTerraLSTDay'
1253 | ),
1254 | ModisTerraSnowCover = TileProvider(
1255 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1256 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1257 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1258 | min_zoom = 1,
1259 | max_zoom = 8,
1260 | format = 'png',
1261 | time = '',
1262 | tilematrixset = 'GoogleMapsCompatible_Level',
1263 | variant = 'MODIS_Terra_Snow_Cover',
1264 | opacity = 0.75,
1265 | name = 'NASAGIBS.ModisTerraSnowCover'
1266 | ),
1267 | ModisTerraAOD = TileProvider(
1268 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1269 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1270 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1271 | min_zoom = 1,
1272 | max_zoom = 6,
1273 | format = 'png',
1274 | time = '',
1275 | tilematrixset = 'GoogleMapsCompatible_Level',
1276 | variant = 'MODIS_Terra_Aerosol',
1277 | opacity = 0.75,
1278 | name = 'NASAGIBS.ModisTerraAOD'
1279 | ),
1280 | ModisTerraChlorophyll = TileProvider(
1281 | url = 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{max_zoom}/{z}/{y}/{x}.{format}',
1282 | attribution = 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ.',
1283 | bounds = [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
1284 | min_zoom = 1,
1285 | max_zoom = 7,
1286 | format = 'png',
1287 | time = '',
1288 | tilematrixset = 'GoogleMapsCompatible_Level',
1289 | variant = 'MODIS_Terra_Chlorophyll_A',
1290 | opacity = 0.75,
1291 | name = 'NASAGIBS.ModisTerraChlorophyll'
1292 | )
1293 | ),
1294 | NLS = TileProvider(
1295 | url = 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg',
1296 | attribution = 'National Library of Scotland Historic Maps',
1297 | bounds = [[49.6, -12], [61.7, 3]],
1298 | min_zoom = 1,
1299 | max_zoom = 18,
1300 | subdomains = '0123',
1301 | name = 'NLS'
1302 | ),
1303 | JusticeMap = Bunch(
1304 | income = TileProvider(
1305 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1306 | attribution = 'Justice Map',
1307 | size = 'county',
1308 | bounds = [[14, -180], [72, -56]],
1309 | variant = 'income',
1310 | name = 'JusticeMap.income'
1311 | ),
1312 | americanIndian = TileProvider(
1313 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1314 | attribution = 'Justice Map',
1315 | size = 'county',
1316 | bounds = [[14, -180], [72, -56]],
1317 | variant = 'indian',
1318 | name = 'JusticeMap.americanIndian'
1319 | ),
1320 | asian = TileProvider(
1321 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1322 | attribution = 'Justice Map',
1323 | size = 'county',
1324 | bounds = [[14, -180], [72, -56]],
1325 | variant = 'asian',
1326 | name = 'JusticeMap.asian'
1327 | ),
1328 | black = TileProvider(
1329 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1330 | attribution = 'Justice Map',
1331 | size = 'county',
1332 | bounds = [[14, -180], [72, -56]],
1333 | variant = 'black',
1334 | name = 'JusticeMap.black'
1335 | ),
1336 | hispanic = TileProvider(
1337 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1338 | attribution = 'Justice Map',
1339 | size = 'county',
1340 | bounds = [[14, -180], [72, -56]],
1341 | variant = 'hispanic',
1342 | name = 'JusticeMap.hispanic'
1343 | ),
1344 | multi = TileProvider(
1345 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1346 | attribution = 'Justice Map',
1347 | size = 'county',
1348 | bounds = [[14, -180], [72, -56]],
1349 | variant = 'multi',
1350 | name = 'JusticeMap.multi'
1351 | ),
1352 | nonWhite = TileProvider(
1353 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1354 | attribution = 'Justice Map',
1355 | size = 'county',
1356 | bounds = [[14, -180], [72, -56]],
1357 | variant = 'nonwhite',
1358 | name = 'JusticeMap.nonWhite'
1359 | ),
1360 | white = TileProvider(
1361 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1362 | attribution = 'Justice Map',
1363 | size = 'county',
1364 | bounds = [[14, -180], [72, -56]],
1365 | variant = 'white',
1366 | name = 'JusticeMap.white'
1367 | ),
1368 | plurality = TileProvider(
1369 | url = 'http://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png',
1370 | attribution = 'Justice Map',
1371 | size = 'county',
1372 | bounds = [[14, -180], [72, -56]],
1373 | variant = 'plural',
1374 | name = 'JusticeMap.plurality'
1375 | )
1376 | ),
1377 | Wikimedia = TileProvider(
1378 | url = 'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}{r}.png',
1379 | attribution = 'Wikimedia',
1380 | min_zoom = 1,
1381 | max_zoom = 19,
1382 | name = 'Wikimedia'
1383 | ),
1384 | GeoportailFrance = Bunch(
1385 | parcels = TileProvider(
1386 | url = 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
1387 | attribution = 'Geoportail France',
1388 | bounds = [[-75, -180], [81, 180]],
1389 | min_zoom = 2,
1390 | max_zoom = 20,
1391 | apikey = 'choisirgeoportail',
1392 | format = 'image/png',
1393 | style = 'bdparcellaire',
1394 | variant = 'CADASTRALPARCELS.PARCELS',
1395 | name = 'GeoportailFrance.parcels'
1396 | ),
1397 | ignMaps = TileProvider(
1398 | url = 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
1399 | attribution = 'Geoportail France',
1400 | bounds = [[-75, -180], [81, 180]],
1401 | min_zoom = 2,
1402 | max_zoom = 18,
1403 | apikey = 'choisirgeoportail',
1404 | format = 'image/jpeg',
1405 | style = 'normal',
1406 | variant = 'GEOGRAPHICALGRIDSYSTEMS.MAPS',
1407 | name = 'GeoportailFrance.ignMaps'
1408 | ),
1409 | maps = TileProvider(
1410 | url = 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
1411 | attribution = 'Geoportail France',
1412 | bounds = [[-75, -180], [81, 180]],
1413 | min_zoom = 2,
1414 | max_zoom = 18,
1415 | apikey = 'choisirgeoportail',
1416 | format = 'image/jpeg',
1417 | style = 'normal',
1418 | variant = 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD',
1419 | name = 'GeoportailFrance.maps'
1420 | ),
1421 | orthos = TileProvider(
1422 | url = 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
1423 | attribution = 'Geoportail France',
1424 | bounds = [[-75, -180], [81, 180]],
1425 | min_zoom = 2,
1426 | max_zoom = 19,
1427 | apikey = 'choisirgeoportail',
1428 | format = 'image/jpeg',
1429 | style = 'normal',
1430 | variant = 'ORTHOIMAGERY.ORTHOPHOTOS',
1431 | name = 'GeoportailFrance.orthos'
1432 | )
1433 | ),
1434 | OneMapSG = Bunch(
1435 | Default = TileProvider(
1436 | url = 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
1437 | variant = 'Default',
1438 | min_zoom = 11,
1439 | max_zoom = 18,
1440 | bounds = [[1.56073, 104.11475], [1.16, 103.502]],
1441 | attribution = ' New OneMap | Map data (C) contributors, Singapore Land Authority',
1442 | name = 'OneMapSG.Default'
1443 | ),
1444 | Night = TileProvider(
1445 | url = 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
1446 | variant = 'Night',
1447 | min_zoom = 11,
1448 | max_zoom = 18,
1449 | bounds = [[1.56073, 104.11475], [1.16, 103.502]],
1450 | attribution = ' New OneMap | Map data (C) contributors, Singapore Land Authority',
1451 | name = 'OneMapSG.Night'
1452 | ),
1453 | Original = TileProvider(
1454 | url = 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
1455 | variant = 'Original',
1456 | min_zoom = 11,
1457 | max_zoom = 18,
1458 | bounds = [[1.56073, 104.11475], [1.16, 103.502]],
1459 | attribution = ' New OneMap | Map data (C) contributors, Singapore Land Authority',
1460 | name = 'OneMapSG.Original'
1461 | ),
1462 | Grey = TileProvider(
1463 | url = 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
1464 | variant = 'Grey',
1465 | min_zoom = 11,
1466 | max_zoom = 18,
1467 | bounds = [[1.56073, 104.11475], [1.16, 103.502]],
1468 | attribution = ' New OneMap | Map data (C) contributors, Singapore Land Authority',
1469 | name = 'OneMapSG.Grey'
1470 | ),
1471 | LandLot = TileProvider(
1472 | url = 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png',
1473 | variant = 'LandLot',
1474 | min_zoom = 11,
1475 | max_zoom = 18,
1476 | bounds = [[1.56073, 104.11475], [1.16, 103.502]],
1477 | attribution = ' New OneMap | Map data (C) contributors, Singapore Land Authority',
1478 | name = 'OneMapSG.LandLot'
1479 | )
1480 | )
1481 | )
1482 |
1483 |
--------------------------------------------------------------------------------