From c98b77df806a9183e2306dd6a6506a462d65ce85 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 16 Oct 2024 17:47:01 +0200 Subject: [PATCH 1/3] add matrix lookup dict and remove duplicated call to parse_tile_arg --- CHANGES.md | 5 ++++ morecantile/models.py | 55 ++++++++++++++++++--------------------- morecantile/utils.py | 1 + tests/test_models.py | 2 +- tests/test_morecantile.py | 15 +++++------ 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fcb0d60..69a84af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,9 @@ +## 7.0.0 (TBD) + +* add `_tile_matrices_idx: Dict[str, int]` private attribute to improve `matrices` lookup +* change `_ul`, `ul`, `_lr` and `lr` method to only accept `morecantile.Tile` object **breaking change** + ## 6.0.0 (2024-10-16) * remove `_geographic_crs` private attribute in `TileMatrixSet` model **breaking change** diff --git a/morecantile/models.py b/morecantile/models.py index e5052b0..d9cd26f 100644 --- a/morecantile/models.py +++ b/morecantile/models.py @@ -487,11 +487,16 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True): # Private attributes _to_geographic: pyproj.Transformer = PrivateAttr() _from_geographic: pyproj.Transformer = PrivateAttr() + _tile_matrices_idx: Dict[str, int] = PrivateAttr() def __init__(self, **data): """Set private attributes.""" super().__init__(**data) + self._tile_matrices_idx = { + mat.id: idx for idx, mat in enumerate(self.tileMatrices) + } + try: self._to_geographic = pyproj.Transformer.from_crs( self.crs._pyproj_crs, self.crs._pyproj_crs.geodetic_crs, always_xy=True @@ -765,9 +770,8 @@ def custom( 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(str(zoom), None)) is not None: + return self.tileMatrices[idx] ####################################################################### # If user wants a deeper matrix we calculate it @@ -1021,61 +1025,58 @@ def tile( x, y = self.xy(lng, lat, truncate=truncate) return self._tile(x, y, zoom, ignore_coalescence=ignore_coalescence) - def _ul(self, *tile: Tile) -> Coords: + def _ul(self, tile: Tile) -> Coords: """ Return the upper left coordinate of the tile in TMS coordinate reference system. Attributes ---------- - tile: (x, y, z) tile coordinates or a Tile object we want the upper left coordinates of. + tile: Tile object we want the upper left coordinates of. Returns ------- Coords: The upper left coordinates of the input tile. """ - t = _parse_tile_arg(*tile) - - matrix = self.matrix(t.z) + matrix = self.matrix(tile.z) origin_x, origin_y = self._matrix_origin(matrix) cf = ( - matrix.get_coalesce_factor(t.y) + matrix.get_coalesce_factor(tile.y) if matrix.variableMatrixWidths is not None else 1 ) return Coords( - origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth, - origin_y - t.y * matrix.cellSize * matrix.tileHeight, + origin_x + + math.floor(tile.x / cf) * matrix.cellSize * cf * matrix.tileWidth, + origin_y - tile.y * matrix.cellSize * matrix.tileHeight, ) - def _lr(self, *tile: Tile) -> Coords: + def _lr(self, tile: Tile) -> Coords: """ Return the lower right coordinate of the tile in TMS coordinate reference system. Attributes ---------- - tile: (x, y, z) tile coordinates or a Tile object we want the lower right coordinates of. + tile: Tile object we want the lower right coordinates of. Returns ------- Coords: The lower right coordinates of the input tile. """ - t = _parse_tile_arg(*tile) - - matrix = self.matrix(t.z) + matrix = self.matrix(tile.z) origin_x, origin_y = self._matrix_origin(matrix) cf = ( - matrix.get_coalesce_factor(t.y) + matrix.get_coalesce_factor(tile.y) if matrix.variableMatrixWidths is not None else 1 ) return Coords( origin_x - + (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth, - origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight, + + (math.floor(tile.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth, + origin_y - (tile.y + 1) * matrix.cellSize * matrix.tileHeight, ) def xy_bounds(self, *tile: Tile) -> BoundingBox: @@ -1097,40 +1098,36 @@ def xy_bounds(self, *tile: Tile) -> BoundingBox: right, bottom = self._lr(t) return BoundingBox(left, bottom, right, top) - def ul(self, *tile: Tile) -> Coords: + def ul(self, tile: Tile) -> Coords: """ Return the upper left coordinates of the tile in geographic coordinate reference system. Attributes ---------- - tile (tuple or Tile): (x, y, z) tile coordinates or a Tile object we want the upper left geographic coordinates of. + tile (Tile): Tile object we want the upper left geographic coordinates of. Returns ------- Coords: The upper left geographic coordinates of the input tile. """ - t = _parse_tile_arg(*tile) - - x, y = self._ul(t) + x, y = self._ul(tile) return Coords(*self.lnglat(x, y)) - def lr(self, *tile: Tile) -> Coords: + def lr(self, tile: Tile) -> Coords: """ Return the lower right coordinates of the tile in geographic coordinate reference system. Attributes ---------- - tile (tuple or Tile): (x, y, z) tile coordinates or a Tile object we want the lower right geographic coordinates of. + tile (Tile): Tile object we want the lower right geographic coordinates of. Returns ------- Coords: The lower right geographic coordinates of the input tile. """ - t = _parse_tile_arg(*tile) - - x, y = self._lr(t) + x, y = self._lr(tile) return Coords(*self.lnglat(x, y)) def bounds(self, *tile: Tile) -> BoundingBox: diff --git a/morecantile/utils.py b/morecantile/utils.py index 5da80cb..da98fe3 100644 --- a/morecantile/utils.py +++ b/morecantile/utils.py @@ -32,6 +32,7 @@ def _parse_tile_arg(*args) -> Tile: """ if len(args) == 1: args = args[0] + if len(args) == 3: return Tile(*args) else: diff --git a/tests/test_models.py b/tests/test_models.py index c1a84ca..9e51da9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -384,7 +384,7 @@ def test_mars_local_tms(): assert syrtis_tms.geographic_crs assert syrtis_tms.model_dump(mode="json") - center = syrtis_tms.ul(1, 1, 1) + center = syrtis_tms.ul(Tile(1, 1, 1)) assert round(center.x, 6) == 76.5 assert round(center.y, 6) == 17 diff --git a/tests/test_morecantile.py b/tests/test_morecantile.py index c49f8cc..d42f0e5 100644 --- a/tests/test_morecantile.py +++ b/tests/test_morecantile.py @@ -133,7 +133,7 @@ def test_ul_tile(): test form https://github.com/mapbox/mercantile/blob/master/tests/test_funcs.py """ tms = morecantile.tms.get("WebMercatorQuad") - xy = tms.ul(486, 332, 10) + xy = tms.ul(morecantile.Tile(486, 332, 10)) expected = (-9.140625, 53.33087298301705) for a, b in zip(expected, xy): assert round(a - b, 6) == 0 @@ -146,7 +146,7 @@ def test_projul_tile(): test form https://github.com/mapbox/mercantile/blob/master/tests/test_funcs.py """ tms = morecantile.tms.get("WebMercatorQuad") - xy = tms._ul(486, 332, 10) + xy = tms._ul(morecantile.Tile(486, 332, 10)) expected = (-1017529.7205322663, 7044436.526761846) for a, b in zip(expected, xy): assert round(a - b, 6) == 0 @@ -191,15 +191,12 @@ def test_feature(): ################################################################################ # replicate mercantile tests -# https://github.com/mapbox/mercantile/blob/master/tests/test_funcs.py -@pytest.mark.parametrize( - "args", [(486, 332, 10), [(486, 332, 10)], [morecantile.Tile(486, 332, 10)]] -) -def test_ul(args): +def test_ul(): """test args.""" + tile = morecantile.Tile(486, 332, 10) tms = morecantile.tms.get("WebMercatorQuad") expected = (-9.140625, 53.33087298301705) - lnglat = tms.ul(*args) + lnglat = tms.ul(tile) for a, b in zip(expected, lnglat): assert round(a - b, 6) == 0 assert lnglat[0] == lnglat.x @@ -225,7 +222,7 @@ def test_bbox(args): def test_xy_tile(): """x, y for the 486-332-10 tile is correctly calculated.""" tms = morecantile.tms.get("WebMercatorQuad") - ul = tms.ul(486, 332, 10) + ul = tms.ul(morecantile.Tile(486, 332, 10)) xy = tms.xy(*ul) expected = (-1017529.7205322663, 7044436.526761846) for a, b in zip(expected, xy): From 3a8bed8f610f8c5bcfb732bdbe86014046484a94 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 16 Oct 2024 23:50:48 +0200 Subject: [PATCH 2/3] revert and update xy_bounds and bounds methods --- morecantile/models.py | 77 +++++++++++++++++++++++++-------------- morecantile/utils.py | 1 - tests/test_models.py | 2 +- tests/test_morecantile.py | 15 +++++--- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/morecantile/models.py b/morecantile/models.py index d9cd26f..84bcd08 100644 --- a/morecantile/models.py +++ b/morecantile/models.py @@ -487,14 +487,15 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True): # Private attributes _to_geographic: pyproj.Transformer = PrivateAttr() _from_geographic: pyproj.Transformer = PrivateAttr() - _tile_matrices_idx: Dict[str, int] = PrivateAttr() + + _tile_matrices_idx: Dict[int, int] = PrivateAttr() def __init__(self, **data): """Set private attributes.""" super().__init__(**data) self._tile_matrices_idx = { - mat.id: idx for idx, mat in enumerate(self.tileMatrices) + int(mat.id): idx for idx, mat in enumerate(self.tileMatrices) } try: @@ -770,7 +771,7 @@ def custom( def matrix(self, zoom: int) -> TileMatrix: """Return the TileMatrix for a specific zoom.""" - if (idx := self._tile_matrices_idx.get(str(zoom), None)) is not None: + if (idx := self._tile_matrices_idx.get(zoom, None)) is not None: return self.tileMatrices[idx] ####################################################################### @@ -1025,58 +1026,61 @@ def tile( x, y = self.xy(lng, lat, truncate=truncate) return self._tile(x, y, zoom, ignore_coalescence=ignore_coalescence) - def _ul(self, tile: Tile) -> Coords: + def _ul(self, *tile: Tile) -> Coords: """ Return the upper left coordinate of the tile in TMS coordinate reference system. Attributes ---------- - tile: Tile object we want the upper left coordinates of. + tile: (x, y, z) tile coordinates or a Tile object we want the upper left coordinates of. Returns ------- Coords: The upper left coordinates of the input tile. """ - matrix = self.matrix(tile.z) + t = _parse_tile_arg(*tile) + + matrix = self.matrix(t.z) origin_x, origin_y = self._matrix_origin(matrix) cf = ( - matrix.get_coalesce_factor(tile.y) + matrix.get_coalesce_factor(t.y) if matrix.variableMatrixWidths is not None else 1 ) return Coords( - origin_x - + math.floor(tile.x / cf) * matrix.cellSize * cf * matrix.tileWidth, - origin_y - tile.y * matrix.cellSize * matrix.tileHeight, + origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth, + origin_y - t.y * matrix.cellSize * matrix.tileHeight, ) - def _lr(self, tile: Tile) -> Coords: + def _lr(self, *tile: Tile) -> Coords: """ Return the lower right coordinate of the tile in TMS coordinate reference system. Attributes ---------- - tile: Tile object we want the lower right coordinates of. + tile: (x, y, z) tile coordinates or a Tile object we want the lower right coordinates of. Returns ------- Coords: The lower right coordinates of the input tile. """ - matrix = self.matrix(tile.z) + t = _parse_tile_arg(*tile) + + matrix = self.matrix(t.z) origin_x, origin_y = self._matrix_origin(matrix) cf = ( - matrix.get_coalesce_factor(tile.y) + matrix.get_coalesce_factor(t.y) if matrix.variableMatrixWidths is not None else 1 ) return Coords( origin_x - + (math.floor(tile.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth, - origin_y - (tile.y + 1) * matrix.cellSize * matrix.tileHeight, + + (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth, + origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight, ) def xy_bounds(self, *tile: Tile) -> BoundingBox: @@ -1094,40 +1098,59 @@ 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: + def ul(self, *tile: Tile) -> Coords: """ Return the upper left coordinates of the tile in geographic coordinate reference system. Attributes ---------- - tile (Tile): Tile object we want the upper left geographic coordinates of. + tile (tuple or Tile): (x, y, z) tile coordinates or a Tile object we want the upper left geographic coordinates of. Returns ------- Coords: The upper left geographic coordinates of the input tile. """ - x, y = self._ul(tile) + t = _parse_tile_arg(*tile) + + x, y = self._ul(t) return Coords(*self.lnglat(x, y)) - def lr(self, tile: Tile) -> Coords: + def lr(self, *tile: Tile) -> Coords: """ Return the lower right coordinates of the tile in geographic coordinate reference system. Attributes ---------- - tile (Tile): Tile object we want the lower right geographic coordinates of. + tile (tuple or Tile): (x, y, z) tile coordinates or a Tile object we want the lower right geographic coordinates of. Returns ------- Coords: The lower right geographic coordinates of the input tile. """ - x, y = self._lr(tile) + t = _parse_tile_arg(*tile) + + x, y = self._lr(t) return Coords(*self.lnglat(x, y)) def bounds(self, *tile: Tile) -> BoundingBox: @@ -1143,10 +1166,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/morecantile/utils.py b/morecantile/utils.py index da98fe3..5da80cb 100644 --- a/morecantile/utils.py +++ b/morecantile/utils.py @@ -32,7 +32,6 @@ def _parse_tile_arg(*args) -> Tile: """ if len(args) == 1: args = args[0] - if len(args) == 3: return Tile(*args) else: diff --git a/tests/test_models.py b/tests/test_models.py index 9e51da9..c1a84ca 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -384,7 +384,7 @@ def test_mars_local_tms(): assert syrtis_tms.geographic_crs assert syrtis_tms.model_dump(mode="json") - center = syrtis_tms.ul(Tile(1, 1, 1)) + center = syrtis_tms.ul(1, 1, 1) assert round(center.x, 6) == 76.5 assert round(center.y, 6) == 17 diff --git a/tests/test_morecantile.py b/tests/test_morecantile.py index d42f0e5..c49f8cc 100644 --- a/tests/test_morecantile.py +++ b/tests/test_morecantile.py @@ -133,7 +133,7 @@ def test_ul_tile(): test form https://github.com/mapbox/mercantile/blob/master/tests/test_funcs.py """ tms = morecantile.tms.get("WebMercatorQuad") - xy = tms.ul(morecantile.Tile(486, 332, 10)) + xy = tms.ul(486, 332, 10) expected = (-9.140625, 53.33087298301705) for a, b in zip(expected, xy): assert round(a - b, 6) == 0 @@ -146,7 +146,7 @@ def test_projul_tile(): test form https://github.com/mapbox/mercantile/blob/master/tests/test_funcs.py """ tms = morecantile.tms.get("WebMercatorQuad") - xy = tms._ul(morecantile.Tile(486, 332, 10)) + xy = tms._ul(486, 332, 10) expected = (-1017529.7205322663, 7044436.526761846) for a, b in zip(expected, xy): assert round(a - b, 6) == 0 @@ -191,12 +191,15 @@ def test_feature(): ################################################################################ # replicate mercantile tests -def test_ul(): +# https://github.com/mapbox/mercantile/blob/master/tests/test_funcs.py +@pytest.mark.parametrize( + "args", [(486, 332, 10), [(486, 332, 10)], [morecantile.Tile(486, 332, 10)]] +) +def test_ul(args): """test args.""" - tile = morecantile.Tile(486, 332, 10) tms = morecantile.tms.get("WebMercatorQuad") expected = (-9.140625, 53.33087298301705) - lnglat = tms.ul(tile) + lnglat = tms.ul(*args) for a, b in zip(expected, lnglat): assert round(a - b, 6) == 0 assert lnglat[0] == lnglat.x @@ -222,7 +225,7 @@ def test_bbox(args): def test_xy_tile(): """x, y for the 486-332-10 tile is correctly calculated.""" tms = morecantile.tms.get("WebMercatorQuad") - ul = tms.ul(morecantile.Tile(486, 332, 10)) + ul = tms.ul(486, 332, 10) xy = tms.xy(*ul) expected = (-1017529.7205322663, 7044436.526761846) for a, b in zip(expected, xy): From 57e9b1abd2dd4e5491cbe23780c896691bc7f0d1 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 17 Oct 2024 22:56:35 +0200 Subject: [PATCH 3/3] update changelog --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 69a84af..37bcfdb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,8 @@ -## 7.0.0 (TBD) +## 6.1.0 (TBD) * add `_tile_matrices_idx: Dict[str, int]` private attribute to improve `matrices` lookup -* change `_ul`, `ul`, `_lr` and `lr` method to only accept `morecantile.Tile` object **breaking change** +* change `xy_bounds()` and `bounds()` methods to avoid calculation duplication ## 6.0.0 (2024-10-16)