diff --git a/CHANGES.md b/CHANGES.md index ed60858..6efba30 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,9 @@ +## 5.1.0 (2023-12-21) + +* Simplify bounds calculation by using `TileMatrix.cellSize` instead of `TileMatrix.scaleDenominator` +* remove `TileMatrixSet._resolution` private method + ## 5.0.2 (2023-12-01) * Remove *alias* tiles in `.parent()`, `.children()`, `.neighbors()` and `.tiles()` methods for Variable Matrix Width TileMatrixSets (https://github.com/developmentseed/morecantile/pull/136) diff --git a/morecantile/data/WebMercatorQuad.json b/morecantile/data/WebMercatorQuad.json index 7aaa63a..4fb1c49 100644 --- a/morecantile/data/WebMercatorQuad.json +++ b/morecantile/data/WebMercatorQuad.json @@ -258,4 +258,4 @@ "matrixHeight": 16777216 } ] -} \ No newline at end of file +} diff --git a/morecantile/models.py b/morecantile/models.py index 2f40d72..7017010 100644 --- a/morecantile/models.py +++ b/morecantile/models.py @@ -526,7 +526,7 @@ def from_v1(cls, tms: Dict) -> "TileMatrixSet": mpu = meters_per_unit(CRS.from_user_input(v2_tms["crs"])) for i in range(len(v2_tms["tileMatrices"])): v2_tms["tileMatrices"][i]["cellSize"] = ( - v2_tms["tileMatrices"][i]["scaleDenominator"] * 0.00028 / mpu + v2_tms["tileMatrices"][i]["scaleDenominator"] * 0.28e-3 / mpu ) v2_tms["tileMatrices"][i]["pointOfOrigin"] = v2_tms["tileMatrices"][i].pop( "topLeftCorner" @@ -553,6 +553,7 @@ def custom( id: Optional[str] = None, ordered_axes: Optional[List[str]] = None, geographic_crs: CRS = WGS84_CRS, + screen_pixel_size: float = 0.28e-3, **kwargs: Any, ): """ @@ -585,6 +586,8 @@ def custom( Tile Matrix Set identifier geographic_crs: pyproj.CRS Geographic (lat,lon) coordinate reference system (default is EPSG:4326) + 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. kwargs: Any Attributes to forward to the TileMatrixSet @@ -621,7 +624,7 @@ def custom( TileMatrix( **{ "id": str(zoom), - "scaleDenominator": res * mpu / 0.00028, + "scaleDenominator": res * mpu / screen_pixel_size, "cellSize": res, "pointOfOrigin": [x_origin, y_origin], "tileWidth": tile_width, @@ -705,17 +708,6 @@ def matrix(self, zoom: int) -> TileMatrix: return tile_matrix - def _resolution(self, matrix: TileMatrix) -> float: - """ - Tile resolution for a TileMatrix. - - From note g in http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2: - The pixel size of the tile can be obtained from the scaleDenominator - by multiplying the later by 0.28 10-3 / metersPerUnit. - - """ - return matrix.scaleDenominator * 0.28e-3 / meters_per_unit(self.crs._pyproj_crs) - def _matrix_origin(self, matrix: TileMatrix) -> Coords: """Return the Origin coordinates of the matrix.""" origin_x = ( @@ -760,7 +752,7 @@ def zoom_for_res( # Freely adapted from https://github.com/OSGeo/gdal/blob/dc38aa64d779ecc45e3cd15b1817b83216cf96b8/gdal/frmts/gtiff/cogdriver.cpp#L272-L305 for zoom_level in range(min_z, max_z + 1): - matrix_res = self._resolution(self.matrix(zoom_level)) + matrix_res = self.matrix(zoom_level).cellSize if res > matrix_res or abs(res - matrix_res) / matrix_res <= 1e-8: break @@ -772,7 +764,7 @@ def zoom_for_res( zoom_level = min(zoom_level, max_z) elif zoom_level_strategy.lower() == "auto": - if (self._resolution(self.matrix(max(zoom_level - 1, min_z))) / res) < ( + if (self.matrix(max(zoom_level - 1, min_z)).cellSize / res) < ( res / matrix_res ): zoom_level = max(zoom_level - 1, min_z) @@ -858,16 +850,15 @@ def _tile( """ matrix = self.matrix(zoom) - res = self._resolution(matrix) origin_x, origin_y = self._matrix_origin(matrix) xtile = ( - math.floor((xcoord - origin_x) / float(res * matrix.tileWidth)) + math.floor((xcoord - origin_x) / float(matrix.cellSize * matrix.tileWidth)) if not math.isinf(xcoord) else 0 ) ytile = ( - math.floor((origin_y - ycoord) / float(res * matrix.tileHeight)) + math.floor((origin_y - ycoord) / float(matrix.cellSize * matrix.tileHeight)) if not math.isinf(ycoord) else 0 ) @@ -940,7 +931,6 @@ def _ul(self, *tile: Tile) -> Coords: t = _parse_tile_arg(*tile) matrix = self.matrix(t.z) - res = self._resolution(matrix) origin_x, origin_y = self._matrix_origin(matrix) cf = ( @@ -949,8 +939,8 @@ def _ul(self, *tile: Tile) -> Coords: else 1 ) return Coords( - origin_x + math.floor(t.x / cf) * res * cf * matrix.tileWidth, - origin_y - t.y * res * 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: @@ -969,7 +959,6 @@ def _lr(self, *tile: Tile) -> Coords: t = _parse_tile_arg(*tile) matrix = self.matrix(t.z) - res = self._resolution(matrix) origin_x, origin_y = self._matrix_origin(matrix) cf = ( @@ -978,8 +967,9 @@ def _lr(self, *tile: Tile) -> Coords: else 1 ) return Coords( - origin_x + (math.floor(t.x / cf) + 1) * res * cf * matrix.tileWidth, - origin_y - (t.y + 1) * res * matrix.tileHeight, + origin_x + + (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: @@ -1466,8 +1456,7 @@ def parent(self, *tile: Tile, zoom: int = None): target_zoom = t.z - 1 if zoom is None else zoom # buffer value to apply on bbox - res = self._resolution(self.matrix(t.z)) / 10.0 - + res = self.matrix(t.z).cellSize / 10.0 bbox = self.xy_bounds(t) ul_tile = self._tile(bbox.left + res, bbox.top - res, target_zoom) lr_tile = self._tile(bbox.right - res, bbox.bottom + res, target_zoom) @@ -1513,7 +1502,7 @@ def children(self, *tile: Tile, zoom: int = None): target_zoom = t.z + 1 if zoom is None else zoom # buffer value to apply on bbox - res = self._resolution(self.matrix(t.z)) / 10.0 + res = self.matrix(t.z).cellSize / 10.0 bbox = self.xy_bounds(t) ul_tile = self._tile(bbox.left + res, bbox.top - res, target_zoom) diff --git a/tests/test_mercantile_conform.py b/tests/test_mercantile_conform.py index 98964dc..cfe3204 100644 --- a/tests/test_mercantile_conform.py +++ b/tests/test_mercantile_conform.py @@ -13,8 +13,8 @@ @pytest.mark.parametrize("zoom", range(0, 20)) def test_get_tile(zoom: int): """Make sure mercantile and morecantile returns the same thing.""" - tile = mercantile.tile(0, 0, zoom=zoom) - morecantile_tile = tms.tile(0, 0, zoom=zoom) + tile = mercantile.tile(-10, 10, zoom=zoom) + morecantile_tile = tms.tile(-10, 10, zoom=zoom) assert tile == morecantile_tile diff --git a/tests/test_morecantile.py b/tests/test_morecantile.py index 1c50d2f..110d4a2 100644 --- a/tests/test_morecantile.py +++ b/tests/test_morecantile.py @@ -98,7 +98,7 @@ def test_bounds(args): tms = morecantile.tms.get("WebMercatorQuad") bbox = tms.bounds(*args) for a, b in zip(expected, bbox): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 assert bbox.left == bbox[0] assert bbox.bottom == bbox[1] assert bbox.right == bbox[2] @@ -123,7 +123,7 @@ def test_xy_bounds(args): tms = morecantile.tms.get("WebMercatorQuad") bounds = tms.xy_bounds(*args) for a, b in zip(expected, bounds): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 def test_ul_tile(): @@ -136,7 +136,7 @@ def test_ul_tile(): xy = tms.ul(486, 332, 10) expected = (-9.140625, 53.33087298301705) for a, b in zip(expected, xy): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 def test_projul_tile(): @@ -149,7 +149,7 @@ def test_projul_tile(): xy = tms._ul(486, 332, 10) expected = (-1017529.7205322663, 7044436.526761846) for a, b in zip(expected, xy): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 def test_projtile(): @@ -201,7 +201,7 @@ def test_ul(args): expected = (-9.140625, 53.33087298301705) lnglat = tms.ul(*args) for a, b in zip(expected, lnglat): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 assert lnglat[0] == lnglat.x assert lnglat[1] == lnglat.y @@ -215,7 +215,7 @@ def test_bbox(args): expected = (-9.140625, 53.12040528310657, -8.7890625, 53.33087298301705) bbox = tms.bounds(*args) for a, b in zip(expected, bbox): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 assert bbox.left == bbox[0] assert bbox.bottom == bbox[1] assert bbox.right == bbox[2] @@ -229,7 +229,7 @@ def test_xy_tile(): xy = tms.xy(*ul) expected = (-1017529.7205322663, 7044436.526761846) for a, b in zip(expected, xy): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 def test_xy_null_island(): @@ -238,7 +238,7 @@ def test_xy_null_island(): xy = tms.xy(0.0, 0.0) expected = (0.0, 0.0) for a, b in zip(expected, xy): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 @pytest.mark.xfail @@ -320,7 +320,7 @@ def test_lnglat_xy_roundtrip(): lnglat = (-105.0844, 40.5853) roundtrip = tms.lnglat(*tms.xy(*lnglat)) for a, b in zip(roundtrip, lnglat): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 @pytest.mark.parametrize( @@ -338,7 +338,7 @@ def test_xy_bounds_mercantile(args): bounds = tms.xy_bounds(*args) for a, b in zip(expected, bounds): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 def test_tile_not_truncated(): @@ -462,25 +462,25 @@ def test_extend_zoom(): with pytest.warns(UserWarning): more = tms.xy_bounds(1000, 1000, 25) for a, b in zip(more, merc): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 merc = mercantile.xy_bounds(2000, 2000, 26) with pytest.warns(UserWarning): more = tms.xy_bounds(2000, 2000, 26) for a, b in zip(more, merc): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 merc = mercantile.xy_bounds(2000, 2000, 27) with pytest.warns(UserWarning): more = tms.xy_bounds(2000, 2000, 27) for a, b in zip(more, merc): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 merc = mercantile.xy_bounds(2000, 2000, 30) with pytest.warns(UserWarning): more = tms.xy_bounds(2000, 2000, 30) for a, b in zip(more, merc): - assert round(a - b, 7) == 0 + assert round(a - b, 6) == 0 def test_is_power_of_two():