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
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"""
70 changes: 69 additions & 1 deletion morecantile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@
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 @@ -162,6 +168,11 @@ def _invert_axis(self) -> bool:
"""Check if CRS has inverted AXIS (lat,lon) instead of (lon,lat)."""
return crs_axis_inverted(self.crs)

@property
def quadkey_support(self) -> bool:
adrian-knauer marked this conversation as resolved.
Show resolved Hide resolved
"""Indicator if the Tile Matrix Set supports the creation of quadkeys"""
return check_quadkey_support(self.tileMatrix)
adrian-knauer marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def load(cls, name: str):
"""Load default TileMatrixSet."""
Expand Down Expand Up @@ -783,3 +794,60 @@ 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.quadkey_support:
raise NoQuadkeySupport(
"This Tile Matrix Set doesn't support 2 x 2 quadkeys."
)
tile = _parse_tile_arg(*tile)
xtile, ytile, zoom = tile
qk = []
for z in range(zoom, self.minzoom, -1):
digit = 0
mask = 1 << (z - 1)
if xtile & mask:
digit += 1
if ytile & 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.quadkey_support:
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])
]
)
35 changes: 35 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,40 @@ def test_load():
TileMatrixSet.load("ANotValidName")


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

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

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_findMatrix():
"""Should raise an error when TileMatrix is not found."""
tms = morecantile.tms.get("WebMercatorQuad")
Expand Down