├── tests ├── __init__.py ├── test_providers.py └── test_ctx.py ├── MANIFEST.in ├── tiles.png ├── requirements.txt ├── .coveragerc ├── readthedocs.yml ├── docs ├── _templates │ ├── layout.html │ └── docs-sidebar.html ├── environment.yml ├── index.rst ├── Makefile ├── reference.rst ├── make.bat ├── _static │ └── css │ │ └── custom.css └── conf.py ├── ci └── travis │ ├── 36-minimal.yaml │ └── 37-latest-conda-forge.yaml ├── contextily ├── __init__.py ├── tile_providers.py ├── plotting.py ├── place.py ├── tile.py └── _providers.py ├── Dockerfile ├── .gitignore ├── notebooks ├── places_guide.ipynb └── add_basemap_deepdive.ipynb ├── .travis.yml ├── README_dev.md ├── setup.py ├── LICENSE.txt ├── examples └── plot_map.py ├── README.md └── scripts └── parse_leaflet_providers.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt README.md requirements.txt 2 | -------------------------------------------------------------------------------- /tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darribas/contextily/HEAD/tiles.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | geopy 2 | matplotlib 3 | mercantile 4 | pillow 5 | rasterio 6 | requests 7 | joblib 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */tests/* 4 | */miniconda/* 5 | [report] 6 | omit = 7 | */tests/* 8 | */miniconda/* 9 | 10 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | formats: [] 3 | conda: 4 | environment: docs/environment.yml 5 | python: 6 | version: 3 7 | install: 8 | - method: pip 9 | path: . 10 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "pydata_sphinx_theme/layout.html" %} 2 | 3 | 4 | 5 | {# Silence the navbar #} 6 | {% block docs_navbar %} 7 | {% endblock %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /ci/travis/36-minimal.yaml: -------------------------------------------------------------------------------- 1 | name: test-environment 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.6 6 | # required 7 | - geopy 8 | - matplotlib 9 | - mercantile 10 | - pillow 11 | - rasterio 12 | - requests 13 | - joblib 14 | # testing 15 | - pip 16 | - pytest 17 | - pytest-cov 18 | 19 | -------------------------------------------------------------------------------- /ci/travis/37-latest-conda-forge.yaml: -------------------------------------------------------------------------------- 1 | name: test-environment 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.7 6 | # required 7 | - geopy 8 | - matplotlib 9 | - mercantile 10 | - pillow 11 | - rasterio 12 | - requests 13 | - joblib 14 | # testing 15 | - pip 16 | - pytest 17 | - pytest-cov 18 | -------------------------------------------------------------------------------- /contextily/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | `contextily`: create context with map tiles in Python 3 | """ 4 | 5 | from . import tile_providers as sources 6 | from ._providers import providers 7 | from .place import Place, plot_map 8 | from .tile import * 9 | from .plotting import add_basemap, add_attribution 10 | 11 | __version__ = "1.0.0" 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM darribas/gds_py:4.1 2 | 3 | # Install contextily master 4 | RUN pip install -U git+https://github.com/geopandas/contextily.git@master 5 | # Add notebooks 6 | RUN rm -R work/ 7 | COPY ./README.md ${HOME}/README.md 8 | COPY ./notebooks ${HOME}/notebooks 9 | # Fix permissions 10 | USER root 11 | RUN chown -R ${NB_UID} ${HOME} 12 | USER ${NB_USER} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | */.ipynb_checkpoints/ 3 | .ipynb_checkpoints/ 4 | */*.swp 5 | *.swp 6 | tx.tif 7 | test.tif 8 | test2.tif 9 | .DS_Store 10 | .cache 11 | build/ 12 | *.egg-info/ 13 | dist/ 14 | .coverage 15 | .pytest_cache/ 16 | notebooks/warp_tst.tif 17 | notebooks/*.tif 18 | 19 | # Sphinx documentation 20 | docs/_build/ 21 | docs/*.ipynb 22 | docs/README.rst 23 | docs/tiles.png 24 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: contextily_docs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.7 6 | # dependencies 7 | - numpy 8 | - geopy 9 | - matplotlib-base 10 | - mercantile 11 | - pillow 12 | - rasterio 13 | - requests 14 | - joblib 15 | # doc dependencies 16 | - sphinx 17 | - numpydoc 18 | - nbsphinx 19 | - pandoc 20 | - ipython 21 | - pip: 22 | - git+https://github.com/pandas-dev/pydata-sphinx-theme.git@master 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. contextily documentation master file, created by 2 | sphinx-quickstart on Tue Apr 7 15:16:59 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | .. include:: README.rst 8 | 9 | Contents 10 | -------- 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | intro_guide 16 | warping_guide 17 | working_with_local_files 18 | providers_deepdive 19 | friends_gee 20 | reference 21 | 22 | 23 | Indices and tables 24 | ------------------ 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /notebooks/places_guide.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Guide to the `Place` API" 8 | ] 9 | } 10 | ], 11 | "metadata": { 12 | "kernelspec": { 13 | "display_name": "Python 3", 14 | "language": "python", 15 | "name": "python3" 16 | }, 17 | "language_info": { 18 | "codemirror_mode": { 19 | "name": "ipython", 20 | "version": 3 21 | }, 22 | "file_extension": ".py", 23 | "mimetype": "text/x-python", 24 | "name": "python", 25 | "nbconvert_exporter": "python", 26 | "pygments_lexer": "ipython3", 27 | "version": "3.7.6" 28 | } 29 | }, 30 | "nbformat": 4, 31 | "nbformat_minor": 4 32 | } 33 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /notebooks/add_basemap_deepdive.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# The in's and out's of `add_basemap`" 8 | ] 9 | } 10 | ], 11 | "metadata": { 12 | "kernelspec": { 13 | "display_name": "Python 3", 14 | "language": "python", 15 | "name": "python3" 16 | }, 17 | "language_info": { 18 | "codemirror_mode": { 19 | "name": "ipython", 20 | "version": 3 21 | }, 22 | "file_extension": ".py", 23 | "mimetype": "text/x-python", 24 | "name": "python", 25 | "nbconvert_exporter": "python", 26 | "pygments_lexer": "ipython3", 27 | "version": "3.7.6" 28 | } 29 | }, 30 | "nbformat": 4, 31 | "nbformat_minor": 4 32 | } 33 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | Reference Guide 4 | =============== 5 | 6 | Plotting basemaps 7 | ----------------- 8 | 9 | .. autofunction:: contextily.add_basemap 10 | 11 | .. autofunction:: contextily.add_attribution 12 | 13 | 14 | Working with tiles 15 | ------------------ 16 | 17 | .. autofunction:: contextily.bounds2raster 18 | 19 | .. autofunction:: contextily.bounds2img 20 | 21 | .. autofunction:: contextily.warp_tiles 22 | 23 | .. autofunction:: contextily.warp_img_transform 24 | 25 | .. autofunction:: contextily.howmany 26 | 27 | 28 | Geocoding and plotting places 29 | ----------------------------- 30 | 31 | .. autoclass:: contextily.Place 32 | 33 | .. automethod:: contextily.Place.plot 34 | 35 | .. autofunction:: contextily.plot_map 36 | 37 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Override some aspects of the pydata-sphinx-theme */ 2 | 3 | body { 4 | padding-top: 0px; 5 | } 6 | 7 | h1, 8 | h2 { 9 | color: #333; 10 | } 11 | 12 | @media (min-width: 768px) { 13 | @supports (position: -webkit-sticky) or (position: sticky) { 14 | .bd-sidebar { 15 | top: 40px; 16 | } 17 | } 18 | } 19 | 20 | /* no pink for code */ 21 | code { 22 | color: #3b444b; 23 | } 24 | 25 | /* Default link color for active + larger font size for sidebar*/ 26 | .bd-sidebar .nav > li > a { 27 | font-size: 1em; 28 | } 29 | 30 | .bd-sidebar .nav>li>a:hover, 31 | .bd-sidebar .nav > .active:hover > a, 32 | .bd-sidebar .nav > .active > a { 33 | color: #005b81; 34 | } 35 | .toc-entry > .nav-link.active { 36 | color: #005b81; 37 | border-left:2px solid #005b81; 38 | } 39 | 40 | 41 | /* New element: brand text instead of logo */ 42 | 43 | /* .navbar-brand-text { 44 | padding: 1rem; 45 | } */ 46 | 47 | a.navbar-brand-text { 48 | color: #333; 49 | font-size: xx-large; 50 | } 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | branches: 4 | only: 5 | - master 6 | os: 7 | - linux 8 | 9 | matrix: 10 | include: 11 | - env: ENV_FILE="ci/travis/36-minimal.yaml" 12 | - env: ENV_FILE="ci/travis/37-latest-conda-forge.yaml" 13 | 14 | before_install: 15 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 16 | - chmod +x miniconda.sh 17 | - ./miniconda.sh -b -p ./miniconda 18 | - export PATH=`pwd`/miniconda/bin:$PATH 19 | - conda config --set always_yes yes --set changeps1 no 20 | - conda update conda 21 | - conda info 22 | - conda env create --file="${ENV_FILE}" 23 | - source activate test-environment 24 | - pip install coveralls 25 | 26 | install: 27 | - python -m pip install -e . 28 | - conda list 29 | 30 | script: 31 | - python setup.py sdist >/dev/null 32 | - python -m pytest -v tests/ --cov contextily 33 | 34 | notifications: 35 | email: 36 | recipients: 37 | - daniel.arribas.bel@gmail.com 38 | on_failure: always 39 | 40 | after_success: 41 | - coveralls 42 | -------------------------------------------------------------------------------- /README_dev.md: -------------------------------------------------------------------------------- 1 | # Development notes 2 | 3 | ## Testing 4 | 5 | Testing relies on `pytest` and `pytest-cov`. To run the test suite locally: 6 | 7 | ``` 8 | python -m pytest -v tests/ --cov contextily 9 | ``` 10 | 11 | This assumes you also have installed `pytest-cov`. 12 | 13 | ## Releasing 14 | 15 | Cutting a release and updating to `pypi` requires the following steps (from 16 | [here](https://packaging.python.org/tutorials/packaging-projects/)]): 17 | 18 | * Make sure you have installed the following libraries: 19 | * `twine` 20 | * `setuptools` 21 | * `wheel` 22 | * Make sure tests pass locally and on CI. 23 | * Update the version on `setup.py` and `__init__.py` 24 | * Commit those changes as `git commit 'RLS: v1.0.0'` 25 | * Tag the commit using an annotated tag. ``git tag -a v1.0.0 -m "Version 1.0.0"`` 26 | * Push the RLS commit ``git push upstream master`` 27 | * Also push the tag! ``git push upstream --tags`` 28 | * Create sdist and wheel: `python setup.py sdist bdist_wheel` 29 | * Make github release from the tag (also add the sdist as asset) 30 | * When ready to push up, run `twine upload dist/*`. 31 | 32 | -------------------------------------------------------------------------------- /docs/_templates/docs-sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | {% if logo %} 3 | 4 | 5 | 6 | {% else %} 7 | 8 | CONTEXTILY 9 | 10 |

Context geo tiles in Python

11 | {% endif %} 12 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /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 | [![Build Status](https://travis-ci.org/geopandas/contextily.svg?branch=master)](https://travis-ci.org/geopandas/contextily) 10 | [![Coverage Status](https://coveralls.io/repos/github/darribas/contextily/badge.svg?branch=master)](https://coveralls.io/github/darribas/contextily?branch=master) 11 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/geopandas/contextily/master?urlpath=lab/tree/notebooks/intro_guide.ipynb) 12 | 13 | ![Tiles](tiles.png) 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 = '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) 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 = '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) 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 = '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) 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 = '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) 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 = '![](https://docs.onemap.sg/maps/images/oneMap64-01.png) New OneMap | Map data (C) contributors, Singapore Land Authority', 1478 | name = 'OneMapSG.LandLot' 1479 | ) 1480 | ) 1481 | ) 1482 | 1483 | --------------------------------------------------------------------------------