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

Backport fixes from odc-geo #1242

Merged
merged 2 commits into from
Mar 8, 2022
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
81 changes: 45 additions & 36 deletions datacube/utils/geometry/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,25 @@ def from_points(p1: Tuple[float, float],
(p1[1], p2[1]))


@cachetools.cached({})
def _make_crs(crs_str: str) -> Tuple[_CRS, Optional[int]]:
crs = _CRS.from_user_input(crs_str)
return (crs, crs.to_epsg())
def _make_crs_key(crs_spec: Union[str, _CRS]) -> str:
if isinstance(crs_spec, str):
normed_epsg = crs_spec.upper()
if normed_epsg.startswith("EPSG:"):
return normed_epsg
return crs_spec
return crs_spec.to_wkt()


@cachetools.cached({}, key=_make_crs_key)
def _make_crs(crs: Union[str, _CRS]) -> Tuple[_CRS, str, Optional[int]]:
if isinstance(crs, str):
crs = _CRS.from_user_input(crs)
epsg = crs.to_epsg()
if epsg is not None:
crs_str = f"EPSG:{epsg}"
else:
crs_str = crs.to_wkt()
return (crs, crs_str, crs.to_epsg())


def _make_crs_transform_key(from_crs, to_crs, always_xy):
Expand All @@ -131,26 +146,6 @@ def _make_crs_transform(from_crs, to_crs, always_xy):
return Transformer.from_crs(from_crs, to_crs, always_xy=always_xy).transform


def _guess_crs_str(crs_spec: Any) -> Optional[str]:
"""
Returns a string representation of the crs spec.
Returns `None` if it does not understand the spec.
"""
if isinstance(crs_spec, str):
return crs_spec
if isinstance(crs_spec, dict):
crs_spec = _CRS.from_dict(crs_spec)

if hasattr(crs_spec, 'to_wkt'):
return crs_spec.to_wkt()
if hasattr(crs_spec, 'to_epsg'):
epsg = crs_spec.to_epsg()
if epsg is not None:
return 'EPSG:{}'.format(crs_spec.to_epsg())

return None


class CRS:
"""
Wrapper around `pyproj.CRS` for backwards compatibility.
Expand All @@ -160,20 +155,34 @@ class CRS:

__slots__ = ('_crs', '_epsg', '_str')

def __init__(self, crs_str: Any):
def __init__(self, crs_spec: Any):
"""
:param crs_str: string representation of a CRS, often an EPSG code like 'EPSG:4326'
:raises: `pyproj.exceptions.CRSError`
"""
crs_str = _guess_crs_str(crs_str)
if crs_str is None:
raise CRSError("Expect string or any object with `.to_epsg()` or `.to_wkt()` method")

_crs, _epsg = _make_crs(crs_str)

self._crs = _crs
self._epsg = _epsg
self._str = crs_str
if isinstance(crs_spec, str):
self._crs, self._str, self._epsg = _make_crs(crs_spec)
elif isinstance(crs_spec, CRS):
self._crs = crs_spec._crs
self._epsg = crs_spec._epsg
self._str = crs_spec._str
elif isinstance(crs_spec, _CRS):
self._crs, self._str, self._epsg = _make_crs(crs_spec)
elif isinstance(crs_spec, dict):
self._crs, self._str, self._epsg = _make_crs(_CRS.from_dict(crs_spec))
else:
_to_epsg = getattr(crs_spec, "to_epsg", None)
if _to_epsg is not None:
self._crs, self._str, self._epsg = _make_crs(f"EPSG:{_to_epsg()}")
return
_to_wkt = getattr(crs_spec, "to_wkt", None)
if _to_wkt is not None:
self._crs, self._str, self._epsg = _make_crs(_to_wkt())
return

raise CRSError(
"Expect string or any object with `.to_epsg()` or `.to_wkt()` methods"
)

def __getstate__(self):
return {'crs_str': self._str}
Expand Down Expand Up @@ -257,7 +266,7 @@ def __str__(self) -> str:
return self._str

def __hash__(self) -> int:
return hash(self.to_wkt())
return hash(self._str)

def __repr__(self) -> str:
return "CRS('%s')" % self._str
Expand Down Expand Up @@ -987,7 +996,6 @@ class GeoBox:
"""

def __init__(self, width: int, height: int, affine: Affine, crs: MaybeCRS):
assert is_affine_st(affine), "Only axis-aligned geoboxes are currently supported"
self.width = width
self.height = height
self.affine = affine
Expand Down Expand Up @@ -1110,6 +1118,7 @@ def coordinates(self) -> Dict[str, Coordinate]:
"""
dict of coordinate labels
"""
assert is_affine_st(self.affine), "Only axis-aligned geoboxes are currently supported"
yres, xres = self.resolution
yoff, xoff = self.affine.yoff, self.affine.xoff

Expand Down
6 changes: 3 additions & 3 deletions tests/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
bounding_box_in_pixel_domain,
geobox_intersection_conservative,
geobox_union_conservative,
_guess_crs_str,
force_2d,
_align_pix,
_round_to_res,
_norm_crs,
_norm_crs_or_error,
_make_crs_key,
)
from datacube.testutils.geom import (
epsg4326,
Expand Down Expand Up @@ -1475,9 +1475,9 @@ def test_crs_hash():


def test_base_internals():
assert _guess_crs_str(CRS("epsg:3577")) == epsg3577.to_wkt()
assert _make_crs_key("epsg:3577") == "EPSG:3577"
no_epsg_crs = CRS(SAMPLE_WKT_WITHOUT_AUTHORITY)
assert _guess_crs_str(no_epsg_crs) == no_epsg_crs.to_wkt()
assert _make_crs_key(no_epsg_crs.proj) == no_epsg_crs.proj.to_wkt()

gjson_bad = {'type': 'a', 'coordinates': [1, [2, 3, 4]]}
assert force_2d(gjson_bad) == {'type': 'a', 'coordinates': [1, [2, 3]]}
Expand Down