Skip to content

Commit

Permalink
Merge pull request #744 from dcs4cop/forman-xxx-volumes_api
Browse files Browse the repository at this point in the history
Experimental 3-D volume API
  • Loading branch information
forman authored Feb 4, 2023
2 parents 6a73c79 + f48ef5c commit 7d00957
Show file tree
Hide file tree
Showing 15 changed files with 536 additions and 65 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@

- The `--show` option of `xcube serve`
now outputs various aspects of the server configuration.

- Added experimental endpoint `/volumes`.
It is used by xcube Viewer to render 3-D volumes.

* xcube Server is now more tolerant with respect to datasets it can not
open without errors. Implementation detail: It no longer fails if
Expand Down
10 changes: 3 additions & 7 deletions test/core/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import json
import math
import unittest
from collections.abc import MutableMapping, Mapping
from typing import Dict, KeysView, Iterator, Sequence, Any, Callable, \
Union
from collections.abc import MutableMapping
from typing import Dict, KeysView, Iterator, Sequence, Any

import cftime
import numpy as np
Expand All @@ -14,11 +13,11 @@
from test.sampledata import create_highroc_dataset
from xcube.core.gridmapping import GridMapping
from xcube.core.new import new_cube
from xcube.core.select import select_label_subset
from xcube.core.select import select_spatial_subset
from xcube.core.select import select_subset
from xcube.core.select import select_temporal_subset
from xcube.core.select import select_variables_subset
from xcube.core.select import select_label_subset


class SelectVariablesSubsetTest(unittest.TestCase):
Expand Down Expand Up @@ -99,9 +98,6 @@ def test_select_spatial_subset_with_gm(self):
ds2 = select_spatial_subset(ds1, xy_bbox=(40., 40., 42., 42.),
grid_mapping=GridMapping.from_dataset(ds1))
self.assertEqual(((2,), (2,)), (ds2.lon.shape, ds2.lat.shape))
ds2 = select_spatial_subset(ds1, xy_bbox=(40., 40., 42., 42.),
geo_coding=GridMapping.from_dataset(ds1))
self.assertEqual(((2,), (2,)), (ds2.lon.shape, ds2.lat.shape))


class SelectTemporalSubsetTest(unittest.TestCase):
Expand Down
1 change: 1 addition & 0 deletions test/server/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def test_server_ctx(self):
server.ctx.config)

def test_config_schema_effectively_merged(self):
self.maxDiff = None
extension_registry = mock_extension_registry([
(
"datasets",
Expand Down
51 changes: 26 additions & 25 deletions test/util/test_cmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ def test_colormaps(self):
self.assertEqual('Perceptually Uniform Sequential',
colormap.cat_name)
self.assertEqual('viridis', colormap.cm_name)
self.assertEqual('iVBORw0KGgoAAAANSUhEUgAAAQAAAAACCAYAAAC3zQLZ'
'AAAAzklEQVR4nO2TQZLFIAhEX7dXmyPM/Y8SZwEqMcnU'
'3/9QZTU8GszC6Ee/HQlk5FAsJIENqVGv/piZ3uqf3nX6'
'Vtd+l8D8UwNOLhZL3+BLh796OXvMdWaqtrrqnZ/tjvuZ'
'T/0XxnN/5f25z9X7tIMTKzV7/5yrME3NHoPlUzvplgOe'
'vOcz6ZO5eCqzOmark1nHDQveHuuYaazZkTcdmE110HJu'
'6doR3tgfPHyL51zNc0fd2xjf0vPukUPL36YBTcpcWArF'
'yY0RTca88cYbXxt/gUOJC8yRF1kAAAAASUVORK5CYII=',
self.assertEqual('iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAA'
'xWXB3AAAAxElEQVR4nI2TQZLEIAwDW+Jr84T5/1'
'PCHGzAkGRrDynZbclwIPro25FARg7FQhLYkBr9m'
'o/M9Fb/9K6vH32ddwnMHz3g5GKx9A2+dPirl91j'
'7pmpOvqqT36OM54z//XfGO/zVff3OXfv2w42Vnr'
'O+V6rME3NGYPlU9v0qAFP3vOZ9MlcPJVZHXP0ya'
'zrgQVvr31kGis76qYLc6guWuaWrh3hjf3Bw7d45'
'mqdO+rexrhLz7NHDS1/mwY0KWthKRQnN0Y0mR+s'
'DYkJKNzLygAAAABJRU5ErkJggg==',
colormap.cmap_png_base64)

def test_to_json(self):
Expand Down Expand Up @@ -144,15 +145,16 @@ def test_colormaps_ocean(self):
self.assertIsInstance(colormap, Colormap)
self.assertEqual("Ocean", colormap.cat_name)
self.assertEqual("thermal", colormap.cm_name)
self.assertEqual('iVBORw0KGgoAAAANSUhEUgAAAQAAAAACCAYAAAC3zQLZ'
'AAAA2klEQVR4nO2S6xHDMAiDP+FROkL3Xy30RwBju52g'
'8V0OIcnyKxqvtwsD5SfAUPZNE6M4VR2hJTdQeBX6UhlY'
'8xgDY8V24A15pMuIXcQHJo4qwOQYIHlojpT6zWnzqDxR'
'o+/+zFZbR7H2Tx3WvMPf1qDvq+17zz/m7TV97YxHbefE'
'W27ve+7Oe9xZZu3cdXCdr17XokurfvYOcmTXxHJkE2P3'
'2ei8eVxww1WJecRlBxZr/cndj+T5MKULbzqm5pnY56MF'
'jnkmPH7cb7xXzvR49RRO3njGM57xt+MDC391Pt11tkYA'
'AAAASUVORK5CYII=',
self.assertEqual('iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAA'
'xWXB3AAAAz0lEQVR4nHWSyZUEIQxDv0woHULnn1'
'p5DiWDgekDz7IkxKrx+aYIUA0BgapvmhiT06zDW'
'nED2SvrW2UQzRMMgh3HhQ+UTlfgXXhAiKsKCCUB'
'SGktkUp/OR0eTY+r++6vbLV15LV/6rDnXf62Bn1'
'fbd9n/jXvrOVrZ7xqOyfZcnvfc08+fWeVdXLPxX'
'V+9no2Xdr1u09QongWVqJYmHjPRucjfcENz4rn4'
'cs2Fnv9yb2PlPUwUxfZdELNs3CuRzP2vBDpj/sf'
'nzNneXL2TFz8H4K3dTwq9eoAAAAAAElFTkSuQmC'
'C',
colormap.cmap_png_base64)

def test_load_snap_cpd_colormap(self):
Expand Down Expand Up @@ -220,13 +222,12 @@ def test_cmap_png_base64(self):
self.assertIsInstance(base64, str)
self.assertIs(base64, self.colormap.cmap_png_base64)
self.assertEqual(
'iVBORw0KGgoAAAANSUhEUgAAAQAAAAACCAYAAAC3zQLZAAAA'
'yUlEQVR4nO2SsWFEIQxDZY+VGW7o7HVBSgEGG7gidT6NZenJ'
'Ffb1+paZwdzh3mfsNnbP+5U1eNLmdnR8dnMe93HllsbizTYe'
'qYdxG7VnmNyhDYWrHqq36eppy3VqZH/wSDvihhYLXffZy/tF'
'e2jwzMPX0OKH2TXCU2WgNMHimQSkWVjx8CDCGOzywLiz9J6J'
'p3dkJDQmpO5PpuZzUrMvVi76mpwg7V7K/qDZqt/34TWBwTf2'
'rCWuCSSht8Af3ee7/6vnPe95//T9Ai79U+7uqc9UAAAAAElF'
'TkSuQmCC',
'iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAAxWXB3AAAA'
'u0lEQVR4nJWMwQEDIAwCScbqDB26e7VCHxpN1D76gsARezxf'
'MjOYO9y7xm3j9nxfWYMnb27Hxuc29/EfV255LN5s45F2GL9R'
'd4bJHd5QuJqhZpuvmbZep0fOB490I35osdD1nrt8X7yHB88+'
'cg0v/tDuEZkqAyUFS2YSkLSw4pFBhDHYlYHxZ/m9E8/s6Eho'
'KKSeT6b2U6m5FysXe01OkPYsdX94tpr3e2RNYPCNvWuJawJJ'
'6C3wo7u+hS8PX1PsPRnqDgAAAABJRU5ErkJggg==',
base64
)
20 changes: 20 additions & 0 deletions test/webapi/volumes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The MIT License (MIT)
# Copyright (c) 2022 by the xcube team and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
65 changes: 65 additions & 0 deletions test/webapi/volumes/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# The MIT License (MIT)
# Copyright (c) 2022 by the xcube team and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

import unittest

import jsonschema
import pytest

# noinspection PyProtectedMember
from xcube.webapi.volumes.config import CONFIG_SCHEMA


class VolumesAccessConfigTest(unittest.TestCase):
def test_config_ok(self):
self.assertIsNone(CONFIG_SCHEMA.validate_instance({}))

self.assertIsNone(CONFIG_SCHEMA.validate_instance({
"VolumesAccess": {}
}))

self.assertIsNone(CONFIG_SCHEMA.validate_instance({
"VolumesAccess": {
"MaxVoxelCount": 100 ** 3
}
}))

def test_config_fails(self):
with pytest.raises(jsonschema.exceptions.ValidationError):
CONFIG_SCHEMA.validate_instance({
"VolumesAccess": {
"MaxVoxelCount": 1
}
})

with pytest.raises(jsonschema.exceptions.ValidationError):
CONFIG_SCHEMA.validate_instance({
"VolumesAccess": {
"MaxVoxelCount": True
}
})

with pytest.raises(jsonschema.exceptions.ValidationError):
CONFIG_SCHEMA.validate_instance({
"VolumesAccess": {
"MaxPoxelCount": 100 ** 3
}
})
37 changes: 37 additions & 0 deletions test/webapi/volumes/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# The MIT License (MIT)
# Copyright (c) 2022 by the xcube team and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from ..helpers import RoutesTestCase


class VolumesRoutesTest(RoutesTestCase):

def test_fetch_dataset_volume_ok(self):
response = self.fetch('/volumes/demo/conc_chl?bbox=1.0,51.0,2.0,51.5')
self.assertResponseOK(response)

def test_fetch_dataset_volume_404(self):
response = self.fetch('/volumes/demo/conc_x?bbox=1.0,51.0,2.0,51.5')
self.assertResourceNotFoundResponse(response)

def test_fetch_dataset_volume_400(self):
response = self.fetch('/volumes/demo/conc_chl?bbox=1.0,51.0')
self.assertBadRequestResponse(response)
67 changes: 35 additions & 32 deletions xcube/core/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import warnings
from typing import Collection, Optional, Tuple, Callable, Dict, Any, \
List, Mapping
from typing import Union

import cftime
import dask.array as da
import numpy as np
import pandas as pd
import warnings
import xarray as xr
from typing import Collection, Optional, Tuple, Callable, Dict, Any, \
List, Mapping
from typing import Union

from xcube.core.gridmapping import GridMapping
from xcube.util.assertions import assert_given
Expand All @@ -38,11 +39,14 @@
Tuple[Optional[pd.Timestamp], Optional[pd.Timestamp]]]


def select_subset(dataset: xr.Dataset,
*,
var_names: Collection[str] = None,
bbox: Bbox = None,
time_range: TimeRange = None):
def select_subset(
dataset: xr.Dataset,
*,
var_names: Optional[Collection[str]] = None,
bbox: Optional[Bbox] = None,
time_range: Optional[TimeRange] = None,
grid_mapping: Optional[GridMapping] = None
):
"""
Create a subset from *dataset* given *var_names*,
*bbox*, *time_range*.
Expand All @@ -58,20 +62,29 @@ def select_subset(dataset: xr.Dataset,
:param bbox: Optional bounding box in the dataset's
CRS coordinate units.
:param time_range: Optional time range
:param grid_mapping: Optional dataset grid mapping.
:return: a subset of *dataset*, or unchanged *dataset*
if no keyword-arguments are used.
"""
if var_names is not None:
dataset = select_variables_subset(dataset, var_names=var_names)
dataset = select_variables_subset(
dataset, var_names=var_names
)
if bbox is not None:
dataset = select_spatial_subset(dataset, xy_bbox=bbox)
dataset = select_spatial_subset(
dataset, xy_bbox=bbox, grid_mapping=grid_mapping
)
if time_range is not None:
dataset = select_temporal_subset(dataset, time_range=time_range)
dataset = select_temporal_subset(
dataset, time_range=time_range
)
return dataset


def select_variables_subset(dataset: xr.Dataset,
var_names: Collection[str] = None) -> xr.Dataset:
def select_variables_subset(
dataset: xr.Dataset,
var_names: Optional[Collection[str]] = None
) -> xr.Dataset:
"""
Select data variable from given *dataset* and create new dataset.
Expand All @@ -88,15 +101,14 @@ def select_variables_subset(dataset: xr.Dataset,
return dataset.drop_vars(dropped_variables)


def select_spatial_subset(dataset: xr.Dataset,
ij_bbox: Tuple[int, int, int, int] = None,
ij_border: int = 0,
xy_bbox: Tuple[float, float, float, float] = None,
xy_border: float = 0.,
grid_mapping: GridMapping = None,
geo_coding: GridMapping = None,
xy_names: Tuple[str, str] = None) \
-> Optional[xr.Dataset]:
def select_spatial_subset(
dataset: xr.Dataset,
ij_bbox: Optional[Tuple[int, int, int, int]] = None,
ij_border: int = 0,
xy_bbox: Optional[Tuple[float, float, float, float]] = None,
xy_border: float = 0.,
grid_mapping: Optional[GridMapping] = None,
) -> Optional[xr.Dataset]:
"""
Select a spatial subset of *dataset* for the
bounding box *ij_bbox* or *xy_bbox*.
Expand All @@ -113,9 +125,6 @@ def select_spatial_subset(dataset: xr.Dataset,
:param xy_bbox: The bounding box in x,y coordinates.
:param xy_border: Border in units of the x,y coordinates.
:param grid_mapping: Optional dataset grid mapping.
:param geo_coding: Deprecated. Use *grid_mapping* instead.
:param xy_names: Optional tuple of the x- and y-coordinate
variables in *dataset*. Ignored if *geo_coding* is given.
:return: Spatial dataset subset
"""

Expand All @@ -124,12 +133,6 @@ def select_spatial_subset(dataset: xr.Dataset,
if ij_bbox and xy_bbox:
raise ValueError('Only one of ij_bbox and xy_bbox can be given')

if geo_coding:
warnings.warn('keyword "geo_coding" has been deprecated,'
' use "grid_mapping" instead',
DeprecationWarning)

grid_mapping = grid_mapping or geo_coding
if grid_mapping is None:
grid_mapping = GridMapping.from_dataset(dataset)
x_name, y_name = grid_mapping.xy_var_names
Expand Down
1 change: 1 addition & 0 deletions xcube/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def _register_server_apis(ext_registry: extension.ExtensionRegistry):
'datasets',
'tiles',
'timeseries',
'volumes',
'ows.wmts',
's3',
'viewer',
Expand Down
2 changes: 1 addition & 1 deletion xcube/util/cmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def get_alpha_cmap(cm_name: str, cmap: matplotlib.colors.Colormap) \

def get_cmap_png_base64(cmap: matplotlib.colors.Colormap) -> str:
gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))
gradient = np.vstack((gradient,))
image_data = cmap(gradient, bytes=True)
image = Image.fromarray(image_data, 'RGBA')

Expand Down
Loading

0 comments on commit 7d00957

Please sign in to comment.