diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ce69c82..a368421 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,6 +52,41 @@ jobs:
name: Python${{ matrix.python-version }}
fail_ci_if_error: false
+ benchmark:
+ needs: [tests]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -e ".[benchmark]"
+
+ - name: Run Benchmark
+ run: |
+ python -m pytest tests/benchmarks.py --benchmark-only --benchmark-columns 'min, max, mean, median' --benchmark-sort 'min' --benchmark-json output.json
+
+ - name: Store and Compare benchmark result
+ uses: benchmark-action/github-action-benchmark@v1
+ with:
+ name: morecantile Benchmarks
+ tool: 'pytest'
+ output-file-path: output.json
+ alert-threshold: '150%'
+ comment-on-alert: true
+ fail-on-alert: false
+ # GitHub API token to make a commit comment
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ gh-pages-branch: 'gh-benchmarks'
+ # Make a commit on `gh-pages` only if main
+ auto-push: ${{ github.ref == 'refs/heads/main' }}
+ benchmark-data-dir-path: dev/benchmarks
+
publish:
needs: [tests]
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 23077b4..788139f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,3 +130,7 @@ dmypy.json
# PyCharm:
.idea
+
+# VSCode
+.vscode
+.vscode/
diff --git a/CHANGES.md b/CHANGES.md
index b2c8882..d82944c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,14 @@
+## 6.1.0 (2024-10-17)
+
+* add `_tile_matrices_idx: Dict[str, int]` private attribute to improve `matrices` lookup
+* change `xy_bounds()` and `bounds()` methods to avoid calculation duplication
+
+## 6.0.0 (2024-10-16)
+
+* remove `_geographic_crs` private attribute in `TileMatrixSet` model **breaking change**
+* use `crs.geodetic_crs` property as `geographic_crs` **breaking change**
+
## 5.4.2 (2024-08-29)
* better handle anti-meridian crossing bbox in `tms.tiles()` (author @ljstrnadiii, https://github.com/developmentseed/morecantile/pull/154)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7ea8ce0..d443410 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,6 +24,13 @@ This repo is set to use `pre-commit` to run *isort*, *flake8*, *pydocstring*, *b
$ pre-commit install
```
+##### Performance tests
+
+```sh
+python -m pip install -e ".[benchmark]"
+python -m pytest tests/benchmarks.py --benchmark-only --benchmark-columns 'min, max, mean, median' --benchmark-sort 'min'
+```
+
### Docs
```bash
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..a2d4f66
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,63 @@
+
+## Benchmark
+
+Compare `mercantile`, `utiles` and `morecantile`
+
+```sh
+python -m pip install -r requirements.txt
+python -m pytest benchmarks.py --benchmark-only --benchmark-columns 'min, max, mean, median' --benchmark-sort 'min'
+```
+
+```
+----------------------------------------------------- benchmark 'bounds': 21 tests ----------------------------------------------------
+Name (time in ns) Min Max Mean Median
+---------------------------------------------------------------------------------------------------------------------------------------
+test_bounds[utiles-(486, 332, 30)] 83.0000 (1.0) 52,000.0000 (52.97) 195.3371 (1.48) 208.0000 (1.60)
+test_bounds[utiles-(0, 0, 0)] 83.0000 (1.00) 31,167.0000 (31.75) 193.5957 (1.47) 208.0000 (1.60)
+test_bounds[utiles-(1, 0, 1)] 125.0000 (1.51) 981.6700 (1.0) 132.0177 (1.0) 130.8300 (1.01)
+test_bounds[utiles-(1, 40, 7)] 125.0000 (1.51) 8,606.2500 (8.77) 132.5298 (1.00) 130.0000 (1.0)
+test_bounds[utiles-(486, 332, 10)] 128.3300 (1.55) 1,600.8300 (1.63) 133.4046 (1.01) 131.6700 (1.01)
+test_bounds[utiles-(1, 1, 1)] 129.5900 (1.56) 16,846.2500 (17.16) 141.9846 (1.08) 134.1700 (1.03)
+test_bounds[utiles-(486, 332, 20)] 138.3300 (1.67) 1,123.3300 (1.14) 142.5420 (1.08) 141.2500 (1.09)
+test_bounds[mercantile-(1, 40, 7)] 1,000.0000 (12.05) 92,416.0000 (94.14) 1,193.0112 (9.04) 1,167.0000 (8.98)
+test_bounds[mercantile-(1, 1, 1)] 1,075.0000 (12.95) 48,066.8000 (48.96) 1,154.8100 (8.75) 1,141.6000 (8.78)
+test_bounds[mercantile-(0, 0, 0)] 1,083.0000 (13.05) 21,041.0000 (21.43) 1,214.6049 (9.20) 1,208.0000 (9.29)
+test_bounds[mercantile-(1, 0, 1)] 1,083.0000 (13.05) 75,291.0000 (76.70) 1,213.7837 (9.19) 1,208.0000 (9.29)
+test_bounds[mercantile-(486, 332, 10)] 1,083.0000 (13.05) 55,750.0000 (56.79) 1,223.7598 (9.27) 1,208.0000 (9.29)
+test_bounds[mercantile-(486, 332, 20)] 1,083.0000 (13.05) 89,041.0000 (90.70) 1,248.4256 (9.46) 1,209.0000 (9.30)
+test_bounds[mercantile-(486, 332, 30)] 1,083.0000 (13.05) 194,958.0000 (198.60) 1,239.4339 (9.39) 1,209.0000 (9.30)
+test_bounds[morecantile-(1, 0, 1)] 20,083.0000 (241.96) 132,625.0000 (135.10) 20,893.1235 (158.26) 20,667.0000 (158.98)
+test_bounds[morecantile-(0, 0, 0)] 20,166.0000 (242.96) 120,333.0000 (122.58) 20,974.1961 (158.87) 20,708.0000 (159.29)
+test_bounds[morecantile-(1, 1, 1)] 20,334.0000 (244.99) 60,958.0000 (62.10) 21,236.2953 (160.86) 20,750.0000 (159.62)
+test_bounds[morecantile-(1, 40, 7)] 21,667.0000 (261.05) 173,458.0000 (176.70) 22,622.6537 (171.36) 22,333.0000 (171.79)
+test_bounds[morecantile-(486, 332, 10)] 22,166.0000 (267.06) 205,125.0000 (208.96) 23,512.1607 (178.10) 22,875.0000 (175.96)
+test_bounds[morecantile-(486, 332, 20)] 24,708.0000 (297.69) 129,250.0000 (131.66) 25,775.0796 (195.24) 25,375.0000 (195.19)
+test_bounds[morecantile-(486, 332, 30)] 71,292.0000 (858.94) 205,625.0000 (209.46) 73,569.4565 (557.27) 72,667.0000 (558.98)
+---------------------------------------------------------------------------------------------------------------------------------------
+
+---------------------------------------------------- benchmark 'xy_bounds': 21 tests -----------------------------------------------------
+Name (time in ns) Min Max Mean Median
+------------------------------------------------------------------------------------------------------------------------------------------
+test_xy_bounds[utiles-(0, 0, 0)] 83.0000 (1.0) 57,375.0000 (112.55) 145.9127 (1.57) 125.0000 (1.36)
+test_xy_bounds[utiles-(1, 1, 1)] 88.3400 (1.06) 1,335.4200 (2.62) 93.1478 (1.00) 92.5000 (1.00)
+test_xy_bounds[utiles-(1, 40, 7)] 88.7500 (1.07) 1,451.2500 (2.85) 93.1258 (1.0) 92.0900 (1.0)
+test_xy_bounds[utiles-(1, 0, 1)] 89.5900 (1.08) 861.6600 (1.69) 93.6428 (1.01) 93.3300 (1.01)
+test_xy_bounds[utiles-(486, 332, 10)] 90.0000 (1.08) 509.7950 (1.0) 93.2818 (1.00) 92.9150 (1.01)
+test_xy_bounds[utiles-(486, 332, 20)] 90.4100 (1.09) 864.1700 (1.70) 94.2974 (1.01) 94.1600 (1.02)
+test_xy_bounds[utiles-(486, 332, 30)] 90.4100 (1.09) 1,377.5000 (2.70) 94.7366 (1.02) 94.1600 (1.02)
+test_xy_bounds[mercantile-(0, 0, 0)] 708.0000 (8.53) 42,250.0000 (82.88) 843.2292 (9.05) 833.0000 (9.05)
+test_xy_bounds[mercantile-(486, 332, 10)] 731.2500 (8.81) 5,393.7500 (10.58) 770.8331 (8.28) 764.6000 (8.30)
+test_xy_bounds[mercantile-(486, 332, 30)] 731.2500 (8.81) 7,316.6500 (14.35) 770.2423 (8.27) 764.6000 (8.30)
+test_xy_bounds[mercantile-(1, 0, 1)] 732.1429 (8.82) 9,404.8571 (18.45) 791.7721 (8.50) 785.7143 (8.53)
+test_xy_bounds[mercantile-(1, 40, 7)] 733.3000 (8.83) 5,950.0000 (11.67) 772.3273 (8.29) 766.7000 (8.33)
+test_xy_bounds[mercantile-(486, 332, 20)] 733.3500 (8.84) 7,408.3000 (14.53) 773.4718 (8.31) 768.7500 (8.35)
+test_xy_bounds[mercantile-(1, 1, 1)] 735.4000 (8.86) 4,762.5000 (9.34) 783.2100 (8.41) 777.1000 (8.44)
+test_xy_bounds[morecantile-(0, 0, 0)] 3,042.0000 (36.65) 108,208.0000 (212.26) 3,287.1089 (35.30) 3,250.0000 (35.29)
+test_xy_bounds[morecantile-(1, 0, 1)] 3,291.0000 (39.65) 68,166.0000 (133.71) 3,503.4695 (37.62) 3,459.0000 (37.56)
+test_xy_bounds[morecantile-(1, 1, 1)] 3,291.0000 (39.65) 72,875.0000 (142.95) 3,506.8544 (37.66) 3,500.0000 (38.01)
+test_xy_bounds[morecantile-(1, 40, 7)] 4,375.0000 (52.71) 76,375.0000 (149.82) 4,630.2893 (49.72) 4,584.0000 (49.78)
+test_xy_bounds[morecantile-(486, 332, 10)] 4,875.0000 (58.73) 85,208.0000 (167.14) 5,197.3581 (55.81) 5,125.0000 (55.65)
+test_xy_bounds[morecantile-(486, 332, 20)] 6,916.0000 (83.33) 78,458.0000 (153.90) 7,221.0669 (77.54) 7,166.0000 (77.82)
+test_xy_bounds[morecantile-(486, 332, 30)] 52,667.0000 (634.54) 404,208.0000 (792.88) 54,749.1232 (587.90) 54,333.0000 (590.00)
+------------------------------------------------------------------------------------------------------------------------------------------
+```
diff --git a/benchmarks/benchmarks.py b/benchmarks/benchmarks.py
new file mode 100644
index 0000000..f801a70
--- /dev/null
+++ b/benchmarks/benchmarks.py
@@ -0,0 +1,96 @@
+"""Morecantile/Mercantile/Utiles comparison benchmark
+
+The benchmark suite is adapted from jessekrubin/utiles
+https://github.com/jessekrubin/utiles/blob/ea58b9a017a2e3528f03cc20f16ef531737b863f/utiles-pyo3/bench/test_bench.py#L17-L25
+"""
+# This file is a modified version of https://github.com/jessekrubin/utiles/blob/ea58b9a017a2e3528f03cc20f16ef531737b863f/utiles-pyo3/bench/test_bench.py.
+#
+# The original license follows.
+#
+# MIT License
+#
+# Copyright (c) 2023 jessekrubin
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from typing import Callable, Tuple
+
+import mercantile
+import pytest
+import utiles
+
+import morecantile
+
+tms = morecantile.tms.get("WebMercatorQuad")
+
+TEST_TILES = (
+ (0, 0, 0),
+ (1, 0, 1),
+ (1, 1, 1),
+ (1, 40, 7),
+ (486, 332, 10),
+ # HIGH ZOOM
+ (486, 332, 20),
+ # OUTSIDE TMS Range
+ (486, 332, 30),
+)
+
+
+@pytest.mark.parametrize(
+ "tile",
+ [pytest.param(t, id=str(t)) for t in TEST_TILES],
+)
+@pytest.mark.parametrize(
+ "func",
+ [
+ pytest.param(mercantile.bounds, id="mercantile"),
+ pytest.param(tms.bounds, id="morecantile"),
+ pytest.param(utiles.bounds, id="utiles"),
+ ],
+)
+@pytest.mark.benchmark(group="bounds")
+def test_bounds(
+ tile: Tuple[int, int, int],
+ func: Callable[[Tuple[int, int, int]], Tuple[float, float]],
+ benchmark,
+) -> None:
+ """Benchmark bounds() method."""
+ _ = benchmark(func, *tile)
+
+
+@pytest.mark.parametrize(
+ "tile",
+ [pytest.param(t, id=str(t)) for t in TEST_TILES],
+)
+@pytest.mark.parametrize(
+ "func",
+ [
+ pytest.param(mercantile.xy_bounds, id="mercantile"),
+ pytest.param(tms.xy_bounds, id="morecantile"),
+ pytest.param(utiles.xy_bounds, id="utiles"),
+ ],
+)
+@pytest.mark.benchmark(group="xy_bounds")
+def test_xy_bounds(
+ tile: Tuple[int, int, int],
+ func: Callable[[Tuple[int, int, int]], Tuple[float, float]],
+ benchmark,
+) -> None:
+ """Benchmark xy_bounds() method."""
+ _ = benchmark(func, *tile)
diff --git a/benchmarks/requirements.txt b/benchmarks/requirements.txt
new file mode 100644
index 0000000..e11e111
--- /dev/null
+++ b/benchmarks/requirements.txt
@@ -0,0 +1,6 @@
+pytest
+pytest-benchmark
+
+morecantile
+mercantile
+utiles
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 98a8695..1cae0f4 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -31,6 +31,7 @@ nav:
- morecantile.models: api/morecantile/models.md
- morecantile.utils: api/morecantile/utils.md
- CLI: 'cli.md'
+ - Benchmarking: benchmark.html
- Development - Contributing: 'contributing.md'
- Release: 'release-notes.md'
diff --git a/docs/src/benchmark.html b/docs/src/benchmark.html
new file mode 100644
index 0000000..fb9bee3
--- /dev/null
+++ b/docs/src/benchmark.html
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+ Benchmarks
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/usage.md b/docs/src/usage.md
index d43f6c5..496aea5 100644
--- a/docs/src/usage.md
+++ b/docs/src/usage.md
@@ -103,8 +103,6 @@ Here are the available options:
- **id** (*str, defaults to `Custom`*): Tile Matrix Set identifier
-- **geographic_crs** (*pyproj.CRS, defaults to `EPSG:4326`*): Geographic (lat,lon) coordinate reference system
-
- **ordered_axes** (*list of str, Optional*): Override Axis order (e.g `["N", "S"]`) else default to CRS's metadata
- **screen_pixel_size** (*float, optional*): Rendering pixel size. `0.28` mm was the actual pixel size of a common display from 2005 and considered as standard by OGC.
diff --git a/morecantile/__init__.py b/morecantile/__init__.py
index 3c96b86..74bf0dd 100644
--- a/morecantile/__init__.py
+++ b/morecantile/__init__.py
@@ -8,7 +8,7 @@
"""
-__version__ = "5.4.2"
+__version__ = "6.1.0"
from .commons import BoundingBox, Coords, Tile # noqa
from .defaults import TileMatrixSets, tms # noqa
diff --git a/morecantile/models.py b/morecantile/models.py
index 5ef1715..8f1b194 100644
--- a/morecantile/models.py
+++ b/morecantile/models.py
@@ -40,7 +40,6 @@
NumType = Union[float, int]
BoundsType = Tuple[NumType, NumType]
LL_EPSILON = 1e-11
-WGS84_CRS = pyproj.CRS.from_epsg(4326)
axesInfo = Annotated[List[str], Field(min_length=2, max_length=2)]
@@ -499,24 +498,25 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
]
# Private attributes
- _geographic_crs: pyproj.CRS = PrivateAttr(default=WGS84_CRS)
_to_geographic: pyproj.Transformer = PrivateAttr()
_from_geographic: pyproj.Transformer = PrivateAttr()
+ _tile_matrices_idx: Dict[int, int] = PrivateAttr()
+
def __init__(self, **data):
"""Set private attributes."""
super().__init__(**data)
- self._geographic_crs = pyproj.CRS.from_user_input(
- data.get("_geographic_crs", WGS84_CRS)
- )
+ self._tile_matrices_idx = {
+ int(mat.id): idx for idx, mat in enumerate(self.tileMatrices)
+ }
try:
self._to_geographic = pyproj.Transformer.from_crs(
- self.crs._pyproj_crs, self._geographic_crs, always_xy=True
+ self.crs._pyproj_crs, self.crs._pyproj_crs.geodetic_crs, always_xy=True
)
self._from_geographic = pyproj.Transformer.from_crs(
- self._geographic_crs, self.crs._pyproj_crs, always_xy=True
+ self.crs._pyproj_crs.geodetic_crs, self.crs._pyproj_crs, always_xy=True
)
except ProjError:
warnings.warn(
@@ -568,7 +568,7 @@ def __repr__(self):
@cached_property
def geographic_crs(self) -> pyproj.CRS:
"""Return the TMS's geographic CRS."""
- return self._geographic_crs
+ return self.crs._pyproj_crs.geodetic_crs
@cached_property
def rasterio_crs(self):
@@ -578,7 +578,7 @@ def rasterio_crs(self):
@cached_property
def rasterio_geographic_crs(self):
"""Return the geographic CRS as a rasterio CRS."""
- return to_rasterio_crs(self._geographic_crs)
+ return to_rasterio_crs(self.crs._pyproj_crs.geodetic_crs)
@property
def minzoom(self) -> int:
@@ -669,7 +669,6 @@ def custom(
title: Optional[str] = None,
id: Optional[str] = None,
ordered_axes: Optional[List[str]] = None,
- geographic_crs: pyproj.CRS = WGS84_CRS,
screen_pixel_size: float = 0.28e-3,
decimation_base: int = 2,
**kwargs: Any,
@@ -702,8 +701,6 @@ def custom(
Tile Matrix Set title
id: str, optional
Tile Matrix Set identifier
- geographic_crs: pyproj.CRS
- Geographic (lat,lon) coordinate reference system (default is EPSG:4326)
ordered_axes: list of str, optional
Override Axis order (e.g `["N", "S"]`) else default to CRS's metadata
screen_pixel_size: float, optional
@@ -782,15 +779,13 @@ def custom(
tileMatrices=tile_matrices,
id=id,
title=title,
- _geographic_crs=geographic_crs,
**kwargs,
)
def matrix(self, zoom: int) -> TileMatrix:
"""Return the TileMatrix for a specific zoom."""
- for m in self.tileMatrices:
- if m.id == str(zoom):
- return m
+ if (idx := self._tile_matrices_idx.get(zoom, None)) is not None:
+ return self.tileMatrices[idx]
#######################################################################
# If user wants a deeper matrix we calculate it
@@ -1116,8 +1111,23 @@ def xy_bounds(self, *tile: Tile) -> BoundingBox:
"""
t = _parse_tile_arg(*tile)
- left, top = self._ul(t)
- right, bottom = self._lr(t)
+ matrix = self.matrix(t.z)
+ origin_x, origin_y = self._matrix_origin(matrix)
+
+ cf = (
+ matrix.get_coalesce_factor(t.y)
+ if matrix.variableMatrixWidths is not None
+ else 1
+ )
+
+ left = origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
+ top = origin_y - t.y * matrix.cellSize * matrix.tileHeight
+ right = (
+ origin_x
+ + (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
+ )
+ bottom = origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight
+
return BoundingBox(left, bottom, right, top)
def ul(self, *tile: Tile) -> Coords:
@@ -1169,10 +1179,10 @@ def bounds(self, *tile: Tile) -> BoundingBox:
BoundingBox: The bounding box of the input tile.
"""
- t = _parse_tile_arg(*tile)
+ _left, _bottom, _right, _top = self.xy_bounds(*tile)
+ left, top = self.lnglat(_left, _top)
+ right, bottom = self.lnglat(_right, _bottom)
- left, top = self.ul(t)
- right, bottom = self.lr(t)
return BoundingBox(left, bottom, right, top)
@property
diff --git a/pyproject.toml b/pyproject.toml
index 91cdb20..63d7bb0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,6 +36,10 @@ test = [
"pytest-cov",
"rasterio>=1.2.1",
]
+benchmark = [
+ "pytest",
+ "pytest-benchmark",
+]
dev = [
"pre-commit",
"bump-my-version",
@@ -116,7 +120,7 @@ filterwarnings = [
]
[tool.bumpversion]
-current_version = "5.4.2"
+current_version = "6.1.0"
search = "{current_version}"
replace = "{new_version}"
diff --git a/tests/benchmarks.py b/tests/benchmarks.py
new file mode 100644
index 0000000..166d9de
--- /dev/null
+++ b/tests/benchmarks.py
@@ -0,0 +1,43 @@
+"""Morecantile benchmark."""
+
+import pytest
+
+import morecantile
+from morecantile.commons import BoundingBox
+
+tms = morecantile.tms.get("WebMercatorQuad")
+
+# Test tiles from https://github.com/jessekrubin/utiles/blob/ea58b9a017a2e3528f03cc20f16ef531737b863f/utiles-pyo3/bench/test_bench.py
+TEST_TILES = (
+ (0, 0, 0),
+ (1, 0, 1),
+ (1, 1, 1),
+ (1, 40, 7),
+ (486, 332, 10),
+ # HIGH ZOOM
+ (486, 332, 20),
+ # OUTSIDE TMS Range
+ (486, 332, 30),
+)
+
+
+@pytest.mark.parametrize("tile", TEST_TILES)
+def test_bounds(tile, benchmark):
+ str_tile = "Tile(x={},y={},z={})".format(*tile)
+ benchmark.name = f"morecantile.bounds-{str_tile}"
+ benchmark.fullname = f"morecantile.bounds-{str_tile}"
+ benchmark.group = "morecantile.bounds"
+
+ r = benchmark(tms.bounds, *tile)
+ assert isinstance(r, BoundingBox)
+
+
+@pytest.mark.parametrize("tile", TEST_TILES)
+def test_xy_bounds(tile, benchmark) -> None:
+ str_tile = "Tile(x={},y={},z={})".format(*tile)
+ benchmark.name = f"morecantile.xy_bounds-{str_tile}"
+ benchmark.fullname = f"morecantile.xy_bounds-{str_tile}"
+ benchmark.group = "morecantile.xy_bounds"
+
+ r = benchmark(tms.xy_bounds, *tile)
+ assert isinstance(r, BoundingBox)
diff --git a/tests/test_models.py b/tests/test_models.py
index 95e4c33..c1a84ca 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -30,9 +30,24 @@ def test_tile_matrix_set(tileset):
ts = TileMatrixSet.model_validate_json(f.read())
# This would fail if `crs` isn't supported by PROJ
assert isinstance(ts.crs._pyproj_crs, pyproj.CRS)
+ assert isinstance(ts.geographic_crs, pyproj.CRS)
assert repr(ts)
+@pytest.mark.parametrize("tileset", tilesets)
+def test_geographic_crs_bbox(tileset):
+ """check that geographic bounds are correct."""
+ with open(tileset, "r") as f:
+ ts = TileMatrixSet.model_validate_json(f.read())
+
+ if not pyproj.CRS.from_epsg(4326) == ts.geographic_crs:
+ _to_geographic = pyproj.Transformer.from_crs(
+ ts.crs._pyproj_crs, pyproj.CRS.from_epsg(4326), always_xy=True
+ )
+ bbox = _to_geographic.transform_bounds(*ts.xy_bbox, densify_pts=21)
+ assert bbox == ts.bbox
+
+
def test_tile_matrix_iter():
"""Test iterator"""
tms = morecantile.tms.get("WebMercatorQuad")
@@ -163,17 +178,13 @@ def test_Custom():
assert round(wmMat.pointOfOrigin[0], 6) == round(cusMat.pointOfOrigin[0], 6)
extent = (-20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892)
- custom_tms = TileMatrixSet.custom(
- extent, pyproj.CRS.from_epsg(3857), geographic_crs="epsg:4326"
- )
- assert isinstance(custom_tms._geographic_crs, pyproj.CRS)
- assert custom_tms._geographic_crs == pyproj.CRS.from_epsg(4326)
+ custom_tms = TileMatrixSet.custom(extent, pyproj.CRS.from_epsg(3857))
+ assert isinstance(custom_tms.geographic_crs, pyproj.CRS)
+ assert custom_tms.geographic_crs == pyproj.CRS.from_epsg(4326)
extent = (-20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892)
- custom_tms = TileMatrixSet.custom(
- extent, pyproj.CRS.from_epsg(3857), geographic_crs=pyproj.CRS.from_epsg(4326)
- )
- assert isinstance(custom_tms._geographic_crs, pyproj.CRS)
+ custom_tms = TileMatrixSet.custom(extent, pyproj.CRS.from_epsg(3857))
+ assert isinstance(custom_tms.geographic_crs, pyproj.CRS)
def test_custom_tms_bounds_epsg4326():
@@ -300,8 +311,7 @@ def test_schema():
"+proj=stere +lat_0=90 +lon_0=0 +k=2 +x_0=0 +y_0=0 +R=3396190 +units=m +no_defs"
)
extent = [-13584760.000, -13585240.000, 13585240.000, 13584760.000]
- with pytest.warns(UserWarning):
- tms = morecantile.TileMatrixSet.custom(extent, crs, id="MarsNPolek2MOLA5k")
+ tms = morecantile.TileMatrixSet.custom(extent, crs, id="MarsNPolek2MOLA5k")
assert tms.model_json_schema()
assert tms.model_dump(exclude_none=True)
json_doc = json.loads(tms.model_dump_json(exclude_none=True))
@@ -316,13 +326,12 @@ def test_schema():
assert json_doc["crs"] == "http://www.opengis.net/def/crs/EPSG/0/3031"
-MARS2000_SPHERE = pyproj.CRS.from_proj4("+proj=longlat +R=3396190 +no_defs")
-
-
def test_mars_tms():
"""The Mars global mercator scheme should broadly align with the Earth
Web Mercator CRS, despite the different planetary radius and scale.
"""
+ MARS2000_SPHERE = pyproj.CRS.from_proj4("+proj=longlat +R=3396190 +no_defs")
+
MARS_MERCATOR = pyproj.CRS.from_proj4(
"+proj=merc +R=3396190 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +no_defs"
)
@@ -338,8 +347,8 @@ def test_mars_tms():
MARS_MERCATOR,
extent_crs=MARS2000_SPHERE,
title="Web Mercator Mars",
- geographic_crs=MARS2000_SPHERE,
)
+ assert mars_tms.geographic_crs == MARS2000_SPHERE
pos = (35, 40, 3)
mars_tile = mars_tms.tile(*pos)
@@ -350,9 +359,17 @@ def test_mars_tms():
assert mars_tile.y == earth_tile.y
assert mars_tile.z == earth_tile.z == 3
+ _to_geographic = pyproj.Transformer.from_crs(
+ mars_tms.crs._pyproj_crs, MARS2000_SPHERE, always_xy=True
+ )
+ bbox = _to_geographic.transform_bounds(*mars_tms.xy_bbox, densify_pts=21)
+ assert bbox == mars_tms.bbox
+
def test_mars_local_tms():
"""Local TMS using Mars CRS"""
+ MARS2000_SPHERE = pyproj.CRS.from_proj4("+proj=longlat +R=3396190 +no_defs")
+
# A transverse mercator projection for the landing site of the Perseverance rover.
SYRTIS_TM = pyproj.CRS.from_proj4(
"+proj=tmerc +lat_0=17 +lon_0=76.5 +k=0.9996 +x_0=0 +y_0=0 +a=3396190 +b=3376200 +units=m +no_defs"
@@ -362,15 +379,77 @@ def test_mars_local_tms():
[-5e5, -5e5, 5e5, 5e5],
SYRTIS_TM,
title="Web Mercator Mars",
- geographic_crs=MARS2000_SPHERE,
)
assert SYRTIS_TM == syrtis_tms.crs._pyproj_crs
+ assert syrtis_tms.geographic_crs
assert syrtis_tms.model_dump(mode="json")
center = syrtis_tms.ul(1, 1, 1)
assert round(center.x, 6) == 76.5
assert round(center.y, 6) == 17
+ _to_geographic = pyproj.Transformer.from_crs(
+ syrtis_tms.crs._pyproj_crs, MARS2000_SPHERE, always_xy=True
+ )
+ bbox = _to_geographic.transform_bounds(*syrtis_tms.xy_bbox, densify_pts=21)
+ assert bbox == syrtis_tms.bbox
+
+
+def test_mars_tms_construction():
+ mars_sphere_crs = pyproj.CRS.from_user_input("IAU_2015:49900")
+ extent = [-180.0, -90.0, 180.0, 90.0]
+ mars_tms = morecantile.TileMatrixSet.custom(
+ extent,
+ crs=mars_sphere_crs,
+ id="MarsGeographicCRS",
+ matrix_scale=[2, 1],
+ )
+ assert "4326" not in mars_tms.geographic_crs.to_wkt()
+ assert "4326" not in mars_tms.rasterio_geographic_crs.to_wkt()
+ assert mars_tms.xy_bbox.left == pytest.approx(-180.0)
+ assert mars_tms.xy_bbox.bottom == pytest.approx(-90.0)
+ assert mars_tms.xy_bbox.right == pytest.approx(180.0)
+ assert mars_tms.xy_bbox.top == pytest.approx(90.0)
+
+
+def test_mars_web_mercator_long_lat():
+ wkt_mars_web_mercator = 'PROJCRS["Mars (2015) - Sphere XY / Pseudo-Mercator",BASEGEOGCRS["Mars (2015) - Sphere",DATUM["Mars (2015) - Sphere",ELLIPSOID["Mars (2015) - Sphere",3396190,0,LENGTHUNIT["metre",1,ID["EPSG",9001]]],ANCHOR["Viking 1 lander : 47.95137 W"]],PRIMEM["Reference Meridian",0,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1,ID["EPSG",9001]]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1,ID["EPSG",9001]]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06 S and 85.06 N."],BBOX[-85.050511287,-180,85.050511287,180]],REMARK["Use semi-major radius as sphere radius for interoperability. Source of IAU Coordinate systems: doi:10.1007/s10569-017-9805-5"]]'
+ crs_mars_web_mercator = pyproj.CRS.from_wkt(wkt_mars_web_mercator)
+ extent_wm = [
+ -10669445.554195119,
+ -10669445.554195119,
+ 10669445.554195119,
+ 10669445.554195119,
+ ]
+ mars_tms_wm = morecantile.TileMatrixSet.custom(
+ extent_wm,
+ crs=crs_mars_web_mercator,
+ id="MarsWebMercator",
+ )
+ assert "4326" not in mars_tms_wm.geographic_crs.to_wkt()
+ assert "4326" not in mars_tms_wm.rasterio_geographic_crs.to_wkt()
+ assert mars_tms_wm.bbox.left == pytest.approx(-180.0)
+ assert mars_tms_wm.bbox.bottom == pytest.approx(-85.0511287)
+ assert mars_tms_wm.bbox.right == pytest.approx(180.0)
+ assert mars_tms_wm.bbox.top == pytest.approx(85.0511287)
+ extent_wm_geog = [
+ -179.9999999999996,
+ -85.05112877980656,
+ 179.9999999999996,
+ 85.05112877980656,
+ ]
+ mars_sphere_crs = pyproj.CRS.from_user_input("IAU_2015:49900")
+ mars_tms_wm_geog_ext = morecantile.TileMatrixSet.custom(
+ extent_wm_geog,
+ extent_crs=mars_sphere_crs,
+ crs=crs_mars_web_mercator,
+ id="MarsWebMercator",
+ )
+ assert mars_tms_wm_geog_ext.bbox.left == pytest.approx(-180.0)
+ assert mars_tms_wm_geog_ext.bbox.bottom == pytest.approx(-85.0511287)
+ assert mars_tms_wm_geog_ext.bbox.right == pytest.approx(180.0)
+ assert mars_tms_wm_geog_ext.bbox.top == pytest.approx(85.0511287)
+
@pytest.mark.parametrize(
"identifier, file, crs",
@@ -558,7 +637,6 @@ def test_boundingbox():
def test_private_attr():
"""Check private attr."""
tms = morecantile.tms.get("WebMercatorQuad")
- assert "_geographic_crs" in tms.__private_attributes__
assert "_to_geographic" in tms.__private_attributes__
assert "_from_geographic" in tms.__private_attributes__