Skip to content

Commit

Permalink
Merge pull request #917 from dcs4cop/pont-916-domain-range
Browse files Browse the repository at this point in the history
STAC / OGC collections: implement RangeType schema
  • Loading branch information
pont-us authored Jan 9, 2024
2 parents 1a67e1e + 48fffd5 commit 3bec012
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* STAC
* Provide links for multiple coverages data formats
* Add `crs` and `crs_storage` properties to STAC data
* Add spatial and temporal grid data to collection descriptions
* Add a schema endpoint returning a JSON schema of a dataset's data
variables
* Add links to domain set, range type, and range schema to collection
descriptions
* OGC Coverages:
* Support scaling parameters `scale-factor`, `scale-axes`, and `scale-size`
* Improve handling of bbox parameters
Expand Down
24 changes: 23 additions & 1 deletion test/webapi/ows/coverages/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
get_dataarray_description,
get_units,
is_xy_order,
transform_bbox,
transform_bbox,
get_coverage_rangetype_for_dataset,
)


Expand Down Expand Up @@ -324,3 +325,24 @@ def test_transform_bbox_same_crs(self):
bbox := [1, 2, 3, 4],
transform_bbox(bbox, crs := pyproj.CRS('EPSG:4326'), crs),
)

def test_get_coverage_rangetype_for_dataset(self):
self.assertEqual({
'type': 'DataRecord',
'field': [{
'description': 'v',
'encodingInfo':
{'dataType': 'http://www.opengis.net/def/'
'dataType/OGC/0/signedLong'},
'name': 'v',
'type': 'Quantity'}
]},
get_coverage_rangetype_for_dataset(
xr.Dataset({
'x': [1, 2, 3],
'v': (['x'], [0, 0, 0]),
'dimensionless1': ([], None),
'dimensionless2': ([], None)
})
)
)
40 changes: 39 additions & 1 deletion test/webapi/ows/stac/demo-collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
50.0,
5.0,
52.5
],
"grid": [
{
"cellsCount": 2000,
"resolution": 0.0025
},
{
"cellsCount": 1000,
"resolution": 0.0025
}
]
},
"temporal": {
Expand All @@ -125,7 +135,17 @@
"2017-01-16T10:09:21Z",
"2017-01-30T10:46:33Z"
]
]
],
"grid": {
"cellsCount": 5,
"coordinates": [
"2017-01-16T10:09:21.834255872",
"2017-01-25T09:35:51.060063488",
"2017-01-26T10:50:16.686192896",
"2017-01-28T09:58:11.350386176",
"2017-01-30T10:46:33.836892416"
]
}
}
},
"id": "demo",
Expand Down Expand Up @@ -177,6 +197,24 @@
"href": "http://localhost:8080/ogc/collections/demo/coverage?f=geotiff",
"type": "image/tiff; application=geotiff",
"title": "Coverage for the dataset \"demo\" using OGC API \u2013 Coverages, as GeoTIFF"
},
{
"rel": "http://www.opengis.net/def/rel/ogc/1.0/schema",
"href": "http://localhost:8080/ogc/collections/demo/schema?f=json",
"type": "application/json",
"title": "Schema (as JSON)"
},
{
"rel": "http://www.opengis.net/def/rel/ogc/1.0/coverage-rangetype",
"href": "http://localhost:8080/ogc/collections/demo/coverage/rangetype?f=json",
"type": "application/json",
"title": "Range type of the coverage"
},
{
"rel": "http://www.opengis.net/def/rel/ogc/1.0/coverage-domainset",
"href": "http://localhost:8080/ogc/collections/demo/coverage/domainset?f=json",
"type": "application/json",
"title": "Domain set of the coverage"
}
],
"providers": [],
Expand Down
61 changes: 59 additions & 2 deletions test/webapi/ows/stac/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pathlib import Path
import functools

import pandas as pd
import pyproj
import xarray as xr
import xcube
Expand All @@ -44,15 +45,18 @@
STAC_VERSION,
get_collection_queryables,
get_datacube_dimensions,
get_single_collection_items,
crs_to_uri_or_wkt,
get_single_collection_items,
crs_to_uri_or_wkt,
get_time_grid,
)
from xcube.webapi.ows.stac.controllers import get_collection
from xcube.webapi.ows.stac.controllers import get_collection_item
from xcube.webapi.ows.stac.controllers import get_datasets_collection_items
from xcube.webapi.ows.stac.controllers import get_collections
from xcube.webapi.ows.stac.controllers import get_conformance
from xcube.webapi.ows.stac.controllers import get_root
from xcube.webapi.ows.stac.controllers import get_collection_schema

from .test_context import get_stac_ctx

PATH_PREFIX = '/ogc'
Expand Down Expand Up @@ -103,6 +107,7 @@
(['get'], '/collections/{collectionId}/items/{featureId}'),
(['get', 'post'], '/search'),
(['get'], '/collections/{collectionId}/queryables'),
(['get'], '/collections/{collectionId}/schema'),
],
[],
)
Expand Down Expand Up @@ -386,6 +391,58 @@ def test_get_collection_queryables(self):
result,
)

def test_get_collection_schema(self):
self.assertEqual({
'$id': f'{BASE_URL}{PATH_PREFIX}/demo/schema',
'$schema': 'https://json-schema.org/draft/2020-12/schema',
'properties': {
'c2rcc_flags': {
'title': 'C2RCC quality flags',
'type': 'number',
'x-ogc-property-seq': 1},
'conc_chl': {
'title': 'Chlorophyll concentration',
'type': 'number',
'x-ogc-property-seq': 2},
'conc_tsm': {
'title': 'Total suspended matter dry weight concentration',
'type': 'number',
'x-ogc-property-seq': 3},
'kd489': {
'title': 'Irradiance attenuation coefficient at 489 nm',
'type': 'number',
'x-ogc-property-seq': 4},
'quality_flags': {
'title': 'Classification and quality flags',
'type': 'number',
'x-ogc-property-seq': 5}},
'title': 'demo',
'type': 'object',
},
get_collection_schema(
get_stac_ctx().datasets_ctx, BASE_URL, 'demo'
)
)

def test_get_collection_schema_invalid(self):
with self.assertRaises(ValueError):
get_collection_schema(
get_stac_ctx().datasets_ctx, BASE_URL, DEFAULT_COLLECTION_ID
)

def test_get_time_grid(self):
self.assertEqual(
{},
get_time_grid(xr.Dataset({'not_time': [1, 2, 3]}))
)
self.assertEqual(
{'cellsCount': 1, 'coordinates': ['2000-01-01T00:00:00']},
get_time_grid(
xr.Dataset({'time': [pd.Timestamp('2000-01-01T00:00:00Z')]})
)
)


def test_get_datacube_dimensions(self):
dim_name_0 = 'new_dimension_0'
dim_name_1 = 'new_dimension_1'
Expand Down
7 changes: 7 additions & 0 deletions test/webapi/ows/stac/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ def test_fetch_collection_queryables(self):
)
self.assertResponseOK(response)

def test_fetch_collection_schema(self):
response = self.fetch(
f'{PATH_PREFIX}/collections/demo/schema',
method='GET'
)
self.assertResponseOK(response)


class StacRoutesTestCog(RoutesTestCase):

Expand Down
35 changes: 29 additions & 6 deletions xcube/webapi/ows/coverages/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ def get_coverage_data(

def _apply_properties(collection_id, ds, properties):
requested_vars = set(properties)
data_vars = set(map(str, ds.data_vars))
data_vars = set(map(
# Filter out 0-dimensional variables (usually grid mapping variables)
str, {k: v for k, v in ds.data_vars.items() if v.dims != ()}
))
unrecognized_vars = requested_vars - data_vars
if unrecognized_vars == set():
ds = ds.drop_vars(
Expand Down Expand Up @@ -596,21 +599,41 @@ def get_crs_from_dataset(ds: xr.Dataset) -> pyproj.CRS:
def get_coverage_rangetype(
ctx: DatasetsContext, collection_id: str
) -> dict[str, list]:
"""Return the range type of a dataset
The range type describes the data types of the dataset's variables
using a format defined in https://docs.ogc.org/is/09-146r6/09-146r6.html
:param ctx: datasets context
:param collection_id: ID of the dataset in the supplied context
:return: a dictionary representing the specified dataset's range type
"""
Return the range type of a dataset
return get_coverage_rangetype_for_dataset(get_dataset(ctx, collection_id))


def get_coverage_rangetype_for_dataset(ds) -> dict[str, list]:
"""Return the range type of a dataset
The range type describes the data types of the dataset's variables
using a format defined in https://docs.ogc.org/is/09-146r6/09-146r6.html
:param ds: a dataset
:return: a dictionary representing the supplied dataset's range type
"""
ds = get_dataset(ctx, collection_id)
result = dict(type='DataRecord', field=[])
for var_name in ds.data_vars:
for var_name, variable in ds.data_vars.items():
if variable.dims == ():
# A 0-dimensional variable is probably a grid mapping variable;
# in any case, it doesn't have the dimensions of the cube, so
# isn't part of the range.
continue
result['field'].append(
dict(
type='Quantity',
name=var_name,
description=get_dataarray_description(ds[var_name]),
description=get_dataarray_description(variable),
encodingInfo=dict(
dataType=dtype_to_opengis_datatype(ds[var_name].dtype)
dataType=dtype_to_opengis_datatype(variable.dtype)
),
)
)
Expand Down
Loading

0 comments on commit 3bec012

Please sign in to comment.