diff --git a/CHANGES.md b/CHANGES.md index 868db7cd..9c76940b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ # unreleased +* raise `MissingCRS` or `InvalidGeographicBounds` errors when Xarray datasets have wrong geographic metadata * better error message for `TileOutsideBounds` errors (author @abarciauskas-bgse, https://github.com/cogeotiff/rio-tiler/pull/712) * handle of inverted latitude in `reader.point` (author @georgespill, https://github.com/cogeotiff/rio-tiler/pull/716) diff --git a/docs/src/readers.md b/docs/src/readers.md index 4d484056..db04e5be 100644 --- a/docs/src/readers.md +++ b/docs/src/readers.md @@ -1179,4 +1179,4 @@ print(info.json(exclude_none=True)) !!! Important Not Implemented -``` + diff --git a/rio_tiler/errors.py b/rio_tiler/errors.py index 1fec6a8a..44373533 100644 --- a/rio_tiler/errors.py +++ b/rio_tiler/errors.py @@ -83,3 +83,11 @@ class AssetAsBandError(RioTilerError): class InvalidPointDataError(RioTilerError): """Invalid PointData.""" + + +class MissingCRS(RioTilerError): + """Dataset doesn't have CRS information.""" + + +class InvalidGeographicBounds(RioTilerError): + """Invalid Geographic bounds.""" diff --git a/rio_tiler/io/xarray.py b/rio_tiler/io/xarray.py index c4bb323b..c0f88585 100644 --- a/rio_tiler/io/xarray.py +++ b/rio_tiler/io/xarray.py @@ -15,7 +15,12 @@ from rasterio.warp import transform as transform_coords from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS -from rio_tiler.errors import PointOutsideBounds, TileOutsideBounds +from rio_tiler.errors import ( + InvalidGeographicBounds, + MissingCRS, + PointOutsideBounds, + TileOutsideBounds, +) from rio_tiler.io.base import BaseReader from rio_tiler.models import BandStatistics, ImageData, Info, PointData from rio_tiler.types import BBox, NoData, WarpResampling @@ -70,8 +75,23 @@ def __attrs_post_init__(self): assert xarray is not None, "xarray must be installed to use XarrayReader" assert rioxarray is not None, "rioxarray must be installed to use XarrayReader" + # NOTE: rioxarray returns **ordered** bounds in form of (minx, miny, maxx, maxx) self.bounds = tuple(self.input.rio.bounds()) self.crs = self.input.rio.crs + if not self.crs: + raise MissingCRS( + "Dataset doesn't have CRS information, please add it before using rio-tiler (e.g. `ds.rio.write_crs('epsg:4326', inplace=True)`)" + ) + + if self.crs == WGS84_CRS and ( + self.bounds[0] < -180 + or self.bounds[1] < -90 + or self.bounds[2] > 180 + or self.bounds[3] > 90 + ): + raise InvalidGeographicBounds( + f"Invalid geographic bounds: {self.bounds}. Must be within (-180, -90, 180, 90)." + ) self._dims = [ d diff --git a/tests/test_io_xarray.py b/tests/test_io_xarray.py index 1d128f93..57515a1f 100644 --- a/tests/test_io_xarray.py +++ b/tests/test_io_xarray.py @@ -8,6 +8,7 @@ import rioxarray import xarray +from rio_tiler.errors import InvalidGeographicBounds, MissingCRS from rio_tiler.io import XarrayReader @@ -18,8 +19,8 @@ def test_xarray_reader(): arr, dims=("time", "y", "x"), coords={ - "x": list(range(-170, 180, 10)), - "y": list(range(-80, 85, 5)), + "x": numpy.arange(-170, 180, 10), + "y": numpy.arange(-80, 85, 5), "time": [datetime(2022, 1, 1)], }, ) @@ -268,8 +269,8 @@ def test_xarray_reader_resampling(): arr, dims=("time", "y", "x"), coords={ - "x": list(range(-170, 180, 10)), - "y": list(range(-80, 85, 5)), + "x": numpy.arange(-170, 180, 10), + "y": numpy.arange(-80, 85, 5), "time": [datetime(2022, 1, 1)], }, ) @@ -321,3 +322,81 @@ def test_xarray_reader_resampling(): with pytest.warns(DeprecationWarning): _ = dst.feature(feat, resampling_method="nearest") + + +def test_xarray_reader_no_crs(): + """Should raise MissingCRS.""" + arr = numpy.arange(0.0, 33 * 35).reshape(1, 33, 35) + data = xarray.DataArray( + arr, + dims=("time", "y", "x"), + coords={ + "x": numpy.arange(-170, 180, 10), + "y": numpy.arange(-80, 85, 5), + "time": [datetime(2022, 1, 1)], + }, + ) + data.attrs.update({"valid_min": arr.min(), "valid_max": arr.max()}) + with pytest.raises(MissingCRS): + with XarrayReader(data): + pass + + +def test_xarray_reader_invalid_bounds_crs(): + """Should raise InvalidGeographicBounds.""" + arr = numpy.arange(0.0, 33 * 35).reshape(1, 33, 35) + data = xarray.DataArray( + arr, + dims=("time", "y", "x"), + coords={ + "x": numpy.arange(10, 360, 10), + "y": numpy.arange(-80, 85, 5), + "time": [datetime(2022, 1, 1)], + }, + ) + data.rio.write_crs("epsg:4326", inplace=True) + with pytest.raises(InvalidGeographicBounds): + with XarrayReader(data): + pass + + data = xarray.DataArray( + arr, + dims=("time", "y", "x"), + coords={ + "x": numpy.arange(-170, 180, 10), + "y": numpy.arange(15, 180, 5), + "time": [datetime(2022, 1, 1)], + }, + ) + data.rio.write_crs("epsg:4326", inplace=True) + with pytest.raises(InvalidGeographicBounds): + with XarrayReader(data): + pass + + data = xarray.DataArray( + arr, + dims=("time", "y", "x"), + coords={ + "x": numpy.arange(-170, 180, 10), + "y": numpy.arange(15, 180, 5), + "time": [datetime(2022, 1, 1)], + }, + ) + data.rio.write_crs("epsg:4326", inplace=True) + with pytest.raises(InvalidGeographicBounds): + with XarrayReader(data): + pass + + # Inverted bounds are still ok because rioxarray reorder the bounds + data = xarray.DataArray( + arr, + dims=("time", "y", "x"), + coords={ + "x": numpy.flip(numpy.arange(-170, 180, 10)), + "y": numpy.flip(numpy.arange(-80, 85, 5)), + "time": [datetime(2022, 1, 1)], + }, + ) + data.rio.write_crs("epsg:4326", inplace=True) + with XarrayReader(data): + pass