Skip to content

Commit

Permalink
Merge branch 'issue437_better-documentation-of-crs-code-usage'
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Sep 12, 2023
2 parents 6bb1eda + e464bc3 commit 4efb921
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 21 deletions.
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ openeo.util
-------------

.. automodule:: openeo.util
:members: to_bbox_dict, BBoxDict, load_json_resource, string_to_temporal_extent
:members: to_bbox_dict, BBoxDict, load_json_resource, string_to_temporal_extent, normalize_crs


openeo.processes
Expand Down
38 changes: 27 additions & 11 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import typing
import warnings
from builtins import staticmethod
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, Sequence

import numpy as np
import shapely.geometry
Expand Down Expand Up @@ -287,12 +287,16 @@ def filter_temporal(

@openeo_process
def filter_bbox(
self,
*args,
west=None, south=None, east=None, north=None,
crs=None,
base=None, height=None,
bbox=None
self,
*args,
west: Optional[float] = None,
south: Optional[float] = None,
east: Optional[float] = None,
north: Optional[float] = None,
crs: Optional[Union[int, str]] = None,
base: Optional[float] = None,
height: Optional[float] = None,
bbox: Optional[Sequence[float]] = None,
) -> DataCube:
"""
Limits the data cube to the specified bounding box.
Expand All @@ -304,7 +308,7 @@ def filter_bbox(
>>> cube.filter_bbox(west=3, south=51, east=4, north=52, crs=4326)
- With a (west, south, east, north) list or tuple
(note that EPSG:4326 is the default CRS, so it's not nececarry to specify it explicitly)::
(note that EPSG:4326 is the default CRS, so it's not necessary to specify it explicitly)::
>>> cube.filter_bbox([3, 51, 4, 52])
>>> cube.filter_bbox(bbox=[3, 51, 4, 52])
Expand All @@ -329,14 +333,21 @@ def filter_bbox(
- With a CRS other than EPSG 4326::
>>> cube.filter_bbox(west=652000, east=672000, north=5161000, south=5181000, crs=32632)
>>> cube.filter_bbox(
... west=652000, east=672000, north=5161000, south=5181000,
... crs=32632
... )
- Deprecated: positional arguments are also supported,
but follow a non-standard order for legacy reasons::
>>> west, east, north, south = 3, 4, 52, 51
>>> cube.filter_bbox(west, east, north, south)
:param crs: value describing the coordinate reference system.
Typically just an int (interpreted as EPSG code, e.g. ``4326``)
or a string (handled as authority string, e.g. ``"EPSG:4326"``).
See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument.
"""
if args and any(k is not None for k in (west, south, east, north, bbox)):
raise ValueError("Don't mix positional arguments with keyword arguments.")
Expand Down Expand Up @@ -827,6 +838,9 @@ def _get_geometry_argument(
) -> Union[dict, Parameter, PGNode]:
"""
Convert input to a geometry as "geojson" subtype object.
:param crs: value that encodes a coordinate reference system.
See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument.
"""
if isinstance(geometry, (str, pathlib.Path)):
# Assumption: `geometry` is path to polygon is a path to vector file at backend.
Expand Down Expand Up @@ -875,7 +889,7 @@ def aggregate_spatial(
],
reducer: Union[str, typing.Callable, PGNode],
target_dimension: Optional[str] = None,
crs: Optional[str] = None,
crs: Optional[Union[int, str]] = None,
context: Optional[dict] = None,
# TODO arguments: target dimension, context
) -> VectorCube:
Expand All @@ -901,7 +915,9 @@ def aggregate_spatial(
:param target_dimension: The new dimension name to be used for storing the results.
:param crs: The spatial reference system of the provided polygon.
By default longitude-latitude (EPSG:4326) is assumed.
By default, longitude-latitude (EPSG:4326) is assumed.
See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument.
:param context: Additional data to be passed to the reducer process.
.. note:: this ``crs`` argument is a non-standard/experimental feature, only supported by specific back-ends.
Expand Down
32 changes: 23 additions & 9 deletions openeo/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,11 @@ class BBoxDict(dict):
Dictionary based helper to easily create/work with bounding box dictionaries
(having keys "west", "south", "east", "north", and optionally "crs").
:param crs: value describing the coordinate reference system.
Typically just an int (interpreted as EPSG code, e.g. ``4326``)
or a string (handled as authority string, e.g. ``"EPSG:4326"``).
See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument.
.. versionadded:: 0.10.1
"""

Expand Down Expand Up @@ -798,24 +803,33 @@ def get(self, fraction: float) -> str:

def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]:
"""
Normalize given data structure (typically just an int or string)
that encodes a CRS (Coordinate Reference System) to an EPSG (int) code or WKT2 CRS string.
Normalize the given value (describing a CRS or Coordinate Reference System)
to an openEO compatible EPSG code (int) or WKT2 CRS string.
At minimum, the following input values are handled:
Behavior and data structure support depends on the availability of the ``pyproj`` library:
- an integer value (e.g. ``4326``) is interpreted as an EPSG code
- a string that just contains an integer (e.g. ``"4326"``)
or with and additional ``"EPSG:"`` prefix (e.g. ``"EPSG:4326"``)
will also be interpreted as an EPSG value
- If the ``pyproj`` library is available: use that to do parsing and conversion.
This means that anything that is supported by ``pyproj.CRS.from_user_input`` is allowed.
Additional support and behavior depends on the availability of the ``pyproj`` library:
- When available, it will be used for parsing and validation:
everything supported by `pyproj.CRS.from_user_input <https://pyproj4.github.io/pyproj/dev/api/crs/crs.html#pyproj.crs.CRS.from_user_input>`_ is allowed.
See the ``pyproj`` docs for more details.
- Otherwise, some best effort validation is done:
EPSG looking int/str values will be parsed as such, other strings will be assumed to be WKT2 already.
EPSG looking integer or string values will be parsed as such as discussed above.
Other strings will be assumed to be WKT2 already.
Other data structures will not be accepted.
:param crs: data structure that encodes a CRS, typically just an int or string value.
If the ``pyproj`` library is available, everything supported by it is allowed
:param crs: value that encodes a coordinate reference system, typically just an int (EPSG code) or string (authority string).
If the ``pyproj`` library is available, everything supported by it is allowed.
:param use_pyproj: whether ``pyproj`` should be leveraged at all
(mainly useful for testing the "no pyproj available" code path)
:return: EPSG code as int, or WKT2 string. Or None if input was empty .
:return: EPSG code as int, or WKT2 string. Or None if input was empty.
:raises ValueError:
When the given CRS data can not be parsed/converted/normalized.
Expand Down
52 changes: 52 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,21 @@ def test_init(self):
"crs": 4326,
}

@pytest.mark.skipif(
# TODO #460 this skip is only necessary for python 3.6 and lower
pyproj.__version__ < ComparableVersion("3.3.1"),
reason="pyproj below 3.3.1 does not support int-like strings",
)
def test_init_python_for_pyprojv331(self):
"""Extra test case that does not work with old pyproj versions that we get on python version 3.7 and below."""
assert BBoxDict(west=1, south=2, east=3, north=4, crs="4326") == {
"west": 1,
"south": 2,
"east": 3,
"north": 4,
"crs": 4326,
}

def test_repr(self):
d = BBoxDict(west=1, south=2, east=3, north=4)
assert repr(d) == "{'west': 1, 'south': 2, 'east': 3, 'north': 4}"
Expand All @@ -732,6 +747,21 @@ def test_to_bbox_dict_from_sequence(self):
"crs": 4326,
}

@pytest.mark.skipif(
# TODO #460 this skip is only necessary for python 3.6 and lower
pyproj.__version__ < ComparableVersion("3.3.1"),
reason="pyproj below 3.3.1 does not support int-like strings",
)
def test_to_bbox_dict_from_sequence_pyprojv331(self):
"""Extra test cases that do not work with old pyproj versions that we get on python version 3.7 and below."""
assert to_bbox_dict([1, 2, 3, 4], crs="4326") == {
"west": 1,
"south": 2,
"east": 3,
"north": 4,
"crs": 4326,
}

def test_to_bbox_dict_from_sequence_mismatch(self):
with pytest.raises(InvalidBBoxException, match="Expected sequence with 4 items, but got 3."):
to_bbox_dict([1, 2, 3])
Expand Down Expand Up @@ -775,6 +805,28 @@ def test_to_bbox_dict_from_dict(self):
}
) == {"west": 1, "south": 2, "east": 3, "north": 4, "crs": 4326}

@pytest.mark.skipif(
# TODO #460 this skip is only necessary for python 3.6 and lower
pyproj.__version__ < ComparableVersion("3.3.1"),
reason="pyproj below 3.3.1 does not support int-like strings",
)
def test_to_bbox_dict_from_dict_for_pyprojv331(self):
"""Extra test cases that do not work with old pyproj versions that we get on python version 3.7 and below."""
assert to_bbox_dict({"west": 1, "south": 2, "east": 3, "north": 4, "crs": "4326"}) == {
"west": 1,
"south": 2,
"east": 3,
"north": 4,
"crs": 4326,
}
assert to_bbox_dict({"west": 1, "south": 2, "east": 3, "north": 4}, crs="4326") == {
"west": 1,
"south": 2,
"east": 3,
"north": 4,
"crs": 4326,
}

def test_to_bbox_dict_from_dict_missing_field(self):
with pytest.raises(InvalidBBoxException, match=re.escape("Missing bbox fields ['north', 'south', 'west']")):
to_bbox_dict({"east": 3})
Expand Down

0 comments on commit 4efb921

Please sign in to comment.