Repository: corteva/rioxarray

Issue:
Allow users to define the x.y coordinates (similar to `set_geometry` in geopandas)
Currently only x,y and latitude,longitude are supported. But, sometime people use `lat`& `lon` etc... See: https://gis.stackexchange.com/questions/328128/extracting-data-within-geometry-shape/328320#comment535454_328320

It would be useful to allow users to define it themselves. Either `set_geometry` or  `set_xy`?
Also, this would mean that the check for x,y coordinates would have to be delayed until users use a function that needs it.

Current workaround:
```python
>>> import xarray
>>> xds = xarray.Dataset(coords={"lon": [1, 2], "lat": [3, 4]})
>>> xds
<xarray.Dataset>
Dimensions:  (lat: 2, lon: 2)
Coordinates:
  * lon      (lon) int64 1 2
  * lat      (lat) int64 3 4
Data variables:
    *empty*
>>> xds.rename({"lon": "longitude", "lat": "latitude"})
<xarray.Dataset>
Dimensions:    (latitude: 2, longitude: 2)
Coordinates:
  * longitude  (longitude) int64 1 2
  * latitude   (latitude) int64 3 4
Data variables:
    *empty*

```

Comments:

Patch:
diff --git a/rioxarray/exceptions.py b/rioxarray/exceptions.py
index 99a58fd..c842097 100644
--- a/rioxarray/exceptions.py
+++ b/rioxarray/exceptions.py
@@ -12,21 +12,25 @@ class NoDataInBounds(RioXarrayError):
     """This is for when there are no data in the bounds for clipping a raster."""
 
 
-class OneDimensionalRaster(RioXarrayError):
-    """This is an error when you have a 1 dimensional raster."""
-
-
 class SingleVariableDataset(RioXarrayError):
     """This is for when you have a dataset with a single variable."""
 
 
-class TooManyDimensions(RioXarrayError):
+class DimensionError(RioXarrayError):
     """This is raised when there are more dimensions than is supported by the method"""
 
 
-class InvalidDimensionOrder(RioXarrayError):
+class TooManyDimensions(DimensionError):
+    """This is raised when there are more dimensions than is supported by the method"""
+
+
+class InvalidDimensionOrder(DimensionError):
     """This is raised when there the dimensions are not ordered correctly."""
 
 
+class OneDimensionalRaster(DimensionError):
+    """This is an error when you have a 1 dimensional raster."""
+
+
 class MissingCRS(RioXarrayError):
     """Missing the CRS in the dataset."""
diff --git a/rioxarray/rioxarray.py b/rioxarray/rioxarray.py
index 1d791fc..3af3dae 100644
--- a/rioxarray/rioxarray.py
+++ b/rioxarray/rioxarray.py
@@ -24,6 +24,7 @@ from rasterio.features import geometry_mask
 from scipy.interpolate import griddata
 
 from rioxarray.exceptions import (
+    DimensionError,
     InvalidDimensionOrder,
     MissingCRS,
     NoDataInBounds,
@@ -204,18 +205,19 @@ class XRasterBase(object):
     def __init__(self, xarray_obj):
         self._obj = xarray_obj
 
+        self._x_dim = None
+        self._y_dim = None
         # Determine the spatial dimensions of the `xarray.DataArray`
         if "x" in self._obj.dims and "y" in self._obj.dims:
-            self.x_dim = "x"
-            self.y_dim = "y"
+            self._x_dim = "x"
+            self._y_dim = "y"
         elif "longitude" in self._obj.dims and "latitude" in self._obj.dims:
-            self.x_dim = "longitude"
-            self.y_dim = "latitude"
-        else:
-            raise KeyError("Missing x,y dimensions ...")
+            self._x_dim = "longitude"
+            self._y_dim = "latitude"
 
         # properties
-        self._shape = None
+        self._width = None
+        self._height = None
         self._crs = None
 
     @property
@@ -312,12 +314,80 @@ class XRasterBase(object):
             data_obj.attrs = var_attrs
         return data_obj
 
+    def set_spatial_dims(self, x_dim, y_dim, inplace=True):
+        """
+        This sets the spatial dimensions of the dataset.
+
+        Parameters:
+        -----------
+        x_dim: str
+            The name of the x dimension.
+        y_dim: str
+            The name of the y dimension.
+        inplace: bool, optional
+            If True, it will modify the dataframe in place.
+            Otherwise it will return a modified copy.
+
+        Returns:
+        --------
+        xarray.Dataset or xarray.DataArray:
+            Dataset with spatial dimensions set.
+
+        """
+
+        def set_dims(obj, in_x_dim, in_y_dim):
+            if in_x_dim in obj.dims:
+                obj.rio._x_dim = x_dim
+            else:
+                raise DimensionError("x dimension not found: {}".format(x_dim))
+            if y_dim in obj.dims:
+                obj.rio._y_dim = y_dim
+            else:
+                raise DimensionError("y dimension not found: {}".format(y_dim))
+
+        if not inplace:
+            obj_copy = self._obj.copy()
+            set_dims(obj_copy, x_dim, y_dim)
+            return obj_copy
+        set_dims(self._obj, x_dim, y_dim)
+        return self._obj
+
+    @property
+    def x_dim(self):
+        if self._x_dim is not None:
+            return self._x_dim
+        raise DimensionError(
+            "x dimension not found. 'set_spatial_dims()' can address this."
+        )
+
+    @property
+    def y_dim(self):
+        if self._y_dim is not None:
+            return self._y_dim
+        raise DimensionError(
+            "x dimension not found. 'set_spatial_dims()' can address this."
+        )
+
+    @property
+    def width(self):
+        """int: Returns the width of the dataset (x dimension size)"""
+        if self._width is not None:
+            return self._width
+        self._width = self._obj[self.x_dim].size
+        return self._width
+
+    @property
+    def height(self):
+        """int: Returns the height of the dataset (y dimension size)"""
+        if self._height is not None:
+            return self._height
+        self._height = self._obj[self.y_dim].size
+        return self._height
+
     @property
     def shape(self):
-        """tuple: Returns the shape (x_size, y_size)"""
-        if self._shape is None:
-            self._shape = self._obj[self.x_dim].size, self._obj[self.y_dim].size
-        return self._shape
+        """tuple: Returns the shape (width, height)"""
+        return (self.width, self.height)
 
 
 @xarray.register_dataarray_accessor("rio")
@@ -402,8 +472,7 @@ class RasterArray(XRasterBase):
             transform attribute.
 
         """
-        width, height = self.shape
-        if not recalc or width == 1 or height == 1:
+        if not recalc or self.width == 1 or self.height == 1:
             try:
                 # get resolution from xarray rasterio
                 data_transform = Affine(*self._obj.attrs["transform"][:6])
@@ -414,19 +483,19 @@ class RasterArray(XRasterBase):
                 recalc = True
 
         if recalc:
-            left, bottom, right, top = self._int_bounds()
+            left, bottom, right, top = self._internal_bounds()
 
-            if width == 1 or height == 1:
+            if self.width == 1 or self.height == 1:
                 raise OneDimensionalRaster(
                     "Only 1 dimenional array found. Cannot calculate the resolution."
                 )
 
-            resolution_x = (right - left) / (width - 1)
-            resolution_y = (bottom - top) / (height - 1)
+            resolution_x = (right - left) / (self.width - 1)
+            resolution_y = (bottom - top) / (self.height - 1)
 
         return resolution_x, resolution_y
 
-    def _int_bounds(self):
+    def _internal_bounds(self):
         """Determine the internal bounds of the `xarray.DataArray`"""
         left = float(self._obj[self.x_dim][0])
         right = float(self._obj[self.x_dim][-1])
@@ -476,7 +545,7 @@ class RasterArray(XRasterBase):
         left, bottom, right, top: float
             Outermost coordinates.
         """
-        left, bottom, right, top = self._int_bounds()
+        left, bottom, right, top = self._internal_bounds()
         src_resolution_x, src_resolution_y = self.resolution(recalc=recalc)
         left -= src_resolution_x / 2.0
         right += src_resolution_x / 2.0
@@ -563,9 +632,6 @@ class RasterArray(XRasterBase):
         :class:`xarray.DataArray`: A reprojected DataArray.
 
         """
-        # TODO: Support lazy loading of data with dask imperative function
-        src_data = np.copy(self._obj.load().data)
-
         src_affine = self.transform()
         if dst_affine_width_height is not None:
             dst_affine, dst_width, dst_height = dst_affine_width_height
@@ -594,7 +660,7 @@ class RasterArray(XRasterBase):
             self.nodata if self.nodata is not None else dst_nodata
         )
         rasterio.warp.reproject(
-            source=src_data,
+            source=np.copy(self._obj.load().data),
             destination=dst_data,
             src_transform=src_affine,
             src_crs=self.crs,
@@ -682,12 +748,13 @@ class RasterArray(XRasterBase):
         DataArray: A sliced :class:`xarray.DataArray` object.
 
         """
-        if self._obj.y[0] > self._obj.y[-1]:
+        left, bottom, right, top = self._internal_bounds()
+        if top > bottom:
             y_slice = slice(maxy, miny)
         else:
             y_slice = slice(miny, maxy)
 
-        if self._obj.x[0] > self._obj.x[-1]:
+        if left > right:
             x_slice = slice(maxx, minx)
         else:
             x_slice = slice(minx, maxx)
@@ -718,9 +785,9 @@ class RasterArray(XRasterBase):
         DataArray: A clipped :class:`xarray.DataArray` object.
 
         """
-        if self._obj.coords["x"].size == 1 or self._obj.coords["y"].size == 1:
+        if self.width == 1 or self.height == 1:
             raise OneDimensionalRaster(
-                "At least one of the raster x,y coordinates" " has only one point."
+                "At least one of the raster x,y coordinates has only one point."
             )
 
         resolution_x, resolution_y = self.resolution()
@@ -732,10 +799,10 @@ class RasterArray(XRasterBase):
 
         cl_array = self.slice_xy(clip_minx, clip_miny, clip_maxx, clip_maxy)
 
-        if cl_array.coords["x"].size < 1 or cl_array.coords["y"].size < 1:
+        if cl_array.rio.width < 1 or cl_array.rio.height < 1:
             raise NoDataInBounds("No data found in bounds.")
 
-        if cl_array.coords["x"].size == 1 or cl_array.coords["y"].size == 1:
+        if cl_array.rio.width == 1 or cl_array.rio.height == 1:
             if auto_expand and auto_expand < auto_expand_limit:
                 return self.clip_box(
                     clip_minx,
@@ -799,10 +866,9 @@ class RasterArray(XRasterBase):
             for geometry in geometries
         ]
 
-        width, height = self.shape
         clip_mask_arr = geometry_mask(
             geometries=geometries,
-            out_shape=(int(height), int(width)),
+            out_shape=(int(self.height), int(self.width)),
             transform=self.transform(),
             invert=True,
             all_touched=all_touched,
@@ -933,7 +999,6 @@ class RasterArray(XRasterBase):
             are ignored.
 
         """
-        width, height = self.shape
         dtype = str(self._obj.dtype) if dtype is None else dtype
         extra_dim = self._check_dimensions()
         count = 1
@@ -969,8 +1034,8 @@ class RasterArray(XRasterBase):
             raster_path,
             "w",
             driver=driver,
-            height=int(height),
-            width=int(width),
+            height=int(self.height),
+            width=int(self.width),
             count=count,
             dtype=dtype,
             crs=self.crs,
diff --git a/sphinx/history.rst b/sphinx/history.rst
index 6c761b3..06ca300 100644
--- a/sphinx/history.rst
+++ b/sphinx/history.rst
@@ -9,6 +9,7 @@ History
 - Preserve None nodata if opened with `xarray.open_rasterio` (issue #20)
 - Added `drop` argument for `clip()` (issue #25)
 - Fix order of `CRS` for reprojecting geometries in `clip()` (pull #24)
+- Added `set_spatial_dims()` method for datasets when dimensions not found (issue #27)
 
 0.0.5
 -----


Test patch:
diff --git a/test/integration/test_integration_rioxarray.py b/test/integration/test_integration_rioxarray.py
index 049b15a..f711adc 100644
--- a/test/integration/test_integration_rioxarray.py
+++ b/test/integration/test_integration_rioxarray.py
@@ -8,7 +8,12 @@ from affine import Affine
 from numpy.testing import assert_almost_equal, assert_array_equal
 from rasterio.crs import CRS
 
-from rioxarray.exceptions import MissingCRS, NoDataInBounds, OneDimensionalRaster
+from rioxarray.exceptions import (
+    DimensionError,
+    MissingCRS,
+    NoDataInBounds,
+    OneDimensionalRaster,
+)
 from rioxarray.rioxarray import UNWANTED_RIO_ATTRS, _make_coords
 from test.conftest import TEST_COMPARE_DATA_DIR, TEST_INPUT_DATA_DIR
 
@@ -796,10 +801,61 @@ def test_to_raster__preserve_profile__none_nodata(tmpdir):
         assert rds.nodata is None
 
 
-def test_missing_dimensions():
+def test_missing_spatial_dimensions():
     test_ds = xarray.Dataset()
-    with pytest.raises(KeyError):
-        test_ds.rio
+    with pytest.raises(DimensionError):
+        test_ds.rio.shape
+    with pytest.raises(DimensionError):
+        test_ds.rio.width
+    with pytest.raises(DimensionError):
+        test_ds.rio.height
+    test_da = xarray.DataArray(1)
+    with pytest.raises(DimensionError):
+        test_da.rio.shape
+    with pytest.raises(DimensionError):
+        test_da.rio.width
+    with pytest.raises(DimensionError):
+        test_da.rio.height
+
+
+def test_set_spatial_dims():
+    test_da = xarray.DataArray(
+        numpy.zeros((5, 5)),
+        dims=("lat", "lon"),
+        coords={"lat": numpy.arange(1, 6), "lon": numpy.arange(2, 7)},
+    )
+    test_da_copy = test_da.rio.set_spatial_dims(x_dim="lon", y_dim="lat", inplace=False)
+    assert test_da_copy.rio.x_dim == "lon"
+    assert test_da_copy.rio.y_dim == "lat"
+    assert test_da_copy.rio.width == 5
+    assert test_da_copy.rio.height == 5
+    assert test_da_copy.rio.shape == (5, 5)
+    with pytest.raises(DimensionError):
+        test_da.rio.shape
+    with pytest.raises(DimensionError):
+        test_da.rio.width
+    with pytest.raises(DimensionError):
+        test_da.rio.height
+
+    test_da.rio.set_spatial_dims(x_dim="lon", y_dim="lat", inplace=True)
+    assert test_da.rio.x_dim == "lon"
+    assert test_da.rio.y_dim == "lat"
+    assert test_da.rio.width == 5
+    assert test_da.rio.height == 5
+    assert test_da.rio.shape == (5, 5)
+
+
+def test_set_spatial_dims__missing():
+    test_ds = xarray.Dataset()
+    with pytest.raises(DimensionError):
+        test_ds.rio.set_spatial_dims(x_dim="lon", y_dim="lat")
+    test_da = xarray.DataArray(
+        numpy.zeros((5, 5)),
+        dims=("lat", "lon"),
+        coords={"lat": numpy.arange(1, 6), "lon": numpy.arange(2, 7)},
+    )
+    with pytest.raises(DimensionError):
+        test_da.rio.set_spatial_dims(x_dim="long", y_dim="lati")
 
 
 def test_crs_setter():