├── xyzservices
├── data
│ └── __init__.py
├── tests
│ ├── __init__.py
│ ├── test_providers.py
│ └── test_lib.py
├── __init__.py
├── providers.py
└── lib.py
├── doc
├── source
│ ├── changelog.rst
│ ├── tiles.png
│ ├── _static
│ │ ├── xyzmaps.jpg
│ │ ├── custom.css
│ │ └── generate_gallery.js
│ ├── gallery.rst
│ ├── api.rst
│ ├── conf.py
│ ├── index.md
│ ├── registration.md
│ ├── contributing.md
│ └── introduction.ipynb
├── requirements.txt
├── Makefile
└── make.bat
├── MANIFEST.in
├── codecov.yml
├── ci
├── latest.yaml
└── update_providers.yaml
├── .github
├── dependabot.yml
├── PULL_REQUEST_TEMPLATE
│ └── community_contribution.md
└── workflows
│ ├── test_providers.yml
│ ├── update_providers.yaml
│ ├── tests.yaml
│ └── release_to_pypi.yml
├── .pre-commit-config.yaml
├── readthedocs.yml
├── Makefile
├── pyproject.toml
├── setup.py
├── LICENSE
├── .gitignore
├── README.md
├── CONTRIBUTING.md
├── provider_sources
├── _parse_leaflet_providers.py
├── _compress_providers.py
└── xyzservices-providers.json
└── CHANGELOG.md
/xyzservices/data/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/xyzservices/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/doc/source/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../CHANGELOG.md
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include xyzservices/data/providers.json
--------------------------------------------------------------------------------
/doc/requirements.txt:
--------------------------------------------------------------------------------
1 | myst-nb
2 | numpydoc
3 | sphinx
4 | sphinx-copybutton
5 | furo
6 | folium
--------------------------------------------------------------------------------
/doc/source/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geopandas/xyzservices/main/doc/source/tiles.png
--------------------------------------------------------------------------------
/doc/source/_static/xyzmaps.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geopandas/xyzservices/main/doc/source/_static/xyzmaps.jpg
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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.13.3"
9 | hooks:
10 | - id: ruff
11 | - id: ruff-format
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/*"]
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v6
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/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@v6
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 'github-actions[bot]'
41 | git config --global user.email '41898282+github-actions[bot]@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 |
--------------------------------------------------------------------------------
/.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@v6
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v6
16 |
17 | - name: Set up Python
18 | uses: actions/setup-python@v6
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](https://github.com/geopandas/xyzservices/actions/workflows/tests.yaml) [](https://codecov.io/gh/geopandas/xyzservices) [](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 |
--------------------------------------------------------------------------------
/doc/source/index.md:
--------------------------------------------------------------------------------
1 | # xyzservices
2 |
3 | Source of XYZ tiles providers.
4 |
5 | 
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 | [](https://github.com/geopandas/xyzservices/actions/workflows/tests.yaml) [](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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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.
--------------------------------------------------------------------------------
/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/_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 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | xyzservices 2025.10.0 (October 30, 2025)
5 | ----------------------------------------
6 |
7 | - Remove deprecated HERE API v2 and rename HEREv3 to HERE updated to API v3
8 |
9 | xyzservices 2025.4.0 (April 25, 2025)
10 | -------------------------------------
11 |
12 | - add UN clear map by @fede-bello in #186
13 |
14 | xyzservices 2025.1.0 (January 18, 2025)
15 | ---------------------------------------
16 |
17 | - Remove OpenStreetMap.BlackAndWhite and properly mark broken as broken by @martinfleis in #183
18 |
19 | xyzservices 2024.9.0 (September 3, 2025)
20 | ----------------------------------------
21 |
22 | - Adding back NASA's BlueMarble by @betolink in #172
23 | - Automated update of GeoportailFrance tiles
24 |
25 | xyzservices 2024.6.0 (June 6, 2024)
26 | ------------------------------------
27 |
28 | Providers:
29 |
30 | - Added ``BlueMarbleBathymetry`` and ``MEaSUREsIceVelocity`` tiles to the ``NASAGIBS``
31 | provider (#168)
32 | - Updated ``GeoportailFrance`` TileMatrixSet information.
33 |
34 | xyzservices 2024.4.0 (April 3, 2024)
35 | ------------------------------------
36 |
37 | Providers:
38 |
39 | - ``GeoportailFrance`` tiles are now using the new ``data.geopf.fr`` domain instead
40 | of deprecated ``wxs.ign.fr`` (#166)
41 | - ``NASAGIBS.BlueMarble3413`` and ``NASAGIBS.BlueMarble3031`` URLs are now fixed (#162)
42 | - ```NASAGIBS.BlueMarble`` was removed as the URL no longer works.
43 | - ``Esri.DeLorme`` and ``NASAGIBS.ModisTerraChlorophyll`` are marked as broken.
44 | - Added ``BaseMapDE`` and ``TopPlusOpen`` providers.
45 | - Addded ``Jawg.Lagoon``, ``MapTiler.Ocean``, ``MapTiler.Backdrop``, ``MapTiler.Dataviz`` tiles.
46 | - Updated ``NLS`` to use their new ``MapTiler`` service.
47 |
48 |
49 | xyzservices 2023.10.1 (October 26, 2023)
50 | ----------------------------------------
51 |
52 | Providers:
53 |
54 | - ``Stamen`` tiles have been removed due to their upstream deprecation.
55 | Use ``Stamen`` styles of ``Stadia`` provider instead.
56 | - ``JusticeMap`` tiles are temporarily marked as broken.
57 |
58 | xyzservices 2023.10.0 (October 5, 2023)
59 | ---------------------------------------
60 |
61 | Providers:
62 |
63 | - ``Stamen`` tiles are now available under ``Stadia`` provider.
64 |
65 | xyzservices 2023.7.0 (July 13, 2023)
66 | ------------------------------------
67 |
68 | Providers:
69 |
70 | - Added ``GeoportailFrance`` ``Orthoimagery_Orthophotos_Irc_express_2023`` and
71 | ``Orthoimagery_Orthophotos_Ortho_express_2023`` layers
72 | - Updated domain for ``OpenStreetMap.DE``
73 | - Marked ``GeoportailFrance.Orthoimagery_Orthophotos_1980_1995`` as possibly broken
74 |
75 | xyzservices 2023.5.0 (May 19, 2023)
76 | -----------------------------------
77 |
78 | Providers:
79 |
80 | - Added ``OrdnanceSurvey`` layers
81 |
82 | xyzservices 2023.2.0 (February 19, 2023)
83 | ----------------------------------------
84 |
85 | Providers:
86 |
87 | - Updated available layers of ``GeoportailFrance``
88 |
89 | Bug fixes:
90 |
91 | - Use ``pkgutil`` instead of ``importlib`` to fetch the JSON if the default in ``share``
92 | is not available. Fixes this fallback for Python 3.8.
93 |
94 | xyzservices 2022.09.0 (September 19, 2022)
95 | ------------------------------------------
96 |
97 | Providers:
98 |
99 | - Added ``GeoportailFrance`` tile layers (#126)
100 |
101 | Enhancements:
102 |
103 | - Better cleaning of names in ``query_name`` method
104 |
105 | Documentation:
106 |
107 | - Added a gallery of included tiles to the documentation (#114)
108 |
109 | xyzservices 2022.06.0 (June 21, 2022)
110 | -------------------------------------
111 |
112 | Providers:
113 |
114 | - Added ``NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief``
115 | - Added ``Esri.ArcticImagery`` (EPSG:5936) and ``Esri.AntarcticImagery`` (EPSG:3031)
116 |
117 | xyzservices 2022.04.0 (April 14, 2022)
118 | --------------------------------------
119 |
120 | Providers:
121 |
122 | - Update ``OpenStreetMap.DE`` URL
123 | - Remove broken Hydda tiles
124 |
125 | xyzservices 2022.03.0 (March 9, 2022)
126 | -------------------------------------
127 |
128 | Providers:
129 |
130 | - Added ``Esri`` ``ArcticOceanBase``, ``ArcticOceanReference`` and ``AntarcticBasemap``
131 |
132 | xyzservices 2022.02.0 (February 10, 2022)
133 | ----------------------------------------
134 |
135 | Providers:
136 |
137 | - Fixed ``MapTiler.Winter``
138 | - Updated ``AzureMaps`` links
139 |
140 | xyzservices 2022.01.1 (January 20, 2022)
141 | ----------------------------------------
142 |
143 | Providers:
144 |
145 | - Added ``NASAGIBS.BlueMarble`` datasets in EPSG 3857 (default), 3413, and 3031
146 | - Added more ``MapTiler`` providers (``Outdoor``, ``Topographique``, ``Winter``, ``Satellite``, ``Terrain``, and ``Basic4326`` in ESPG 4326).
147 |
148 | xyzservices 2022.01.0 (January 17, 2022)
149 | ----------------------------------------
150 |
151 | Providers:
152 |
153 | - Added ``SwissFederalGeoportal`` providers (``NationalMapColor``, ``NationalMapGrey``, ``SWISSIMAGE``, ``JourneyThroughTime``)
154 |
155 | xyzservices 2021.11.0 (November 06, 2021)
156 | ----------------------------------------
157 |
158 | Providers:
159 |
160 | - Updated deprecated links to ``nlmaps`` providers
161 | - Added ``nlmaps.water``
162 |
163 | xyzservices 2021.10.0 (October 19, 2021)
164 | ----------------------------------------
165 |
166 | Providers:
167 |
168 | - Added ``OPNVKarte`` map
169 | - Removed discontinued ``OpenPtMap``
170 | - Max zoom of ``CartoDB`` tiles changed from 19 to 20
171 |
172 | xyzservices 2021.09.1 (September 20, 2021)
173 | ------------------------------------------
174 |
175 | New functionality:
176 |
177 | - Added ``Bunch.query_name()`` method allowing to fetch the ``TileProvider`` object based on the name with flexible formatting. (#93)
178 |
179 | xyzservices 2021.09.0 (September 3, 2021)
180 | -----------------------------------------
181 |
182 | Providers:
183 |
184 | - Fixed ``Strava`` maps (#85)
185 | - Fixed ``nlmaps.luchtfoto`` (#90)
186 | - Fixed ``NASAGIBS.ModisTerraSnowCover`` (#90)
187 | - ``JusticeMap`` and ``OpenAIP`` now use https instead of http
188 |
189 | xyzservices 2021.08.1 (August 12, 2021)
190 | ---------------------------------------
191 |
192 | Providers:
193 |
194 | - Added ``OpenStreetMap.BlackAndWhite`` (#83)
195 | - Added ``Gaode`` tiles (``Normal`` and ``Satellite``) (#83)
196 | - Expanded ``NASAGIBS`` tiles with ``ModisTerraBands721CR``, ``ModisAquaTrueColorCR``, ``ModisAquaBands721CR`` and ``ViirsTrueColorCR`` (#83)
197 | - Added metadata to ``Strava`` maps (currently down) (#83)
198 |
199 | xyzservices 2021.08.0 (August 8, 2021)
200 | --------------------------------------
201 |
202 | New functionality:
203 |
204 | - Added ``TileProvider.from_qms()`` allowing to create a ``TileProvider`` object from the remote [Quick Map Services](https://qms.nextgis.com/about) repository (#71)
205 | - Added support of ``html_attribution`` to have live links in attributions in HTML-based outputs like leaflet (#60)
206 | - New ``Bunch.flatten`` method creating a flat dictionary of ``TileProvider`` objects based on a nested ``Bunch`` (#68)
207 | - Added ``fill_subdomain`` keyword to ``TileProvider.build_url`` to control ``{s}`` placeholder in the URL (#75)
208 | - New Bunch.filter method to filter specific providers based on keywords and other criteria (#76)
209 |
210 | Minor enhancements:
211 |
212 | - Indent providers JSON file for better readability (#64)
213 | - Support dark themes in HTML repr (#70)
214 | - Mark broken providers with ``status="broken"`` attribute (#78)
215 | - Document providers requiring registrations (#79)
216 |
217 | xyzservices 2021.07 (July 30, 2021)
218 | -----------------------------------
219 |
220 | The initial release provides ``TileProvider`` and ``Bunch`` classes and an initial set of providers.
221 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | '