Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quadkey Support #56

Merged
merged 14 commits into from
Aug 20, 2021
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# PyCharm:
.idea
15 changes: 15 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
## 2.1.3 (TBD)

* add **NZTM2000Quad** tile matrix set from LINZ (author @blacha, https://github.com/developmentseed/morecantile/pull/57)
* add **quadkey** supports (@author adrian-knauer, https://github.com/developmentseed/morecantile/pull/56)

```python
import morecantile

tms = morecantile.tms.get("WebMercatorQuad")

# Tile to Quadkey
tms.quadkey(486, 332, 10)
>>> "0313102310"

# Quadkey to Tile
tms.quadkey_to_tile("0313102310")
>>> Tile(486, 332, 10)
```

## 2.1.2 (2021-05-18)

Expand Down
8 changes: 8 additions & 0 deletions morecantile/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ class TileArgParsingError(MorecantileError):

class PointOutsideTMSBounds(UserWarning):
"""Point is outside TMS bounds."""


class NoQuadkeySupport(MorecantileError):
"""Raised when a custom TileMatrixSet doesn't support quadkeys"""


class QuadKeyError(MorecantileError):
"""Raised when errors occur in computing or parsing quad keys"""
74 changes: 72 additions & 2 deletions morecantile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@
import warnings
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union

from pydantic import AnyHttpUrl, BaseModel, Field, validator
from pydantic import AnyHttpUrl, BaseModel, Field, PrivateAttr, validator
from rasterio.crs import CRS, epsg_treats_as_latlong, epsg_treats_as_northingeasting
from rasterio.features import bounds as feature_bounds
from rasterio.warp import transform, transform_bounds, transform_geom

from .commons import BoundingBox, Coords, Tile
from .errors import InvalidIdentifier, PointOutsideTMSBounds
from .errors import (
InvalidIdentifier,
NoQuadkeySupport,
PointOutsideTMSBounds,
QuadKeyError,
)
from .utils import (
_parse_tile_arg,
bbox_to_feature,
check_quadkey_support,
meters_per_unit,
point_in_bbox,
truncate_lnglat,
Expand Down Expand Up @@ -121,6 +127,7 @@ class TileMatrixSet(BaseModel):
wellKnownScaleSet: Optional[AnyHttpUrl] = None
boundingBox: Optional[TMSBoundingBox]
tileMatrix: List[TileMatrix]
_is_quadtree: bool = PrivateAttr()

class Config:
"""Configure TileMatrixSet."""
Expand All @@ -133,6 +140,11 @@ def sort_tile_matrices(cls, v):
"""Sort matrices by identifier"""
return sorted(v, key=lambda m: int(m.identifier))

def __init__(self, **kwargs):
"""Check if TileMatrixSet supports quadkeys"""
super().__init__(**kwargs)
self._is_quadtree = check_quadkey_support(self.tileMatrix)

def __iter__(self):
"""Iterate over matrices"""
for matrix in self.tileMatrix:
Expand Down Expand Up @@ -783,3 +795,61 @@ def feature(
feat["id"] = fid

return feat

def quadkey(self, *tile: Tile) -> str:
"""Get the quadkey of a tile
Parameters
----------
tile : Tile or sequence of int
May be be either an instance of Tile or 3 ints, X, Y, Z.
Returns
-------
str
"""
if not self._is_quadtree:
raise NoQuadkeySupport(
"This Tile Matrix Set doesn't support 2 x 2 quadkeys."
)

tile = _parse_tile_arg(*tile)
qk = []
for z in range(tile.z, self.minzoom, -1):
digit = 0
mask = 1 << (z - 1)
if tile.x & mask:
digit += 1
if tile.y & mask:
digit += 2
qk.append(str(digit))

return "".join(qk)

def quadkey_to_tile(self, qk: str) -> Tile:
"""Get the tile corresponding to a quadkey
Parameters
----------
qk : str
A quadkey string.
Returns
-------
Tile
"""
if not self._is_quadtree:
raise NoQuadkeySupport(
"This Tile Matrix Set doesn't support 2 x 2 quadkeys."
)
if len(qk) == 0:
return Tile(0, 0, 0)
xtile, ytile = 0, 0
for i, digit in enumerate(reversed(qk)):
mask = 1 << i
if digit == "1":
xtile = xtile | mask
elif digit == "2":
ytile = ytile | mask
elif digit == "3":
xtile = xtile | mask
ytile = ytile | mask
elif digit != "0":
raise QuadKeyError("Unexpected quadkey digit: %r", digit)
return Tile(xtile, ytile, i + 1)
19 changes: 18 additions & 1 deletion morecantile/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""morecantile utils."""

import math
from typing import Dict, Tuple
from typing import Dict, List, Tuple

from rasterio.crs import CRS

Expand Down Expand Up @@ -92,3 +92,20 @@ def point_in_bbox(point: Coords, bbox: BoundingBox, precision: int = 5) -> bool:
and round(point.y, precision) >= round(bbox.bottom, precision)
and round(point.y, precision) <= round(bbox.top, precision)
)


def is_power_of_two(number: int) -> bool:
"""Check if a number is a power of 2"""
return (number & (number - 1) == 0) and number != 0


def check_quadkey_support(tms: List) -> bool:
"""Check if a Tile Matrix Set supports quadkeys"""
return all(
[
(t.matrixWidth == t.matrixHeight)
and is_power_of_two(t.matrixWidth)
and ((t.matrixWidth * 2) == tms[i + 1].matrixWidth)
for i, t in enumerate(tms[:-1])
]
)
47 changes: 47 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from rasterio.crs import CRS

import morecantile
from morecantile.commons import Tile
from morecantile.errors import InvalidIdentifier
from morecantile.models import TileMatrix, TileMatrixSet

Expand Down Expand Up @@ -99,6 +100,52 @@ def test_load():
TileMatrixSet.load("ANotValidName")


def test_quadkey_support():
tms = TileMatrixSet.load("CanadianNAD83_LCC")
assert not tms._is_quadtree

tms = TileMatrixSet.load("UPSArcticWGS84Quad")
assert tms._is_quadtree

adrian-knauer marked this conversation as resolved.
Show resolved Hide resolved

def test_quadkey():
tms = morecantile.tms.get("WebMercatorQuad")
expected = "0313102310"
assert tms.quadkey(486, 332, 10) == expected


def test_quadkey_to_tile():
tms = morecantile.tms.get("WebMercatorQuad")
qk = "0313102310"
expected = Tile(486, 332, 10)
assert tms.quadkey_to_tile(qk) == expected


def test_empty_quadkey_to_tile():
tms = morecantile.tms.get("WebMercatorQuad")
qk = ""
expected = Tile(0, 0, 0)
assert tms.quadkey_to_tile(qk) == expected


def test_quadkey_failure():
tms = morecantile.tms.get("WebMercatorQuad")
with pytest.raises(morecantile.errors.QuadKeyError):
tms.quadkey_to_tile("lolwut")


adrian-knauer marked this conversation as resolved.
Show resolved Hide resolved
def test_quadkey_not_supported_failure():
tms = TileMatrixSet.load("NZTM2000")
with pytest.raises(morecantile.errors.NoQuadkeySupport):
tms.quadkey(1, 1, 1)


def test_quadkey_to_tile_not_supported_failure():
tms = TileMatrixSet.load("NZTM2000")
with pytest.raises(morecantile.errors.NoQuadkeySupport):
tms.quadkey_to_tile("3")


def test_findMatrix():
"""Should raise an error when TileMatrix is not found."""
tms = morecantile.tms.get("WebMercatorQuad")
Expand Down
7 changes: 6 additions & 1 deletion tests/test_morecantile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import morecantile
from morecantile.errors import InvalidIdentifier, PointOutsideTMSBounds
from morecantile.utils import meters_per_unit
from morecantile.utils import is_power_of_two, meters_per_unit

from .conftest import requires_gdal3, requires_gdal_lt_3

Expand Down Expand Up @@ -467,3 +467,8 @@ def test_extend_zoom():
more = tms.xy_bounds(2000, 2000, 30)
for a, b in zip(more, merc):
assert round(a - b, 7) == 0


def test_is_power_of_two():
assert is_power_of_two(8)
assert not is_power_of_two(7)