From 4228f2dd18745b8c957bfd42eef49eb9984e8e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 12 Jun 2023 18:58:57 +0200 Subject: [PATCH] Improve rioxarray support (#645) Co-authored-by: Andrew <15331990+ahuang11@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- geoviews/tests/test_util.py | 47 ++++++++++++++++++++++++++++++++ geoviews/util.py | 53 +++++++++++++++++++++++-------------- setup.py | 1 + 4 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 geoviews/tests/test_util.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a86339a7..a7492638 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,7 +50,7 @@ jobs: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" USE_PYGEOS: '0' steps: - - uses: holoviz-dev/holoviz_tasks/install@v0.1a9 + - uses: holoviz-dev/holoviz_tasks/install@v0.1a13 with: name: unit_test_suite python-version: ${{ matrix.python-version }} diff --git a/geoviews/tests/test_util.py b/geoviews/tests/test_util.py new file mode 100644 index 00000000..4cd0c9fc --- /dev/null +++ b/geoviews/tests/test_util.py @@ -0,0 +1,47 @@ +import cartopy.crs as ccrs +import pytest + +import geoviews as gv +from geoviews.util import from_xarray, process_crs + +try: + import rioxarray as rxr +except ImportError: + rxr = None + + +@pytest.mark.parametrize( + "raw_crs", + [ + "+init=epsg:26911", + "4326", + 4326, + "epsg:4326", + "EPSG: 4326", + ccrs.PlateCarree(), + ], +) +def test_process_crs(raw_crs) -> None: + crs = process_crs(raw_crs) + assert isinstance(crs, ccrs.CRS) + + +# To avoid '+init=:' syntax is deprecated. +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_process_crs_raises_error(): + with pytest.raises( + ValueError, match="must be defined as a EPSG code, proj4 string" + ): + process_crs(43823) + + +@pytest.mark.skipif(rxr is None, reason="Needs rioxarray to be installed") +def test_from_xarray(): + file = ( + "https://github.com/holoviz/hvplot/raw/main/hvplot/tests/data/RGB-red.byte.tif" + ) + output = from_xarray(rxr.open_rasterio(file)) + + assert isinstance(output, gv.RGB) + assert sorted(map(str, output.kdims)) == ["x", "y"] + assert isinstance(output.crs, ccrs.CRS) diff --git a/geoviews/util.py b/geoviews/util.py index bb6e91f4..550c354f 100644 --- a/geoviews/util.py +++ b/geoviews/util.py @@ -1,7 +1,6 @@ import warnings import numpy as np -import param import shapely import shapely.geometry as sgeom from cartopy import crs as ccrs @@ -15,7 +14,7 @@ from shapely.geometry.base import BaseMultipartGeometry from shapely.ops import transform -from ._warnings import deprecated +from ._warnings import deprecated, warn geom_types = (MultiLineString, LineString, MultiPolygon, Polygon, LinearRing, Point, MultiPoint) @@ -580,28 +579,34 @@ def process_crs(crs): """ try: import cartopy.crs as ccrs - import geoviews as gv # noqa + import pyproj except ImportError: - raise ImportError('Geographic projection support requires GeoViews and cartopy.') + raise ImportError('Geographic projection support requires pyproj and cartopy.') if crs is None: return ccrs.PlateCarree() + elif isinstance(crs, ccrs.CRS): + return crs - if isinstance(crs, str) and crs.lower().startswith('epsg'): + errors = [] + if isinstance(crs, str): try: - crs = ccrs.epsg(crs[5:].lstrip().rstrip()) - except Exception: - raise ValueError("Could not parse EPSG code as CRS, must be of the format 'EPSG: {code}.'") - elif isinstance(crs, int): - crs = ccrs.epsg(crs) - elif isinstance(crs, str) or is_pyproj(crs): + return ccrs.epsg("".join([c for c in crs if c.isdigit()])) + except Exception as e: + errors.append(e) + if isinstance(crs, int): try: - crs = proj_to_cartopy(crs) - except Exception: - raise ValueError("Could not parse EPSG code as CRS, must be of the format 'proj4: {proj4 string}.'") - elif not isinstance(crs, ccrs.CRS): - raise ValueError("Projection must be defined as a EPSG code, proj4 string, cartopy CRS or pyproj.Proj.") - return crs + return ccrs.epsg(crs) + except Exception as e: + crs = str(crs) + errors.append(e) + if isinstance(crs, (str, pyproj.Proj)): + try: + return proj_to_cartopy(crs) + except Exception as e: + errors.append(e) + + raise ValueError("Projection must be defined as a EPSG code, proj4 string, cartopy CRS or pyproj.Proj.") from Exception(*errors) def load_tiff(filename, crs=None, apply_transform=False, nan_nodata=False, **kwargs): @@ -677,13 +682,21 @@ def from_xarray(da, crs=None, apply_transform=False, nan_nodata=False, **kwargs) if crs: kwargs['crs'] = crs elif hasattr(da, 'crs'): + # xarray.open_rasterio (not supported since April 2023) try: kwargs['crs'] = process_crs(da.crs) except Exception: - param.main.warning('Could not decode projection from crs string %r, ' - 'defaulting to non-geographic element.' % da.crs) + warn(f'Could not decode projection from crs string {da.crs}, ' + 'defaulting to non-geographic element.') + elif hasattr(da, 'rio') and da.rio.crs is not None: + # rioxarray.open_rasterio + try: + kwargs['crs'] = process_crs(da.rio.crs.to_proj4()) + except Exception: + warn(f'Could not decode projection from crs string {da.rio.crs}, ' + 'defaulting to non-geographic element.') - coords = list(da.coords) + coords = list(da.dims) if coords not in (['band', 'y', 'x'], ['y', 'x']): from .element.geo import Dataset, HvDataset el = Dataset if 'crs' in kwargs else HvDataset diff --git a/setup.py b/setup.py index bae0d215..e76f16fe 100644 --- a/setup.py +++ b/setup.py @@ -147,6 +147,7 @@ def run(self): 'nbsmoke >=0.2.0', 'pytest', 'fiona', + 'rioxarray', ], }