From ba279b8c010e892cd769c0a714b23043430f4148 Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Mon, 12 May 2025 15:12:30 -0400 Subject: [PATCH 1/2] CoverageJSON: update compliance --- pygeoapi/api/__init__.py | 2 + pygeoapi/api/itemtypes.py | 2 + pygeoapi/provider/rasterio_.py | 10 ++- pygeoapi/provider/xarray_.py | 46 +++++++++---- .../api/test_environmental_data_retrieval.py | 65 +++++++++++++------ tests/test_django.py | 2 +- 6 files changed, 92 insertions(+), 35 deletions(-) diff --git a/pygeoapi/api/__init__.py b/pygeoapi/api/__init__.py index f96220080..6466cbf04 100644 --- a/pygeoapi/api/__init__.py +++ b/pygeoapi/api/__init__.py @@ -1425,6 +1425,8 @@ def get_collection_schema(api: API, request: Union[APIRequest, Any], for k, v in p.fields.items(): schema['properties'][k] = v + if v['type'] == 'float': + schema['properties'][k]['type'] = 'number' if v.get('format') is None: schema['properties'][k].pop('format', None) diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 5e86cb058..979fcd390 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -199,6 +199,8 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any], 'title': k, 'type': v['type'] } + if v['type'] == 'float': + queryables['properties'][k]['type'] = 'number' if v.get('format') is not None: queryables['properties'][k]['format'] = v['format'] if 'values' in v: diff --git a/pygeoapi/provider/rasterio_.py b/pygeoapi/provider/rasterio_.py index 3b0fbc2c7..68b614be2 100644 --- a/pygeoapi/provider/rasterio_.py +++ b/pygeoapi/provider/rasterio_.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2024 Tom Kralidis +# Copyright (c) 2025 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -79,9 +79,11 @@ def get_fields(self): dtype2 = dtype if dtype.startswith('float'): - dtype2 = 'number' + dtype2 = 'float' elif dtype.startswith('int'): dtype2 = 'integer' + elif dtype.startswith('str'): + dtype2 = 'string' self._fields[i2] = { 'title': name, @@ -306,7 +308,9 @@ def gen_covjson(self, metadata, data): parameter = { 'type': 'Parameter', - 'description': pm['description'], + 'description': { + 'en': pm['description'] + }, 'unit': { 'symbol': pm['unit_label'] }, diff --git a/pygeoapi/provider/xarray_.py b/pygeoapi/provider/xarray_.py index 9ed2726b1..110e80a31 100644 --- a/pygeoapi/provider/xarray_.py +++ b/pygeoapi/provider/xarray_.py @@ -4,7 +4,7 @@ # Authors: Tom Kralidis # # Copyright (c) 2020 Gregory Petrochenkov -# Copyright (c) 2022 Tom Kralidis +# Copyright (c) 2025 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -111,9 +111,11 @@ def get_fields(self): LOGGER.debug('Adding variable') dtype = value.dtype if dtype.name.startswith('float'): - dtype = 'number' + dtype = 'float' elif dtype.name.startswith('int'): dtype = 'integer' + elif dtype.name.startswith('str'): + dtype = 'string' self._fields[key] = { 'type': dtype, @@ -330,18 +332,35 @@ def gen_covjson(self, metadata, data, fields): 'ranges': {} } + if (data.coords[self.x_field].size == 1 and + data.coords[self.y_field].size == 1): + LOGGER.debug('Modelling as PointSeries') + cj['domain']['axes']['x'] = { + 'values': [float(data.coords[self.x_field].values)] + } + cj['domain']['axes']['y'] = { + 'values': [float(data.coords[self.y_field].values)] + } + cj['domain']['domainType'] = 'PointSeries' + if self.time_field is not None: - mint, maxt = metadata['time'] - cj['domain']['axes'][self.time_field] = { - 'start': mint, - 'stop': maxt, - 'num': metadata['time_steps'], + cj['domain']['axes']['t'] = { + 'values': [str(v) for v in data[self.time_field].values] } + cj['domain']['referencing'].append({ + 'coordinates': ['t'], + 'system': { + 'type': 'TemporalRS', + 'calendar': 'Gregorian' + } + }) for key, value in selected_fields.items(): parameter = { 'type': 'Parameter', - 'description': value['title'], + 'description': { + 'en': value['title'] + }, 'unit': { 'symbol': value['x-ogc-unit'] }, @@ -368,12 +387,15 @@ def gen_covjson(self, metadata, data, fields): 'shape': [metadata['height'], metadata['width']] } - cj['ranges'][key]['values'] = data[key].values.flatten().tolist() # noqa + cj['ranges'][key]['values'] = [ + None if ( + v is None or np.isnan(v) + ) else v + for v in data[key].values.flatten() + ] if self.time_field is not None: - cj['ranges'][key]['axisNames'].append( - self._coverage_properties['time_axis_label'] - ) + cj['ranges'][key]['axisNames'].append('t') cj['ranges'][key]['shape'].append(metadata['time_steps']) except IndexError as err: LOGGER.warning(err) diff --git a/tests/api/test_environmental_data_retrieval.py b/tests/api/test_environmental_data_retrieval.py index 59232e7f6..230218499 100644 --- a/tests/api/test_environmental_data_retrieval.py +++ b/tests/api/test_environmental_data_retrieval.py @@ -5,7 +5,7 @@ # Colin Blackburn # Bernhard Mallinger # -# Copyright (c) 2024 Tom Kralidis +# Copyright (c) 2025 Tom Kralidis # Copyright (c) 2022 John A Stevenson and Colin Blackburn # # Permission is hereby granted, free of charge, to any person @@ -87,12 +87,12 @@ def test_get_collection_edr_query(config, api_): axes = list(data['domain']['axes'].keys()) axes.sort() assert len(axes) == 3 - assert axes == ['TIME', 'x', 'y'] + assert axes == ['t', 'x', 'y'] - assert data['domain']['axes']['x']['start'] == 11.0 - assert data['domain']['axes']['x']['stop'] == 11.0 - assert data['domain']['axes']['y']['start'] == 11.0 - assert data['domain']['axes']['y']['stop'] == 11.0 + assert isinstance(data['domain']['axes']['x'], dict) + assert isinstance(data['domain']['axes']['x']['values'], list) + assert data['domain']['axes']['x']['values'][0] == 11.0 + assert data['domain']['axes']['y']['values'][0] == 11.0 parameters = list(data['parameters'].keys()) parameters.sort() @@ -131,11 +131,19 @@ def test_get_collection_edr_query(config, api_): assert code == HTTPStatus.OK data = json.loads(response) - time_dict = data['domain']['axes']['TIME'] + time_dict = data['domain']['axes']['t'] + assert isinstance(time_dict, dict) + assert isinstance(time_dict['values'], list) - assert time_dict['start'] == '2000-02-15T16:29:05.999999999' - assert time_dict['stop'] == '2000-06-16T10:25:30.000000000' - assert time_dict['num'] == 5 + t_values = [ + '2000-02-15T16:29:05.999999999', + '2000-03-17T02:58:12.000000000', + '2000-04-16T13:27:18.000000000', + '2000-05-16T23:56:24.000000000', + '2000-06-16T10:25:30.000000000' + ] + + assert sorted(time_dict['values']) == t_values # unbounded date range - start req = mock_api_request({ @@ -147,11 +155,20 @@ def test_get_collection_edr_query(config, api_): assert code == HTTPStatus.OK data = json.loads(response) - time_dict = data['domain']['axes']['TIME'] + time_dict = data['domain']['axes']['t'] + assert isinstance(time_dict, dict) + assert isinstance(time_dict['values'], list) + + t_values = [ + '2000-01-16T06:00:00.000000000', + '2000-02-15T16:29:05.999999999', + '2000-03-17T02:58:12.000000000', + '2000-04-16T13:27:18.000000000', + '2000-05-16T23:56:24.000000000', + '2000-06-16T10:25:30.000000000' + ] - assert time_dict['start'] == '2000-01-16T06:00:00.000000000' - assert time_dict['stop'] == '2000-06-16T10:25:30.000000000' - assert time_dict['num'] == 6 + assert sorted(time_dict['values']) == t_values # unbounded date range - end req = mock_api_request({ @@ -163,11 +180,21 @@ def test_get_collection_edr_query(config, api_): assert code == HTTPStatus.OK data = json.loads(response) - time_dict = data['domain']['axes']['TIME'] - - assert time_dict['start'] == '2000-06-16T10:25:30.000000000' - assert time_dict['stop'] == '2000-12-16T01:20:05.999999996' - assert time_dict['num'] == 7 + time_dict = data['domain']['axes']['t'] + assert isinstance(time_dict, dict) + assert isinstance(time_dict['values'], list) + + t_values = [ + '2000-06-16T10:25:30.000000000', + '2000-07-16T20:54:36.000000000', + '2000-08-16T07:23:42.000000000', + '2000-09-15T17:52:48.000000000', + '2000-10-16T04:21:54.000000000', + '2000-11-15T14:51:00.000000000', + '2000-12-16T01:20:05.999999996' + ] + + assert sorted(time_dict['values']) == t_values # some data req = mock_api_request({ diff --git a/tests/test_django.py b/tests/test_django.py index 06cda4f09..10af13bc2 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -38,4 +38,4 @@ def test_django_edr_without_instance_id(django_): # Validate CoverageJSON is returned response_json = response.json() assert response_json["type"] == "Coverage" - assert response_json["domain"]["domainType"] == "Grid" + assert response_json["domain"]["domainType"] == "PointSeries" From 9cae227ccb3800066e2d8c42af7be40a3e11cebd Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Tue, 13 May 2025 08:51:31 -0400 Subject: [PATCH 2/2] remove extra None check --- pygeoapi/provider/xarray_.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pygeoapi/provider/xarray_.py b/pygeoapi/provider/xarray_.py index 110e80a31..dd6f423b5 100644 --- a/pygeoapi/provider/xarray_.py +++ b/pygeoapi/provider/xarray_.py @@ -388,9 +388,7 @@ def gen_covjson(self, metadata, data, fields): metadata['width']] } cj['ranges'][key]['values'] = [ - None if ( - v is None or np.isnan(v) - ) else v + None if np.isnan(v) else v for v in data[key].values.flatten() ]