Repository: opendatacube/datacube-core

Issue:
`query_geopolygoon` doesn't work when `crs=` parameter is CRS (and not string)
### Expected behaviour

```python
from datacube.api.query import query_geopolygon
from datacube.utils.geometry import CRS

p1 = query_geopolygon(crs='epsg:3577', x=(0,1), y=(2,3))
p2 = query_geopolygon(crs=CRS('epsg:3577'), x=(0,1), y=(2,3))
```

I expect both lines to be valid and produce the same polygon.

### Actual behaviour

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-5a66aea5d98a> in <module>
      3 
      4 p1 = query_geopolygon(crs='epsg:3577', x=(0,1), y=(2,3))
----> 5 p2 = query_geopolygon(crs=CRS('epsg:3577'), x=(0,1), y=(2,3))

~/wk/datacube-core/datacube/api/query.py in query_geopolygon(geopolygon, **kwargs)
    142 def query_geopolygon(geopolygon=None, **kwargs):
    143     spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}
--> 144     crs = {v for k, v in kwargs.items() if k in CRS_KEYS}
    145     if len(crs) == 1:
    146         spatial_dims['crs'] = crs.pop()

~/wk/datacube-core/datacube/api/query.py in <setcomp>(.0)
    142 def query_geopolygon(geopolygon=None, **kwargs):
    143     spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}
--> 144     crs = {v for k, v in kwargs.items() if k in CRS_KEYS}
    145     if len(crs) == 1:
    146         spatial_dims['crs'] = crs.pop()

TypeError: unhashable type: 'CRS'
```

this was reported by @benjimin in #807.
`query_geopolygoon` doesn't work when `crs=` parameter is CRS (and not string)
### Expected behaviour

```python
from datacube.api.query import query_geopolygon
from datacube.utils.geometry import CRS

p1 = query_geopolygon(crs='epsg:3577', x=(0,1), y=(2,3))
p2 = query_geopolygon(crs=CRS('epsg:3577'), x=(0,1), y=(2,3))
```

I expect both lines to be valid and produce the same polygon.

### Actual behaviour

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-5a66aea5d98a> in <module>
      3 
      4 p1 = query_geopolygon(crs='epsg:3577', x=(0,1), y=(2,3))
----> 5 p2 = query_geopolygon(crs=CRS('epsg:3577'), x=(0,1), y=(2,3))

~/wk/datacube-core/datacube/api/query.py in query_geopolygon(geopolygon, **kwargs)
    142 def query_geopolygon(geopolygon=None, **kwargs):
    143     spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}
--> 144     crs = {v for k, v in kwargs.items() if k in CRS_KEYS}
    145     if len(crs) == 1:
    146         spatial_dims['crs'] = crs.pop()

~/wk/datacube-core/datacube/api/query.py in <setcomp>(.0)
    142 def query_geopolygon(geopolygon=None, **kwargs):
    143     spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}
--> 144     crs = {v for k, v in kwargs.items() if k in CRS_KEYS}
    145     if len(crs) == 1:
    146         spatial_dims['crs'] = crs.pop()

TypeError: unhashable type: 'CRS'
```

this was reported by @benjimin in #807.


Discussion:

Resolving patch:
diff --git a/datacube/api/query.py b/datacube/api/query.py
--- a/datacube/api/query.py
+++ b/datacube/api/query.py
@@ -141,11 +141,11 @@ def __str__(self):
 
 def query_geopolygon(geopolygon=None, **kwargs):
     spatial_dims = {dim: v for dim, v in kwargs.items() if dim in SPATIAL_KEYS}
-    crs = {v for k, v in kwargs.items() if k in CRS_KEYS}
+    crs = [v for k, v in kwargs.items() if k in CRS_KEYS]
     if len(crs) == 1:
-        spatial_dims['crs'] = crs.pop()
+        spatial_dims['crs'] = crs[0]
     elif len(crs) > 1:
-        raise ValueError('Spatial dimensions must be in the same coordinate reference system: {}'.format(crs))
+        raise ValueError('CRS is supplied twice')
 
     if geopolygon is not None and len(spatial_dims) > 0:
         raise ValueError('Cannot specify "geopolygon" and one of %s at the same time' % (SPATIAL_KEYS + CRS_KEYS,))
diff --git a/datacube/utils/geometry/_base.py b/datacube/utils/geometry/_base.py
--- a/datacube/utils/geometry/_base.py
+++ b/datacube/utils/geometry/_base.py
@@ -101,8 +101,12 @@ def __init__(self, crs_str):
         :param crs_str: string representation of a CRS, often an EPSG code like 'EPSG:4326'
         :raises: InvalidCRSError
         """
-        if isinstance(crs_str, CRS):
-            crs_str = crs_str.crs_str
+        if not isinstance(crs_str, str):
+            to_wkt = getattr(crs_str, 'to_wkt', None)
+            if to_wkt is None:
+                raise ValueError("Expect string or any object with `.to_wkt()` method")
+            crs_str = to_wkt()
+
         self.crs_str = crs_str
         self._crs = _make_crs(crs_str)
         # compatible with GDAL 3.0+
@@ -120,6 +124,14 @@ def __getstate__(self):
     def __setstate__(self, state):
         self.__init__(state['crs_str'])
 
+    def to_wkt(self):
+        """
+        WKT representation of the CRS
+
+        :type: str
+        """
+        return self._crs.ExportToWkt()
+
     @property
     def wkt(self):
         """
@@ -127,7 +139,7 @@ def wkt(self):
 
         :type: str
         """
-        return self._crs.ExportToWkt()
+        return self.to_wkt()
 
     @property
     def epsg(self):
@@ -207,6 +219,9 @@ def units(self):
     def __str__(self):
         return self.crs_str
 
+    def __hash__(self):
+        return hash(self.to_wkt())
+
     def __repr__(self):
         return "CRS('%s')" % self.crs_str
 
@@ -216,7 +231,10 @@ def __eq__(self, other):
         if isinstance(other, str):
             other = CRS(other)
         elif not isinstance(other, CRS):
-            return False
+            to_wkt = getattr(other, 'to_wkt', None)
+            if to_wkt is None:
+                return False
+            other = CRS(to_wkt())
         gdal_thinks_issame = self._crs.IsSame(other._crs) == 1  # pylint: disable=protected-access
         if gdal_thinks_issame:
             return True


Test patch:
diff --git a/tests/api/test_query.py b/tests/api/test_query.py
--- a/tests/api/test_query.py
+++ b/tests/api/test_query.py
@@ -20,6 +20,7 @@
 from datacube.api.query import Query, _datetime_to_timestamp, query_group_by, solar_day, GroupBy
 from datacube.model import Range
 from datacube.utils import parse_time
+from datacube.utils.geometry import CRS
 
 
 def test_datetime_to_timestamp():
@@ -67,6 +68,11 @@ def test_query_kwargs():
     assert 'lat' in query.search_terms
     assert 'lon' in query.search_terms
 
+    query = Query(index=mock_index, y=-4174726, x=1515184, crs=CRS('EPSG:3577'))
+    assert query.geopolygon
+    assert 'lat' in query.search_terms
+    assert 'lon' in query.search_terms
+
     query = Query(index=mock_index, time='2001')
     assert 'time' in query.search
 
diff --git a/tests/test_geometry.py b/tests/test_geometry.py
--- a/tests/test_geometry.py
+++ b/tests/test_geometry.py
@@ -7,6 +7,7 @@
 from datacube.utils import geometry
 from datacube.utils.geometry import (
     GeoBox,
+    CRS,
     BoundingBox,
     bbox_union,
     decompose_rws,
@@ -1081,3 +1082,30 @@ def test_axis_overlap():
     # D: |<--->|
     assert compute_axis_overlap(10, 10, 1, -11) == s_[0:0, 10:10]
     assert compute_axis_overlap(40, 10, 1, -11) == s_[0:0, 10:10]
+
+
+def test_crs_compat():
+    import rasterio.crs
+
+    crs = CRS("epsg:3577")
+    assert crs.epsg == 3577
+    crs2 = CRS(crs)
+    assert crs.epsg == crs2.epsg
+
+    crs_rio = rasterio.crs.CRS(init='epsg:3577')
+    assert CRS(crs_rio).epsg == 3577
+
+    assert (CRS(crs_rio) == crs_rio) is True
+
+    assert rasterio.crs.CRS.from_user_input(crs).to_epsg() == 3577
+
+    with pytest.raises(ValueError):
+        CRS(("random", "tuple"))
+
+
+def test_crs_hash():
+    crs = CRS("epsg:3577")
+    crs2 = CRS(crs)
+
+    assert crs is not crs2
+    assert len(set([crs, crs2])) == 1