├── .github ├── PULL_REQUEST_TEMPLATE │ └── community_contribution.md ├── dependabot.yml └── workflows │ ├── release_to_pypi.yml │ ├── test_providers.yml │ ├── tests.yaml │ └── update_providers.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── ci ├── latest.yaml └── update_providers.yaml ├── codecov.yml ├── doc ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── _static │ ├── custom.css │ ├── generate_gallery.js │ └── xyzmaps.jpg │ ├── api.rst │ ├── changelog.rst │ ├── conf.py │ ├── contributing.md │ ├── gallery.rst │ ├── index.md │ ├── introduction.ipynb │ ├── registration.md │ └── tiles.png ├── provider_sources ├── _compress_providers.py ├── _parse_leaflet_providers.py ├── leaflet-providers-parsed.json └── xyzservices-providers.json ├── pyproject.toml ├── readthedocs.yml ├── setup.py └── xyzservices ├── __init__.py ├── data ├── __init__.py └── providers.json ├── lib.py ├── providers.py └── tests ├── __init__.py ├── test_lib.py └── test_providers.py /.github/PULL_REQUEST_TEMPLATE/community_contribution.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Contribution of a new provider 4 | about: Contributing additional providers to a xyzservices-providers.JSON 5 | title: "PRO:" 6 | labels: "community_contribution" 7 | 8 | --- 9 | Before adding a new provider, please check that: 10 | 11 | - [ ] The provider does not exist in `provider_sources/leaflet-providers-parsed.json`. 12 | 13 | - [ ] The provider does not exist in `provider_sources/xyzservices-providers.json`. 14 | 15 | - [ ] The provider URL is correct and tiles properly load. 16 | 17 | - [ ] The provider contains at least `name`, `url` and `attribution`. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/workflows/release_to_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish xyzservices to PyPI / GitHub 2 | 3 | on: 4 | push: 5 | tags: 6 | - "2*" 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish xyzservices to PyPI 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout source 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.x" 21 | 22 | - name: Build a binary wheel and a source tarball 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel 26 | python setup.py sdist bdist_wheel 27 | 28 | - name: Publish distribution to PyPI 29 | uses: pypa/gh-action-pypi-publish@release/v1 30 | with: 31 | user: __token__ 32 | password: ${{ secrets.PYPI_API_TOKEN }} 33 | 34 | - name: Create GitHub Release 35 | id: create_release 36 | uses: actions/create-release@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 39 | with: 40 | tag_name: ${{ github.ref }} 41 | release_name: ${{ github.ref }} 42 | draft: false 43 | prerelease: false 44 | 45 | - name: Get Asset name 46 | run: | 47 | export PKG=$(ls dist/ | grep tar) 48 | set -- $PKG 49 | echo "name=$1" >> $GITHUB_ENV 50 | 51 | - name: Upload Release Asset (sdist) to GitHub 52 | id: upload-release-asset 53 | uses: actions/upload-release-asset@v1 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | with: 57 | upload_url: ${{ steps.create_release.outputs.upload_url }} 58 | asset_path: dist/${{ env.name }} 59 | asset_name: ${{ env.name }} 60 | asset_content_type: application/zip 61 | -------------------------------------------------------------------------------- /.github/workflows/test_providers.yml: -------------------------------------------------------------------------------- 1 | name: Test providers 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | schedule: 11 | - cron: "59 23 * * 3" 12 | 13 | jobs: 14 | provider_testing: 15 | name: Test providers 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 120 18 | env: 19 | THUNDERFOREST: ${{ secrets.THUNDERFOREST }} 20 | JAWG: ${{ secrets.JAWG }} 21 | MAPBOX: ${{ secrets.MAPBOX }} 22 | MAPTILER: ${{ secrets.MAPTILER }} 23 | TOMTOM: ${{ secrets.TOMTOM }} 24 | OPENWEATHERMAP: ${{ secrets.OPENWEATHERMAP }} 25 | HEREV3: ${{ secrets.HEREV3 }} 26 | STADIA: ${{ secrets.STADIA }} 27 | defaults: 28 | run: 29 | shell: bash -l {0} 30 | 31 | steps: 32 | - name: checkout repo 33 | uses: actions/checkout@v4 34 | 35 | - name: setup micromamba 36 | uses: mamba-org/setup-micromamba@v2 37 | with: 38 | environment-file: ci/latest.yaml 39 | micromamba-version: "latest" 40 | 41 | - name: Install xyzservices 42 | run: pip install . 43 | 44 | 45 | - name: test providers - bash 46 | run: pytest -v . -m request --cov=xyzservices --cov-append --cov-report term-missing --cov-report xml --color=yes -n auto 47 | 48 | - uses: codecov/codecov-action@v5 49 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | schedule: 11 | - cron: "59 23 * * 3" 12 | 13 | jobs: 14 | unittests: 15 | name: ${{ matrix.os }}, ${{ matrix.environment-file }} 16 | runs-on: ${{ matrix.os }} 17 | timeout-minutes: 120 18 | strategy: 19 | matrix: 20 | os: [macos-latest, ubuntu-latest, windows-latest] 21 | environment-file: [ci/latest.yaml] 22 | defaults: 23 | run: 24 | shell: bash -l {0} 25 | 26 | steps: 27 | - name: checkout repo 28 | uses: actions/checkout@v4 29 | 30 | - name: setup micromamba 31 | uses: mamba-org/setup-micromamba@v2 32 | with: 33 | environment-file: ${{ matrix.environment-file }} 34 | micromamba-version: "latest" 35 | 36 | - name: Install xyzservices 37 | run: pip install . 38 | 39 | - name: run tests - bash 40 | run: pytest -v . -m "not request" --cov=xyzservices --cov-append --cov-report term-missing --cov-report xml --color=yes 41 | 42 | - name: remove JSON from share and test fallback 43 | run: | 44 | python -c 'import os, sys; os.remove(os.path.join(sys.prefix, "share", "xyzservices", "providers.json"))' 45 | pytest -v . -m "not request" --cov=xyzservices --cov-append --cov-report term-missing --cov-report xml --color=yes 46 | if: matrix.os != 'windows-latest' 47 | 48 | - uses: codecov/codecov-action@v5 49 | -------------------------------------------------------------------------------- /.github/workflows/update_providers.yaml: -------------------------------------------------------------------------------- 1 | name: Update leaflet providers/compress JSON 2 | 3 | on: 4 | schedule: 5 | - cron: '42 23 1,15 * *' 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: Manual update reason 10 | default: refresh 11 | required: false 12 | 13 | jobs: 14 | unittests: 15 | name: Update leaflet providers 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | strategy: 19 | matrix: 20 | environment-file: [ci/update_providers.yaml] 21 | 22 | steps: 23 | - name: checkout repo 24 | uses: actions/checkout@v4 25 | 26 | - name: setup micromamba 27 | uses: mamba-org/setup-micromamba@v2 28 | with: 29 | environment-file: ${{ matrix.environment-file }} 30 | micromamba-version: 'latest' 31 | 32 | - name: Parse leaflet providers/compress output 33 | shell: bash -l {0} 34 | run: | 35 | make update-leaflet 36 | make compress 37 | 38 | - name: Commit files 39 | run: | 40 | git config --global user.name 'Martin Fleischmann' 41 | git config --global user.email 'martinfleis@users.noreply.github.com' 42 | git add provider_sources/leaflet-providers-parsed.json 43 | git add xyzservices/data/providers.json 44 | git commit -am "Update leaflet providers/compress JSON [automated]" 45 | git push 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Extra XYZservices 2 | provider_sources/leaflet-providers-raw.json 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | doc/build/ 76 | 77 | # Copied file 78 | doc/source/_static/providers.json 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | .vscode/settings.json 137 | .DS_Store 138 | .ruff_cache -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | autoupdate_schedule: quarterly 4 | 5 | files: 'xyzservices\/' 6 | repos: 7 | - repo: https://github.com/astral-sh/ruff-pre-commit 8 | rev: "v0.11.4" 9 | hooks: 10 | - id: ruff 11 | - id: ruff-format 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | xyzservices 2024.6.0 (June 6, 2024) 5 | ------------------------------------ 6 | 7 | Providers: 8 | 9 | - Added ``BlueMarbleBathymetry`` and ``MEaSUREsIceVelocity`` tiles to the ``NASAGIBS`` 10 | provider (#168) 11 | - Updated ``GeoportailFrance`` TileMatrixSet information. 12 | 13 | xyzservices 2024.4.0 (April 3, 2024) 14 | ------------------------------------ 15 | 16 | Providers: 17 | 18 | - ``GeoportailFrance`` tiles are now using the new ``data.geopf.fr`` domain instead 19 | of deprecated ``wxs.ign.fr`` (#166) 20 | - ``NASAGIBS.BlueMarble3413`` and ``NASAGIBS.BlueMarble3031`` URLs are now fixed (#162) 21 | - ```NASAGIBS.BlueMarble`` was removed as the URL no longer works. 22 | - ``Esri.DeLorme`` and ``NASAGIBS.ModisTerraChlorophyll`` are marked as broken. 23 | - Added ``BaseMapDE`` and ``TopPlusOpen`` providers. 24 | - Addded ``Jawg.Lagoon``, ``MapTiler.Ocean``, ``MapTiler.Backdrop``, ``MapTiler.Dataviz`` tiles. 25 | - Updated ``NLS`` to use their new ``MapTiler`` service. 26 | 27 | 28 | xyzservices 2023.10.1 (October 26, 2023) 29 | ---------------------------------------- 30 | 31 | Providers: 32 | 33 | - ``Stamen`` tiles have been removed due to their upstream deprecation. 34 | Use ``Stamen`` styles of ``Stadia`` provider instead. 35 | - ``JusticeMap`` tiles are temporarily marked as broken. 36 | 37 | xyzservices 2023.10.0 (October 5, 2023) 38 | --------------------------------------- 39 | 40 | Providers: 41 | 42 | - ``Stamen`` tiles are now available under ``Stadia`` provider. 43 | 44 | xyzservices 2023.7.0 (July 13, 2023) 45 | ------------------------------------ 46 | 47 | Providers: 48 | 49 | - Added ``GeoportailFrance`` ``Orthoimagery_Orthophotos_Irc_express_2023`` and 50 | ``Orthoimagery_Orthophotos_Ortho_express_2023`` layers 51 | - Updated domain for ``OpenStreetMap.DE`` 52 | - Marked ``GeoportailFrance.Orthoimagery_Orthophotos_1980_1995`` as possibly broken 53 | 54 | xyzservices 2023.5.0 (May 19, 2023) 55 | ----------------------------------- 56 | 57 | Providers: 58 | 59 | - Added ``OrdnanceSurvey`` layers 60 | 61 | xyzservices 2023.2.0 (February 19, 2023) 62 | ---------------------------------------- 63 | 64 | Providers: 65 | 66 | - Updated available layers of ``GeoportailFrance`` 67 | 68 | Bug fixes: 69 | 70 | - Use ``pkgutil`` instead of ``importlib`` to fetch the JSON if the default in ``share`` 71 | is not available. Fixes this fallback for Python 3.8. 72 | 73 | xyzservices 2022.09.0 (September 19, 2022) 74 | ------------------------------------------ 75 | 76 | Providers: 77 | 78 | - Added ``GeoportailFrance`` tile layers (#126) 79 | 80 | Enhancements: 81 | 82 | - Better cleaning of names in ``query_name`` method 83 | 84 | Documentation: 85 | 86 | - Added a gallery of included tiles to the documentation (#114) 87 | 88 | xyzservices 2022.06.0 (June 21, 2022) 89 | ------------------------------------- 90 | 91 | Providers: 92 | 93 | - Added ``NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief`` 94 | - Added ``Esri.ArcticImagery`` (EPSG:5936) and ``Esri.AntarcticImagery`` (EPSG:3031) 95 | 96 | xyzservices 2022.04.0 (April 14, 2022) 97 | -------------------------------------- 98 | 99 | Providers: 100 | 101 | - Update ``OpenStreetMap.DE`` URL 102 | - Remove broken Hydda tiles 103 | 104 | xyzservices 2022.03.0 (March 9, 2022) 105 | ------------------------------------- 106 | 107 | Providers: 108 | 109 | - Added ``Esri`` ``ArcticOceanBase``, ``ArcticOceanReference`` and ``AntarcticBasemap`` 110 | 111 | xyzservices 2022.02.0 (February 10, 2022) 112 | ---------------------------------------- 113 | 114 | Providers: 115 | 116 | - Fixed ``MapTiler.Winter`` 117 | - Updated ``AzureMaps`` links 118 | 119 | xyzservices 2022.01.1 (January 20, 2022) 120 | ---------------------------------------- 121 | 122 | Providers: 123 | 124 | - Added ``NASAGIBS.BlueMarble`` datasets in EPSG 3857 (default), 3413, and 3031 125 | - Added more ``MapTiler`` providers (``Outdoor``, ``Topographique``, ``Winter``, ``Satellite``, ``Terrain``, and ``Basic4326`` in ESPG 4326). 126 | 127 | xyzservices 2022.01.0 (January 17, 2022) 128 | ---------------------------------------- 129 | 130 | Providers: 131 | 132 | - Added ``SwissFederalGeoportal`` providers (``NationalMapColor``, ``NationalMapGrey``, ``SWISSIMAGE``, ``JourneyThroughTime``) 133 | 134 | xyzservices 2021.11.0 (November 06, 2021) 135 | ---------------------------------------- 136 | 137 | Providers: 138 | 139 | - Updated deprecated links to ``nlmaps`` providers 140 | - Added ``nlmaps.water`` 141 | 142 | xyzservices 2021.10.0 (October 19, 2021) 143 | ---------------------------------------- 144 | 145 | Providers: 146 | 147 | - Added ``OPNVKarte`` map 148 | - Removed discontinued ``OpenPtMap`` 149 | - Max zoom of ``CartoDB`` tiles changed from 19 to 20 150 | 151 | xyzservices 2021.09.1 (September 20, 2021) 152 | ------------------------------------------ 153 | 154 | New functionality: 155 | 156 | - Added ``Bunch.query_name()`` method allowing to fetch the ``TileProvider`` object based on the name with flexible formatting. (#93) 157 | 158 | xyzservices 2021.09.0 (September 3, 2021) 159 | ----------------------------------------- 160 | 161 | Providers: 162 | 163 | - Fixed ``Strava`` maps (#85) 164 | - Fixed ``nlmaps.luchtfoto`` (#90) 165 | - Fixed ``NASAGIBS.ModisTerraSnowCover`` (#90) 166 | - ``JusticeMap`` and ``OpenAIP`` now use https instead of http 167 | 168 | xyzservices 2021.08.1 (August 12, 2021) 169 | --------------------------------------- 170 | 171 | Providers: 172 | 173 | - Added ``OpenStreetMap.BlackAndWhite`` (#83) 174 | - Added ``Gaode`` tiles (``Normal`` and ``Satellite``) (#83) 175 | - Expanded ``NASAGIBS`` tiles with ``ModisTerraBands721CR``, ``ModisAquaTrueColorCR``, ``ModisAquaBands721CR`` and ``ViirsTrueColorCR`` (#83) 176 | - Added metadata to ``Strava`` maps (currently down) (#83) 177 | 178 | xyzservices 2021.08.0 (August 8, 2021) 179 | -------------------------------------- 180 | 181 | New functionality: 182 | 183 | - Added ``TileProvider.from_qms()`` allowing to create a ``TileProvider`` object from the remote [Quick Map Services](https://qms.nextgis.com/about) repository (#71) 184 | - Added support of ``html_attribution`` to have live links in attributions in HTML-based outputs like leaflet (#60) 185 | - New ``Bunch.flatten`` method creating a flat dictionary of ``TileProvider`` objects based on a nested ``Bunch`` (#68) 186 | - Added ``fill_subdomain`` keyword to ``TileProvider.build_url`` to control ``{s}`` placeholder in the URL (#75) 187 | - New Bunch.filter method to filter specific providers based on keywords and other criteria (#76) 188 | 189 | Minor enhancements: 190 | 191 | - Indent providers JSON file for better readability (#64) 192 | - Support dark themes in HTML repr (#70) 193 | - Mark broken providers with ``status="broken"`` attribute (#78) 194 | - Document providers requiring registrations (#79) 195 | 196 | xyzservices 2021.07 (July 30, 2021) 197 | ----------------------------------- 198 | 199 | The initial release provides ``TileProvider`` and ``Bunch`` classes and an initial set of providers. 200 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `xyzservices` 2 | 3 | Contributions to `xyzservices` are very welcome. They are likely to be accepted more 4 | quickly if they follow these guidelines. 5 | 6 | There are two main groups of contributions - adding new provider sources and 7 | contributions to the codebase and documentation. 8 | 9 | ## Providers 10 | 11 | If you want to add a new provider, simply add its details to 12 | `provider_sources/xyzservices-providers.json`. 13 | 14 | You can add a single `TileProvider` or a `Bunch` of `TileProviders`. Use the following 15 | schema to add a single provider: 16 | 17 | ```json 18 | { 19 | ... 20 | "single_provider_name": { 21 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 22 | "max_zoom": 19, 23 | "attribution": "(C) OpenStreetMap contributors", 24 | "name": "OpenStreetMap.Mapnik" 25 | }, 26 | ... 27 | } 28 | ``` 29 | 30 | If you want to add a bunch of related providers (different versions from a single source 31 | like `Stamen.Toner` and `Stamen.TonerLite`), you can group then within a `Bunch` using 32 | the following schema: 33 | 34 | ```json 35 | { 36 | ... 37 | "provider_bunch_name": { 38 | "first_provider_name": { 39 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 40 | "max_zoom": 19, 41 | "attribution": "(C) OpenStreetMap contributors", 42 | "name": "OpenStreetMap.Mapnik" 43 | }, 44 | "second_provider_name": { 45 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?access-token={accessToken}", 46 | "max_zoom": 19, 47 | "attribution": "(C) OpenStreetMap contributors", 48 | "name": "OpenStreetMap.Mapnik", 49 | "accessToken": "" 50 | } 51 | }, 52 | ... 53 | } 54 | ``` 55 | 56 | It is mandatory to always specify at least `name`, `url`, and `attribution`. 57 | Don't forget to add any other custom attribute 58 | required by the provider. When specifying a placeholder for the access token, please use 59 | the `""` string to ensure that `requires_token()` method 60 | works properly. 61 | 62 | Once updated, you can (optionally) compress the provider sources by executing `make compress` from the 63 | repository root. 64 | 65 | ```bash 66 | cd xyzservices make compress 67 | ``` 68 | 69 | ## Code and documentation 70 | 71 | At this stage of `xyzservices` development, the priorities are to define a simple, 72 | usable, and stable API and to have clean, maintainable, readable code. 73 | 74 | In general, `xyzservices` follows the conventions of the GeoPandas project where 75 | applicable. 76 | 77 | In particular, when submitting a pull request: 78 | 79 | - All existing tests should pass. Please make sure that the test suite passes, both 80 | locally and on GitHub Actions. Status on GHA will be visible on a pull request. GHA 81 | are automatically enabled on your own fork as well. To trigger a check, make a PR to 82 | your own fork. 83 | - Ensure that documentation has built correctly. It will be automatically built for each 84 | PR. 85 | - New functionality should include tests. Please write reasonable tests for your code 86 | and make sure that they pass on your pull request. 87 | - Classes, methods, functions, etc. should have docstrings and type hints. The first 88 | line of a docstring should be a standalone summary. Parameters and return values 89 | should be documented explicitly. 90 | - Follow PEP 8 when possible. We use Black and Flake8 to ensure a consistent code format 91 | throughout the project. For more details see the [GeoPandas contributing 92 | guide](https://geopandas.readthedocs.io/en/latest/community/contributing.html). 93 | - Imports should be grouped with standard library imports first, 3rd-party libraries 94 | next, and `xyzservices` imports third. Within each grouping, imports should be 95 | alphabetized. Always use absolute imports when possible, and explicit relative imports 96 | for local imports when necessary in tests. 97 | - `xyzservices` supports Python 3.7+ only. When possible, do not introduce additional 98 | dependencies. If that is necessary, make sure they can be treated as optional. 99 | 100 | 101 | ## Updating sources from leaflet 102 | 103 | `leaflet-providers-parsed.json` is an automatically generated file. You can create a fresh version 104 | using `make update-leaflet` from the repository root: 105 | 106 | ```bash 107 | cd xyzservices make update-leaflet 108 | ``` 109 | 110 | Note that you will need functional installation of `selenium` with Firefox webdriver, `git` and `html2text` packages. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, GeoPandas 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include xyzservices/data/providers.json -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: update-leaflet compress 2 | 3 | # update leaflet-providers_parsed.json from source 4 | update-leaflet: 5 | cd provider_sources && \ 6 | python _parse_leaflet_providers.py 7 | 8 | # compress json sources to data/providers.json 9 | compress: 10 | cd provider_sources && \ 11 | python _compress_providers.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xyzservices - Source of XYZ tiles providers 2 | 3 | `xyzservices` is a lightweight library providing a repository of available XYZ services 4 | offering raster basemap tiles. The repository is provided via Python API and as a 5 | compressed JSON file. 6 | 7 | XYZ tiles can be used as background for your maps to provide necessary spatial context. 8 | `xyzservices` offer specifications of many tile services and provide an easy-to-use 9 | tools to plug them into your work, no matter if interactive or static. 10 | 11 | [![Tests](https://github.com/geopandas/xyzservices/actions/workflows/tests.yaml/badge.svg)](https://github.com/geopandas/xyzservices/actions/workflows/tests.yaml) [![codecov](https://codecov.io/gh/geopandas/xyzservices/branch/main/graph/badge.svg?token=PBSZQA48GY)](https://codecov.io/gh/geopandas/xyzservices) [![PyPi](https://img.shields.io/pypi/v/xyzservices.svg)](https://pypi.python.org/pypi/xyzservices) 12 | 13 | ## Quick Start 14 | 15 | Using `xyzservices` is simple and in most cases does not involve more than a line of 16 | code. 17 | 18 | ### Installation 19 | 20 | You can install `xyzservices` from `conda` or `pip`: 21 | 22 | ```shell 23 | conda install xyzservices -c conda-forge 24 | ``` 25 | 26 | ```shell 27 | pip install xyzservices 28 | ``` 29 | 30 | The package does not depend on any other apart from those built-in in Python. 31 | 32 | ### Providers API 33 | 34 | The key part of `xyzservices` are providers: 35 | 36 | ```py 37 | >>> import xyzservices.providers as xyz 38 | ``` 39 | 40 | `xyzservices.providers` or just `xyz` for short is a `Bunch` of providers, an enhanced 41 | `dict`. If you are in Jupyter-like environment, `xyz` will offer collapsible inventory 42 | of available XYZ tile sources. You can also explore it as a standard `dict` using 43 | `xyz.keys()`. Once you have picked your provider, you get its details as a 44 | `TileProvider` object with all the details you may need: 45 | 46 | ```py 47 | >>> xyz.CartoDB.Positron.url 48 | 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png' 49 | 50 | >>> xyz.CartoDB.Positron.attribution 51 | '(C) OpenStreetMap contributors (C) CARTO' 52 | ``` 53 | 54 | You can also check if the `TileProvider` needs API token and pass it to the object if 55 | needed. 56 | 57 | ```py 58 | >>> xyz.MapBox.requires_token() 59 | True 60 | 61 | >>> xyz.MapBox["accessToken"] = "my_personal_token" 62 | >>> xyz.MapBox.requires_token() 63 | False 64 | ``` 65 | 66 | ### Providers JSON 67 | 68 | After the installation, you will find the JSON used as a database of providers in 69 | `share/xyzservices/providers.json` if you want to use it outside of a Python ecosystem. 70 | 71 | ## Contributors 72 | 73 | `xyzservices` is developed by a community of enthusiastic volunteers and lives under 74 | [`geopandas`](https://github.com/geopandas) GitHub organization. You can see a full list 75 | of contributors [here](https://github.com/geopandas/xyzservices/graphs/contributors). 76 | 77 | The main group of providers is retrieved from the [`leaflet-providers` 78 | project](https://github.com/leaflet-extras/leaflet-providers) that contains both openly 79 | accessible providers as well as those requiring registration. All of them are considered 80 | [free](https://github.com/leaflet-extras/leaflet-providers/blob/master/README.md#what-do-we-mean-by-free). 81 | 82 | If you would like to contribute to the project, have a look at the list of 83 | [open issues](https://github.com/geopandas/contextily/issues), particularly those labeled as 84 | [good first issue](https://github.com/geopandas/xyzservices/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). 85 | 86 | ## License 87 | 88 | BSD 3-Clause License 89 | 90 | Resources coming from the [`leaflet-providers` 91 | project](https://github.com/leaflet-extras/leaflet-providers) are licensed under BSD 92 | 2-Clause License (© 2013 Leaflet Providers) 93 | -------------------------------------------------------------------------------- /ci/latest.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python 6 | - mercantile 7 | - requests 8 | # tests 9 | - pytest 10 | - pytest-cov 11 | - pytest-xdist -------------------------------------------------------------------------------- /ci/update_providers.yaml: -------------------------------------------------------------------------------- 1 | name: update_providers 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python 6 | # tests 7 | - pytest 8 | - pytest-cov 9 | - selenium==4.10 10 | - geckodriver 11 | - firefox 12 | - gitpython 13 | - html2text 14 | - xmltodict 15 | - requests 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 90% # the required coverage value 6 | threshold: 0.2% # the leniency in hitting the target -------------------------------------------------------------------------------- /doc/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 = source 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 | -------------------------------------------------------------------------------- /doc/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=source 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 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | myst-nb 2 | numpydoc 3 | sphinx 4 | sphinx-copybutton 5 | furo 6 | folium -------------------------------------------------------------------------------- /doc/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | span#release { 2 | font-size: x-small; 3 | } 4 | 5 | .main-container { 6 | position: relative; 7 | margin-left: auto; 8 | margin-right: auto; 9 | margin-top: 50px; 10 | margin-bottom: 10px; 11 | } 12 | 13 | .table-container { 14 | font-size: 18px; 15 | width: 100%; 16 | margin-top: 30px; 17 | margin-bottom: 30px; 18 | background-color: rgba(128, 128, 128, 0.1); 19 | } 20 | 21 | .map-container { 22 | height: 250px; 23 | margin-left: auto; 24 | margin-right: auto; 25 | margin-top: 20px; 26 | margin-bottom: 20px; 27 | padding: 20px; 28 | } 29 | 30 | .key-container { 31 | font-size: 16px; 32 | } 33 | 34 | .key-cell { 35 | font-size: 16px; 36 | line-height: 22px; 37 | vertical-align: top; 38 | width: 200px; 39 | color: rgba(128, 128, 128, 1); 40 | align-items: top; 41 | } 42 | 43 | .val-cell { 44 | font-size: 16px; 45 | width: 200px; 46 | margin-right: 50px; 47 | line-height: 22px; 48 | } -------------------------------------------------------------------------------- /doc/source/_static/generate_gallery.js: -------------------------------------------------------------------------------- 1 | function getBaseMapName(data) { 2 | var name = data["name"]; 3 | if (name.includes(".")) { 4 | var basemap = name.split(".")[0]; 5 | } else { 6 | var basemap = name; 7 | } 8 | return basemap; 9 | } 10 | 11 | var accessData = { 12 | // contains the keys for basemaps that need some identification data (apikey, access token or ID code) 13 | Thunderforest: { 14 | keyString: "apikey", 15 | idString: "", 16 | name: "Thunderforest", 17 | }, 18 | OpenWeatherMap: { 19 | keyString: "apiKey", 20 | idString: "", 21 | name: "OpenWeatherMap", 22 | }, 23 | MapTiler: { 24 | keyString: "key", 25 | idString: "", 26 | name: "MapTiler", 27 | }, 28 | MapBox: { 29 | keyString: "accessToken", 30 | idString: "", 31 | name: "MapBox", 32 | }, 33 | Jawg: { 34 | keyString: "accessToken", 35 | idString: "", 36 | name: "Jawg", 37 | }, 38 | TomTom: { 39 | keyString: "apikey", 40 | idString: "", 41 | name: "TomTom", 42 | }, 43 | HERE: { 44 | keyString: "app_code", 45 | idString: "app_id", 46 | name: "HERE", 47 | }, 48 | HEREv3: { 49 | keyString: "apiKey", 50 | idString: "", 51 | name: "HEREv3", 52 | }, 53 | AzureMaps: { 54 | keyString: "subscriptionKey", 55 | idString: "", 56 | name: "AzureMaps", 57 | }, 58 | OrdnanceSurvey: { 59 | keyString: "key", 60 | idString: "", 61 | name: "OrdnanceSurvey", 62 | }, 63 | }; 64 | 65 | function initMap(el, data, accessData) { 66 | basemap = getBaseMapName(data); 67 | 68 | const mainContainer = document.createElement("div"); 69 | mainContainer.className = "main-container"; 70 | el.append(mainContainer); 71 | 72 | var titleDiv = document.createElement("h2"); 73 | titleDiv.className = "title-container"; 74 | titleDiv.textContent = data["name"]; 75 | mainContainer.append(titleDiv); 76 | 77 | var mapContainer = document.createElement("div"); 78 | mapContainer.className = "map-container"; 79 | mainContainer.append(mapContainer); 80 | 81 | key = Object.keys(data); 82 | val = Object.values(data); 83 | nbOfRows = Object.keys(data).length; 84 | var latitude = 0; 85 | var longitude = 0; 86 | var zoom = 1; 87 | 88 | try { 89 | //---------------------Basemaps with specific locations ------------------------------- 90 | //----and zooms to optimize the map views restricted to given geographic areas--------- 91 | if ( 92 | data["name"] === "Esri.ArcticOceanReference" || 93 | data["name"] === "Esri.ArcticOceanBase" 94 | ) { 95 | latitude = 65.421505; // Artic ocean 96 | longitude = -70.965421; 97 | zoom = 1; 98 | } else if (data["name"] === "Esri.AntarcticBasemap") { 99 | latitude = 82.8628; // Antarctic ocean 100 | longitude = 135.0; 101 | zoom = 6; 102 | } else if (basemap == 'GeoportailFrance') { 103 | latitude = 46.749998; 104 | longitude = 1.85; 105 | zoom = 6 106 | } else if (basemap === "OpenFireMap" || basemap === "OpenSeaMap") { 107 | latitude = 50.1109; // Frankfurt 108 | longitude = 8.6821; 109 | zoom = 14; 110 | } else if (basemap === "OpenAIP") { 111 | latitude = 50.1109; // Frankfurt 112 | longitude = 8.6821; 113 | zoom = 9; 114 | } else if (basemap === "Stamen") { 115 | latitude = 32.7766642; // Dallas 116 | longitude = -96.7969879; 117 | zoom = 6; 118 | } else if (basemap === "FreeMapSK") { 119 | latitude = 48.736277; // Banská Bystrica Slovaky 120 | longitude = 19.146192; 121 | zoom = 14; 122 | } else if (basemap === "JusticeMap") { 123 | latitude = 39.7392358; // Denver 124 | longitude = -104.990251; 125 | zoom = 3; 126 | } else if ( 127 | basemap === "OpenWeatherMap" || 128 | basemap === "Esri" || 129 | basemap === "USGS" || 130 | basemap === "WaymarkedTrails" 131 | ) { 132 | latitude = 32.7766642; // Dallas 133 | longitude = -96.7969879; 134 | zoom = 4; 135 | } else if (basemap === "BasemapAT") { 136 | latitude = 47.5652; // Liezen 137 | longitude = 14.2424; 138 | zoom = 14; 139 | } else if (basemap === "nlmaps") { 140 | latitude = 52.370216; // Amsterdam 141 | longitude = 4.895168; 142 | zoom = 14; 143 | } else if (basemap === "NLS" || basemap === "OrdnanceSurvey") { 144 | latitude = 53.381129; // Sheffield 145 | longitude = -1.470085; 146 | zoom = 12; 147 | } else if (basemap === "OneMapSG") { 148 | latitude = 1.352083; // Singapore 149 | longitude = 103.819836; 150 | zoom = 14; 151 | } else if (basemap === "SwissFederalGeoportal") { 152 | latitude = 46.5196535; // Lausanne 153 | longitude = 6.6322734; 154 | zoom = 10; 155 | } else if (basemap === "OpenSnowMap") { 156 | latitude = 45.923697; // Chamonix 157 | longitude = 6.869433; 158 | zoom = 14; 159 | } else if (basemap === "Gaode") { 160 | latitude = 39.904211; // Pekin 161 | longitude = 116.407395; 162 | zoom = 14; 163 | } else if (basemap === "NASAGIBS" || basemap === "Strava") { 164 | latitude = 48.856614; // Paris 165 | longitude = 2.3522219; 166 | zoom = 4; 167 | } else { 168 | latitude = 48.856614; // Paris 169 | longitude = 2.3522219; 170 | zoom = 14; 171 | } 172 | 173 | var sampleMap = L.map(mapContainer, { attributionControl: true }).setView( 174 | [latitude, longitude], 175 | zoom 176 | ); 177 | 178 | // Case with no apikey 179 | if (accessData[basemap] === undefined) { 180 | L.tileLayer(data["url"], data).addTo(sampleMap); 181 | tbl1 = document.createElement("table"); 182 | tbl1.className = "table-container"; 183 | 184 | for (let i = 0; i < nbOfRows; i++) { 185 | const tr1 = tbl1.insertRow(); 186 | tr1.className = "line-container"; 187 | for (let j = 0; j < 2; j++) { 188 | if (i === nbOfRows - 2 && j === 2) { 189 | break; 190 | } else { 191 | const td1 = tr1.insertCell(); 192 | if (j == 0) { 193 | // First column of the table : the one with the keys 194 | td1.className = "key-cell"; 195 | td1.textContent = key[i]; 196 | } else { 197 | // Second column of the table : the one with the values of the metadata 198 | td1.className = "val-cell"; 199 | td1.textContent = val[i]; 200 | } 201 | } 202 | } 203 | } 204 | mainContainer.appendChild(tbl1); 205 | } else { 206 | // Case with apikey 207 | var dict = accessData[basemap]; 208 | var keyString = dict["keyString"]; 209 | 210 | tbl2 = document.createElement("table"); 211 | tbl2.className = "table-container"; 212 | for (let i = 0; i < nbOfRows; i++) { 213 | const tr2 = tbl2.insertRow(); 214 | tr2.className = "line-container"; 215 | 216 | for (let j = 0; j < 2; j++) { 217 | if (i === nbOfRows - 2 && j === 2) { 218 | break; 219 | } else { 220 | const td2 = tr2.insertCell(); 221 | 222 | if (j == 0) { 223 | // First column of the table containing the keys of the metadata 224 | td2.className = "key-cell"; 225 | td2.textContent = key[i]; 226 | } else { 227 | // Second column of the table containing the values of the metadata 228 | td2.className = "val-cell"; 229 | 230 | // create a single input and a button with onclick function for apikey 231 | if (key[i] === keyString) { 232 | var keyInput = document.createElement("input"); 233 | keyInput.type = "password"; 234 | keyInput.placeholder = "Enter your API key please"; 235 | keyInput.className = "key-container"; 236 | td2.append(keyInput); 237 | 238 | var validationButton = document.createElement("button"); 239 | validationButton.className = "button-container"; 240 | td2.append(validationButton); 241 | validationButton.innerHTML = "validate"; 242 | validationButton.onclick = get_keyCode; 243 | 244 | function get_keyCode() { 245 | val[i] = keyInput.value; 246 | data[keyString] = keyInput.value; 247 | L.tileLayer(data["url"], data).addTo(sampleMap); 248 | } 249 | } else { 250 | td2.textContent = val[i]; 251 | } 252 | } 253 | } 254 | } 255 | } 256 | mainContainer.appendChild(tbl2); 257 | } 258 | } catch {} 259 | } 260 | 261 | function initLeafletGallery(el) { 262 | fetch('_static/providers.json') 263 | .then(response => response.json()) 264 | .then(data => { 265 | var dataList = []; 266 | for ([key, val] of Object.entries(data)) { 267 | if (val["url"] === undefined) { 268 | // check if url is a key of the JSON object, if not go one level deeper and define the val as the new object 269 | newData = val; 270 | 271 | for ([newKey, newVal] of Object.entries(newData)) { 272 | /*if (newVal["bounds"] !== undefined) { 273 | newVal["bounds"] = undefined; 274 | }*/ 275 | dataList.push(newVal); 276 | } 277 | } else { 278 | dataList.push(val); 279 | } 280 | } 281 | dataList.forEach((baseMapData) => { 282 | initMap(el, baseMapData, accessData); 283 | }); 284 | }); 285 | } -------------------------------------------------------------------------------- /doc/source/_static/xyzmaps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/xyzservices/3ba73ba29be2f892fb2405c4662a09165e838a9b/doc/source/_static/xyzmaps.jpg -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _reference: 2 | 3 | 4 | API reference 5 | ============= 6 | 7 | Python API 8 | ---------- 9 | 10 | .. currentmodule:: xyzservices 11 | 12 | .. autoclass:: TileProvider 13 | :members: build_url, requires_token, from_qms, 14 | 15 | .. autoclass:: Bunch 16 | :exclude-members: clear, copy, fromkeys, get, items, keys, pop, popitem, setdefault, update, values 17 | :members: filter, flatten, query_name 18 | 19 | Providers JSON 20 | -------------- 21 | 22 | After the installation, you will find the JSON used as a database of providers in 23 | ``share/xyzservices/providers.json`` if you want to use it outside of a Python ecosystem. 24 | The JSON is structured along the following model example: 25 | 26 | .. code-block:: json 27 | 28 | { 29 | "single_provider_name": { 30 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 31 | "max_zoom": 19, 32 | "attribution": "(C) OpenStreetMap contributors", 33 | "html_attribution": "© OpenStreetMap contributors", 34 | "name": "OpenStreetMap.Mapnik" 35 | }, 36 | "provider_bunch_name": { 37 | "first_provider_name": { 38 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 39 | "max_zoom": 19, 40 | "attribution": "(C) OpenStreetMap contributors", 41 | "html_attribution": "© OpenStreetMap contributors", 42 | "name": "OpenStreetMap.Mapnik" 43 | }, 44 | "second_provider_name": { 45 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?access-token={accessToken}", 46 | "max_zoom": 19, 47 | "attribution": "(C) OpenStreetMap contributors", 48 | "html_attribution": "© OpenStreetMap contributors", 49 | "name": "OpenStreetMap.Mapnik", 50 | "accessToken": "" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /doc/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG.md -------------------------------------------------------------------------------- /doc/source/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 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import shutil 15 | import sys 16 | from pathlib import Path 17 | sys.path.insert(0, os.path.abspath("../..")) 18 | import xyzservices # noqa 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "xyzservices" 23 | copyright = "2021, Martin Fleischmann, Dani Arribas-Bel" 24 | author = "Martin Fleischmann, Dani Arribas-Bel" 25 | 26 | version = xyzservices.__version__ 27 | # The full version, including alpha/beta/rc tags 28 | release = version 29 | 30 | html_title = f'xyzservices {release}' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | "sphinx.ext.autodoc", 40 | "numpydoc", 41 | "sphinx.ext.autosummary", 42 | "myst_nb", 43 | "sphinx_copybutton", 44 | ] 45 | 46 | jupyter_execute_notebooks = "force" 47 | autosummary_generate = True 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ["_templates"] 51 | 52 | # List of patterns, relative to source directory, that match files and 53 | # directories to ignore when looking for source files. 54 | # This pattern also affects html_static_path and html_extra_path. 55 | exclude_patterns = [] 56 | 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | html_theme = "furo" 64 | 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_static_path = ["_static"] 69 | 70 | html_css_files = [ 71 | "custom.css", 72 | ] 73 | # html_sidebars = { 74 | # "**": ["docs-sidebar.html"], 75 | # } 76 | # html_logo = "_static/logo.svg" 77 | 78 | p = Path().absolute() 79 | shutil.copy(p.parents[1] / "xyzservices" / "data" / "providers.json", p / "_static") 80 | -------------------------------------------------------------------------------- /doc/source/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to `xyzservices` 2 | 3 | Contributions to `xyzservices` are very welcome. They are likely to be accepted more 4 | quickly if they follow these guidelines. 5 | 6 | There are two main groups of contributions - adding new provider sources and 7 | contributions to the codebase and documentation. 8 | 9 | ## Providers 10 | 11 | If you want to add a new provider, simply add its details to 12 | `provider_sources/xyzservices-providers.json`. 13 | 14 | You can add a single `TileProvider` or a `Bunch` of `TileProviders`. Use the following 15 | schema to add a single provider: 16 | 17 | ```json 18 | { 19 | "single_provider_name": { 20 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 21 | "max_zoom": 19, 22 | "attribution": "(C) OpenStreetMap contributors", 23 | "html_attribution": "© OpenStreetMap contributors", 24 | "name": "OpenStreetMap.Mapnik" 25 | }, 26 | } 27 | ``` 28 | 29 | If you want to add a bunch of related providers (different versions from a single source 30 | like `Stamen.Toner` and `Stamen.TonerLite`), you can group then within a `Bunch` using 31 | the following schema: 32 | 33 | ```json 34 | { 35 | "provider_bunch_name": { 36 | "first_provider_name": { 37 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 38 | "max_zoom": 19, 39 | "attribution": "(C) OpenStreetMap contributors", 40 | "html_attribution": "© OpenStreetMap contributors", 41 | "name": "OpenStreetMap.Mapnik" 42 | }, 43 | "second_provider_name": { 44 | "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?access-token={accessToken}", 45 | "max_zoom": 19, 46 | "attribution": "(C) OpenStreetMap contributors", 47 | "html_attribution": "© OpenStreetMap contributors", 48 | "name": "OpenStreetMap.Mapnik", 49 | "accessToken": "" 50 | } 51 | }, 52 | } 53 | ``` 54 | 55 | It is mandatory to always specify at least `name`, `url`, and `attribution`. 56 | Don't forget to add any other custom attribute 57 | required by the provider. When specifying a placeholder for the access token, please use 58 | the `""` string to ensure that `requires_token()` method 59 | works properly. You can also specify the extent of the tile coverage using the `bounds` 60 | keyword and the format `[[lat_min, lon_min], [lat_max, lon_max]]`. See the example for the area 61 | surrounding Switzerland: 62 | 63 | ```json 64 | { 65 | "bounds": [ 66 | [ 67 | 45, 68 | 5 69 | ], 70 | [ 71 | 48, 72 | 11 73 | ] 74 | ], 75 | } 76 | ``` 77 | 78 | Once updated, you can (optionally) compress the provider sources by executing `make compress` from the 79 | repository root. 80 | 81 | ```bash 82 | cd xyzservices 83 | make compress 84 | ``` 85 | 86 | ## Code and documentation 87 | 88 | At this stage of `xyzservices` development, the priorities are to define a simple, 89 | usable, and stable API and to have clean, maintainable, readable code. 90 | 91 | In general, `xyzservices` follows the conventions of the GeoPandas project where 92 | applicable. 93 | 94 | In particular, when submitting a pull request: 95 | 96 | - All existing tests should pass. Please make sure that the test suite passes, both 97 | locally and on GitHub Actions. Status on GHA will be visible on a pull request. GHA 98 | are automatically enabled on your own fork as well. To trigger a check, make a PR to 99 | your own fork. 100 | - Ensure that documentation has built correctly. It will be automatically built for each 101 | PR. 102 | - New functionality should include tests. Please write reasonable tests for your code 103 | and make sure that they pass on your pull request. 104 | - Classes, methods, functions, etc. should have docstrings and type hints. The first 105 | line of a docstring should be a standalone summary. Parameters and return values 106 | should be documented explicitly. 107 | - Follow PEP 8 when possible. We use Black and Flake8 to ensure a consistent code format 108 | throughout the project. For more details see the [GeoPandas contributing 109 | guide](https://geopandas.readthedocs.io/en/latest/community/contributing.html). 110 | - Imports should be grouped with standard library imports first, 3rd-party libraries 111 | next, and `xyzservices` imports third. Within each grouping, imports should be 112 | alphabetized. Always use absolute imports when possible, and explicit relative imports 113 | for local imports when necessary in tests. 114 | - `xyzservices` supports Python 3.7+ only. When possible, do not introduce additional 115 | dependencies. If that is necessary, make sure they can be treated as optional. 116 | 117 | 118 | ## Updating sources from leaflet 119 | 120 | `leaflet-providers-parsed.json` is an automatically generated file by GHA. You can create a fresh version 121 | using `make update-leaflet` from the repository root: 122 | 123 | ```bash 124 | cd xyzservices 125 | make update-leaflet 126 | ``` 127 | 128 | Note that you will need functional installation of `selenium` with Firefox webdriver, `git` and `html2text` packages. -------------------------------------------------------------------------------- /doc/source/gallery.rst: -------------------------------------------------------------------------------- 1 | Gallery 2 | ======= 3 | 4 | This page shows the different basemaps available in xyzservices. Some providers require 5 | an API key which you need to provide yourself and then validate it using the 6 | ``validate`` button to load the tiles. Other providers (e.g. Stadia) may require 7 | white-listing of a domain which may not have been done. 8 | 9 | .. raw:: html 10 | 11 | 12 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /doc/source/index.md: -------------------------------------------------------------------------------- 1 | # xyzservices 2 | 3 | Source of XYZ tiles providers. 4 | 5 | ![Illustrative tiles. (C) OpenStreetMap, (C) OpenMapTIles, (C) Stadia Maps, (C) OpenTopoMap, (C) Thunderforest, (C) JawgMaps, (C) Stamen Design, (C) Esri -- Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community](_static/xyzmaps.jpg) 6 | 7 | `xyzservices` is a lightweight library providing a repository of available XYZ services 8 | offering raster basemap tiles. The repository is provided via Python API and as a 9 | compressed JSON file. 10 | 11 | XYZ tiles can be used as background for your maps to provide necessary spatial context. 12 | `xyzservices` offer specifications of many tile services and provide an easy-to-use 13 | tools to plug them into your work, no matter if interactive or static. 14 | 15 | [![Tests](https://github.com/geopandas/xyzservices/actions/workflows/tests.yaml/badge.svg)](https://github.com/geopandas/xyzservices/actions/workflows/tests.yaml) [![codecov](https://codecov.io/gh/geopandas/xyzservices/branch/main/graph/badge.svg?token=PBSZQA48GY)](https://codecov.io/gh/geopandas/xyzservices) 16 | 17 | ## Quick Start 18 | 19 | Using `xyzservices` is simple and in most cases does not involve more than a line of 20 | code. 21 | 22 | ### Installation 23 | 24 | You can install `xyzservices` from `conda` or `pip`: 25 | 26 | ```shell 27 | conda install xyzservices -c conda-forge 28 | ``` 29 | 30 | ```shell 31 | pip install xyzservices 32 | ``` 33 | 34 | The package does not depend on any other apart from those built-in in Python. 35 | 36 | ### Providers API 37 | 38 | The key part of `xyzservices` are providers: 39 | 40 | ```py 41 | >>> import xyzservices.providers as xyz 42 | ``` 43 | 44 | `xyzservices.providers` or just `xyz` for short is a `Bunch` of providers, an enhanced 45 | `dict`. If you are in Jupyter-like environment, `xyz` will offer collapsible inventory 46 | of available XYZ tile sources. You can also explore it as a standard `dict` using 47 | `xyz.keys()`. Once you have picked your provider, you get its details as a 48 | `TileProvider` object with all the details you may need: 49 | 50 | ```py 51 | >>> xyz.CartoDB.Positron.url 52 | 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png' 53 | 54 | >>> xyz.CartoDB.Positron.attribution 55 | '(C) OpenStreetMap contributors (C) CARTO' 56 | ``` 57 | 58 | You can also check if the `TileProvider` needs API token and pass it to the object if 59 | needed. 60 | 61 | ```py 62 | >>> xyz.MapBox.requires_token() 63 | True 64 | 65 | >>> xyz.MapBox["accessToken"] = "my_personal_token" 66 | >>> xyz.MapBox.requires_token() 67 | False 68 | ``` 69 | 70 | ```{important} 71 | You should always check the license and terms and conditions of XYZ tiles you want to use. Not all of them can be used in all circumstances. 72 | ``` 73 | 74 | ### Providers JSON 75 | 76 | After the installation, you will find the JSON used as a database of providers in 77 | `share/xyzservices/providers.json` if you want to use it outside of a Python ecosystem. 78 | 79 | ## Contributors 80 | 81 | `xyzservices` is developed by a community of enthusiastic volunteers and lives under 82 | [`geopandas`](https://github.com/geopandas) GitHub organization. You can see a full list 83 | of contributors [here](https://github.com/geopandas/xyzservices/graphs/contributors). 84 | 85 | The main group of providers is retrieved from the [`leaflet-providers` 86 | project](https://github.com/leaflet-extras/leaflet-providers) that contains both openly 87 | accessible providers as well as those requiring registration. All of them are considered 88 | [free](https://github.com/leaflet-extras/leaflet-providers/blob/master/README.md#what-do-we-mean-by-free). 89 | 90 | If you would like to contribute to the project, have a look at the list of 91 | [open issues](https://github.com/geopandas/contextily/issues), particularly those labeled as 92 | [good first issue](https://github.com/geopandas/xyzservices/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). 93 | 94 | ## License 95 | 96 | BSD 3-Clause License 97 | 98 | Resources coming from the [`leaflet-providers` 99 | project](https://github.com/leaflet-extras/leaflet-providers) are licensed under BSD 100 | 2-Clause License (© 2013 Leaflet Providers) 101 | 102 | 103 | ```{toctree} 104 | --- 105 | maxdepth: 2 106 | caption: Documentation 107 | hidden: true 108 | --- 109 | introduction 110 | registration 111 | api 112 | gallery 113 | contributing 114 | changelog 115 | GitHub 116 | ``` 117 | -------------------------------------------------------------------------------- /doc/source/introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "# A full look into `providers` objects" 7 | ], 8 | "metadata": {} 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "source": [ 14 | "import xyzservices.providers as xyz" 15 | ], 16 | "outputs": [], 17 | "metadata": {} 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "source": [ 22 | "## What is this \"provider\" object ?" 23 | ], 24 | "metadata": {} 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "source": [ 29 | "The provider objects are stored as dictionaries (with attribute access for easy access), so we can explore them in Python (or using tab completion in an interactive session or as a collapsible inventory in Jupyter!):" 30 | ], 31 | "metadata": {} 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "source": [ 37 | "xyz" 38 | ], 39 | "outputs": [], 40 | "metadata": {} 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "source": [ 45 | "Most of those providers have themselves multiple options. For example, OpenStreetMap provides multiple background styles:" 46 | ], 47 | "metadata": {} 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "source": [ 53 | "xyz.OpenStreetMap" 54 | ], 55 | "outputs": [], 56 | "metadata": {} 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "source": [ 61 | "Looking at a single one of those options, for example \"Mapnik\" (the default OpenStreetMap background), we can see this is a `TileProvider` object, which behaves as a dict:" 62 | ], 63 | "metadata": {} 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "source": [ 69 | "type(xyz.OpenStreetMap.Mapnik)" 70 | ], 71 | "outputs": [], 72 | "metadata": {} 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "source": [ 77 | "We can explore its contents:" 78 | ], 79 | "metadata": {} 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "source": [ 85 | "xyz.OpenStreetMap.Mapnik" 86 | ], 87 | "outputs": [], 88 | "metadata": {} 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "source": [ 93 | "A \"provider object\" is then a dictionary with a few required entries, such as the url (required, with `{z}/{x}/{y}` to be replaced), with some additional metadata information (optional). These can be the attribution text to be used on the plot (`attribution`), or the maximum zoom level available for these tiles (`max_zoom`). When this information is available, other libraries like `geopandas` or `contextily` may be smart enough to access and use it automatically." 94 | ], 95 | "metadata": {} 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "source": [ 100 | "## Which providers are available by default?" 101 | ], 102 | "metadata": {} 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "source": [ 107 | "The providers definitions that are shipped with `xyzservices` in the `xyzservices.providers` submodule, are coming from the [`leaflet-providers`](https://github.com/leaflet-extras/leaflet-providers) package, an extension to javascript mapping library Leaflet that contains configurations for various free tile providers. Thus, all providers that are listed on their preview [http://leaflet-extras.github.io/leaflet-providers/preview/](http://leaflet-extras.github.io/leaflet-providers/preview/) should also be available in `xyzservices`. On top of that, `xyzservices` can have a few more as a bonus.\n", 108 | "\n", 109 | "```{important}\n", 110 | "You should always check the license and terms and conditions of XYZ tiles you want to use. Not all of them can be used in all circumstances.\n", 111 | "```" 112 | ], 113 | "metadata": {} 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "source": [ 118 | "## Specifying options for a provider" 119 | ], 120 | "metadata": {} 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "source": [ 125 | "Some providers require additional information, such as an API access key. This can be specified by calling the provider object. Any keyword specified will override the default value in the provider object.\n", 126 | "\n", 127 | "For example, the OpenWeatherMap requires an API key:" 128 | ], 129 | "metadata": {} 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "source": [ 135 | "xyz.OpenWeatherMap.Clouds" 136 | ], 137 | "outputs": [], 138 | "metadata": {} 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "source": [ 144 | "xyz.OpenWeatherMap.Clouds.requires_token()" 145 | ], 146 | "outputs": [], 147 | "metadata": {} 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "source": [ 152 | "We can specify this API key by calling the object or overriding the attribute:" 153 | ], 154 | "metadata": {} 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "source": [ 160 | "# Overriding the attribute will alter existing object\n", 161 | "xyz.OpenWeatherMap.Clouds[\"apiKey\"] = \"my-private-api-key\"\n", 162 | "\n", 163 | "# Calling the object will return a copy\n", 164 | "xyz.OpenWeatherMap.Clouds(apiKey=\"my-private-api-key\")" 165 | ], 166 | "outputs": [], 167 | "metadata": {} 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "source": [ 172 | "This can then be specified where a key or token is expected. For example:\n", 173 | " \n", 174 | "```python\n", 175 | "contextily.add_basemap(ax, source=xyz.OpenWeatherMap.Clouds(apiKey=\"my-private-api-key\"))\n", 176 | "```\n", 177 | "\n", 178 | "```{note}\n", 179 | "It may occasionally happen that the `TileProvider` is broken. In such a case, it will have additional attribute `status` with a `\"broken\"` flag. If you think that the `TileProvider` is broken but it is not flagged as such, please [report it](https://github.com/geopandas/xyzservices/issues).\n", 180 | "```\n" 181 | ], 182 | "metadata": {} 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "source": [ 187 | "## Building the tile URL\n", 188 | "\n", 189 | "Some packages like `contextily` can use `xyzservices.TileProvider` directly and parse the required information under the hood. Others need and explicit URL string, attribution and other attributes. However, not every package need the URL in the same format. `xyzsevices` can build the URL in most of the required formats, with or without placeholders.\n", 190 | "\n", 191 | "Typical use case would be a folium map, which needs an URL and an attribution with `{x}`, `{y}` and `{z}` placeholders but already filled API keys if needed. You can get the most standard URL in the `'https://myserver.com/tiles/{z}/{x}/{y}.png'` format via `TileProvider.build_url()`. And if you want a sharper resolution (when supported by the provider), you can pass an optional `scale_factor` attribute." 192 | ], 193 | "metadata": {} 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": null, 198 | "source": [ 199 | "import folium\n", 200 | "\n", 201 | "tiles = xyz.CartoDB.Positron\n", 202 | "\n", 203 | "folium.Map(\n", 204 | " location=[53.4108, -2.9358],\n", 205 | " tiles=tiles.build_url(scale_factor=\"@2x\"),\n", 206 | " attr=tiles.html_attribution,\n", 207 | ")" 208 | ], 209 | "outputs": [], 210 | "metadata": {} 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "source": [ 215 | "If you need an URL of a single tile, you can pass `x`, `y` and `z` directly, alongside other required attributes like `accessToken`." 216 | ], 217 | "metadata": {} 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": null, 222 | "source": [ 223 | "xyz.OpenWeatherMap.Clouds.build_url(x=12, y=21, z=15, apiKey=\"my-private-api-key\")" 224 | ], 225 | "outputs": [], 226 | "metadata": {} 227 | }, 228 | { 229 | "cell_type": "markdown", 230 | "source": [ 231 | "## Overview of built-in providers" 232 | ], 233 | "metadata": {} 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "source": [ 238 | "Let us create a flat dictionary of the built-in providers:" 239 | ], 240 | "metadata": {} 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "source": [ 246 | "providers = xyz.flatten()" 247 | ], 248 | "outputs": [], 249 | "metadata": {} 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "source": [ 254 | "This results in quite a few of them already available:" 255 | ], 256 | "metadata": {} 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": null, 261 | "source": [ 262 | "len(providers)" 263 | ], 264 | "outputs": [], 265 | "metadata": {} 266 | }, 267 | { 268 | "cell_type": "markdown", 269 | "source": [ 270 | "For this illustration, let's single out the following ones and use `folium` to have a look at them:" 271 | ], 272 | "metadata": {} 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "source": [ 278 | "selection = ['OpenStreetMap.Mapnik', \n", 279 | " 'OpenTopoMap', \n", 280 | " 'Stamen.Toner', \n", 281 | " 'Stamen.TonerLite', \n", 282 | " 'Stamen.Terrain', \n", 283 | " 'Stamen.TerrainBackground', \n", 284 | " 'Stamen.Watercolor', \n", 285 | " 'NASAGIBS.ViirsEarthAtNight2012', \n", 286 | " 'CartoDB.Positron', \n", 287 | " 'CartoDB.Voyager'\n", 288 | " ]" 289 | ], 290 | "outputs": [], 291 | "metadata": {} 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "source": [ 296 | "You first create a `Map` instance, then looping through the `selection` add all selected base maps as layers. You can switch between them using the layer control in the top-right corner." 297 | ], 298 | "metadata": {} 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": 24, 303 | "source": [ 304 | "m = folium.Map(location=[53.4108, -2.9358], zoom_start=8)\n", 305 | "\n", 306 | "for tiles_name in selection:\n", 307 | " tiles = providers[tiles_name]\n", 308 | " folium.TileLayer(\n", 309 | " tiles=tiles.build_url(),\n", 310 | " attr=tiles.html_attribution,\n", 311 | " name=tiles.name,\n", 312 | " ).add_to(m)\n", 313 | "\n", 314 | "folium.LayerControl().add_to(m)\n", 315 | "\n", 316 | "m" 317 | ], 318 | "outputs": [ 319 | { 320 | "output_type": "execute_result", 321 | "data": { 322 | "text/plain": [ 323 | "" 324 | ], 325 | "text/html": [ 326 | "
Make this Notebook Trusted to load map: File -> Trust Notebook
" 327 | ] 328 | }, 329 | "metadata": {}, 330 | "execution_count": 24 331 | } 332 | ], 333 | "metadata": {} 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "source": [ 338 | "## Loading providers from [Quick Map Services](https://qms.nextgis.com/)\n", 339 | "\n", 340 | "Even though `xyzservices` comes with a large number of built-in providers, you can load even more directly from [Quick Map Services](https://qms.nextgis.com/) using the `TileProvider.from_qms()` method. Note that the name needs to match the name in the QMS database." 341 | ], 342 | "metadata": {} 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": 25, 347 | "source": [ 348 | "from xyzservices import TileProvider\n", 349 | "\n", 350 | "qms_provider = TileProvider.from_qms(\"OpenTopoMap\")\n", 351 | "qms_provider" 352 | ], 353 | "outputs": [ 354 | { 355 | "output_type": "execute_result", 356 | "data": { 357 | "text/plain": [ 358 | "{'name': 'OpenTopoMap',\n", 359 | " 'url': 'https://tile.opentopomap.org/{z}/{x}/{y}.png',\n", 360 | " 'min_zoom': 0,\n", 361 | " 'max_zoom': 19,\n", 362 | " 'attribution': 'OpenTopoMap (CC-BY-SA)'}" 363 | ], 364 | "text/html": [ 365 | "\n", 366 | "
\n", 367 | " \n", 484 | "
\n", 485 | "
\n", 486 | "
xyzservices.TileProvider
\n", 487 | "
OpenTopoMap
\n", 488 | "
\n", 489 | "
\n", 490 | "
\n", 491 | "
url
https://tile.opentopomap.org/{z}/{x}/{y}.png
min_zoom
0
max_zoom
19
attribution
OpenTopoMap (CC-BY-SA)
\n", 492 | "
\n", 493 | "
\n", 494 | "
\n", 495 | "
\n", 496 | " " 497 | ] 498 | }, 499 | "metadata": {}, 500 | "execution_count": 25 501 | } 502 | ], 503 | "metadata": {} 504 | }, 505 | { 506 | "cell_type": "markdown", 507 | "source": [ 508 | "```{important}\n", 509 | "You should always check the license and terms and conditions of XYZ tiles you want to use. Not all of them can be used in all circumstances and not all Quick Map Services can be considered free even though they may work.\n", 510 | "```" 511 | ], 512 | "metadata": {} 513 | } 514 | ], 515 | "metadata": { 516 | "interpreter": { 517 | "hash": "9914e2881520d4f08a067c2c2c181121476026b863eca2e121cd0758701ab602" 518 | }, 519 | "kernelspec": { 520 | "name": "python3", 521 | "display_name": "Python 3.9.2 64-bit ('geo_dev': conda)" 522 | }, 523 | "language_info": { 524 | "codemirror_mode": { 525 | "name": "ipython", 526 | "version": 3 527 | }, 528 | "file_extension": ".py", 529 | "mimetype": "text/x-python", 530 | "name": "python", 531 | "nbconvert_exporter": "python", 532 | "pygments_lexer": "ipython3", 533 | "version": "3.9.2" 534 | } 535 | }, 536 | "nbformat": 4, 537 | "nbformat_minor": 4 538 | } -------------------------------------------------------------------------------- /doc/source/registration.md: -------------------------------------------------------------------------------- 1 | # Providers requiring registration 2 | 3 | The main group of providers is retrieved from the [`leaflet-providers` 4 | project](https://github.com/leaflet-extras/leaflet-providers) that contains both openly 5 | accessible providers as well as those requiring registration. All of them are considered 6 | [free](https://github.com/leaflet-extras/leaflet-providers/blob/master/README.md#what-do-we-mean-by-free). 7 | 8 | Below is the (potentially incomplete) list of providers requiring registration. 9 | 10 | ```{note} 11 | This page is largely taken directly from the [`leaflet-providers` project](https://github.com/leaflet-extras/leaflet-providers/blob/master/README.md). 12 | ``` 13 | 14 | ## Esri/ArcGIS 15 | 16 | In order to use ArcGIS maps, you must 17 | [register](https://developers.arcgis.com/en/sign-up/) and abide by the [terms of 18 | service](https://developers.arcgis.com/en/terms/). No special syntax is required. 19 | 20 | ## Geoportail France 21 | 22 | In order to use Geoportail France resources, you need to obtain an [api 23 | key](http://professionnels.ign.fr/ign/contrats/) that allows you to access the 24 | [resources](https://geoservices.ign.fr/documentation/donnees-ressources-wmts.html#ressources-servies-en-wmts-en-projection-web-mercator) 25 | you need. Pass this api key to the `TileProvider`: 26 | 27 | ```py 28 | xyz.GeoportailFrance.plan(apikey="") 29 | ``` 30 | 31 | Please note that a public api key (`choisirgeoportail`) is used by default and comes 32 | with no guarantee. 33 | 34 | ## HERE and HEREv3 (formerly Nokia) 35 | 36 | In order to use HEREv3 layers, you must [register](http://developer.here.com/). Once 37 | registered, you can create an `apiKey` which you have to pass to the `TileProvider`: 38 | 39 | ```py 40 | # Overriding the attribute will alter the existing object 41 | xyz.HEREv3.terrainDay["apiKey"] = "my-private-api-key" 42 | 43 | # Calling the object will return a copy 44 | xyz.HEREv3.terrainDay(apiKey="my-private-api-key") 45 | ``` 46 | 47 | You can still pass `app_id` and `app_code` in legacy projects: 48 | 49 | ```py 50 | xyz.HERE.terrainDay(app_id="my-private-app-id", app_code="my-app-code") 51 | ``` 52 | 53 | ## Jawg Maps 54 | 55 | In order to use Jawg Maps, you must [register](https://www.jawg.io/lab). Once 56 | registered, your access token will be located 57 | [here](https://www.jawg.io/lab/access-tokens) and you will access to all Jawg default 58 | maps (variants) and your own customized maps: 59 | 60 | ```py 61 | xyz.Jawg.Streets( 62 | accessToken="", 63 | variant="" 64 | ) 65 | ``` 66 | 67 | ## Mapbox 68 | 69 | In order to use Mapbox maps, you must [register](https://tiles.mapbox.com/signup). You 70 | can get map_ID (e.g. `"mapbox/satellite-v9"`) and `ACCESS_TOKEN` from [Mapbox 71 | projects](https://www.mapbox.com/projects): 72 | 73 | ```py 74 | xyz.MapBox(id="", accessToken="my-private-ACCESS_TOKEN") 75 | ``` 76 | 77 | The currently-valid Mapbox map styles, to use for map_IDs, [are listed in the Mapbox 78 | documentation](https://docs.mapbox.com/api/maps/#mapbox-styles) - only the final part of 79 | each is required, e.g. `"mapbox/light-v10"`. 80 | 81 | ## MapTiler Cloud 82 | 83 | In order to use MapTiler maps, you must [register](https://cloud.maptiler.com/). Once 84 | registered, get your API key from Account/Keys, which you have to pass to the 85 | `TileProvider`: 86 | 87 | ```py 88 | xyz.MapTiler.Streets(key="") 89 | ``` 90 | 91 | ## Thunderforest 92 | 93 | In order to use Thunderforest maps, you must 94 | [register](https://thunderforest.com/pricing/). Once registered, you have an `api_key` 95 | which you have to pass to the `TileProvider`: 96 | 97 | ```py 98 | xyz.Thunderforest.Landscape(apikey="") 99 | ``` 100 | 101 | ## TomTom 102 | 103 | In order to use TomTom layers, you must 104 | [register](https://developer.tomtom.com/user/register). Once registered, you can create 105 | an `apikey` which you have to pass to the `TileProvider`: 106 | 107 | ```py 108 | xyz.TomTom(apikey="") 109 | ``` 110 | 111 | ## Stadia Maps 112 | 113 | In order to use Stadia maps, you must [register](https://client.stadiamaps.com/signup/). 114 | Once registered, you can whitelist your domain within your account settings. 115 | 116 | Alternatively, you can use Stadia maps with an API token but you need to adapt a 117 | provider object to correct form. 118 | 119 | ```py 120 | provider = xyz.Stadia.AlidadeSmooth(api_key="") 121 | provider["url"] = provider["url"] + "?api_key={api_key}" # adding API key placeholder 122 | ``` 123 | 124 | ## Ordnance Survey 125 | 126 | In order to use Ordnance Survey layers, you must 127 | [register](https://osdatahub.os.uk/). Once registered, you can create 128 | a project, assign OS Maps API product to a project and retrieve the `key` which you have to pass to the `TileProvider`: 129 | 130 | ```py 131 | xyz.OrdnanceSurvey.Light(key="") 132 | ``` 133 | -------------------------------------------------------------------------------- /doc/source/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/xyzservices/3ba73ba29be2f892fb2405c4662a09165e838a9b/doc/source/tiles.png -------------------------------------------------------------------------------- /provider_sources/_compress_providers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script takes both provider sources stored in `provider_sources`, removes items 3 | which do not represent actual providers (metadata from leaflet-providers-parsed and 4 | templates from xyzservices-providers), combines them together and saves as a compressed 5 | JSON to data/providers.json. 6 | 7 | The compressed JSON is shipped with the package. 8 | """ 9 | 10 | import json 11 | import warnings 12 | from datetime import date 13 | 14 | import requests 15 | import xmltodict 16 | 17 | # list of providers known to be broken and should be marked as broken in the JSON 18 | # last update: 4 Feb 2024 19 | BROKEN_PROVIDERS = [ 20 | "JusticeMap.income", 21 | "JusticeMap.americanIndian", 22 | "JusticeMap.asian", 23 | "JusticeMap.black", 24 | "JusticeMap.hispanic", 25 | "JusticeMap.multi", 26 | "JusticeMap.nonWhite", 27 | "JusticeMap.white", 28 | "JusticeMap.plurality", 29 | "NASAGIBS.ModisTerraChlorophyll", 30 | "HEREv3.trafficFlow", 31 | "Stadia.AlidadeSatellite", 32 | ] 33 | 34 | with open("./leaflet-providers-parsed.json") as f: 35 | leaflet = json.load(f) 36 | # remove meta data 37 | leaflet.pop("_meta", None) 38 | 39 | 40 | with open("./xyzservices-providers.json") as f: 41 | xyz = json.load(f) 42 | 43 | for provider in BROKEN_PROVIDERS: 44 | provider = provider.split(".") 45 | try: 46 | if len(provider) == 1: 47 | leaflet[provider[0]]["status"] = "broken" 48 | else: 49 | leaflet[provider[0]][provider[1]]["status"] = "broken" 50 | except: 51 | warnings.warn( 52 | f"Attempt to mark {provider} as broken failed. " 53 | "The provider does not exist in leaflet-providers JSON.", 54 | UserWarning, 55 | ) 56 | 57 | 58 | # update year 59 | def update_year(provider_or_tile): 60 | if "attribution" in provider_or_tile: 61 | provider_or_tile["attribution"] = provider_or_tile["attribution"].replace( 62 | "{year}", str(date.today().year) 63 | ) 64 | provider_or_tile["html_attribution"] = provider_or_tile[ 65 | "html_attribution" 66 | ].replace("{year}", str(date.today().year)) 67 | else: 68 | for tile in provider_or_tile.values(): 69 | update_year(tile) 70 | 71 | 72 | update_year(xyz) 73 | 74 | # combine both 75 | 76 | for key, _val in xyz.items(): 77 | if key in leaflet: 78 | if any( 79 | isinstance(i, dict) for i in leaflet[key].values() 80 | ): # for related group of bunch 81 | leaflet[key].update(xyz[key]) 82 | else: 83 | leaflet[key] = xyz[key] 84 | else: 85 | leaflet[key] = xyz[key] 86 | 87 | 88 | # Add IGN WMTS services (Tile images) 89 | 90 | ign_wmts_url = ( 91 | "https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities" 92 | ) 93 | 94 | response = requests.get(ign_wmts_url) 95 | response_dict = xmltodict.parse(response.content) 96 | layers_list = response_dict["Capabilities"]["Contents"]["Layer"] # 556 layers 97 | 98 | wmts_layers_list = [] 99 | for i in range(len(layers_list)): 100 | layer = response_dict["Capabilities"]["Contents"]["Layer"][i] 101 | variant = layer.get("ows:Identifier") 102 | 103 | # Rename for better readability 104 | name = "" 105 | if "." not in variant: 106 | name = variant.lower().capitalize() 107 | else: 108 | name = variant.split(".")[0].lower().capitalize() 109 | for i in range(1, len(variant.split("."))): 110 | name = name + "_" + (variant.split(".")[i]).lower().capitalize() 111 | name = name.replace("-", "_") 112 | 113 | # Rename for better readability (Frequent cases) 114 | variant_to_name = { 115 | "CADASTRALPARCELS.PARCELLAIRE_EXPRESS": "parcels", 116 | "GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2": "plan", 117 | "ORTHOIMAGERY.ORTHOPHOTOS": "orthos" 118 | } 119 | 120 | if variant in variant_to_name: 121 | name = variant_to_name[variant] 122 | 123 | # Get layer style 124 | style = layer.get("Style") 125 | if isinstance(style, dict): 126 | style = style.get("ows:Identifier") 127 | 128 | elif isinstance(style, list): 129 | style = style[1].get("ows:Identifier") if len(style) > 1 else None 130 | else: 131 | style = "normal" 132 | 133 | # Resolution levels (pyramid) 134 | TileMatrixSet = layer["TileMatrixSetLink"]["TileMatrixSet"] 135 | 136 | # Zoom levels 137 | TileMatrixSetLimits = layer["TileMatrixSetLink"]["TileMatrixSetLimits"][ 138 | "TileMatrixLimits" 139 | ] 140 | min_zoom = int(TileMatrixSetLimits[0]["TileMatrix"]) 141 | max_zoom = int(TileMatrixSetLimits[-1]["TileMatrix"]) 142 | 143 | # Tile format 144 | output_format = layer.get("Format") # image/png... 145 | if output_format == "application/x-protobuf" or output_format == "image/x-bil;bits=32": 146 | continue 147 | 148 | # Layer extent 149 | bbox_lower_left = layer["ows:WGS84BoundingBox"][ 150 | "ows:LowerCorner" 151 | ] # given with lon/lat order 152 | bbox_upper_right = layer["ows:WGS84BoundingBox"][ 153 | "ows:UpperCorner" 154 | ] # given with lon/lat order 155 | lower_left_corner_lon, lower_left_corner_lat = bbox_lower_left.split( 156 | " " 157 | ) 158 | upper_right_corner_lon, upper_right_corner_lat = bbox_upper_right.split( 159 | " " 160 | ) 161 | bounds = [ 162 | [float(lower_left_corner_lat), float(lower_left_corner_lon)], 163 | [float(upper_right_corner_lat), float(upper_right_corner_lon)], 164 | ] 165 | 166 | wmts_layers_list.append("GeoportailFrance." + name) 167 | leaflet["GeoportailFrance"][name] = { 168 | "url": """https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&STYLE={style}&TILEMATRIXSET={TileMatrixSet}&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}""", 169 | "html_attribution": """Geoportail France""", 170 | "attribution": "Geoportail France", 171 | "bounds": bounds, 172 | "min_zoom": min_zoom, 173 | "max_zoom": max_zoom, 174 | "format": output_format, 175 | "style": style, 176 | "variant": variant, 177 | "name": "GeoportailFrance." + name, 178 | "TileMatrixSet": TileMatrixSet, 179 | "apikey": "your_api_key_here", 180 | } 181 | 182 | # Handle broken providers 183 | possibly_broken_providers = [ 184 | "Ocsge_Constructions_2002", 185 | "Ocsge_Constructions_2014", 186 | "Orthoimagery_Orthophotos_Coast2000", 187 | "Ocsge_Couverture_2002", 188 | "Ocsge_Couverture_2014", 189 | "Ocsge_Usage_2002", 190 | "Ocsge_Usage_2014", 191 | "Pcrs_Lamb93", 192 | "Geographicalgridsystems_Planignv2_L93", 193 | "Cadastralparcels_Parcellaire_express_L93", 194 | "Hr_Orthoimagery_Orthophotos_L93", 195 | "Raster_zh_centrevdl", 196 | "Raster_zh_centrevdl_et_auvergnera", 197 | "Raster_zone_humide_ara_cvdl", 198 | "Raster_zone_humide_auvergnera", 199 | ] 200 | 201 | if name in possibly_broken_providers: 202 | leaflet["GeoportailFrance"][name]["status"] = "broken" 203 | 204 | with open("../xyzservices/data/providers.json", "w") as f: 205 | json.dump(leaflet, f, indent=4) 206 | -------------------------------------------------------------------------------- /provider_sources/_parse_leaflet_providers.py: -------------------------------------------------------------------------------- 1 | """ 2 | IMPORTANT: core copied from: 3 | 4 | https://github.com/geopandas/contextily/blob/e0bb25741f9448c5b6b0e54d403b0d03d9244abd/scripts/parse_leaflet_providers.py 5 | 6 | ... 7 | 8 | Script to parse the tile providers defined by the leaflet-providers.js 9 | extension to Leaflet (https://github.com/leaflet-extras/leaflet-providers). 10 | It accesses the defined TileLayer.Providers objects through javascript 11 | using Selenium as JSON, and then processes this a fully specified 12 | javascript-independent dictionary and saves that final result as a JSON file. 13 | """ 14 | 15 | import datetime 16 | import json 17 | import os 18 | import tempfile 19 | 20 | import git 21 | import html2text 22 | import selenium.webdriver 23 | 24 | GIT_URL = "https://github.com/leaflet-extras/leaflet-providers.git" 25 | 26 | 27 | # ----------------------------------------------------------------------------- 28 | # Downloading and processing the json data 29 | 30 | 31 | def get_json_data(): 32 | with tempfile.TemporaryDirectory() as tmpdirname: 33 | repo = git.Repo.clone_from(GIT_URL, tmpdirname) 34 | commit_hexsha = repo.head.object.hexsha 35 | commit_message = repo.head.object.message 36 | 37 | index_path = "file://" + os.path.join(tmpdirname, "index.html") 38 | 39 | opts = selenium.webdriver.FirefoxOptions() 40 | opts.add_argument("--headless") 41 | 42 | driver = selenium.webdriver.Firefox(options=opts) 43 | driver.get(index_path) 44 | data = driver.execute_script( 45 | "return JSON.stringify(L.TileLayer.Provider.providers)" 46 | ) 47 | driver.close() 48 | 49 | data = json.loads(data) 50 | description = f"commit {commit_hexsha} ({commit_message.strip()})" 51 | 52 | return data, description 53 | 54 | 55 | def process_data(data): 56 | # extract attributions from raw data that later need to be substituted 57 | global ATTRIBUTIONS 58 | ATTRIBUTIONS = { 59 | "{attribution.OpenStreetMap}": data["OpenStreetMap"]["options"]["attribution"], 60 | "{attribution.Esri}": data["Esri"]["options"]["attribution"], 61 | } 62 | 63 | result = {} 64 | for provider in data: 65 | result[provider] = process_provider(data, provider) 66 | return result 67 | 68 | 69 | def process_provider(data, name="OpenStreetMap"): 70 | provider = data[name].copy() 71 | variants = provider.pop("variants", None) 72 | options = provider.pop("options") 73 | provider_keys = {**provider, **options} 74 | 75 | if variants is None: 76 | provider_keys["name"] = name 77 | provider_keys = pythonize_data(provider_keys) 78 | return provider_keys 79 | 80 | result = {} 81 | 82 | for variant in variants: 83 | var = variants[variant] 84 | if isinstance(var, str): 85 | variant_keys = {"variant": var} 86 | else: 87 | variant_keys = var.copy() 88 | variant_options = variant_keys.pop("options", {}) 89 | variant_keys = {**variant_keys, **variant_options} 90 | variant_keys = {**provider_keys, **variant_keys} 91 | variant_keys["name"] = f"{name}.{variant}" 92 | variant_keys = pythonize_data(variant_keys) 93 | result[variant] = variant_keys 94 | 95 | return result 96 | 97 | 98 | def pythonize_data(data): 99 | """ 100 | Clean-up the javascript based dictionary: 101 | - rename mixedCase keys 102 | - substitute the attribution placeholders 103 | - convert html attribution to plain text 104 | """ 105 | rename_keys = {"maxZoom": "max_zoom", "minZoom": "min_zoom"} 106 | attributions = ATTRIBUTIONS 107 | 108 | items = data.items() 109 | 110 | new_data = [] 111 | for key, value in items: 112 | if key == "attribution": 113 | if "{attribution." in value: 114 | for placeholder, attr in attributions.items(): 115 | if placeholder in value: 116 | value = value.replace(placeholder, attr) 117 | if "{attribution." not in value: 118 | # replaced last attribution 119 | break 120 | else: 121 | raise ValueError(f"Attribution not known: {value}") 122 | new_data.append(("html_attribution", value)) 123 | # convert html text to plain text 124 | converter = html2text.HTML2Text(bodywidth=1000) 125 | converter.ignore_links = True 126 | value = converter.handle(value).strip() 127 | elif key in rename_keys: 128 | key = rename_keys[key] 129 | elif key == "url" and any(k in value for k in rename_keys): 130 | # NASAGIBS providers have {maxZoom} in the url 131 | for old, new in rename_keys.items(): 132 | value = value.replace("{" + old + "}", "{" + new + "}") 133 | new_data.append((key, value)) 134 | 135 | return dict(new_data) 136 | 137 | 138 | if __name__ == "__main__": 139 | data, description = get_json_data() 140 | with open("./leaflet-providers-raw.json", "w") as f: 141 | json.dump(data, f) 142 | 143 | result = process_data(data) 144 | with open("./leaflet-providers-parsed.json", "w") as f: 145 | result["_meta"] = { 146 | "description": ( 147 | "JSON representation of the leaflet providers defined by the " 148 | "leaflet-providers.js extension to Leaflet " 149 | "(https://github.com/leaflet-extras/leaflet-providers)" 150 | ), 151 | "date_of_creation": datetime.datetime.today().strftime("%Y-%m-%d"), 152 | "commit": description, 153 | } 154 | json.dump(result, f, indent=4) 155 | -------------------------------------------------------------------------------- /provider_sources/xyzservices-providers.json: -------------------------------------------------------------------------------- 1 | { 2 | "NASAGIBS": { 3 | "ModisTerraBands721CR": { 4 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/MODIS_Terra_CorrectedReflectance_Bands721/default/{time}/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg", 5 | "max_zoom": 9, 6 | "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.", 7 | "html_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.", 8 | "name": "NASAGIBS.ModisTerraBands721CR", 9 | "time": "" 10 | }, 11 | "ModisAquaTrueColorCR": { 12 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/MODIS_Aqua_CorrectedReflectance_TrueColor/default/{time}/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg", 13 | "max_zoom": 9, 14 | "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.", 15 | "html_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.", 16 | "name": "NASAGIBS.ModisAquaTrueColorCR", 17 | "time": "" 18 | }, 19 | "ModisAquaBands721CR": { 20 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/MODIS_Aqua_CorrectedReflectance_Bands721/default/{time}/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg", 21 | "max_zoom": 9, 22 | "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.", 23 | "html_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.", 24 | "name": "NASAGIBS.ModisAquaBands721CR", 25 | "time": "" 26 | }, 27 | "ViirsTrueColorCR": { 28 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/VIIRS_SNPP_CorrectedReflectance_TrueColor/default/{time}/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg", 29 | "max_zoom": 9, 30 | "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.", 31 | "html_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.", 32 | "name": "NASAGIBS.ViirsTrueColorCR", 33 | "time": "" 34 | }, 35 | "BlueMarble": { 36 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/BlueMarble_NextGeneration/default/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpeg", 37 | "max_zoom": 8, 38 | "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.", 39 | "html_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.", 40 | "name": "NASAGIBS.BlueMarble", 41 | "crs": "EPSG:3857" 42 | }, 43 | "BlueMarble3413": { 44 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3413/best/BlueMarble_NextGeneration/default/500m/{z}/{y}/{x}.jpeg", 45 | "max_zoom": 5, 46 | "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.", 47 | "html_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.", 48 | "name": "NASAGIBS.BlueMarble3413", 49 | "crs": "EPSG:3413" 50 | }, 51 | "BlueMarble3031": { 52 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3031/best/BlueMarble_NextGeneration/default/500m/{z}/{y}/{x}.jpeg", 53 | "max_zoom": 5, 54 | "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.", 55 | "html_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.", 56 | "name": "NASAGIBS.BlueMarble3031", 57 | "crs": "EPSG:3031" 58 | }, 59 | "BlueMarbleBathymetry3413": { 60 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3413/best/BlueMarble_ShadedRelief_Bathymetry/default/500m/{z}/{y}/{x}.jpeg", 61 | "max_zoom": 5, 62 | "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.", 63 | "html_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.", 64 | "name": "NASAGIBS.BlueMarbleBathymetry3413", 65 | "crs": "EPSG:3413" 66 | }, 67 | "BlueMarbleBathymetry3031": { 68 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3031/best/BlueMarble_ShadedRelief_Bathymetry/default/500m/{z}/{y}/{x}.jpeg", 69 | "max_zoom": 5, 70 | "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.", 71 | "html_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.", 72 | "name": "NASAGIBS.BlueMarbleBathymetry3031", 73 | "crs": "EPSG:3031" 74 | }, 75 | "MEaSUREsIceVelocity3413": { 76 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3413/best/MEaSUREs_Ice_Velocity_Greenland/default/500m/{z}/{y}/{x}", 77 | "max_zoom": 4, 78 | "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.", 79 | "html_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.", 80 | "name": "NASAGIBS.MEaSUREsIceVelocity3413", 81 | "crs": "EPSG:3413" 82 | }, 83 | "MEaSUREsIceVelocity3031": { 84 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3031/best/MEaSUREs_Ice_Velocity_Antarctica/default/500m/{z}/{y}/{x}", 85 | "max_zoom": 4, 86 | "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.", 87 | "html_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.", 88 | "name": "NASAGIBS.MEaSUREsIceVelocity3031", 89 | "crs": "EPSG:3031" 90 | }, 91 | "ASTER_GDEM_Greyscale_Shaded_Relief": { 92 | "url": "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/ASTER_GDEM_Greyscale_Shaded_Relief/default/GoogleMapsCompatible_Level12/{z}/{y}/{x}.jpg", 93 | "max_zoom": 12, 94 | "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.", 95 | "html_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.", 96 | "name": "NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief" 97 | } 98 | }, 99 | "Esri": { 100 | "ArcticImagery": { 101 | "url": "http://server.arcgisonline.com/ArcGIS/rest/services/Polar/Arctic_Imagery/MapServer/tile/{z}/{y}/{x}", 102 | "variant": "Arctic_Imagery", 103 | "html_attribution": "Earthstar Geographics", 104 | "attribution": "Earthstar Geographics", 105 | "max_zoom": 24, 106 | "name": "Esri.ArcticImagery", 107 | "crs": "EPSG:5936", 108 | "bounds": [ 109 | [ 110 | -2623285.8808999992907047, 111 | -2623285.8808999992907047 112 | ], 113 | [ 114 | 6623285.8803000003099442, 115 | 6623285.8803000003099442 116 | ] 117 | ] 118 | }, 119 | "ArcticOceanBase": { 120 | "url": "http://server.arcgisonline.com/ArcGIS/rest/services/Polar/Arctic_Ocean_Base/MapServer/tile/{z}/{y}/{x}", 121 | "variant": "Arctic_Ocean_Base", 122 | "html_attribution": "Tiles © 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", 123 | "attribution": "Tiles © 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", 124 | "max_zoom": 24, 125 | "name": "Esri.ArcticOceanBase", 126 | "crs": "EPSG:5936", 127 | "bounds": [ 128 | [ 129 | -2623285.8808999992907047, 130 | -2623285.8808999992907047 131 | ], 132 | [ 133 | 6623285.8803000003099442, 134 | 6623285.8803000003099442 135 | ] 136 | ] 137 | }, 138 | "ArcticOceanReference": { 139 | "url": "http://server.arcgisonline.com/ArcGIS/rest/services/Polar/Arctic_Ocean_Reference/MapServer/tile/{z}/{y}/{x}", 140 | "variant": "Arctic_Ocean_Reference", 141 | "html_attribution": "Tiles © 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", 142 | "attribution": "Tiles © 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", 143 | "max_zoom": 24, 144 | "name": "Esri.ArcticOceanReference", 145 | "crs": "EPSG:5936", 146 | "bounds": [ 147 | [ 148 | -2623285.8808999992907047, 149 | -2623285.8808999992907047 150 | ], 151 | [ 152 | 6623285.8803000003099442, 153 | 6623285.8803000003099442 154 | ] 155 | ] 156 | }, 157 | "AntarcticImagery": { 158 | "url": "http://server.arcgisonline.com/ArcGIS/rest/services/Polar/Antarctic_Imagery/MapServer/tile/{z}/{y}/{x}", 159 | "variant": "Antarctic_Imagery", 160 | "html_attribution": "Earthstar Geographics", 161 | "attribution": "Earthstar Geographics", 162 | "max_zoom": 24, 163 | "name": "Esri.AntarcticImagery", 164 | "crs": "EPSG:3031", 165 | "bounds": [ 166 | [ 167 | -9913957.327914657, 168 | -5730886.461772691 169 | ], 170 | [ 171 | 9913957.327914657, 172 | 5730886.461773157 173 | ] 174 | ] 175 | }, 176 | "AntarcticBasemap": { 177 | "url": "https://tiles.arcgis.com/tiles/C8EMgrsFcRFL6LrL/arcgis/rest/services/Antarctic_Basemap/MapServer/tile/{z}/{y}/{x}", 178 | "variant": "Antarctic_Basemap", 179 | "html_attribution": "Imagery provided by NOAA National Centers for Environmental Information (NCEI); International Bathymetric Chart of the Southern Ocean (IBCSO); General Bathymetric Chart of the Oceans (GEBCO).", 180 | "attribution": "Imagery provided by NOAA National Centers for Environmental Information (NCEI); International Bathymetric Chart of the Southern Ocean (IBCSO); General Bathymetric Chart of the Oceans (GEBCO).", 181 | "max_zoom": 9, 182 | "name": "Esri.AntarcticBasemap", 183 | "crs": "EPSG:3031", 184 | "bounds": [ 185 | [ 186 | -4524583.19363305, 187 | -4524449.487765655 188 | ], 189 | [ 190 | 4524449.4877656475, 191 | 4524583.193633042 192 | ] 193 | ] 194 | } 195 | }, 196 | "Gaode": { 197 | "Normal": { 198 | "url": "http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}", 199 | "max_zoom": 19, 200 | "attribution": "© Gaode.com", 201 | "html_attribution": "© Gaode.com", 202 | "name": "Gaode.Normal" 203 | }, 204 | "Satellite": { 205 | "url": "http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}", 206 | "max_zoom": 19, 207 | "attribution": "© Gaode.com", 208 | "html_attribution": "© Gaode.com", 209 | "name": "Gaode.Satellite" 210 | } 211 | }, 212 | "Strava": { 213 | "All": { 214 | "url": "https://heatmap-external-a.strava.com/tiles/all/hot/{z}/{x}/{y}.png", 215 | "max_zoom": 15, 216 | "attribution": "Map tiles by Strava 2021", 217 | "html_attribution": "Map tiles by Strava 2021", 218 | "name": "Strava.All" 219 | }, 220 | "Ride": { 221 | "url": "https://heatmap-external-a.strava.com/tiles/ride/hot/{z}/{x}/{y}.png", 222 | "max_zoom": 15, 223 | "attribution": "Map tiles by Strava 2021", 224 | "html_attribution": "Map tiles by Strava 2021", 225 | "name": "Strava.Ride" 226 | }, 227 | "Run": { 228 | "url": "https://heatmap-external-a.strava.com/tiles/run/bluered/{z}/{x}/{y}.png", 229 | "max_zoom": 15, 230 | "attribution": "Map tiles by Strava 2021", 231 | "html_attribution": "Map tiles by Strava 2021", 232 | "name": "Strava.Run" 233 | }, 234 | "Water": { 235 | "url": "https://heatmap-external-a.strava.com/tiles/water/blue/{z}/{x}/{y}.png", 236 | "max_zoom": 15, 237 | "attribution": "Map tiles by Strava 2021", 238 | "html_attribution": "Map tiles by Strava 2021", 239 | "name": "Strava.Water" 240 | }, 241 | "Winter": { 242 | "url": "https://heatmap-external-a.strava.com/tiles/winter/hot/{z}/{x}/{y}.png", 243 | "max_zoom": 15, 244 | "attribution": "Map tiles by Strava 2021", 245 | "html_attribution": "Map tiles by Strava 2021", 246 | "name": "Strava.Winter" 247 | } 248 | }, 249 | "SwissFederalGeoportal": { 250 | "NationalMapColor": { 251 | "url": "https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg", 252 | "html_attribution": "swisstopo", 253 | "attribution": "© swisstopo", 254 | "bounds": [ 255 | [ 256 | 45.398181, 257 | 5.140242 258 | ], 259 | [ 260 | 48.230651, 261 | 11.47757 262 | ] 263 | ], 264 | "min_zoom": 2, 265 | "max_zoom": 18, 266 | "name": "SwissFederalGeoportal.NationalMapColor" 267 | }, 268 | "NationalMapGrey": { 269 | "url": "https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-grau/default/current/3857/{z}/{x}/{y}.jpeg", 270 | "html_attribution": "swisstopo", 271 | "attribution": "© swisstopo", 272 | "bounds": [ 273 | [ 274 | 45.398181, 275 | 5.140242 276 | ], 277 | [ 278 | 48.230651, 279 | 11.47757 280 | ] 281 | ], 282 | "min_zoom": 2, 283 | "max_zoom": 18, 284 | "name": "SwissFederalGeoportal.NationalMapGrey" 285 | }, 286 | "SWISSIMAGE": { 287 | "url": "https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg", 288 | "html_attribution": "swisstopo", 289 | "attribution": "© swisstopo", 290 | "bounds": [ 291 | [ 292 | 45.398181, 293 | 5.140242 294 | ], 295 | [ 296 | 48.230651, 297 | 11.47757 298 | ] 299 | ], 300 | "min_zoom": 2, 301 | "max_zoom": 19, 302 | "name": "SwissFederalGeoportal.SWISSIMAGE" 303 | }, 304 | "JourneyThroughTime": { 305 | "url": "https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.zeitreihen/default/{time}/3857/{z}/{x}/{y}.png", 306 | "html_attribution": "swisstopo", 307 | "attribution": "© swisstopo", 308 | "bounds": [ 309 | [ 310 | 45.398181, 311 | 5.140242 312 | ], 313 | [ 314 | 48.230651, 315 | 11.47757 316 | ] 317 | ], 318 | "min_zoom": 2, 319 | "max_zoom": 18, 320 | "time": 18641231, 321 | "name": "SwissFederalGeoportal.JourneyThroughTime" 322 | } 323 | }, 324 | "MapTiler": { 325 | "Basic4326": { 326 | "url": "https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}", 327 | "html_attribution": "© MapTiler © OpenStreetMap contributors", 328 | "attribution": "(C) MapTiler (C) OpenStreetMap contributors", 329 | "variant": "basic-4326", 330 | "ext": "png", 331 | "key": "", 332 | "tileSize": 512, 333 | "zoomOffset": -1, 334 | "min_zoom": 0, 335 | "max_zoom": 21, 336 | "name": "MapTiler.Basic4326", 337 | "crs": "EPSG:4326" 338 | }, 339 | "Outdoor": { 340 | "url": "https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}", 341 | "html_attribution": "© MapTiler © OpenStreetMap contributors", 342 | "attribution": "(C) MapTiler (C) OpenStreetMap contributors", 343 | "variant": "outdoor", 344 | "ext": "png", 345 | "key": "", 346 | "tileSize": 512, 347 | "zoomOffset": -1, 348 | "min_zoom": 0, 349 | "max_zoom": 21, 350 | "name": "MapTiler.Outdoor" 351 | }, 352 | "Topographique": { 353 | "url": "https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}", 354 | "html_attribution": "© MapTiler © OpenStreetMap contributors", 355 | "attribution": "(C) MapTiler (C) OpenStreetMap contributors", 356 | "variant": "topographique", 357 | "ext": "png", 358 | "key": "", 359 | "tileSize": 512, 360 | "zoomOffset": -1, 361 | "min_zoom": 0, 362 | "max_zoom": 21, 363 | "name": "MapTiler.Topographique" 364 | }, 365 | "Winter": { 366 | "url": "https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}", 367 | "html_attribution": "© MapTiler © OpenStreetMap contributors", 368 | "attribution": "(C) MapTiler (C) OpenStreetMap contributors", 369 | "variant": "winter", 370 | "ext": "png", 371 | "key": "", 372 | "tileSize": 512, 373 | "zoomOffset": -1, 374 | "min_zoom": 0, 375 | "max_zoom": 21, 376 | "name": "MapTiler.Winter" 377 | }, 378 | "Satellite": { 379 | "url": "https://api.maptiler.com/tiles/{variant}/{z}/{x}/{y}.{ext}?key={key}", 380 | "html_attribution": "© MapTiler © OpenStreetMap contributors", 381 | "attribution": "(C) MapTiler (C) OpenStreetMap contributors", 382 | "variant": "satellite-v2", 383 | "ext": "jpg", 384 | "key": "", 385 | "min_zoom": 0, 386 | "max_zoom": 20, 387 | "name": "MapTiler.Satellite" 388 | }, 389 | "Terrain": { 390 | "url": "https://api.maptiler.com/tiles/{variant}/{z}/{x}/{y}.{ext}?key={key}", 391 | "html_attribution": "© MapTiler © OpenStreetMap contributors", 392 | "attribution": "(C) MapTiler (C) OpenStreetMap contributors", 393 | "variant": "terrain-rgb", 394 | "ext": "png", 395 | "key": "", 396 | "min_zoom": 0, 397 | "max_zoom": 12, 398 | "name": "MapTiler.Terrain" 399 | } 400 | }, 401 | "OrdnanceSurvey": { 402 | "Road": { 403 | "url": "https://api.os.uk/maps/raster/v1/zxy/Road_3857/{z}/{x}/{y}.png?key={key}", 404 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 405 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 406 | "key": "", 407 | "min_zoom": 7, 408 | "max_zoom": 16, 409 | "max_zoom_premium": 20, 410 | "bounds": [ 411 | [ 412 | 49.766807, 413 | -9.496386 414 | ], 415 | [ 416 | 61.465189, 417 | 3.634745 418 | ] 419 | ], 420 | "name": "OrdnanceSurvey.Road" 421 | }, 422 | "Road_27700": { 423 | "url": "https://api.os.uk/maps/raster/v1/zxy/Road_27700/{z}/{x}/{y}.png?key={key}", 424 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 425 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 426 | "key": "", 427 | "crs": "EPSG:27700", 428 | "min_zoom": 0, 429 | "max_zoom": 9, 430 | "max_zoom_premium": 13, 431 | "bounds": [ 432 | [ 433 | 0, 434 | 0 435 | ], 436 | [ 437 | 700000, 438 | 1300000 439 | ] 440 | ], 441 | "name": "OrdnanceSurvey.Road_27700" 442 | }, 443 | "Outdoor": { 444 | "url": "https://api.os.uk/maps/raster/v1/zxy/Outdoor_3857/{z}/{x}/{y}.png?key={key}", 445 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 446 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 447 | "key": "", 448 | "min_zoom": 7, 449 | "max_zoom": 16, 450 | "max_zoom_premium": 20, 451 | "bounds": [ 452 | [ 453 | 49.766807, 454 | -9.496386 455 | ], 456 | [ 457 | 61.465189, 458 | 3.634745 459 | ] 460 | ], 461 | "name": "OrdnanceSurvey.Outdoor" 462 | }, 463 | "Outdoor_27700": { 464 | "url": "https://api.os.uk/maps/raster/v1/zxy/Outdoor_27700/{z}/{x}/{y}.png?key={key}", 465 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 466 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 467 | "key": "", 468 | "crs": "EPSG:27700", 469 | "min_zoom": 0, 470 | "max_zoom": 9, 471 | "max_zoom_premium": 13, 472 | "bounds": [ 473 | [ 474 | 0, 475 | 0 476 | ], 477 | [ 478 | 700000, 479 | 1300000 480 | ] 481 | ], 482 | "name": "OrdnanceSurvey.Outdoor_27700" 483 | }, 484 | "Light": { 485 | "url": "https://api.os.uk/maps/raster/v1/zxy/Light_3857/{z}/{x}/{y}.png?key={key}", 486 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 487 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 488 | "key": "", 489 | "min_zoom": 7, 490 | "max_zoom": 16, 491 | "max_zoom_premium": 20, 492 | "bounds": [ 493 | [ 494 | 49.766807, 495 | -9.496386 496 | ], 497 | [ 498 | 61.465189, 499 | 3.634745 500 | ] 501 | ], 502 | "name": "OrdnanceSurvey.Light" 503 | }, 504 | "Light_27700": { 505 | "url": "https://api.os.uk/maps/raster/v1/zxy/Light_27700/{z}/{x}/{y}.png?key={key}", 506 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 507 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 508 | "key": "", 509 | "crs": "EPSG:27700", 510 | "min_zoom": 0, 511 | "max_zoom": 9, 512 | "max_zoom_premium": 13, 513 | "bounds": [ 514 | [ 515 | 0, 516 | 0 517 | ], 518 | [ 519 | 700000, 520 | 1300000 521 | ] 522 | ], 523 | "name": "OrdnanceSurvey.Light_27700" 524 | }, 525 | "Leisure_27700": { 526 | "url": "https://api.os.uk/maps/raster/v1/zxy/Leisure_27700/{z}/{x}/{y}.png?key={key}", 527 | "html_attribution": "Contains OS data © Crown copyright and database right {year}", 528 | "attribution": "Contains OS data (C) Crown copyright and database right {year}", 529 | "key": "", 530 | "crs": "EPSG:27700", 531 | "min_zoom": 0, 532 | "max_zoom": 5, 533 | "max_zoom_premium": 9, 534 | "bounds": [ 535 | [ 536 | 0, 537 | 0 538 | ], 539 | [ 540 | 700000, 541 | 1300000 542 | ] 543 | ], 544 | "name": "OrdnanceSurvey.Leisure_27700" 545 | } 546 | }, 547 | "UN": { 548 | "ClearMap": { 549 | "url": "https://geoservices.un.org/arcgis/rest/services/ClearMap_WebTopo/MapServer/tile/{z}/{y}/{x}", 550 | "name": "UN.ClearMap", 551 | "html_attribution": "© United Nations contributors", 552 | "attribution": "United Nations" 553 | } 554 | } 555 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 88 3 | lint.select = ["E", "F", "W", "I", "UP", "N", "B", "A", "C4", "SIM", "ARG"] 4 | exclude = ["provider_sources/_compress_providers.py", "doc"] 5 | target-version = "py38" 6 | lint.ignore = ["B006", "A003", "B904", "C420"] 7 | 8 | [tool.pytest.ini_options] 9 | markers = [ 10 | "request: fetching tiles from remote server.", 11 | ] 12 | 13 | [tool.coverage.run] 14 | omit = ["xyzservices/tests/*"] -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: doc/source/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: doc/requirements.txt 14 | - method: pip 15 | path: . 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", encoding="utf8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="xyzservices", 8 | description="Source of XYZ tiles providers", 9 | long_description=long_description, 10 | long_description_content_type="text/markdown", 11 | url="https://github.com/geopandas/xyzservices", 12 | author="Dani Arribas-Bel, Martin Fleischmann", 13 | author_email="daniel.arribas.bel@gmail.com, martin@martinfleischmann.net", 14 | license="3-Clause BSD", 15 | packages=setuptools.find_packages(exclude=["tests"]), 16 | python_requires=">=3.8", 17 | include_package_data=True, 18 | package_data={ 19 | "xyzservices": ["data/providers.json"], 20 | }, 21 | classifiers=[ 22 | "License :: OSI Approved :: BSD License", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3 :: Only", 26 | ], 27 | use_scm_version=True, 28 | setup_requires=["setuptools_scm"], 29 | data_files=[("share/xyzservices", ["xyzservices/data/providers.json"])], 30 | ) 31 | -------------------------------------------------------------------------------- /xyzservices/__init__.py: -------------------------------------------------------------------------------- 1 | from .lib import Bunch, TileProvider # noqa 2 | from .providers import providers # noqa 3 | 4 | from importlib.metadata import version, PackageNotFoundError 5 | import contextlib 6 | 7 | with contextlib.suppress(PackageNotFoundError): 8 | __version__ = version("xyzservices") 9 | -------------------------------------------------------------------------------- /xyzservices/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/xyzservices/3ba73ba29be2f892fb2405c4662a09165e838a9b/xyzservices/data/__init__.py -------------------------------------------------------------------------------- /xyzservices/lib.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities to support XYZservices 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import json 8 | import urllib.request 9 | import uuid 10 | from typing import Callable 11 | from urllib.parse import quote 12 | 13 | QUERY_NAME_TRANSLATION = str.maketrans({x: "" for x in "., -_/"}) 14 | 15 | 16 | class Bunch(dict): 17 | """A dict with attribute-access 18 | 19 | :class:`Bunch` is used to store :class:`TileProvider` objects. 20 | 21 | Examples 22 | -------- 23 | >>> black_and_white = TileProvider( 24 | ... name="My black and white tiles", 25 | ... url="https://myserver.com/bw/{z}/{x}/{y}", 26 | ... attribution="(C) xyzservices", 27 | ... ) 28 | >>> colorful = TileProvider( 29 | ... name="My colorful tiles", 30 | ... url="https://myserver.com/color/{z}/{x}/{y}", 31 | ... attribution="(C) xyzservices", 32 | ... ) 33 | >>> MyTiles = Bunch(BlackAndWhite=black_and_white, Colorful=colorful) 34 | >>> MyTiles 35 | {'BlackAndWhite': {'name': 'My black and white tiles', 'url': \ 36 | 'https://myserver.com/bw/{z}/{x}/{y}', 'attribution': '(C) xyzservices'}, 'Colorful': \ 37 | {'name': 'My colorful tiles', 'url': 'https://myserver.com/color/{z}/{x}/{y}', \ 38 | 'attribution': '(C) xyzservices'}} 39 | >>> MyTiles.BlackAndWhite.url 40 | 'https://myserver.com/bw/{z}/{x}/{y}' 41 | """ 42 | 43 | def __getattr__(self, key): 44 | try: 45 | return self.__getitem__(key) 46 | except KeyError as err: 47 | raise AttributeError(key) from err 48 | 49 | def __dir__(self): 50 | return self.keys() 51 | 52 | def _repr_html_(self, inside=False): 53 | children = "" 54 | for key in self: 55 | if isinstance(self[key], TileProvider): 56 | obj = "xyzservices.TileProvider" 57 | else: 58 | obj = "xyzservices.Bunch" 59 | uid = str(uuid.uuid4()) 60 | children += f""" 61 |
  • 62 | 63 | 64 |
    65 | {self[key]._repr_html_(inside=True)} 66 |
    67 |
  • 68 | """ 69 | 70 | style = "" if inside else f"" 71 | html = f""" 72 |
    73 | {style} 74 |
    75 |
    76 |
    xyzservices.Bunch
    77 |
    {len(self)} items
    78 |
    79 |
    80 |
      81 | {children} 82 |
    83 |
    84 |
    85 |
    86 | """ 87 | 88 | return html 89 | 90 | def flatten(self) -> dict: 91 | """Return the nested :class:`Bunch` collapsed into the one level dictionary. 92 | 93 | Dictionary keys are :class:`TileProvider` names (e.g. ``OpenStreetMap.Mapnik``) 94 | and its values are :class:`TileProvider` objects. 95 | 96 | Returns 97 | ------- 98 | flattened : dict 99 | dictionary of :class:`TileProvider` objects 100 | 101 | Examples 102 | -------- 103 | >>> import xyzservices.providers as xyz 104 | >>> len(xyz) 105 | 36 106 | 107 | >>> flat = xyz.flatten() 108 | >>> len(xyz) 109 | 207 110 | 111 | """ 112 | 113 | flat = {} 114 | 115 | def _get_providers(provider): 116 | if isinstance(provider, TileProvider): 117 | flat[provider.name] = provider 118 | else: 119 | for prov in provider.values(): 120 | _get_providers(prov) 121 | 122 | _get_providers(self) 123 | 124 | return flat 125 | 126 | def filter( 127 | self, 128 | keyword: str | None = None, 129 | name: str | None = None, 130 | requires_token: bool | None = None, 131 | function: Callable[[TileProvider], bool] = None, 132 | ) -> Bunch: 133 | """Return a subset of the :class:`Bunch` matching the filter conditions 134 | 135 | Each :class:`TileProvider` within a :class:`Bunch` is checked against one or 136 | more specified conditions and kept if they are satisfied or removed if at least 137 | one condition is not met. 138 | 139 | Parameters 140 | ---------- 141 | keyword : str (optional) 142 | Condition returns ``True`` if ``keyword`` string is present in any string 143 | value in a :class:`TileProvider` object. 144 | The comparison is not case sensitive. 145 | name : str (optional) 146 | Condition returns ``True`` if ``name`` string is present in 147 | the name attribute of :class:`TileProvider` object. 148 | The comparison is not case sensitive. 149 | requires_token : bool (optional) 150 | Condition returns ``True`` if :meth:`TileProvider.requires_token` returns 151 | ``True`` (i.e. if the object requires specification of API token). 152 | function : callable (optional) 153 | Custom function taking :class:`TileProvider` as an argument and returns 154 | bool. If ``function`` is given, other parameters are ignored. 155 | 156 | Returns 157 | ------- 158 | filtered : Bunch 159 | 160 | Examples 161 | -------- 162 | >>> import xyzservices.providers as xyz 163 | 164 | You can filter all free providers (not requiring API token): 165 | 166 | >>> free_providers = xyz.filter(requires_token=False) 167 | 168 | Or all providers with ``open`` in the name: 169 | 170 | >>> open_providers = xyz.filter(name="open") 171 | 172 | You can use keyword search to find all providers based on OpenStreetMap data: 173 | 174 | >>> osm_providers = xyz.filter(keyword="openstreetmap") 175 | 176 | You can combine multiple conditions to find providers based on OpenStreetMap 177 | data that require API token: 178 | 179 | >>> osm_locked = xyz.filter(keyword="openstreetmap", requires_token=True) 180 | 181 | You can also pass custom function that takes :class:`TileProvider` and returns 182 | boolean value. You can then find all providers with ``max_zoom`` smaller than 183 | 18: 184 | 185 | >>> def zoom18(provider): 186 | ... if hasattr(provider, "max_zoom") and provider.max_zoom < 18: 187 | ... return True 188 | ... return False 189 | >>> small_zoom = xyz.filter(function=zoom18) 190 | """ 191 | 192 | def _validate(provider, keyword, name, requires_token): 193 | cond = [] 194 | 195 | if keyword is not None: 196 | keyword_match = False 197 | for v in provider.values(): 198 | if isinstance(v, str) and keyword.lower() in v.lower(): 199 | keyword_match = True 200 | break 201 | cond.append(keyword_match) 202 | 203 | if name is not None: 204 | name_match = False 205 | if name.lower() in provider.name.lower(): 206 | name_match = True 207 | cond.append(name_match) 208 | 209 | if requires_token is not None: 210 | token_match = False 211 | if provider.requires_token() is requires_token: 212 | token_match = True 213 | cond.append(token_match) 214 | 215 | return all(cond) 216 | 217 | def _filter_bunch(bunch, keyword, name, requires_token, function): 218 | new = Bunch() 219 | for key, value in bunch.items(): 220 | if isinstance(value, TileProvider): 221 | if function is None: 222 | if _validate( 223 | value, 224 | keyword=keyword, 225 | name=name, 226 | requires_token=requires_token, 227 | ): 228 | new[key] = value 229 | else: 230 | if function(value): 231 | new[key] = value 232 | 233 | else: 234 | filtered = _filter_bunch( 235 | value, 236 | keyword=keyword, 237 | name=name, 238 | requires_token=requires_token, 239 | function=function, 240 | ) 241 | if filtered: 242 | new[key] = filtered 243 | 244 | return new 245 | 246 | return _filter_bunch( 247 | self, 248 | keyword=keyword, 249 | name=name, 250 | requires_token=requires_token, 251 | function=function, 252 | ) 253 | 254 | def query_name(self, name: str) -> TileProvider: 255 | """Return :class:`TileProvider` based on the name query 256 | 257 | Returns a matching :class:`TileProvider` from the :class:`Bunch` if the ``name`` 258 | contains the same letters in the same order as the provider's name irrespective 259 | of the letter case, spaces, dashes and other characters. 260 | See examples for details. 261 | 262 | Parameters 263 | ---------- 264 | name : str 265 | Name of the tile provider. Formatting does not matter. 266 | 267 | Returns 268 | ------- 269 | match: TileProvider 270 | 271 | Examples 272 | -------- 273 | >>> import xyzservices.providers as xyz 274 | 275 | All these queries return the same ``CartoDB.Positron`` TileProvider: 276 | 277 | >>> xyz.query_name("CartoDB Positron") 278 | >>> xyz.query_name("cartodbpositron") 279 | >>> xyz.query_name("cartodb-positron") 280 | >>> xyz.query_name("carto db/positron") 281 | >>> xyz.query_name("CARTO_DB_POSITRON") 282 | >>> xyz.query_name("CartoDB.Positron") 283 | 284 | """ 285 | xyz_flat_lower = { 286 | k.translate(QUERY_NAME_TRANSLATION).lower(): v 287 | for k, v in self.flatten().items() 288 | } 289 | name_clean = name.translate(QUERY_NAME_TRANSLATION).lower() 290 | if name_clean in xyz_flat_lower: 291 | return xyz_flat_lower[name_clean] 292 | 293 | raise ValueError(f"No matching provider found for the query '{name}'.") 294 | 295 | 296 | class TileProvider(Bunch): 297 | """ 298 | A dict with attribute-access and that 299 | can be called to update keys 300 | 301 | 302 | Examples 303 | -------- 304 | 305 | You can create custom :class:`TileProvider` by passing your attributes to the object 306 | as it would have been a ``dict()``. It is required to always specify ``name``, 307 | ``url``, and ``attribution``. 308 | 309 | >>> public_provider = TileProvider( 310 | ... name="My public tiles", 311 | ... url="https://myserver.com/tiles/{z}/{x}/{y}.png", 312 | ... attribution="(C) xyzservices", 313 | ... ) 314 | 315 | Alternatively, you can create it from a dictionary of attributes. When specifying a 316 | placeholder for the access token, please use the ``""`` string to ensure that :meth:`~xyzservices.TileProvider.requires_token` 318 | method works properly. 319 | 320 | >>> private_provider = TileProvider( 321 | ... { 322 | ... "url": "https://myserver.com/tiles/{z}/{x}/{y}.png?apikey={accessToken}", 323 | ... "attribution": "(C) xyzservices", 324 | ... "accessToken": "", 325 | ... "name": "my_private_provider", 326 | ... } 327 | ... ) 328 | 329 | It is customary to include ``html_attribution`` attribute containing HTML string as 330 | ``'© OpenStreetMap 331 | contributors'`` alongisde a plain-text ``attribution``. 332 | 333 | You can then fetch all information as attributes: 334 | 335 | >>> public_provider.url 336 | 'https://myserver.com/tiles/{z}/{x}/{y}.png' 337 | 338 | >>> public_provider.attribution 339 | '(C) xyzservices' 340 | 341 | To ensure you will be able to use the tiles, you can check if the 342 | :class:`TileProvider` requires a token or API key. 343 | 344 | >>> public_provider.requires_token() 345 | False 346 | >>> private_provider.requires_token() 347 | True 348 | 349 | You can also generate URL in the required format with or without placeholders: 350 | 351 | >>> public_provider.build_url() 352 | 'https://myserver.com/tiles/{z}/{x}/{y}.png' 353 | >>> private_provider.build_url(x=12, y=21, z=11, accessToken="my_token") 354 | 'https://myserver.com/tiles/11/12/21.png?access_token=my_token' 355 | 356 | """ 357 | 358 | def __init__(self, *args, **kwargs): 359 | super().__init__(*args, **kwargs) 360 | missing = [] 361 | for el in ["name", "url", "attribution"]: 362 | if el not in self.keys(): 363 | missing.append(el) 364 | if len(missing) > 0: 365 | msg = ( 366 | f"The attributes `name`, `url`, " 367 | f"and `attribution` are required to initialise " 368 | f"a `TileProvider`. Please provide values for: " 369 | f"`{'`, `'.join(missing)}`" 370 | ) 371 | raise AttributeError(msg) 372 | 373 | def __call__(self, **kwargs) -> TileProvider: 374 | new = TileProvider(self) # takes a copy preserving the class 375 | new.update(kwargs) 376 | return new 377 | 378 | def copy(self) -> TileProvider: 379 | new = TileProvider(self) # takes a copy preserving the class 380 | return new 381 | 382 | def build_url( 383 | self, 384 | x: int | str | None = None, 385 | y: int | str | None = None, 386 | z: int | str | None = None, 387 | scale_factor: str | None = None, 388 | fill_subdomain: bool | None = True, 389 | **kwargs, 390 | ) -> str: 391 | """ 392 | Build the URL of tiles from the :class:`TileProvider` object 393 | 394 | Can return URL with placeholders or the final tile URL. 395 | 396 | Parameters 397 | ---------- 398 | 399 | x, y, z : int (optional) 400 | tile number 401 | scale_factor : str (optional) 402 | Scale factor (where supported). For example, you can get double resolution 403 | (512 x 512) instead of standard one (256 x 256) with ``"@2x"``. If you want 404 | to keep a placeholder, pass `"{r}"`. 405 | fill_subdomain : bool (optional, default True) 406 | Fill subdomain placeholder with the first available subdomain. If False, the 407 | URL will contain ``{s}`` placeholder for subdomain. 408 | 409 | **kwargs 410 | Other potential attributes updating the :class:`TileProvider`. 411 | 412 | Returns 413 | ------- 414 | 415 | url : str 416 | Formatted URL 417 | 418 | Examples 419 | -------- 420 | >>> import xyzservices.providers as xyz 421 | 422 | >>> xyz.CartoDB.DarkMatter.build_url() 423 | 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png' 424 | 425 | >>> xyz.CartoDB.DarkMatter.build_url(x=9, y=11, z=5) 426 | 'https://a.basemaps.cartocdn.com/dark_all/5/9/11.png' 427 | 428 | >>> xyz.CartoDB.DarkMatter.build_url(x=9, y=11, z=5, scale_factor="@2x") 429 | 'https://a.basemaps.cartocdn.com/dark_all/5/9/11@2x.png' 430 | 431 | >>> xyz.MapBox.build_url(accessToken="my_token") 432 | 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=my_token' 433 | 434 | """ 435 | provider = self.copy() 436 | 437 | if x is None: 438 | x = "{x}" 439 | if y is None: 440 | y = "{y}" 441 | if z is None: 442 | z = "{z}" 443 | 444 | provider.update(kwargs) 445 | 446 | if provider.requires_token(): 447 | raise ValueError( 448 | "Token is required for this provider, but not provided. " 449 | "You can either update TileProvider or pass respective keywords " 450 | "to build_url()." 451 | ) 452 | 453 | url = provider.pop("url") 454 | 455 | if scale_factor: 456 | r = scale_factor 457 | provider.pop("r", None) 458 | else: 459 | r = provider.pop("r", "") 460 | 461 | if fill_subdomain: 462 | subdomains = provider.pop("subdomains", "abc") 463 | s = subdomains[0] 464 | else: 465 | s = "{s}" 466 | 467 | return url.format(x=x, y=y, z=z, s=s, r=r, **provider) 468 | 469 | def requires_token(self) -> bool: 470 | """ 471 | Returns ``True`` if the TileProvider requires access token to fetch tiles. 472 | 473 | The token attribute name vary and some :class:`TileProvider` objects may require 474 | more than one token (e.g. ``HERE``). The information is deduced from the 475 | presence of `'"`` string to ensure that 478 | :meth:`~xyzservices.TileProvider.requires_token` method works properly. 479 | 480 | Returns 481 | ------- 482 | bool 483 | 484 | Examples 485 | -------- 486 | >>> import xyzservices.providers as xyz 487 | >>> xyz.MapBox.requires_token() 488 | True 489 | 490 | >>> xyz.CartoDB.Positron 491 | False 492 | 493 | We can specify this API key by calling the object or overriding the attribute. 494 | Overriding the attribute will alter existing object: 495 | 496 | >>> xyz.OpenWeatherMap.Clouds["apiKey"] = "my-private-api-key" 497 | 498 | Calling the object will return a copy: 499 | 500 | >>> xyz.OpenWeatherMap.Clouds(apiKey="my-private-api-key") 501 | 502 | 503 | """ 504 | # both attribute and placeholder in url are required to make it work 505 | for key, val in self.items(): 506 | if isinstance(val, str) and "{key}
    {val}
    " 521 | 522 | style = "" if inside else f"" 523 | html = f""" 524 |
    525 | {style} 526 |
    527 |
    528 |
    xyzservices.TileProvider
    529 |
    {self.name}
    530 |
    531 |
    532 |
    533 | {provider_info} 534 |
    535 |
    536 |
    537 |
    538 | """ 539 | 540 | return html 541 | 542 | @classmethod 543 | def from_qms(cls, name: str) -> TileProvider: 544 | """ 545 | Creates a :class:`TileProvider` object based on the definition from 546 | the `Quick Map Services `__ open catalog. 547 | 548 | Parameters 549 | ---------- 550 | name : str 551 | Service name 552 | 553 | Returns 554 | ------- 555 | :class:`TileProvider` 556 | 557 | Examples 558 | -------- 559 | >>> from xyzservices.lib import TileProvider 560 | >>> provider = TileProvider.from_qms("OpenTopoMap") 561 | """ 562 | qms_api_url = "https://qms.nextgis.com/api/v1/geoservices" 563 | 564 | services = json.load( 565 | urllib.request.urlopen(f"{qms_api_url}/?search={quote(name)}&type=tms") 566 | ) 567 | 568 | for service in services: 569 | if service["name"] == name: 570 | break 571 | else: 572 | raise ValueError(f"Service '{name}' not found.") 573 | 574 | service_id = service["id"] 575 | service_details = json.load( 576 | urllib.request.urlopen(f"{qms_api_url}/{service_id}") 577 | ) 578 | 579 | return cls( 580 | name=service_details["name"], 581 | url=service_details["url"], 582 | min_zoom=service_details.get("z_min"), 583 | max_zoom=service_details.get("z_max"), 584 | attribution=service_details.get("copyright_text"), 585 | ) 586 | 587 | 588 | def _load_json(f): 589 | data = json.loads(f) 590 | 591 | providers = Bunch() 592 | 593 | for provider_name in data: 594 | provider = data[provider_name] 595 | 596 | if "url" in provider: 597 | providers[provider_name] = TileProvider(provider) 598 | 599 | else: 600 | providers[provider_name] = Bunch( 601 | {i: TileProvider(provider[i]) for i in provider} 602 | ) 603 | 604 | return providers 605 | 606 | 607 | CSS_STYLE = """ 608 | /* CSS stylesheet for displaying xyzservices objects in Jupyter.*/ 609 | .xyz-wrap { 610 | --xyz-border-color: var(--jp-border-color2, #ddd); 611 | --xyz-font-color2: var(--jp-content-font-color2, rgba(128, 128, 128, 1)); 612 | --xyz-background-color-white: var(--jp-layout-color1, white); 613 | --xyz-background-color: var(--jp-layout-color2, rgba(128, 128, 128, 0.1)); 614 | } 615 | 616 | html[theme=dark] .xyz-wrap, 617 | body.vscode-dark .xyz-wrap, 618 | body.vscode-high-contrast .xyz-wrap { 619 | --xyz-border-color: #222; 620 | --xyz-font-color2: rgba(255, 255, 255, 0.54); 621 | --xyz-background-color-white: rgba(255, 255, 255, 1); 622 | --xyz-background-color: rgba(255, 255, 255, 0.05); 623 | 624 | } 625 | 626 | .xyz-header { 627 | padding-top: 6px; 628 | padding-bottom: 6px; 629 | margin-bottom: 4px; 630 | border-bottom: solid 1px var(--xyz-border-color); 631 | } 632 | 633 | .xyz-header>div { 634 | display: inline; 635 | margin-top: 0; 636 | margin-bottom: 0; 637 | } 638 | 639 | .xyz-obj, 640 | .xyz-name { 641 | margin-left: 2px; 642 | margin-right: 10px; 643 | } 644 | 645 | .xyz-obj { 646 | color: var(--xyz-font-color2); 647 | } 648 | 649 | .xyz-attrs { 650 | grid-column: 1 / -1; 651 | } 652 | 653 | dl.xyz-attrs { 654 | padding: 0 5px 0 5px; 655 | margin: 0; 656 | display: grid; 657 | grid-template-columns: 135px auto; 658 | background-color: var(--xyz-background-color); 659 | } 660 | 661 | .xyz-attrs dt, 662 | dd { 663 | padding: 0; 664 | margin: 0; 665 | float: left; 666 | padding-right: 10px; 667 | width: auto; 668 | } 669 | 670 | .xyz-attrs dt { 671 | font-weight: normal; 672 | grid-column: 1; 673 | } 674 | 675 | .xyz-attrs dd { 676 | grid-column: 2; 677 | white-space: pre-wrap; 678 | word-break: break-all; 679 | } 680 | 681 | .xyz-details ul>li>label>span { 682 | color: var(--xyz-font-color2); 683 | padding-left: 10px; 684 | } 685 | 686 | .xyz-inside { 687 | display: none; 688 | } 689 | 690 | .xyz-checkbox:checked~.xyz-inside { 691 | display: contents; 692 | } 693 | 694 | .xyz-collapsible li>input { 695 | display: none; 696 | } 697 | 698 | .xyz-collapsible>li>label { 699 | cursor: pointer; 700 | } 701 | 702 | .xyz-collapsible>li>label:hover { 703 | color: var(--xyz-font-color2); 704 | } 705 | 706 | ul.xyz-collapsible { 707 | list-style: none!important; 708 | padding-left: 20px!important; 709 | } 710 | 711 | .xyz-checkbox+label:before { 712 | content: '►'; 713 | font-size: 11px; 714 | } 715 | 716 | .xyz-checkbox:checked+label:before { 717 | content: '▼'; 718 | } 719 | 720 | .xyz-wrap { 721 | margin-bottom: 10px; 722 | } 723 | """ 724 | -------------------------------------------------------------------------------- /xyzservices/providers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkgutil 3 | import sys 4 | 5 | from .lib import _load_json 6 | 7 | data_path = os.path.join(sys.prefix, "share", "xyzservices", "providers.json") 8 | 9 | if os.path.exists(data_path): 10 | with open(data_path) as f: 11 | json = f.read() 12 | else: 13 | json = pkgutil.get_data("xyzservices", "data/providers.json") 14 | 15 | providers = _load_json(json) 16 | -------------------------------------------------------------------------------- /xyzservices/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopandas/xyzservices/3ba73ba29be2f892fb2405c4662a09165e838a9b/xyzservices/tests/__init__.py -------------------------------------------------------------------------------- /xyzservices/tests/test_lib.py: -------------------------------------------------------------------------------- 1 | from urllib.error import URLError 2 | 3 | import pytest 4 | 5 | import xyzservices.providers as xyz 6 | from xyzservices import Bunch, TileProvider 7 | 8 | 9 | @pytest.fixture 10 | def basic_provider(): 11 | return TileProvider( 12 | url="https://myserver.com/tiles/{z}/{x}/{y}.png", 13 | attribution="(C) xyzservices", 14 | name="my_public_provider", 15 | ) 16 | 17 | 18 | @pytest.fixture 19 | def retina_provider(): 20 | return TileProvider( 21 | url="https://myserver.com/tiles/{z}/{x}/{y}{r}.png", 22 | attribution="(C) xyzservices", 23 | name="my_public_provider2", 24 | r="@2x", 25 | ) 26 | 27 | 28 | @pytest.fixture 29 | def silent_retina_provider(): 30 | return TileProvider( 31 | url="https://myserver.com/tiles/{z}/{x}/{y}{r}.png", 32 | attribution="(C) xyzservices", 33 | name="my_public_retina_provider3", 34 | ) 35 | 36 | 37 | @pytest.fixture 38 | def private_provider(): 39 | return TileProvider( 40 | url="https://myserver.com/tiles/{z}/{x}/{y}?access_token={accessToken}", 41 | attribution="(C) xyzservices", 42 | accessToken="", 43 | name="my_private_provider", 44 | ) 45 | 46 | 47 | @pytest.fixture 48 | def html_attr_provider(): 49 | return TileProvider( 50 | url="https://myserver.com/tiles/{z}/{x}/{y}.png", 51 | attribution="(C) xyzservices", 52 | html_attribution='© xyzservices', # noqa 53 | name="my_public_provider_html", 54 | ) 55 | 56 | 57 | @pytest.fixture 58 | def subdomain_provider(): 59 | return TileProvider( 60 | url="https://{s}.myserver.com/tiles/{z}/{x}/{y}.png", 61 | attribution="(C) xyzservices", 62 | subdomains="abcd", 63 | name="my_subdomain_provider", 64 | ) 65 | 66 | 67 | @pytest.fixture 68 | def test_bunch( 69 | basic_provider, 70 | retina_provider, 71 | silent_retina_provider, 72 | private_provider, 73 | html_attr_provider, 74 | subdomain_provider, 75 | ): 76 | return Bunch( 77 | basic_provider=basic_provider, 78 | retina_provider=retina_provider, 79 | silent_retina_provider=silent_retina_provider, 80 | private_provider=private_provider, 81 | bunched=Bunch( 82 | html_attr_provider=html_attr_provider, subdomain_provider=subdomain_provider 83 | ), 84 | ) 85 | 86 | 87 | def test_expect_name_url_attribution(): 88 | msg = ( 89 | "The attributes `name`, `url`, and `attribution` are " 90 | "required to initialise a `TileProvider`. Please provide " 91 | "values for: " 92 | ) 93 | with pytest.raises(AttributeError, match=msg + "`name`, `url`, `attribution`"): 94 | TileProvider({}) 95 | with pytest.raises(AttributeError, match=msg + "`url`, `attribution`"): 96 | TileProvider({"name": "myname"}) 97 | with pytest.raises(AttributeError, match=msg + "`attribution`"): 98 | TileProvider({"url": "my_url", "name": "my_name"}) 99 | with pytest.raises(AttributeError, match=msg + "`attribution`"): 100 | TileProvider(url="my_url", name="my_name") 101 | 102 | 103 | def test_build_url( 104 | basic_provider, 105 | retina_provider, 106 | silent_retina_provider, 107 | private_provider, 108 | subdomain_provider, 109 | ): 110 | expected = "https://myserver.com/tiles/{z}/{x}/{y}.png" 111 | assert basic_provider.build_url() == expected 112 | 113 | expected = "https://myserver.com/tiles/3/1/2.png" 114 | assert basic_provider.build_url(1, 2, 3) == expected 115 | assert basic_provider.build_url(1, 2, 3, scale_factor="@2x") == expected 116 | assert silent_retina_provider.build_url(1, 2, 3) == expected 117 | 118 | expected = "https://myserver.com/tiles/3/1/2@2x.png" 119 | assert retina_provider.build_url(1, 2, 3) == expected 120 | assert silent_retina_provider.build_url(1, 2, 3, scale_factor="@2x") == expected 121 | 122 | expected = "https://myserver.com/tiles/3/1/2@5x.png" 123 | assert retina_provider.build_url(1, 2, 3, scale_factor="@5x") == expected 124 | 125 | expected = "https://myserver.com/tiles/{z}/{x}/{y}?access_token=my_token" 126 | assert private_provider.build_url(accessToken="my_token") == expected 127 | 128 | with pytest.raises(ValueError, match="Token is required for this provider"): 129 | private_provider.build_url() 130 | 131 | expected = "https://{s}.myserver.com/tiles/{z}/{x}/{y}.png" 132 | assert subdomain_provider.build_url(fill_subdomain=False) 133 | 134 | expected = "https://a.myserver.com/tiles/{z}/{x}/{y}.png" 135 | assert subdomain_provider.build_url() 136 | 137 | 138 | def test_requires_token(private_provider, basic_provider): 139 | assert private_provider.requires_token() is True 140 | assert basic_provider.requires_token() is False 141 | 142 | 143 | def test_html_repr(basic_provider, retina_provider): 144 | provider_strings = [ 145 | '
    ', 146 | '
    ', 147 | '
    xyzservices.TileProvider
    ', 148 | '
    my_public_provider
    ', 149 | '
    ', 150 | '
    ', 151 | "
    url
    https://myserver.com/tiles/{z}/{x}/{y}.png
    ", 152 | "
    attribution
    (C) xyzservices
    ", 153 | ] 154 | 155 | for html_string in provider_strings: 156 | assert html_string in basic_provider._repr_html_() 157 | 158 | bunch = Bunch({"first": basic_provider, "second": retina_provider}) 159 | 160 | bunch_strings = [ 161 | '
    xyzservices.Bunch
    ', 162 | '
    2 items
    ', 163 | '
      ', 164 | '
    • ', 165 | "xyzservices.TileProvider", 166 | '
      ', 167 | ] 168 | 169 | bunch_repr = bunch._repr_html_() 170 | for html_string in provider_strings + bunch_strings: 171 | assert html_string in bunch_repr 172 | assert bunch_repr.count('
    • ') == 2 173 | assert bunch_repr.count('
      ') == 3 174 | assert bunch_repr.count('
      ') == 3 175 | 176 | 177 | def test_copy(basic_provider): 178 | basic2 = basic_provider.copy() 179 | assert isinstance(basic2, TileProvider) 180 | 181 | 182 | def test_callable(): 183 | # only testing the callable functionality to override a keyword, as we 184 | # cannot test the actual providers that need an API key 185 | original_key = str(xyz.OpenWeatherMap.CloudsClassic["apiKey"]) 186 | updated_provider = xyz.OpenWeatherMap.CloudsClassic(apiKey="mykey") 187 | assert isinstance(updated_provider, TileProvider) 188 | assert "url" in updated_provider 189 | assert updated_provider["apiKey"] == "mykey" 190 | # check that original provider dict is not modified 191 | assert xyz.OpenWeatherMap.CloudsClassic["apiKey"] == original_key 192 | 193 | 194 | def test_html_attribution_fallback(basic_provider, html_attr_provider): 195 | # TileProvider.html_attribution falls back to .attribution if the former not present 196 | assert basic_provider.html_attribution == basic_provider.attribution 197 | assert ( 198 | html_attr_provider.html_attribution 199 | == '© xyzservices' 200 | ) 201 | 202 | 203 | @pytest.mark.xfail(reason="timeout error", raises=URLError) 204 | def test_from_qms(): 205 | provider = TileProvider.from_qms("OpenStreetMap Standard aka Mapnik") 206 | assert isinstance(provider, TileProvider) 207 | 208 | 209 | @pytest.mark.xfail(reason="timeout error", raises=URLError) 210 | def test_from_qms_not_found_error(): 211 | with pytest.raises(ValueError): 212 | TileProvider.from_qms("LolWut") 213 | 214 | 215 | def test_flatten( 216 | basic_provider, retina_provider, silent_retina_provider, private_provider 217 | ): 218 | nested_bunch = Bunch( 219 | first_bunch=Bunch(first=basic_provider, second=retina_provider), 220 | second_bunch=Bunch(first=silent_retina_provider, second=private_provider), 221 | ) 222 | 223 | assert len(nested_bunch) == 2 224 | assert len(nested_bunch.flatten()) == 4 225 | 226 | 227 | def test_filter(test_bunch): 228 | assert len(test_bunch.filter(keyword="private").flatten()) == 1 229 | assert len(test_bunch.filter(keyword="public").flatten()) == 4 230 | assert len(test_bunch.filter(keyword="{s}").flatten()) == 1 231 | assert len(test_bunch.filter(name="retina").flatten()) == 1 232 | assert len(test_bunch.filter(requires_token=True).flatten()) == 1 233 | assert len(test_bunch.filter(requires_token=False).flatten()) == 5 234 | assert len(test_bunch.filter(requires_token=False)) == 4 # check nested structure 235 | assert len(test_bunch.filter(keyword="{s}", requires_token=False).flatten()) == 1 236 | assert len(test_bunch.filter(name="nonsense").flatten()) == 0 237 | 238 | def custom(provider): 239 | if hasattr(provider, "subdomains") and provider.subdomains == "abcd": 240 | return True 241 | return bool(hasattr(provider, "r")) 242 | 243 | assert len(test_bunch.filter(function=custom).flatten()) == 2 244 | 245 | 246 | def test_query_name(): 247 | options = [ 248 | "CartoDB Positron", 249 | "cartodbpositron", 250 | "cartodb-positron", 251 | "carto db/positron", 252 | "CARTO_DB_POSITRON", 253 | "CartoDB.Positron", 254 | "Carto,db,positron", 255 | ] 256 | 257 | for option in options: 258 | queried = xyz.query_name(option) 259 | assert isinstance(queried, TileProvider) 260 | assert queried.name == "CartoDB.Positron" 261 | 262 | with pytest.raises(ValueError, match="No matching provider found"): 263 | xyz.query_name("i don't exist") 264 | 265 | # Name with underscore GH124 266 | option_with_underscore = "NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief" 267 | queried = xyz.query_name(option_with_underscore) 268 | assert isinstance(queried, TileProvider) 269 | assert queried.name == option_with_underscore 270 | -------------------------------------------------------------------------------- /xyzservices/tests/test_providers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import mercantile 4 | import pytest 5 | import requests 6 | 7 | import xyzservices.providers as xyz 8 | 9 | flat_free = xyz.filter(requires_token=False).flatten() 10 | 11 | 12 | def check_provider(provider): 13 | for key in ["attribution", "name"]: 14 | assert key in provider 15 | assert provider.url.startswith("http") 16 | for option in ["{z}", "{y}", "{x}"]: 17 | assert option in provider.url 18 | 19 | 20 | def get_tile(provider): 21 | bounds = provider.get("bounds", [[-180, -90], [180, 90]]) 22 | lat = (bounds[0][0] + bounds[1][0]) / 2 23 | lon = (bounds[0][1] + bounds[1][1]) / 2 24 | zoom = (provider.get("min_zoom", 0) + provider.get("max_zoom", 20)) // 2 25 | tile = mercantile.tile(lon, lat, zoom) 26 | z = tile.z 27 | x = tile.x 28 | y = tile.y 29 | return (z, x, y) 30 | 31 | 32 | def get_response(url): 33 | s = requests.Session() 34 | a = requests.adapters.HTTPAdapter(max_retries=3) 35 | s.mount("http://", a) 36 | s.mount("https://", a) 37 | try: 38 | r = s.get(url, timeout=30) 39 | except requests.ConnectionError: 40 | pytest.xfail("Timeout.") 41 | return r.status_code 42 | 43 | 44 | def get_test_result(provider, allow_403=True): 45 | if provider.get("status"): 46 | pytest.xfail("Provider is known to be broken.") 47 | 48 | z, x, y = get_tile(provider) 49 | 50 | try: 51 | r = get_response(provider.build_url(z=z, x=x, y=y)) 52 | assert r == requests.codes.ok 53 | except AssertionError: 54 | if r == 403 and allow_403: 55 | pytest.xfail("Provider not available due to API restrictions (Error 403).") 56 | 57 | elif r == 503: 58 | pytest.xfail("Service temporarily unavailable (Error 503).") 59 | 60 | elif r == 502: 61 | pytest.xfail("Bad Gateway (Error 502).") 62 | 63 | # check another tiles 64 | elif r == 404: 65 | # in some cases, the computed tile is not available. trying known tiles. 66 | options = [ 67 | (12, 2154, 1363), 68 | (6, 13, 21), 69 | (16, 33149, 22973), 70 | (0, 0, 0), 71 | (2, 6, 7), 72 | (6, 21, 31), 73 | (6, 21, 32), 74 | (6, 21, 33), 75 | (6, 22, 31), 76 | (6, 22, 32), 77 | (6, 22, 33), 78 | (6, 23, 31), 79 | (6, 23, 32), 80 | (6, 23, 33), 81 | (9, 259, 181), 82 | (12, 2074, 1410), 83 | ] 84 | results = [] 85 | for o in options: 86 | z, x, y = o 87 | r = get_response(provider.build_url(z=z, x=x, y=y)) 88 | results.append(r) 89 | if not any(x == requests.codes.ok for x in results): 90 | raise ValueError(f"Response code: {r}") 91 | else: 92 | raise ValueError(f"Response code: {r}") 93 | 94 | 95 | @pytest.mark.parametrize("provider_name", xyz.flatten()) 96 | def test_minimal_provider_metadata(provider_name): 97 | provider = xyz.flatten()[provider_name] 98 | check_provider(provider) 99 | 100 | 101 | @pytest.mark.request 102 | @pytest.mark.parametrize("name", flat_free) 103 | def test_free_providers(name): 104 | provider = flat_free[name] 105 | if "Stadia" in name: 106 | pytest.skip("Stadia doesn't support tile download in this way.") 107 | elif "GeoportailFrance" in name: 108 | try: 109 | get_test_result(provider) 110 | except ValueError: 111 | pytest.xfail("GeoportailFrance API is unstable.") 112 | else: 113 | get_test_result(provider) 114 | 115 | 116 | # test providers requiring API keys. Store API keys in GitHub secrets and load them as 117 | # environment variables in CI Action. Note that env variable is loaded as empty on PRs 118 | # from a fork. 119 | 120 | 121 | @pytest.mark.request 122 | @pytest.mark.parametrize("provider_name", xyz.Thunderforest) 123 | def test_thunderforest(provider_name): 124 | try: 125 | token = os.environ["THUNDERFOREST"] 126 | except KeyError: 127 | pytest.xfail("Missing API token.") 128 | if token == "": 129 | pytest.xfail("Token empty.") 130 | 131 | provider = xyz.Thunderforest[provider_name](apikey=token) 132 | get_test_result(provider, allow_403=False) 133 | 134 | 135 | @pytest.mark.request 136 | @pytest.mark.parametrize("provider_name", xyz.Jawg) 137 | def test_jawg(provider_name): 138 | try: 139 | token = os.environ["JAWG"] 140 | except KeyError: 141 | pytest.xfail("Missing API token.") 142 | if token == "": 143 | pytest.xfail("Token empty.") 144 | 145 | provider = xyz.Jawg[provider_name](accessToken=token) 146 | get_test_result(provider, allow_403=False) 147 | 148 | 149 | @pytest.mark.request 150 | def test_mapbox(): 151 | try: 152 | token = os.environ["MAPBOX"] 153 | except KeyError: 154 | pytest.xfail("Missing API token.") 155 | if token == "": 156 | pytest.xfail("Token empty.") 157 | 158 | provider = xyz.MapBox(accessToken=token) 159 | get_test_result(provider, allow_403=False) 160 | 161 | 162 | @pytest.mark.request 163 | @pytest.mark.parametrize("provider_name", xyz.MapTiler) 164 | def test_maptiler(provider_name): 165 | try: 166 | token = os.environ["MAPTILER"] 167 | except KeyError: 168 | pytest.xfail("Missing API token.") 169 | if token == "": 170 | pytest.xfail("Token empty.") 171 | 172 | provider = xyz.MapTiler[provider_name](key=token) 173 | get_test_result(provider, allow_403=False) 174 | 175 | 176 | @pytest.mark.request 177 | @pytest.mark.parametrize("provider_name", xyz.TomTom) 178 | def test_tomtom(provider_name): 179 | try: 180 | token = os.environ["TOMTOM"] 181 | except KeyError: 182 | pytest.xfail("Missing API token.") 183 | if token == "": 184 | pytest.xfail("Token empty.") 185 | 186 | provider = xyz.TomTom[provider_name](apikey=token) 187 | get_test_result(provider, allow_403=False) 188 | 189 | 190 | @pytest.mark.request 191 | @pytest.mark.parametrize("provider_name", xyz.OpenWeatherMap) 192 | def test_openweathermap(provider_name): 193 | try: 194 | token = os.environ["OPENWEATHERMAP"] 195 | except KeyError: 196 | pytest.xfail("Missing API token.") 197 | if token == "": 198 | pytest.xfail("Token empty.") 199 | 200 | provider = xyz.OpenWeatherMap[provider_name](apiKey=token) 201 | get_test_result(provider, allow_403=False) 202 | 203 | 204 | # HEREV3 seems to block GHA as it errors with E429 205 | # @pytest.mark.request 206 | # @pytest.mark.parametrize("provider_name", xyz.HEREv3) 207 | # def test_herev3(provider_name): 208 | # try: 209 | # token = os.environ["HEREV3"] 210 | # except KeyError: 211 | # pytest.xfail("Missing API token.") 212 | # if token == "": 213 | # pytest.xfail("Token empty.") 214 | 215 | # provider = xyz.HEREv3[provider_name](apiKey=token) 216 | # get_test_result(provider, allow_403=False) 217 | 218 | 219 | @pytest.mark.request 220 | @pytest.mark.parametrize("provider_name", xyz.Stadia) 221 | def test_stadia(provider_name): 222 | try: 223 | token = os.environ["STADIA"] 224 | except KeyError: 225 | pytest.xfail("Missing API token.") 226 | if token == "": 227 | pytest.xfail("Token empty.") 228 | 229 | provider = xyz.Stadia[provider_name](api_key=token) 230 | provider["url"] = provider["url"] + "?api_key={api_key}" 231 | get_test_result(provider, allow_403=False) 232 | 233 | 234 | @pytest.mark.request 235 | @pytest.mark.parametrize("provider_name", xyz.OrdnanceSurvey) 236 | def test_os(provider_name): 237 | try: 238 | token = os.environ["ORDNANCESURVEY"] 239 | except KeyError: 240 | pytest.xfail("Missing API token.") 241 | if token == "": 242 | pytest.xfail("Token empty.") 243 | 244 | provider = xyz.OrdnanceSurvey[provider_name](key=token) 245 | get_test_result(provider, allow_403=False) 246 | 247 | 248 | # NOTE: AzureMaps are not tested as their free account is limited to 249 | # 5000 downloads (total, not per month) 250 | --------------------------------------------------------------------------------