EDR bbox parameter support for cube queries (#1127)

* bbox query parameter support for EDR cube queries

As per Requirement A.61 C, D and E, as well as A.7 in the EDR standard.

* bbox queries support z-axis dimension

* xarray-edr provider cube query support, edr cube unit tests

---------

Co-authored-by: Peter Garnæs <pga@dmi.dk>
This commit is contained in:
Peter Garnæs
2023-02-16 03:26:24 +01:00
committed by GitHub
parent e1af5e1ca5
commit 9834e4db84
3 changed files with 152 additions and 15 deletions
+34 -14
View File
@@ -3572,23 +3572,34 @@ class API:
if isinstance(parameternames, str):
parameternames = parameternames.split(',')
bbox = None
if query_type == 'cube':
try:
bbox = validate_bbox(request.params.get('bbox'))
if not bbox:
raise ValueError('bbox parameter required by cube queries')
except ValueError as err:
return self.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
'InvalidParameterValue', str(err))
LOGGER.debug('Processing coords parameter')
wkt = request.params.get('coords')
if not wkt:
if wkt:
try:
wkt = shapely_loads(wkt)
except WKTReadingError:
msg = 'invalid coords parameter'
return self.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
'InvalidParameterValue', msg)
elif query_type != 'cube':
msg = 'missing coords parameter'
return self.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
'InvalidParameterValue', msg)
try:
wkt = shapely_loads(wkt)
except WKTReadingError:
msg = 'invalid coords parameter'
return self.get_exception(
HTTPStatus.BAD_REQUEST, headers, request.format,
'InvalidParameterValue', msg)
LOGGER.debug('Processing z parameter')
z = request.params.get('z')
@@ -3638,7 +3649,8 @@ class API:
datetime_=datetime_,
select_properties=parameternames,
wkt=wkt,
z=z
z=z,
bbox=bbox
)
try:
@@ -3880,8 +3892,9 @@ def validate_bbox(value=None) -> list:
bbox = value.split(',')
if len(bbox) != 4:
msg = 'bbox should be 4 values (minx,miny,maxx,maxy)'
if len(bbox) not in [4, 6]:
msg = 'bbox should be either 4 values (minx,miny,maxx,maxy) ' \
'or 6 values (minx,miny,minz,maxx,maxy,maxz)'
LOGGER.debug(msg)
raise ValueError(msg)
@@ -3893,15 +3906,22 @@ def validate_bbox(value=None) -> list:
LOGGER.debug(msg)
raise
if bbox[1] > bbox[3]:
if (len(bbox) == 4 and bbox[1] > bbox[3]) \
or (len(bbox) == 6 and bbox[1] > bbox[4]):
msg = 'miny should be less than maxy'
LOGGER.debug(msg)
raise ValueError(msg)
if bbox[0] > bbox[2]:
if (len(bbox) == 4 and bbox[0] > bbox[2]) \
or (len(bbox) == 6 and bbox[0] > bbox[3]):
msg = 'minx is greater than maxx (possibly antimeridian bbox)'
LOGGER.debug(msg)
if len(bbox) == 6 and bbox[2] > bbox[5]:
msg = 'minz should be less than maxz'
LOGGER.debug(msg)
raise ValueError(msg)
return bbox
+79 -1
View File
@@ -29,7 +29,7 @@
import logging
from pygeoapi.provider.base import ProviderNoDataError
from pygeoapi.provider.base import ProviderNoDataError, ProviderQueryError
from pygeoapi.provider.base_edr import BaseEDRProvider
from pygeoapi.provider.xarray_ import _to_datetime_string, XarrayProvider
@@ -148,3 +148,81 @@ class XarrayEDRProvider(BaseEDRProvider, XarrayProvider):
}
return self.gen_covjson(out_meta, data, self.fields)
@BaseEDRProvider.register()
def cube(self, **kwargs):
"""
Extract data from collection
:param query_type: query type
:param bbox: `list` of minx,miny,maxx,maxy coordinate values as `float`
:param datetime_: temporal (datestamp or extent)
:param select_properties: list of parameters
:param z: vertical level(s)
:param format_: data format of output
:returns: coverage data as dict of CoverageJSON or native format
"""
query_params = {}
LOGGER.debug(f'Query parameters: {kwargs}')
LOGGER.debug(f"Query type: {kwargs.get('query_type')}")
bbox = kwargs.get('bbox')
if len(bbox) == 4:
query_params[self.x_field] = slice(bbox[0], bbox[2])
query_params[self.y_field] = slice(bbox[1], bbox[3])
else:
raise ProviderQueryError('z-axis not supported')
LOGGER.debug('Processing parameter-name')
select_properties = kwargs.get('select_properties')
# example of fetching instance passed
# TODO: apply accordingly
instance = kwargs.get('instance')
LOGGER.debug(f'instance: {instance}')
datetime_ = kwargs.get('datetime_')
if datetime_ is not None:
query_params[self._coverage_properties['time_axis_label']] = datetime_ # noqa
LOGGER.debug(f'query parameters: {query_params}')
try:
if select_properties:
self.fields = select_properties
data = self._data[[*select_properties]]
else:
data = self._data
data = data.sel(query_params)
except KeyError:
raise ProviderNoDataError()
if len(data.coords[self.time_field].values) < 1:
raise ProviderNoDataError()
height = data.dims[self.y_field]
width = data.dims[self.x_field]
out_meta = {
'bbox': [
data.coords[self.x_field].values[0],
data.coords[self.y_field].values[0],
data.coords[self.x_field].values[-1],
data.coords[self.y_field].values[-1]
],
"time": [
_to_datetime_string(data.coords[self.time_field].values[0]),
_to_datetime_string(data.coords[self.time_field].values[-1])
],
"driver": "xarray",
"height": height,
"width": width,
"time_steps": data.dims[self.time_field],
"variables": {var_name: var.attrs
for var_name, var in data.variables.items()}
}
return self.gen_covjson(out_meta, data, self.fields)
+39
View File
@@ -1752,12 +1752,45 @@ def test_get_collection_edr_query(config, api_):
req, 'icoads-sst', None, 'position')
assert code == HTTPStatus.NO_CONTENT
# position no coords
req = mock_request({
'datetime': '2000-01-17'
})
rsp_headers, code, response = api_.get_collection_edr_query(
req, 'icoads-sst', None, 'position')
assert code == HTTPStatus.BAD_REQUEST
# cube bbox parameter 4 dimensional
req = mock_request({
'bbox': '0,0,10,10'
})
rsp_headers, code, response = api_.get_collection_edr_query(
req, 'icoads-sst', None, 'cube')
assert code == HTTPStatus.OK
# cube bad bbox parameter
req = mock_request({
'bbox': '0,0,10'
})
rsp_headers, code, response = api_.get_collection_edr_query(
req, 'icoads-sst', None, 'cube')
assert code == HTTPStatus.BAD_REQUEST
# cube no bbox parameter
req = mock_request({})
rsp_headers, code, response = api_.get_collection_edr_query(
req, 'icoads-sst', None, 'cube')
assert code == HTTPStatus.BAD_REQUEST
def test_validate_bbox():
assert validate_bbox('1,2,3,4') == [1, 2, 3, 4]
assert validate_bbox('1,2,3,4,5,6') == [1, 2, 3, 4, 5, 6]
assert validate_bbox('-142,42,-52,84') == [-142, 42, -52, 84]
assert (validate_bbox('-142.1,42.12,-52.22,84.4') ==
[-142.1, 42.12, -52.22, 84.4])
assert (validate_bbox('-142.1,42.12,-5.28,-52.22,84.4,7.39') ==
[-142.1, 42.12, -5.28, -52.22, 84.4, 7.39])
assert (validate_bbox('177.0,65.0,-177.0,70.0') ==
[177.0, 65.0, -177.0, 70.0])
@@ -1765,9 +1798,15 @@ def test_validate_bbox():
with pytest.raises(ValueError):
validate_bbox('1,2,4')
with pytest.raises(ValueError):
validate_bbox('1,2,4,5,6')
with pytest.raises(ValueError):
validate_bbox('3,4,1,2')
with pytest.raises(ValueError):
validate_bbox('1,2,6,4,5,3')
def test_validate_datetime():
config = yaml_load('''