Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix error message for 0D geometry variable in GeoDataset #885

Merged
merged 4 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Fixed
- Bug in `raster.clip_bbox` when bbox doesn't overlap with raster. (#860)
- Allow for string format in zoom_level path, e.g. `{zoom_level:02d}` (#851)
- Fixed incorrect renaming of single variable raster datasets (#883)
- Provide better error message for 0D geometry arrays in GeoDataset (#885)

v0.9.4 (2024-02-26)
===================
Expand Down
87 changes: 49 additions & 38 deletions hydromt/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

import logging
from typing import Any, Union
from typing import Any, Hashable, Optional, Union

import geopandas as gpd
import numpy as np
Expand All @@ -23,13 +23,13 @@ class GeoBase(raster.XGeoBase):

"""Base accessor class for geo data."""

def __init__(self, xarray_obj):
def __init__(self, xarray_obj) -> None:
"""Initialize a new object based on the provided xarray_obj."""
super(GeoBase, self).__init__(xarray_obj)
self._geometry = None

@property
def _all_names(self):
def _all_names(self) -> list[Hashable]:
"""Return names of all variables and coordinates in the dataset/array."""
# TODO: move to geobase
names = [n for n in self._obj.coords]
Expand All @@ -42,23 +42,23 @@ def _get_geom_names_types(self, geom_name: str = None) -> tuple[list, list]:
names, types = [], []
dvars = self._all_names if geom_name is None else [geom_name]
for name in dvars:
if self._obj[name].ndim == 1 and isinstance(
self._obj[name][0].values.item(), BaseGeometry
):
ndim = self._obj[name].ndim
if ndim != 1: # only single dim geometries
continue
item = self._obj[name][0].values.item()
if isinstance(item, BaseGeometry):
names.append(name)
types.append("geom")
elif self._obj[name].ndim == 1 and isinstance(
self._obj[name][0].values.item(), str
):
elif isinstance(item, str):
try:
shapely.wkt.loads(self._obj[name][0].values.item())
shapely.wkt.loads(item)
names.append(name)
types.append("wkt")
except Exception:
pass
return names, types

def _discover_xy(self, x_name=None, y_name=None, index_dim=None):
def _discover_xy(self, x_name=None, y_name=None, index_dim=None) -> None:
"""Discover xy type geometries in the dataset/array."""
# infer x dim
if x_name is None:
Expand Down Expand Up @@ -94,7 +94,7 @@ def _discover_xy(self, x_name=None, y_name=None, index_dim=None):
self.attrs.pop("geom_format", None)
self.attrs.pop("index_dim", None)

def _discover_geom(self, geom_name=None, index_dim=None):
def _discover_geom(self, geom_name=None, index_dim=None) -> None:
"""Discover geom/wkt type geometries in the dataset/array."""
# check /infer geom dim
names, types = self._get_geom_names_types(geom_name=geom_name)
Expand Down Expand Up @@ -153,7 +153,9 @@ def set_spatial_dims(
self._discover_xy(x_name=x_name, y_name=y_name, index_dim=index_dim)
self.attrs.pop("geom_name", None)
if "geom_format" not in self.attrs:
raise ValueError("No geometry data found.")
raise ValueError(
"No geometry data found. Make sure the data has a 1D geometry variable."
)

@property
def geom_format(self) -> str:
Expand Down Expand Up @@ -185,51 +187,54 @@ def geom_type(self) -> str:
return geom_type

@property
def x_name(self):
def x_name(self) -> Optional[str]:
"""Name of x coordinate; only for point geometries in xy format."""
if self.get_attrs("x_name") not in self._obj.dims:
self.set_spatial_dims()
if "x_name" in self.attrs:
return self.attrs["x_name"]

@property
def y_name(self):
def y_name(self) -> Optional[str]:
"""Name of y coordinate; only for point geometries in xy format."""
if self.get_attrs("y_name") not in self._obj.dims:
self.set_spatial_dims()
if "y_name" in self.attrs:
return self.attrs["y_name"]

@property
def index_dim(self):
def index_dim(self) -> Optional[str]:
"""Index dimension name."""
if self.get_attrs("index_dim") not in self._obj.dims:
self.set_spatial_dims()
if "index_dim" in self.attrs:
return self.attrs["index_dim"]

@property
def sindex(self):
def sindex(self) -> Any: # type of sindex can be different
"""Return the spatial index of the geometry."""
return self.geometry.sindex

@property
def index(self):
def index(self) -> Optional[xr.DataArray]:
"""Return the index values."""
return self._obj[self.index_dim]
if self.index_dim:
return self._obj[self.index_dim]

@property
def size(self):
def size(self) -> Optional[int]:
"""Return the length of the index array."""
return self._obj[self.index_dim].size
if self.index_dim:
return self._obj[self.index_dim].size

@property
def bounds(self):
def bounds(self) -> Optional[np.ndarray]:
"""Return the bounds (xmin, ymin, xmax, ymax) of the object."""
return self.geometry.total_bounds
if self.geometry is not None:
return self.geometry.total_bounds

@property
def geometry(self) -> GeoSeries:
def geometry(self) -> Optional[GeoSeries]:
"""Return the geometry of the dataset or array as GeoSeries.

Returns
Expand All @@ -240,7 +245,8 @@ def geometry(self) -> GeoSeries:
if self._geometry is not None and self._geometry.index.size == self.size:
return self._geometry
# if no geometry is present return None self._geometry
# rather than raising an error
# rather than raising an error ->
# FIXME is this the right approach?
try:
self.set_spatial_dims()
except ValueError:
Expand Down Expand Up @@ -279,7 +285,7 @@ def update_geometry(
y_name: str = None,
geom_name: str = None,
replace: bool = True,
):
) -> Union[xr.Dataset, xr.DataArray]:
"""Update the geometry in the Dataset/Array with a new geometry.

if provided or use that, otherwise update the current
Expand Down Expand Up @@ -316,8 +322,8 @@ def update_geometry(
drop_vars = []
if self.geom_format != geom_format:
if geom_format != "xy":
drop_vars = [self.x_name, self.y_name]
else:
drop_vars = [name for name in [self.x_name, self.y_name] if name]
elif self.geom_name is not None:
drop_vars = [self.geom_name]

index_dim = self.index_dim
Expand All @@ -326,13 +332,13 @@ def update_geometry(
geom_name = self.attrs.get("geom_name", "geometry")
elif self.geom_name != geom_name:
drop_vars.append(self.geom_name)
coords = {geom_name: (self.index_dim, geometry.values)}
coords = {geom_name: (index_dim, geometry.values)}
elif geom_format == "wkt":
if geom_name is None:
geom_name = self.attrs.get("geom_name", "ogc_wkt")
elif self.geom_name != geom_name:
drop_vars.append(self.geom_name)
coords = {geom_name: (self.index_dim, geometry.to_wkt().values)}
coords = {geom_name: (index_dim, geometry.to_wkt().values)}
elif geom_format == "xy":
if x_name is None:
x_name = self.attrs.get("x_name", "x")
Expand All @@ -343,8 +349,8 @@ def update_geometry(
elif self.y_name != y_name:
drop_vars.append(self.y_name)
coords = {
x_name: (self.index_dim, geometry.x.values),
y_name: (self.index_dim, geometry.y.values),
x_name: (index_dim, geometry.x.values),
y_name: (index_dim, geometry.y.values),
}
obj = self._obj.copy()
if replace:
Expand All @@ -364,7 +370,6 @@ def update_geometry(
return obj

# Internal conversion and selection methods
# i.e. produces xarray.Dataset/ xarray.DataArray
def ogr_compliant(self, reducer=None) -> xr.Dataset:
"""Create a Dataset/Array which is understood by OGR.

Expand Down Expand Up @@ -495,7 +500,9 @@ def to_wkt(
return obj

## clip
def clip_geom(self, geom, predicate="intersects"):
def clip_geom(
self, geom, predicate="intersects"
) -> Union[xr.DataArray, xr.Dataset]:
"""Select all geometries that intersect with the input geometry.

Arguments
Expand All @@ -517,7 +524,7 @@ def clip_geom(self, geom, predicate="intersects"):
idx = gis_utils.filter_gdf(self.geometry, geom=geom, predicate=predicate)
return self._obj.isel({self.index_dim: idx})

def clip_bbox(self, bbox, crs=None, buffer=None):
def clip_bbox(self, bbox, crs=None, buffer=None) -> Union[xr.DataArray, xr.Dataset]:
"""Select point locations to bounding box.

Arguments
Expand Down Expand Up @@ -545,7 +552,7 @@ def clip_bbox(self, bbox, crs=None, buffer=None):

## wrap GeoSeries functions
# TODO write general wrapper
def to_crs(self, dst_crs):
def to_crs(self, dst_crs) -> Union[xr.DataArray, xr.Dataset]:
"""Transform spatial coordinates to a new coordinate reference system.

The ``crs`` attribute on the current GeoDataArray must be set.
Expand All @@ -570,7 +577,7 @@ def to_crs(self, dst_crs):

## Output methods
## Either writes to files or other data types
def to_gdf(self, reducer=None):
def to_gdf(self, reducer=None) -> gpd.GeoDataFrame:
"""Return geopandas GeoDataFrame with Point geometry.

Geometry is based on Dataset coordinates. If a reducer is
Expand All @@ -588,6 +595,10 @@ def to_gdf(self, reducer=None):
gdf: geopandas.GeoDataFrame
GeoDataFrame
"""
if self.geometry is None:
raise ValueError(
"No geometry data found. Make sure the data has a 1D geometry variable."
)
if isinstance(reducer, str):
reducer = getattr(np, reducer)
obj = self._obj
Expand Down Expand Up @@ -835,7 +846,7 @@ def __init__(self, xarray_obj):
# Properties
# Will probably be deleted in the future but now needed for compatibility
@property
def vars(self):
def vars(self) -> list[str]:
"""list: Returns non-coordinate varibles."""
return list(self._obj.data_vars.keys())

Expand Down
16 changes: 15 additions & 1 deletion tests/test_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np
import pytest
import xarray as xr
from geopandas import GeoDataFrame
from geopandas import GeoDataFrame, GeoSeries
from pyproj import CRS
from shapely.geometry import MultiPolygon, Polygon

Expand Down Expand Up @@ -82,6 +82,20 @@ def test_vector(geoda, geodf):
assert da1.vector.crs == gdf1.crs


def test_single_geom_vector(geoda, tmp_dir):
geom = geoda.isel(index=0).vector.geometry
assert geom is None
# write to file
with pytest.raises(ValueError, match="No geometry data found"):
geoda.isel(index=0).vector.to_netcdf(tmp_dir / "test.nc")

geom1 = geoda.isel(index=[0]).vector.geometry
assert isinstance(geom1, GeoSeries)
fn_nc = tmp_dir / "test.nc"
geoda.isel(index=[0]).vector.to_netcdf(fn_nc)
assert fn_nc.is_file()


def test_from_gdf(geoda, geodf):
geoda0 = geoda.reset_coords(drop=True) # drop geometries
dims = list(geoda0.dims)
Expand Down
Loading