From 9834e4db8436e4f4763ebe49799345b6d8485177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Garn=C3=A6s?= Date: Thu, 16 Feb 2023 03:26:24 +0100 Subject: [PATCH] EDR bbox parameter support for cube queries (#1127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- pygeoapi/api.py | 48 ++++++++++++++------ pygeoapi/provider/xarray_edr.py | 80 ++++++++++++++++++++++++++++++++- tests/test_api.py | 39 ++++++++++++++++ 3 files changed, 152 insertions(+), 15 deletions(-) diff --git a/pygeoapi/api.py b/pygeoapi/api.py index a61f6ac..f8c2c74 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -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 diff --git a/pygeoapi/provider/xarray_edr.py b/pygeoapi/provider/xarray_edr.py index 7281d81..1f937fb 100644 --- a/pygeoapi/provider/xarray_edr.py +++ b/pygeoapi/provider/xarray_edr.py @@ -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) diff --git a/tests/test_api.py b/tests/test_api.py index 1c47e01..d3db283 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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('''