Skip to content

Commit

Permalink
Merge pull request #915 from dcs4cop/pont-914-coverages-scaling
Browse files Browse the repository at this point in the history
 Implement OGC Coverages scaling requirements class
  • Loading branch information
pont-us authored Jan 3, 2024
2 parents c929727 + 3792981 commit 1a67e1e
Show file tree
Hide file tree
Showing 9 changed files with 471 additions and 146 deletions.
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Provide links for multiple coverages data formats
* Add `crs` and `crs_storage` properties to STAC data
* OGC Coverages:
* Support `scale-factor` parameter
* Support scaling parameters `scale-factor`, `scale-axes`, and `scale-size`
* Improve handling of bbox parameters
* Handle half-open datetime intervals
* More robust and standard-compliant parameter parsing and checking
Expand Down
54 changes: 31 additions & 23 deletions test/webapi/ows/coverages/test_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
get_dataarray_description,
get_units,
is_xy_order,
transform_bbox,
)


Expand All @@ -58,7 +59,25 @@ def test_get_coverage_data_tiff(self):
'bbox': ['51,1,52,2'],
'bbox-crs': ['[EPSG:4326]'],
'datetime': ['2017-01-25T00:00:00Z'],
'properties': ['conc_chl']
'properties': ['conc_chl'],
}
content, content_bbox, content_crs = get_coverage_data(
get_coverages_ctx().datasets_ctx, 'demo', query, 'image/tiff'
)
with BytesIO(content) as fh:
da = rioxarray.open_rasterio(fh)
self.assertIsInstance(da, xr.DataArray)
self.assertEqual(('band', 'y', 'x'), da.dims)
self.assertEqual('Chlorophyll concentration', da.long_name)
self.assertEqual((1, 400, 400), da.shape)

def test_get_coverage_data_geo_subset(self):
query = {
'subset': ['Lat(51:52),Lon(1:2)'],
'subset-crs': ['[EPSG:4326]'],
'datetime': ['2017-01-25T00:00:00Z'],
'properties': ['conc_chl'],
'crs': ['[OGC:CRS84]']
}
content, content_bbox, content_crs = get_coverage_data(
get_coverages_ctx().datasets_ctx, 'demo', query, 'image/tiff'
Expand All @@ -72,11 +91,12 @@ def test_get_coverage_data_tiff(self):

def test_get_coverage_data_netcdf(self):
crs = 'OGC:CRS84'
# Unscaled size is 400, 400
query = {
'bbox': ['1,51,2,52'],
'datetime': ['2017-01-24T00:00:00Z/2017-01-27T00:00:00Z'],
'properties': ['conc_chl,kd489'],
'scale-factor': [2],
'scale-axes': ['lat(2),lon(2)'],
'crs': [crs],
}
content, content_bbox, content_crs = get_coverage_data(
Expand Down Expand Up @@ -116,7 +136,7 @@ def test_get_coverage_data_netcdf(self):
'time_bnds',
'conc_chl',
'kd489',
'crs'
'crs',
},
set(ds.variables),
)
Expand Down Expand Up @@ -160,7 +180,7 @@ def test_get_coverage_data_time_slice_subset(self):
'time',
'time_bnds',
'conc_chl',
'spatial_ref'
'spatial_ref',
},
set(ds.variables),
)
Expand All @@ -170,7 +190,7 @@ def test_get_coverage_data_png(self):
query = {
'subset': ['lat(52:51),lon(1:2),time(2017-01-25)'],
'properties': ['conc_chl'],
'scale-factor': ['4'],
'scale-size': ['lat(100),lon(100)'],
}
content, content_bbox, content_crs = get_coverage_data(
get_coverages_ctx().datasets_ctx, 'demo', query, 'png'
Expand Down Expand Up @@ -262,24 +282,6 @@ def test_get_crs_from_dataset(self):
ds = xr.Dataset({'crs': ([], None, {'spatial_ref': '3035'})})
self.assertEqual('EPSG:3035', get_crs_from_dataset(ds).to_string())

def test_get_coverage_scale_axes(self):
with self.assertRaises(ApiError.NotImplemented):
get_coverage_data(
get_coverages_ctx().datasets_ctx,
'demo',
{'scale-axes': ['x(2)']},
'application/netcdf',
)

def test_get_coverage_scale_size(self):
with self.assertRaises(ApiError.NotImplemented):
get_coverage_data(
get_coverages_ctx().datasets_ctx,
'demo',
{'scale-size': ['x(2)']},
'application/netcdf',
)

def test_dtype_to_opengis_datatype(self):
expected = [
(
Expand Down Expand Up @@ -316,3 +318,9 @@ def test_is_xy(self):
)
)
)

def test_transform_bbox_same_crs(self):
self.assertEqual(
bbox := [1, 2, 3, 4],
transform_bbox(bbox, crs := pyproj.CRS('EPSG:4326'), crs),
)
78 changes: 39 additions & 39 deletions test/webapi/ows/coverages/test_request.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,70 @@
import unittest
import pyproj

from xcube.webapi.ows.coverages.request import CoveragesRequest
from xcube.webapi.ows.coverages.request import CoverageRequest


class CoveragesRequestTest(unittest.TestCase):
def test_parse_bbox(self):
self.assertIsNone(CoveragesRequest({}).bbox)
self.assertIsNone(CoverageRequest({}).bbox)
self.assertEqual(
[1.1, 2.2, 3.3, 4.4],
CoveragesRequest(dict(bbox=['1.1,2.2,3.3,4.4'])).bbox,
CoverageRequest(dict(bbox=['1.1,2.2,3.3,4.4'])).bbox,
)
with self.assertRaises(ValueError):
CoveragesRequest(dict(bbox=['foo,bar,baz']))
CoverageRequest(dict(bbox=['foo,bar,baz']))
with self.assertRaises(ValueError):
CoveragesRequest(dict(bbox=['1.1,2.2,3.3']))
CoverageRequest(dict(bbox=['1.1,2.2,3.3']))

def test_parse_bbox_crs(self):
self.assertEqual(
pyproj.CRS('OGC:CRS84'),
CoveragesRequest({}).bbox_crs,
CoverageRequest({}).bbox_crs,
)
self.assertEqual(
pyproj.CRS(crs_spec := 'EPSG:4326'),
CoveragesRequest({'bbox-crs': [crs_spec]}).bbox_crs,
CoverageRequest({'bbox-crs': [crs_spec]}).bbox_crs,
)
self.assertEqual(
pyproj.CRS(crs_spec := 'OGC:CRS84'),
CoveragesRequest({'bbox-crs': [f'[{crs_spec}]']}).bbox_crs,
CoverageRequest({'bbox-crs': [f'[{crs_spec}]']}).bbox_crs,
)
with self.assertRaises(ValueError):
CoveragesRequest({'bbox-crs': ['not a CRS specifier']})
CoverageRequest({'bbox-crs': ['not a CRS specifier']})

def test_parse_datetime(self):
dt0 = '2018-02-12T23:20:52Z'
dt1 = '2019-02-12T23:20:52Z'
self.assertIsNone(CoveragesRequest({}).datetime)
self.assertEqual(dt0, CoveragesRequest({'datetime': [dt0]}).datetime)
self.assertIsNone(CoverageRequest({}).datetime)
self.assertEqual(dt0, CoverageRequest({'datetime': [dt0]}).datetime)
self.assertEqual(
(dt0, None), CoveragesRequest({'datetime': [f'{dt0}/..']}).datetime
(dt0, None), CoverageRequest({'datetime': [f'{dt0}/..']}).datetime
)
self.assertEqual(
(None, dt1), CoveragesRequest({'datetime': [f'../{dt1}']}).datetime
(None, dt1), CoverageRequest({'datetime': [f'../{dt1}']}).datetime
)
self.assertEqual(
(dt0, dt1),
CoveragesRequest({'datetime': [f'{dt0}/{dt1}']}).datetime,
CoverageRequest({'datetime': [f'{dt0}/{dt1}']}).datetime,
)
with self.assertRaises(ValueError):
CoveragesRequest({'datetime': [f'{dt0}/{dt0}/{dt1}']})
CoverageRequest({'datetime': [f'{dt0}/{dt0}/{dt1}']})
with self.assertRaises(ValueError):
CoveragesRequest({'datetime': ['not a valid time string']})
CoverageRequest({'datetime': ['not a valid time string']})

def test_parse_subset(self):
self.assertIsNone(CoveragesRequest({}).subset)
self.assertIsNone(CoverageRequest({}).subset)
self.assertEqual(
dict(Lat=('10', '20'), Lon=('30', None), time='2019-03-27'),
CoveragesRequest(
CoverageRequest(
dict(subset=['Lat(10:20),Lon(30:*),time("2019-03-27")'])
).subset,
)
self.assertEqual(
dict(
Lat=(None, '20'), Lon='30', time=('2019-03-27', '2020-03-27')
),
CoveragesRequest(
CoverageRequest(
dict(
subset=[
'Lat(*:20),Lon(30),time("2019-03-27":"2020-03-27")'
Expand All @@ -73,62 +73,62 @@ def test_parse_subset(self):
).subset,
)
with self.assertRaises(ValueError):
CoveragesRequest({'subset': ['not a valid specifier']})
CoverageRequest({'subset': ['not a valid specifier']})

def test_parse_subset_crs(self):
self.assertEqual(
pyproj.CRS('OGC:CRS84'),
CoveragesRequest({}).subset_crs,
CoverageRequest({}).subset_crs,
)
self.assertEqual(
pyproj.CRS(crs_spec := 'EPSG:4326'),
CoveragesRequest({'subset-crs': [crs_spec]}).subset_crs,
CoverageRequest({'subset-crs': [crs_spec]}).subset_crs,
)
with self.assertRaises(ValueError):
CoveragesRequest({'subset-crs': ['not a CRS specifier']})
CoverageRequest({'subset-crs': ['not a CRS specifier']})

def test_parse_properties(self):
self.assertIsNone(CoveragesRequest({}).properties)
self.assertIsNone(CoverageRequest({}).properties)
self.assertEqual(
['foo', 'bar', 'baz'],
CoveragesRequest(dict(properties=['foo,bar,baz'])).properties,
CoverageRequest(dict(properties=['foo,bar,baz'])).properties,
)

def test_parse_scale_factor(self):
self.assertEqual(1, CoveragesRequest({}).scale_factor)
self.assertEqual(None, CoverageRequest({}).scale_factor)
self.assertEqual(
1.5, CoveragesRequest({'scale-factor': ['1.5']}).scale_factor
1.5, CoverageRequest({'scale-factor': ['1.5']}).scale_factor
)
with self.assertRaises(ValueError):
CoveragesRequest({'scale-factor': ['this is not a number']})
CoverageRequest({'scale-factor': ['this is not a number']})

def test_parse_scale_axes(self):
self.assertIsNone(CoveragesRequest({}).scale_axes)
self.assertIsNone(CoverageRequest({}).scale_axes)
self.assertEqual(
dict(Lat=1.5, Lon=2.5),
CoveragesRequest({'scale-axes': ['Lat(1.5),Lon(2.5)']}).scale_axes
CoverageRequest({'scale-axes': ['Lat(1.5),Lon(2.5)']}).scale_axes
)
with self.assertRaises(ValueError):
CoveragesRequest({'scale-axes': ['Lat(1.5']})
CoverageRequest({'scale-axes': ['Lat(1.5']})
with self.assertRaises(ValueError):
CoveragesRequest({'scale-axes': ['Lat(not a number)']})
CoverageRequest({'scale-axes': ['Lat(not a number)']})

def test_parse_scale_size(self):
self.assertIsNone(CoveragesRequest({}).scale_size)
self.assertIsNone(CoverageRequest({}).scale_size)
self.assertEqual(
dict(Lat=12.3, Lon=45.6),
CoveragesRequest({'scale-size': ['Lat(12.3),Lon(45.6)']}).scale_size
CoverageRequest({'scale-size': ['Lat(12.3),Lon(45.6)']}).scale_size
)
with self.assertRaises(ValueError):
CoveragesRequest({'scale-size': ['Lat(1.5']})
CoverageRequest({'scale-size': ['Lat(1.5']})
with self.assertRaises(ValueError):
CoveragesRequest({'scale-size': ['Lat(not a number)']})
CoverageRequest({'scale-size': ['Lat(not a number)']})

def test_parse_crs(self):
self.assertIsNone(CoveragesRequest({}).crs)
self.assertIsNone(CoverageRequest({}).crs)
self.assertEqual(
pyproj.CRS(crs := 'EPSG:4326'),
CoveragesRequest({'crs': [crs]}).crs
CoverageRequest({'crs': [crs]}).crs
)
with self.assertRaises(ValueError):
CoveragesRequest({'crs': ['an invalid CRS specifier']})
CoverageRequest({'crs': ['an invalid CRS specifier']})
75 changes: 75 additions & 0 deletions test/webapi/ows/coverages/test_scaling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import unittest
from dataclasses import dataclass

import pyproj

import xcube.core.new
from xcube.server.api import ApiError
from xcube.webapi.ows.coverages.request import CoverageRequest
from xcube.webapi.ows.coverages.scaling import CoverageScaling


class ScalingTest(unittest.TestCase):
def setUp(self):
self.epsg4326 = pyproj.CRS('EPSG:4326')
self.ds = xcube.core.new.new_cube()

def test_default_scaling(self):
scaling = CoverageScaling(CoverageRequest({}), self.epsg4326, self.ds)
self.assertEqual((1, 1), scaling.factor)

def test_no_data(self):
with self.assertRaises(ApiError.NotFound):
CoverageScaling(
CoverageRequest({}),
self.epsg4326,
self.ds.isel(lat=slice(0, 0)),
)

def test_crs_no_valid_axis(self):
@dataclass
class CrsMock:
axis_info = [object()]

# noinspection PyTypeChecker
self.assertIsNone(
CoverageScaling(
CoverageRequest({}), CrsMock(), self.ds
).get_axis_from_crs(set())
)

def test_scale_factor(self):
scaling = CoverageScaling(
CoverageRequest({'scale-factor': ['2']}), self.epsg4326, self.ds
)
self.assertEqual((2, 2), scaling.factor)
self.assertEqual((180, 90), scaling.size)

def test_scale_axes(self):
scaling = CoverageScaling(
CoverageRequest({'scale-axes': ['Lat(3),Lon(1.2)']}),
self.epsg4326,
self.ds,
)
self.assertEqual((1.2, 3), scaling.factor)
self.assertEqual((300, 60), scaling.size)

def test_scale_size(self):
scaling = CoverageScaling(
CoverageRequest({'scale-size': ['Lat(90),Lon(240)']}),
self.epsg4326,
self.ds,
)
self.assertEqual((240, 90), scaling.size)
self.assertEqual((1.5, 2), scaling.factor)

def test_apply_identity_scaling(self):
# noinspection PyTypeChecker
self.assertEqual(
gm_mock := object(),
CoverageScaling(
CoverageRequest({'scale-factor': ['1']}),
self.epsg4326,
self.ds,
).apply(gm_mock),
)
18 changes: 18 additions & 0 deletions test/webapi/ows/coverages/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import unittest
import xcube.core.new
import xcube.webapi.ows.coverages.util as util


class UtilTest(unittest.TestCase):

def setUp(self):
self.ds_latlon = xcube.core.new.new_cube()
self.ds_xy = xcube.core.new.new_cube(x_name='x', y_name='y')

def test_get_h_dim(self):
self.assertEqual('lon', util.get_h_dim(self.ds_latlon))
self.assertEqual('x', util.get_h_dim(self.ds_xy))

def test_get_v_dim(self):
self.assertEqual('lat', util.get_v_dim(self.ds_latlon))
self.assertEqual('y', util.get_v_dim(self.ds_xy))
Loading

0 comments on commit 1a67e1e

Please sign in to comment.