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:
+34
-14
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('''
|
||||
|
||||
Reference in New Issue
Block a user