diff --git a/odc/geo/gcp.py b/odc/geo/gcp.py index 1050fcd2..56fade35 100644 --- a/odc/geo/gcp.py +++ b/odc/geo/gcp.py @@ -11,7 +11,7 @@ from .geobox import GeoBox, GeoBoxBase from .geom import Geometry, multipoint from .math import Poly2d, affine_from_pts, align_up, resolution_from_affine, unstack_xy -from .types import XY, MaybeInt, Resolution, SomeShape, wh_ +from .types import XY, MaybeInt, Resolution, SomeResolution, SomeShape, wh_ SomePointSet = Union[np.ndarray, Geometry, List[Geometry], List[XY[float]]] @@ -255,7 +255,12 @@ def zoom_out(self, factor: float) -> "GCPGeoBox": _shape, _affine = self.compute_zoom_out(factor) return GCPGeoBox(_shape, self._mapping, _affine) - def zoom_to(self, shape: Union[SomeShape, int, float]) -> "GCPGeoBox": + def zoom_to( + self, + shape: Union[SomeShape, int, float, None] = None, + *, + resolution: Optional[SomeResolution] = None, + ) -> "GCPGeoBox": """ Compute :py:class:`~odc.geo.geobox.GCPGeoBox` with changed resolution. @@ -264,7 +269,7 @@ def zoom_to(self, shape: Union[SomeShape, int, float]) -> "GCPGeoBox": :returns: GCPGeoBox covering the same region but with different number of pixels and therefore resolution. """ - _shape, _affine = self.compute_zoom_to(shape) + _shape, _affine = self.compute_zoom_to(shape, resolution=resolution) return GCPGeoBox(_shape, self._mapping, _affine) def __str__(self): diff --git a/odc/geo/geobox.py b/odc/geo/geobox.py index f0603348..e0a4eea4 100644 --- a/odc/geo/geobox.py +++ b/odc/geo/geobox.py @@ -309,7 +309,10 @@ def compute_zoom_out(self, factor: float) -> Tuple[Shape2d, Affine]: return (shape_((ny, nx)), A) def compute_zoom_to( - self, shape: Union[SomeShape, int, float] + self, + shape: Union[SomeShape, int, float, None] = None, + *, + resolution: Optional[SomeResolution] = None, ) -> Tuple[Shape2d, Affine]: """ Change GeoBox shape. @@ -319,6 +322,14 @@ def compute_zoom_to( :returns: GeoBox covering the same region but with different number of pixels and therefore resolution. """ + if shape is None: + if resolution is None: + raise ValueError("Have to supply shape or resolution") + new_geobox = GeoBox.from_bbox( + self.boundingbox, resolution=resolution, tight=True + ) + return new_geobox.shape, new_geobox.affine + if isinstance(shape, (int, float)): nmax = max(*self._shape) return self.compute_zoom_out(nmax / shape) @@ -834,7 +845,12 @@ def zoom_out(self, factor: float) -> "GeoBox": _shape, _affine = self.compute_zoom_out(factor) return GeoBox(_shape, _affine, self._crs) - def zoom_to(self, shape: Union[SomeShape, int, float]) -> "GeoBox": + def zoom_to( + self, + shape: Union[SomeShape, int, float, None] = None, + *, + resolution: Optional[SomeResolution] = None, + ) -> "GeoBox": """ Change GeoBox shape. @@ -843,7 +859,7 @@ def zoom_to(self, shape: Union[SomeShape, int, float]) -> "GeoBox": :returns: GeoBox covering the same region but with different number of pixels and therefore resolution. """ - _shape, _affine = self.compute_zoom_to(shape) + _shape, _affine = self.compute_zoom_to(shape, resolution=resolution) return GeoBox(_shape, _affine, self._crs) def flipy(self) -> "GeoBox": @@ -1130,9 +1146,14 @@ def zoom_out(gbox: GeoBox, factor: float) -> GeoBox: return gbox.zoom_out(factor) -def zoom_to(gbox: GeoBox, shape: SomeShape) -> GeoBox: +def zoom_to( + gbox: GeoBox, + shape: Union[SomeShape, int, float, None] = None, + *, + resolution: Optional[SomeResolution] = None, +) -> GeoBox: """Alias for :py:meth:`odc.geo.geobox.GeoBox.zoom_to`.""" - return gbox.zoom_to(shape) + return gbox.zoom_to(shape, resolution=resolution) def rotate(gbox: GeoBox, deg: float) -> GeoBox: diff --git a/tests/test_geobox.py b/tests/test_geobox.py index e0b098b4..8824d711 100644 --- a/tests/test_geobox.py +++ b/tests/test_geobox.py @@ -7,7 +7,7 @@ import pytest from affine import Affine -from odc.geo import CRS, geom, ixy_, resyx_, wh_, xy_ +from odc.geo import CRS, geom, ixy_, resxy_, resyx_, wh_, xy_ from odc.geo.geobox import ( AnchorEnum, GeoBox, @@ -296,6 +296,36 @@ def test_geobox_scale_down(): assert gbox_.extent.contains(gbox.extent) +@pytest.mark.parametrize( + "geobox", + [ + GeoBox.from_bbox((-10, -2, 5, 4), "epsg:4326", tight=True, resolution=0.2), + GeoBox.from_bbox((-10, -2, 5, 4), "epsg:3857", tight=True, resolution=1), + GeoBox.from_bbox( + (-10, -2, 5, 4), "epsg:3857", tight=True, resolution=resxy_(1, 2) + ), + ], +) +@pytest.mark.parametrize("shape", [256, (128, 128), (33, 11)]) +def test_zoom_to_shape(geobox: GeoBox, shape): + assert geobox.zoom_to(shape).crs == geobox.crs + + if isinstance(shape, int): + assert max(geobox.zoom_to(shape).shape) == shape + else: + assert geobox.zoom_to(shape).shape == shape + + +def test_zoom_to_resolution(): + geobox = GeoBox.from_bbox((-3, -4, 3, 4), epsg4326, resolution=1) + assert geobox.shape == (8, 6) and geobox.resolution.xy == (1, -1) + assert geobox.zoom_to(resolution=2).resolution.xy == (2, -2) + assert geobox.zoom_to(resolution=2).zoom_to(resolution=1) == geobox + + with pytest.raises(ValueError): + geobox.zoom_to() + + def test_non_st(): A = mkA(rot=10, translation=(-10, 20), shear=0.3) assert is_affine_st(A) is False @@ -337,6 +367,13 @@ def test_from_polygon(): assert 32601 <= gbox.crs.epsg <= 32660 +def test_from_polygon_compat_align(): + box = geom.box(1, 13, 17, 37, "epsg:4326") + assert GeoBox.from_geopolygon(box, 2, align=xy_(1, 1)) == GeoBox.from_geopolygon( + box, 2, anchor=xy_(0.5, 0.5) + ) + + def test_from_bbox(): bbox = (1, 13, 17, 37) shape = (23, 47)