diff --git a/environment.yml b/environment.yml
index 5743c7ef2..c421b6777 100644
--- a/environment.yml
+++ b/environment.yml
@@ -20,6 +20,7 @@ dependencies:
- geopandas >=0.8
- jdcal >=1.4
- jsonschema >=3.2
+ - lxml
- matplotlib-base >=3.0
- netcdf4 >=1.5
- numba >=0.52
diff --git a/examples/serve/demo/config.yml b/examples/serve/demo/config.yml
index b2620a2ee..0426ad42d 100644
--- a/examples/serve/demo/config.yml
+++ b/examples/serve/demo/config.yml
@@ -180,6 +180,13 @@ Styles:
Variable: band_3
ValueRange: [ 0., 255. ]
+
+WebCoverageService:
+ Name: "xcube-WCS"
+ Label: "xcube-WCS"
+ Description: "xcube WCS server"
+ Keywords: ["OGC", "WCS", "xcube", "datacubes"]
+
ServiceProvider:
ProviderName: "Brockmann Consult GmbH"
ProviderSite: "https://www.brockmann-consult.de"
diff --git a/test/webapi/ows/res/WCSCapabilities.xml b/test/webapi/ows/res/WCSCapabilities.xml
new file mode 100644
index 000000000..ffb41ae60
--- /dev/null
+++ b/test/webapi/ows/res/WCSCapabilities.xml
@@ -0,0 +1,196 @@
+
+
+
+
+ xcube WCS server
+ xcube-WCS
+
+
+ OGC
+ WCS
+ xcube
+ datacubes
+
+
+ Fomferra, Norman
+ Brockmann Consult GmbH
+ Senior Software Engineer
+
+
+ +49 4152 889 303
+ +49 4152 889 330
+
+
+ HZG / GITZ
+ Geesthacht
+ Herzogtum Lauenburg
+ 21502
+ Germany
+ norman.fomferra@brockmann-consult.de
+
+
+
+
+ NONE
+ NONE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ application/x-ogc-wcs
+
+
+
+
+ demo.c2rcc_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo.conc_chl
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo.conc_tsm
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo.kd489
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo.quality_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.c2rcc_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.c2rcc_flags_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.conc_chl
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.conc_chl_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.conc_tsm
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.conc_tsm_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.kd489
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.kd489_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.quality_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+ demo-1w.quality_flags_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
\ No newline at end of file
diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml
new file mode 100644
index 000000000..22332ab8a
--- /dev/null
+++ b/test/webapi/ows/res/WCSDescribe.xml
@@ -0,0 +1,775 @@
+
+
+
+ C2RCC quality flags
+ demo.c2rcc_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-16T10:09:21Z
+
+ 2017-01-25T09:35:51Z
+
+ 2017-01-26T10:50:16Z
+
+ 2017-01-28T09:58:11Z
+
+ 2017-01-30T10:46:33Z
+
+
+
+
+
+ demo.c2rcc_flags
+
+
+
+ Band
+
+
+
+ 2147483648.0000
+ 2147532813.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ Chlorophyll concentration
+ demo.conc_chl
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-16T10:09:21Z
+
+ 2017-01-25T09:35:51Z
+
+ 2017-01-26T10:50:16Z
+
+ 2017-01-28T09:58:11Z
+
+ 2017-01-30T10:46:33Z
+
+
+
+
+
+ demo.conc_chl
+
+
+
+ Band
+
+
+
+ 0.0001
+ 22.4421
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ Total suspended matter dry weight concentration
+ demo.conc_tsm
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-16T10:09:21Z
+
+ 2017-01-25T09:35:51Z
+
+ 2017-01-26T10:50:16Z
+
+ 2017-01-28T09:58:11Z
+
+ 2017-01-30T10:46:33Z
+
+
+
+
+
+ demo.conc_tsm
+
+
+
+ Band
+
+
+
+ 0.0155
+ 166.5728
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ Irradiance attenuation coefficient at 489 nm
+ demo.kd489
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-16T10:09:21Z
+
+ 2017-01-25T09:35:51Z
+
+ 2017-01-26T10:50:16Z
+
+ 2017-01-28T09:58:11Z
+
+ 2017-01-30T10:46:33Z
+
+
+
+
+
+ demo.kd489
+
+
+
+ Band
+
+
+
+ 0.0224
+ 7.0847
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ Classification and quality flags
+ demo.quality_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-16T10:09:21Z
+
+ 2017-01-25T09:35:51Z
+
+ 2017-01-26T10:50:16Z
+
+ 2017-01-28T09:58:11Z
+
+ 2017-01-30T10:46:33Z
+
+
+
+
+
+ demo.quality_flags
+
+
+
+ Band
+
+
+
+ 8388608.0000
+ 4169138176.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ c2rcc_flags
+ demo-1w.c2rcc_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.c2rcc_flags
+
+
+
+ Band
+
+
+
+ 2147483648.0000
+ 2147532813.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ c2rcc_flags_stdev
+ demo-1w.c2rcc_flags_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.c2rcc_flags_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 24576.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ conc_chl
+ demo-1w.conc_chl
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.conc_chl
+
+
+
+ Band
+
+
+
+ 0.0001
+ 22.4421
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ conc_chl_stdev
+ demo-1w.conc_chl_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.conc_chl_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 10.3862
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ conc_tsm
+ demo-1w.conc_tsm
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.conc_tsm
+
+
+
+ Band
+
+
+
+ 0.0155
+ 166.5728
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ conc_tsm_stdev
+ demo-1w.conc_tsm_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.conc_tsm_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 71.6995
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ kd489
+ demo-1w.kd489
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.kd489
+
+
+
+ Band
+
+
+
+ 0.0224
+ 7.0847
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ kd489_stdev
+ demo-1w.kd489_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.kd489_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 2.7701
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ quality_flags
+ demo-1w.quality_flags
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.quality_flags
+
+
+
+ Band
+
+
+
+ 8388608.0000
+ 4169138176.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ quality_flags_stdev
+ demo-1w.quality_flags_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.quality_flags_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 1883242496.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
\ No newline at end of file
diff --git a/test/webapi/ows/res/WCSDescribe_subset.xml b/test/webapi/ows/res/WCSDescribe_subset.xml
new file mode 100644
index 000000000..08cf80135
--- /dev/null
+++ b/test/webapi/ows/res/WCSDescribe_subset.xml
@@ -0,0 +1,159 @@
+
+
+
+ Chlorophyll concentration
+ demo.conc_chl
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-16T10:09:21Z
+
+ 2017-01-25T09:35:51Z
+
+ 2017-01-26T10:50:16Z
+
+ 2017-01-28T09:58:11Z
+
+ 2017-01-30T10:46:33Z
+
+
+
+
+
+ demo.conc_chl
+
+
+
+ Band
+
+
+
+ 0.0001
+ 22.4421
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ kd489_stdev
+ demo-1w.kd489_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.kd489_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 2.7701
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
+ quality_flags_stdev
+ demo-1w.quality_flags_stdev
+
+
+ 0 50
+ 5 52.5
+
+
+
+
+ 0 50
+ 5 52.5
+
+
+
+ 2017-01-22T00:00:00Z
+
+ 2017-01-29T00:00:00Z
+
+ 2017-02-05T00:00:00Z
+
+
+
+
+
+ demo-1w.quality_flags_stdev
+
+
+
+ Band
+
+
+
+ 0.0000
+ 1883242496.0000
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ geotiff
+ NetCDF4
+
+
+
\ No newline at end of file
diff --git a/test/webapi/ows/wmts/res/WMTSCapabilities-CRS84.xml b/test/webapi/ows/res/WMTSCapabilities-CRS84.xml
similarity index 100%
rename from test/webapi/ows/wmts/res/WMTSCapabilities-CRS84.xml
rename to test/webapi/ows/res/WMTSCapabilities-CRS84.xml
diff --git a/test/webapi/ows/wmts/res/WMTSCapabilities-OSM.xml b/test/webapi/ows/res/WMTSCapabilities-OSM.xml
similarity index 100%
rename from test/webapi/ows/wmts/res/WMTSCapabilities-OSM.xml
rename to test/webapi/ows/res/WMTSCapabilities-OSM.xml
diff --git a/test/webapi/ows/res/__init__.py b/test/webapi/ows/res/__init__.py
new file mode 100644
index 000000000..6d8c6da5e
--- /dev/null
+++ b/test/webapi/ows/res/__init__.py
@@ -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.
diff --git a/test/webapi/ows/res/describe_example.xml b/test/webapi/ows/res/describe_example.xml
new file mode 100644
index 000000000..9adfade3c
--- /dev/null
+++ b/test/webapi/ows/res/describe_example.xml
@@ -0,0 +1,1984 @@
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_BARE_EARTH
+ NRCS_DEM_BARE_EARTH
+
+ -100.06258098407854 33.87493530021817
+ -94.62493530021857 36.625080984078245
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_BARE_EARTH
+
+
+
+
+ -100.06258098407854 33.87493530021817
+ -94.62493530021857 36.625080984078245
+
+
+
+
+ 0 0
+ 302149 152814
+
+
+ x
+ y
+
+ -100.06257198584758 36.6250719858473
+
+ 1.7996461896E-5 0.0
+ 0.0 -1.7996461896E-5
+
+
+
+
+
+ NRCS_DEM_BARE_EARTH
+ NRCS_DEM_BARE_EARTH
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_BARE_EARTH_DegreeOverviews
+ NRCS_DEM_BARE_EARTH_DegreeOverviews
+
+ -100.06258098407854 33.874955883658714
+ -94.62484189357944 36.625080984078245
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_BARE_EARTH_DegreeOverviews
+
+
+
+
+ -100.06258098407854 33.874955883658714
+ -94.62484189357944 36.625080984078245
+
+
+
+
+ 0 0
+ 18883 9549
+
+
+ x
+ y
+
+ -100.06243701238337 36.62493701238308
+
+
+ 2.87943390336E-4 0.0
+ 0.0 -2.87943390336E-4
+
+
+
+
+
+ NRCS_DEM_BARE_EARTH_DegreeOverviews
+ NRCS_DEM_BARE_EARTH_DegreeOverviews
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_BARE_EARTH_HILLSHADE
+ NRCS_DEM_BARE_EARTH_HILLSHADE
+
+ -100.06258098407854 33.68743530021817
+ -94.43743530021855 37.00008098407827
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_BARE_EARTH_HILLSHADE
+
+
+
+
+ -100.06258098407854 33.68743530021817
+ -94.43743530021855 37.00008098407827
+
+
+
+
+ 0 0
+ 302149 152814
+
+
+ x
+ y
+
+ -100.06257198584758 36.6250719858473
+
+ 1.7996461896E-5 0.0
+ 0.0 -1.7996461896E-5
+
+
+
+
+
+ NRCS_DEM_BARE_EARTH_HILLSHADE
+ NRCS_DEM_BARE_EARTH_HILLSHADE
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_BARE_EARTH_HILLSHADE_DegreeOverviews
+
+ NRCS_DEM_BARE_EARTH_HILLSHADE_DegreeOverviews
+
+ -100.06258098407854 33.687504736549975
+ -94.43730932498528 37.00008098407827
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_BARE_EARTH_HILLSHADE_DegreeOverviews
+
+
+
+
+
+ -100.06258098407854 33.687504736549975
+ -94.43730932498528 37.00008098407827
+
+
+
+
+ 0 0
+ 19535 11503
+
+
+ x
+ y
+
+ -100.06243701238337 36.9999370123831
+
+ 2.87943390336E-4 0.0
+ 0.0 -2.87943390336E-4
+
+
+
+
+
+ NRCS_DEM_BARE_EARTH_HILLSHADE_DegreeOverviews
+
+ NRCS_DEM_BARE_EARTH_HILLSHADE_DegreeOverviews
+
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_FIRST_RETURN
+ NRCS_DEM_FIRST_RETURN
+
+ -100.06254499115475 33.87497129314196
+ -94.62497129314237 36.625044991154454
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_FIRST_RETURN
+
+
+
+
+ -100.06254499115475 33.87497129314196
+ -94.62497129314237 36.625044991154454
+
+
+
+
+ 0 0
+ 302145 152810
+
+
+ x
+ y
+
+ -100.0625359929238 36.62503599292351
+
+ 1.7996461896E-5 0.0
+ 0.0 -1.7996461896E-5
+
+
+
+
+
+ NRCS_DEM_FIRST_RETURN
+ NRCS_DEM_FIRST_RETURN
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_FIRST_RETURN_DegreeOverviews
+ NRCS_DEM_FIRST_RETURN_DegreeOverviews
+
+ -100.06254499115475 33.87491989073492
+ -94.6248873221411 36.625044991154454
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_FIRST_RETURN_DegreeOverviews
+
+
+
+
+ -100.06254499115475 33.87491989073492
+ -94.6248873221411 36.625044991154454
+
+
+
+
+ 0 0
+ 18883 9549
+
+
+ x
+ y
+
+ -100.06240101945959 36.62490101945929
+
+
+ 2.87943390336E-4 0.0
+ 0.0 -2.87943390336E-4
+
+
+
+
+
+ NRCS_DEM_FIRST_RETURN_DegreeOverviews
+ NRCS_DEM_FIRST_RETURN_DegreeOverviews
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_FIRST_RETURN_HILLSHADE
+ NRCS_DEM_FIRST_RETURN_HILLSHADE
+
+ -100.06254499115475 33.87497129314196
+ -94.62497129314237 36.625044991154454
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_FIRST_RETURN_HILLSHADE
+
+
+
+
+ -100.06254499115475 33.87491989073492
+ -94.6248873221411 36.625044991154454
+
+
+
+
+ 0 0
+ 302145 152810
+
+
+ x
+ y
+
+ -100.0625359929238 36.62503599292351
+
+ 1.7996461896E-5 0.0
+ 0.0 -1.7996461896E-5
+
+
+
+
+
+ NRCS_DEM_FIRST_RETURN_HILLSHADE
+ NRCS_DEM_FIRST_RETURN_HILLSHADE
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:NRCS_DEM_FIRST_RETURN_HILLSHADE_DegreeOverviews
+
+ NRCS_DEM_FIRST_RETURN_HILLSHADE_DegreeOverviews
+
+ -100.06254499115475 33.87491989073492
+ -94.6248873221411 36.625044991154454
+
+
+ WCS
+ ImageMosaic
+ NRCS_DEM_FIRST_RETURN_HILLSHADE_DegreeOverviews
+
+
+
+
+
+ -100.06254499115475 33.87491989073492
+ -94.6248873221411 36.625044991154454
+
+
+
+
+ 0 0
+ 18883 9549
+
+
+ x
+ y
+
+ -100.06240101945959 36.62490101945929
+
+
+ 2.87943390336E-4 0.0
+ 0.0 -2.87943390336E-4
+
+
+
+
+
+ NRCS_DEM_FIRST_RETURN_HILLSHADE_DegreeOverviews
+
+ NRCS_DEM_FIRST_RETURN_HILLSHADE_DegreeOverviews
+
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:OK_DEM_Bare_Earth
+ OK_DEM_Bare_Earth
+
+ -100.06252249557737 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+ OK_DEM_Bare_Earth
+ WCS
+ ImageMosaic
+ Elevation
+ DEM
+ Lidar
+
+
+
+
+ -100.06252249557737 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+
+
+ 0 0
+ 632071 388968
+
+
+ x
+ y
+
+ -100.06251799646189 37.06251799646164
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ OK_DEM_Bare_Earth
+ OK_DEM_Bare_Earth
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi_lidar:OK_DEM_Bare_Earth_DegreeOverviews
+ OK_DEM_Bare_Earth_DegreeOverviews
+
+ -100.06252249557737 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+ OK_DEM_Bare_Earth_DegreeOverviews
+ WCS
+ ImageMosaic
+ Lidar
+ DEM
+ Elevation
+
+
+
+
+ -100.06252249557737 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+
+
+ 0 0
+ 39503 24309
+
+
+ x
+ y
+
+ -100.06245050972979 37.06245050972953
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ OK_DEM_Bare_Earth_DegreeOverviews
+ OK_DEM_Bare_Earth_DegreeOverviews
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:AR_River_Flood_2019
+ AR_River_Flood_2019
+
+ -96.31252249557737 35.12498479048845
+ -94.37498479048875 36.25002249557709
+
+
+ AR_River_Flood_2019
+ WCS
+ ImageMosaic
+
+
+
+
+ -96.31252249557737 35.12498479048845
+ -94.37498479048875 36.25002249557709
+
+
+
+
+ 0 0
+ 215323 125027
+
+
+ x
+ y
+
+ -96.31251799646189 36.250017996461615
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ AR_River_Flood_2019
+ AR_River_Flood_2019
+
+
+ Band
+ Band
+
+ 1
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:AR_River_Flood_2019_DegreeOverviews
+ AR_River_Flood_2019_DegreeOverviews
+
+ -96.31252249557737 35.12497622966442
+ -94.37505506417153 36.25002249557709
+
+
+ AR_River_Flood_2019_DegreeOverviews
+ WCS
+ ImageMosaic
+ Arkansas River
+ 2019 Flooding
+
+
+
+
+ -96.31252249557737 35.12497622966442
+ -94.37505506417153 36.25002249557709
+
+
+
+
+ 0 0
+ 13456 7813
+
+
+ x
+ y
+
+ -96.31245050972979 36.24995050972951
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ AR_River_Flood_2019_DegreeOverviews
+ AR_River_Flood_2019_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ ArcGrid
+ GeoTIFF
+ GIF
+ ImageMosaic
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2008_Statewide
+ NAIP2008_Statewide
+
+ -103.063 33.563
+ -94.375 37.063
+
+
+ WCS
+ ImageMosaic
+ NAIP2008_Statewide
+ aerial
+ photo
+ ortho
+ image
+
+
+
+
+ -103.063 33.563
+ -94.375 37.063
+
+
+
+
+ 0 0
+ 965467 388965
+
+
+ x
+ y
+
+ -103.0625000000012 37.06250000000512
+
+ 8.99822854011195E-6 0.0
+
+ 0.0 -8.998220184193299E-6
+
+
+
+
+
+
+ NAIP2008_Statewide
+ NAIP2008_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2008_Statewide_DegreeOverviews
+ NAIP2008_Statewide_DegreeOverviews
+
+ -103.06250449911546 33.5624608201811
+ -94.37503706770964 37.062504499115214
+
+
+ WCS
+ ImageMosaic
+ NAIP2008_Statewide_DegreeOverviews
+
+
+
+
+ -103.06250449911546 33.5624608201811
+ -94.37503706770964 37.062504499115214
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06243251326788 37.06243251326763
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2008_Statewide_DegreeOverviews
+ NAIP2008_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2010_Statewide_DegreeOverviews
+ NAIP2010_Statewide_DegreeOverviews
+
+ -103.06250449911546 33.5624608201811
+ -94.37503706770964 37.062504499115214
+
+
+ WCS
+ ImageMosaic
+ NAIP2010_Statewide_DegreeOverviews
+
+
+
+
+ -103.06250449911546 33.5624608201811
+ -94.37503706770964 37.062504499115214
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06243251326788 37.06243251326763
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2010_Statewide_DegreeOverviews
+ NAIP2010_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2013_Statewide
+ NAIP2013_Statewide
+
+ -103.062504499115 33.5625027869503
+ -94.3750027869507 37.0625044991152
+
+
+ WCS
+ ImageMosaic
+ NAIP2013_Statewide
+ Aerial Photography
+ orthophotography
+ orthos
+
+
+
+
+ -103.062504499115 33.5625027869503
+ -94.3750027869507 37.0625044991152
+
+
+
+
+ 0 0
+ 965466 388964
+
+
+ x
+ y
+
+ -103.06249999999952 37.06249999999972
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ NAIP2013_Statewide
+ NAIP2013_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2013_Statewide_DegreeOverviews
+ NAIP2013_Statewide_DegreeOverviews
+
+ -103.062504499115 33.5624608201811
+ -94.3750370677096 37.0625044991152
+
+
+ NAIP2013_Statewide_DegreeOverviews
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.062504499115 33.5624608201811
+ -94.3750370677096 37.0625044991152
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06243251326741 37.062432513267616
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2013_Statewide_DegreeOverviews
+ NAIP2013_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2015_Statewide
+ NAIP2015_Statewide
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+ WCS
+ ImageMosaic
+ NAIP2015_Statewide
+ Oklahoma
+ aerial
+ orthophotography
+
+
+
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+
+
+ 0 0
+ 965470 388968
+
+
+ x
+ y
+
+ -103.06251799646188 37.06251799646164
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ NAIP2015_Statewide
+ NAIP2015_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2015_Statewide_DegreeOverviews
+ NAIP2015_Statewide_DegreeOverviews
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+ WCS
+ ImageMosaic
+ NAIP2015_Statewide_DegreeOverviews
+
+
+
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06245050972977 37.06245050972953
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2015_Statewide_DegreeOverviews
+ NAIP2015_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ ArcGrid
+ GeoTIFF
+ GIF
+ Gtopo30
+ ImageMosaic
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2017_Statewide
+ NAIP2017_Statewide
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+ NAIP2017_Statewide
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+
+
+ 0 0
+ 965470 388968
+
+
+ x
+ y
+
+ -103.06251799646188 37.06251799646164
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ NAIP2017_Statewide
+ NAIP2017_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2017_Statewide_DegreeOverviews
+ NAIP2017_Statewide_DegreeOverviews
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+ NAIP2017_Statewide_DegreeOverviews
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06245050972977 37.06245050972953
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2017_Statewide_DegreeOverviews
+ NAIP2017_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2019_Statewide
+ NAIP2019_Statewide
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+ NAIP2019_Statewide
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+
+
+ 0 0
+ 965470 388968
+
+
+ x
+ y
+
+ -103.06251799646188 37.06251799646164
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ NAIP2019_Statewide
+ NAIP2019_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2019_Statewide_DegreeOverviews
+ NAIP2019_Statewide_DegreeOverviews
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+ NAIP2019_Statewide_DegreeOverviews
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06245050972977 37.06245050972953
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2019_Statewide_DegreeOverviews
+ NAIP2019_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2021_CIR_Statewide
+ NAIP2021_CIR_Statewide
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+ NAIP2021_CIR_Statewide
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+
+
+ 0 0
+ 965470 388968
+
+
+ x
+ y
+
+ -103.06251799646188 37.06251799646164
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ NAIP2021_CIR_Statewide
+ NAIP2021_CIR_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2021_CIR_Statewide_DegreeOverviews
+ NAIP2021_CIR_Statewide_DegreeOverviews
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+ NAIP2021_CIR_Statewide_DegreeOverviews
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06245050972977 37.06245050972953
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2021_CIR_Statewide_DegreeOverviews
+ NAIP2021_CIR_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP2021_NC_Statewide_DegreeOverviews
+ NAIP2021_NC_Statewide_DegreeOverviews
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+ WCS
+ ImageMosaic
+ NAIP2021
+
+
+
+
+ -103.06252249557735 33.562478816643
+ -94.37505506417153 37.06252249557711
+
+
+
+
+ 0 0
+ 60340 24309
+
+
+ x
+ y
+
+ -103.06245050972977 37.06245050972953
+
+
+ 1.43971695168E-4 0.0
+ 0.0 -1.43971695168E-4
+
+
+
+
+
+ NAIP2021_NC_Statewide_DegreeOverviews
+ NAIP2021_NC_Statewide_DegreeOverviews
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:NAIP_2021_NC_Statewide
+ NAIP_2021_NC_Statewide
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+ NAIP_2021_NC
+ WCS
+ ImageMosaic
+
+
+
+
+ -103.06252249557735 33.56248479048839
+ -94.37498479048875 37.06252249557711
+
+
+
+
+ 0 0
+ 965470 388968
+
+
+ x
+ y
+
+ -103.06251799646188 37.06251799646164
+
+
+ 8.998230948E-6 0.0
+ 0.0 -8.998230948E-6
+
+
+
+
+
+ NAIP_2021_NC_Statewide
+ NAIP_2021_NC_Statewide
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+ EPSG:4326
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ nearest neighbor
+ bilinear
+ bicubic
+
+
+
+ Generated from ImageMosaic
+ ogi:ok_24K_DRG_Mosaic
+ ok_24K_DRG_Mosaic
+
+ -103.063 33.563
+ -94.375 37.063
+
+
+ WCS
+ ImageMosaic
+ ok_24K_DRG_Mosaic
+ USGS DRG
+ 24K Quad
+ topo map
+ Quads
+ quadrangle
+
+
+
+
+ -103.063 33.563
+ -94.375 37.063
+
+
+
+
+ 0 0
+ 395942 159516
+
+
+ x
+ y
+
+ -103.06249999999983 37.06249999999972
+
+
+ 2.19412863436032E-5 0.0
+
+ 0.0 -2.19412863436032E-5
+
+
+
+
+
+
+ ok_24K_DRG_Mosaic
+ ok_24K_DRG_Mosaic
+
+
+ Band
+ Band
+
+
+ 1
+ 3
+
+
+
+
+
+
+
+
+
+
+ GeoTIFF
+ GIF
+ JPEG
+ PNG
+ TIFF
+
+
+ bilinear
+ bicubic
+
+
+
\ No newline at end of file
diff --git a/test/webapi/ows/wcs/__init__.py b/test/webapi/ows/wcs/__init__.py
new file mode 100644
index 000000000..6d8c6da5e
--- /dev/null
+++ b/test/webapi/ows/wcs/__init__.py
@@ -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.
diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py
new file mode 100644
index 000000000..95b69d636
--- /dev/null
+++ b/test/webapi/ows/wcs/test_controller.py
@@ -0,0 +1,366 @@
+import unittest
+import xml.etree.ElementTree as ElementTree
+from importlib import resources as resources
+
+from lxml import etree
+
+from test.webapi.helpers import get_api_ctx
+from test.webapi.ows import res as test_res
+from xcube.core.gen2 import CubeGeneratorRequest
+from xcube.webapi.ows.wcs import res
+from xcube.webapi.ows.wcs.context import WcsContext
+from xcube.webapi.ows.wcs.controllers import get_wcs_capabilities_xml, \
+ _validate_coverage_req, CoverageRequest, get_coverage, \
+ translate_to_generator_request
+from xcube.webapi.ows.wcs.controllers import get_describe_coverage_xml
+
+
+# noinspection PyMethodMayBeStatic
+class ControllerTest(unittest.TestCase):
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.maxDiff = None
+ self.wcs_ctx = get_api_ctx('ows.wcs', WcsContext)
+
+ def test_get_capabilities(self):
+ actual_xml = get_wcs_capabilities_xml(
+ self.wcs_ctx, 'https://xcube.brockmann-consult.de')
+
+ self.check_xml(actual_xml, 'WCSCapabilities.xml',
+ 'wcsCapabilities.xsd')
+
+ def test_describe_coverage(self):
+ actual_xml = get_describe_coverage_xml(self.wcs_ctx)
+ self.check_xml(actual_xml, 'WCSDescribe.xml', 'wcsDescribe.xsd')
+
+ def test_describe_coverage_subset(self):
+ actual_xml = get_describe_coverage_xml(self.wcs_ctx,
+ [
+ 'demo-1w.quality_flags_stdev',
+ 'demo-1w.kd489_stdev',
+ 'demo.conc_chl'
+ ])
+ self.check_xml(actual_xml, 'WCSDescribe_subset.xml', 'wcsDescribe.xsd')
+
+ def test_validate_coverage_request(self):
+ # request is fine
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+
+ # TIME given in addition to BBOX -> fine
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'TIME': '2017-01-28 20:23:55.123456',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+
+ # COVERAGE is missing -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('No valid value for parameter COVERAGE provided. '
+ 'COVERAGE must be a variable name prefixed with '
+ 'its dataset name. Example: my_dataset.my_var',
+ str(e))
+
+ # COVERAGE is given but not found -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'invalid_coverage!',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('No valid value for parameter COVERAGE provided. '
+ 'COVERAGE must be a variable name prefixed with '
+ 'its dataset name. Example: my_dataset.my_var',
+ str(e))
+
+ # TIME is used instead of BBOX -> fine
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-01-28',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+
+ # use invalid TIME format -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '20201208',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ # todo - validate time in controllers.py (see todo), test here
+ # self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('TIME value must be given in the format'
+ '\'YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]]'
+ '[+HH:MM[:SS[.ffffff]]]]\'',
+ str(e))
+
+ # RESX and RESY are given instead of W/H -> fine
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-01-28',
+ 'RESX': 23.56,
+ 'RESY': 23.56,
+ 'FORMAT': 'NetCDF4'
+ }))
+
+ # PARAMETER is given -> expect a failure (not yet supported)
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'PARAMETER': 'I expect nothing from using this parameter',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-12-08',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('PARAMETER not yet supported', str(e))
+
+ # BBOX is given in wrong format -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '-10 3 -5 4',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('BBOX must be given as `minx,miny,maxx,maxy`',
+ str(e))
+
+ # WIDTH, but not HEIGHT is given -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'RESY': 156.45,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and '
+ 'RESY must be provided.', str(e))
+
+ # HEIGHT, but not WIDTH is given -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and '
+ 'RESY must be provided.', str(e))
+
+ # WIDTH and HEIGHT and RESX and RESY are given -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'RESX': 200,
+ 'RESY': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and '
+ 'RESY must be provided.', str(e))
+
+ # INTERPOLATION is given -> expect a failure (not yet supported)
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'INTERPOLATION': 'Farest Neighbor',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-12-08',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('INTERPOLATION not yet supported', str(e))
+
+ # EXCEPTIONS is given -> expect a failure (not yet supported)
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'EXCEPTIONS': 'exceptions',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-12-08',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'NetCDF4'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('EXCEPTIONS not yet supported', str(e))
+
+ # FORMAT is missing -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-12-08',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('FORMAT wrong or missing. Must be one of GeoTIFF,'
+ ' NetCDF4. Was: None', str(e))
+
+ # FORMAT is invalid -> expect a failure
+ try:
+ _validate_coverage_req(self.wcs_ctx, CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'TIME': '2020-12-08',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'MettCDF'
+ }))
+ self.fail('Classified invalid request as valid.')
+ except ValueError as e:
+ self.assertEqual('FORMAT wrong or missing. Must be one of GeoTIFF, '
+ 'NetCDF4. Was: MettCDF', str(e))
+
+ def test_get_coverage(self):
+ coverage_request = CoverageRequest({
+ 'COVERAGE': 'demo.conc_chl',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'GeoTIFF'
+ })
+ cube = get_coverage(self.wcs_ctx, coverage_request)
+ self.assertIsNotNone(cube.coords)
+ self.assertDictEqual(
+ {
+ 'time': 5,
+ 'lat': 400,
+ 'lon': 1200,
+ 'bnds': 2
+ },
+ cube.coords.dims.mapping
+ )
+ self.assertTrue('conc_chl' in cube.data_vars.variables.keys())
+ self.assertEqual('demo.conc_chl.zarr', cube.title)
+
+ def test_translate_request(self):
+ coverage = 'demo.conc_chl'
+ coverage_request = CoverageRequest({
+ 'COVERAGE': f'{coverage}',
+ 'CRS': 'EPSG:4326',
+ 'BBOX': '1,51,4,52',
+ 'WIDTH': 200,
+ 'HEIGHT': 200,
+ 'FORMAT': 'zarr'
+ })
+ gen_req = translate_to_generator_request(self.wcs_ctx,
+ coverage_request)
+ expected = CubeGeneratorRequest.from_dict(
+ {'input_config': {
+ 'store_id': 'file',
+ 'store_params': {
+ 'root': '../../../../examples/serve/demo'
+ },
+ 'data_id': 'cube-1-250-250.zarr'
+ }, 'cube_config': {
+ 'variable_names': ['conc_chl'],
+ 'crs': 'EPSG:4326',
+ 'bbox': (1, 51, 4, 52)
+ },
+ 'output_config': {
+ 'store_id': 'memory',
+ 'replace': True,
+ 'data_id': f'{coverage}.zarr'
+ }
+ })
+ expected_dict = expected.to_dict()
+ actual_dict = gen_req.to_dict()
+ # removing the store root as this depends on the runtime environment
+ del expected_dict['input_config']['store_params']['root']
+ del actual_dict['input_config']['store_params']['root']
+ self.assertDictEqual(expected_dict, actual_dict)
+
+ def check_xml(self, actual_xml, expected_xml_resource, xsd):
+ self.maxDiff = None
+ # Do not delete, useful for debugging
+ print(80 * '=')
+ print(actual_xml)
+ print(80 * '=')
+
+ expected_xml = resources.read_text(test_res, expected_xml_resource)
+
+ actual_xml = self.strip_whitespace(actual_xml)
+ expected_xml = self.strip_whitespace(expected_xml)
+
+ self.assertTrue(self.is_schema_compliant(
+ expected_xml_resource, xsd)
+ )
+ self.assertEqual(expected_xml, actual_xml)
+
+ @staticmethod
+ def strip_whitespace(xml: str) -> str:
+ root = ElementTree.fromstring(xml)
+ return ElementTree.canonicalize(ElementTree.tostring(root),
+ strip_text=True)
+
+ def is_schema_compliant(self,
+ xml_resource,
+ xsd_resource):
+ xml_text = resources.read_text(test_res, xml_resource)
+ xml = etree.fromstring(xml_text)
+
+ schema_text = resources.read_text(res, xsd_resource)
+ xsd = etree.XMLSchema(etree.fromstring(schema_text))
+ xsd.assertValid(xml) # fail if xml is invalid
+ return True
diff --git a/test/webapi/ows/wmts/test_controller.py b/test/webapi/ows/wmts/test_controller.py
index 2d2540f46..03478926f 100644
--- a/test/webapi/ows/wmts/test_controller.py
+++ b/test/webapi/ows/wmts/test_controller.py
@@ -19,13 +19,14 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
-import os
import unittest
+import importlib.resources as resources
import pyproj
from test.webapi.helpers import get_api_ctx
from test.webapi.helpers import get_server
+from test.webapi.ows import res as test_res
from xcube.core.gridmapping import GridMapping
from xcube.core.tilingscheme import TilingScheme
from xcube.webapi.ows.wmts.context import WmtsContext
@@ -42,11 +43,6 @@
)
-def get_test_res_path(path: str) -> str:
- return os.path.normpath(os.path.join(os.path.dirname(__file__),
- 'res', path))
-
-
class WmtsControllerTest(unittest.TestCase):
def setUp(self) -> None:
super().setUp()
@@ -54,8 +50,8 @@ def setUp(self) -> None:
def test_get_wmts_capabilities_xml_crs84(self):
self.maxDiff = None
- with open(get_test_res_path('WMTSCapabilities-CRS84.xml')) as fp:
- expected_xml = fp.read()
+ expected_xml = resources.read_text(test_res,
+ 'WMTSCapabilities-CRS84.xml')
actual_xml = get_wmts_capabilities_xml(self.wmts_ctx,
'http://bibo',
tms_id=WMTS_CRS84_TMS_ID)
@@ -67,8 +63,8 @@ def test_get_wmts_capabilities_xml_crs84(self):
def test_get_wmts_capabilities_xml_web_mercator(self):
self.maxDiff = None
- with open(get_test_res_path('WMTSCapabilities-OSM.xml')) as fp:
- expected_xml = fp.read()
+ expected_xml = resources.read_text(test_res,
+ 'WMTSCapabilities-OSM.xml')
actual_xml = get_wmts_capabilities_xml(self.wmts_ctx,
'http://bibo',
WMTS_WEB_MERCATOR_TMS_ID)
diff --git a/test/webapi/res/config.yml b/test/webapi/res/config.yml
index ea3a752ab..128d82b41 100644
--- a/test/webapi/res/config.yml
+++ b/test/webapi/res/config.yml
@@ -57,4 +57,7 @@ ServiceProvider:
PostalCode: "21502"
Country: "Germany"
ElectronicMailAddress: "norman.fomferra@brockmann-consult.de"
-
+ WCS-description: "xcube WCS server"
+ WCS-name: "xcube-WCS"
+ WCS-label: "xcube-WCS"
+ keywords: ["OGC", "WCS", "xcube", "datacubes"]
diff --git a/xcube/core/_tile2.py b/xcube/core/_tile2.py
index 142f99d76..63e8d6ac7 100644
--- a/xcube/core/_tile2.py
+++ b/xcube/core/_tile2.py
@@ -51,13 +51,14 @@
DEFAULT_FORMAT = 'png'
DEFAULT_TILE_ENLARGEMENT = 1
+BBox = Tuple[float, float, float, float]
ValueRange = Tuple[float, float]
def compute_tiles(
ml_dataset: MultiLevelDataset,
variable_names: Union[str, Sequence[str]],
- tile_bbox: Tuple[float, float, float, float],
+ tile_bbox: BBox,
tile_crs: Union[str, pyproj.CRS] = DEFAULT_CRS_NAME,
tile_size: ScalarOrPair[int] = DEFAULT_TILE_SIZE,
level: int = 0,
@@ -276,6 +277,7 @@ def compute_tiles(
var_tiles,
(ds_x_name, ds_y_name),
(tile_x_1d, tile_y_1d),
+ ml_dataset.grid_mapping.is_j_axis_up,
tile_crs
)
@@ -287,6 +289,7 @@ def _new_tile_dataset(
tiles: List[np.ndarray],
xy_names: Tuple[str, str],
xy_coords: Tuple[np.ndarray, np.ndarray],
+ is_j_axis_up: bool,
crs: Union[str, pyproj.CRS]):
data_vars = {}
non_spatial_coords = {}
@@ -299,7 +302,7 @@ def _new_tile_dataset(
if dim not in non_spatial_coords \
and dim in original_var.coords:
non_spatial_coords[dim] = original_var.coords[dim]
- data_2d = tiles[i]
+ data_2d = tiles[i][::-1, :]
data_nd = data_2d[(*(len(non_spatial_dims) * [np.newaxis]), ...)]
data_vars[var_name] = xr.DataArray(
data=data_nd,
@@ -307,23 +310,27 @@ def _new_tile_dataset(
name=var_name,
attrs=dict(**original_var.attrs, grid_mapping="crs"),
)
- print(pyproj.CRS(crs).to_cf())
+ x_coords, y_coords = xy_coords
return xr.Dataset(
data_vars=dict(
**data_vars,
- crs=xr.DataArray((), attrs=pyproj.CRS(crs).to_cf())
+ crs=xr.DataArray(0, attrs=pyproj.CRS(crs).to_cf())
),
coords=dict(
**{k: xr.DataArray([v.values], dims=k, attrs=v.attrs)
for k, v in non_spatial_coords.items()},
- y=xr.DataArray(xy_coords[1], dims="y", attrs=dict(
- long_name="y coordinate of projection",
- standard_name="projection_y_coordinate"
- )),
- x=xr.DataArray(xy_coords[0], dims="x", attrs=dict(
- long_name="x coordinate of projection",
- standard_name="projection_x_coordinate"
- )),
+ y=xr.DataArray(y_coords[::-1],
+ dims="y",
+ attrs=dict(
+ long_name="y coordinate of projection",
+ standard_name="projection_y_coordinate"
+ )),
+ x=xr.DataArray(x_coords,
+ dims="x",
+ attrs=dict(
+ long_name="x coordinate of projection",
+ standard_name="projection_x_coordinate"
+ )),
)
)
diff --git a/xcube/core/gen2/local/generator.py b/xcube/core/gen2/local/generator.py
index 367f7b0c1..1f7bedf10 100644
--- a/xcube/core/gen2/local/generator.py
+++ b/xcube/core/gen2/local/generator.py
@@ -219,21 +219,23 @@ def __generate_cube(self, request: CubeGeneratorRequest) \
self._generated_gm = None
total_time = progress.state.total_time
+ if isinstance(total_time, float):
+ duration_msg = f' after {total_time:.2f} seconds'
+ else:
+ duration_msg = ''
if self._generated_data_id is not None:
return CubeGeneratorResult(
status='ok',
status_code=201,
result=CubeReference(data_id=data_id),
- message=f'Cube generated successfully'
- f' after {total_time:.2f} seconds'
+ message=f'Cube generated successfully{duration_msg}'
)
else:
return CubeGeneratorResult(
status='warning',
status_code=422,
- message=f'An empty cube has been generated'
- f' after {total_time:.2f} seconds.'
+ message=f'An empty cube has been generated{duration_msg}.'
f' No data has been written at all.'
)
diff --git a/xcube/core/tile.py b/xcube/core/tile.py
index d09bc37ed..252838f1e 100644
--- a/xcube/core/tile.py
+++ b/xcube/core/tile.py
@@ -31,7 +31,9 @@
# Exported for backward compatibility only
# noinspection PyUnresolvedReferences
-from ._tile2 import (compute_tiles,
+from ._tile2 import (BBox,
+ ValueRange,
+ compute_tiles,
compute_rgba_tile,
get_var_valid_range,
get_var_cmap_params,
diff --git a/xcube/plugin.py b/xcube/plugin.py
index 21bb8038d..b93d5eaaa 100644
--- a/xcube/plugin.py
+++ b/xcube/plugin.py
@@ -226,6 +226,7 @@ def _register_server_apis(ext_registry: extension.ExtensionRegistry):
'tiles',
'timeseries',
'ows.wmts',
+ 'ows.wcs',
's3',
'viewer',
]
diff --git a/xcube/server/webservers/tornado.py b/xcube/server/webservers/tornado.py
index f53d32d0b..00f21ef38 100644
--- a/xcube/server/webservers/tornado.py
+++ b/xcube/server/webservers/tornado.py
@@ -394,6 +394,7 @@ def __init__(self,
# print("query:", self._request.query)
def make_query_lower_case(self):
+ # todo this does not always work, reason unclear
self._is_query_lower_case = True
@functools.cached_property
diff --git a/xcube/util/progress.py b/xcube/util/progress.py
index 79a6b622d..191e453ab 100644
--- a/xcube/util/progress.py
+++ b/xcube/util/progress.py
@@ -43,8 +43,7 @@ def __init__(self, label: str, total_work: float, super_work: float):
self._traceback = None
self._completed_work = 0.
self._finished = False
- self._start_time = None
- self._start_time = time.perf_counter()
+ self._start_time = time.process_time()
self._total_time = None
@property
@@ -110,7 +109,7 @@ def inc_work(self, work: float):
def finish(self):
self._finished = True
- self._total_time = time.perf_counter() - self._start_time
+ self._total_time = time.process_time() - self._start_time
class ProgressObserver(ABC):
diff --git a/xcube/webapi/ows/wcs/__init__.py b/xcube/webapi/ows/wcs/__init__.py
new file mode 100644
index 000000000..98cc7a60d
--- /dev/null
+++ b/xcube/webapi/ows/wcs/__init__.py
@@ -0,0 +1,22 @@
+# The MIT License (MIT)
+# Copyright (c) 2021/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 .routes import api
diff --git a/xcube/webapi/ows/wcs/api.py b/xcube/webapi/ows/wcs/api.py
new file mode 100644
index 000000000..29d545cae
--- /dev/null
+++ b/xcube/webapi/ows/wcs/api.py
@@ -0,0 +1,32 @@
+# 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 xcube.server.api import Api
+from .config import CONFIG_SCHEMA
+from .context import WcsContext
+
+api = Api(
+ 'ows.wcs',
+ description='xcube OGC WCS API',
+ required_apis=['tiles', 'datasets'],
+ config_schema=CONFIG_SCHEMA,
+ create_ctx=WcsContext
+)
diff --git a/xcube/webapi/ows/wcs/config.py b/xcube/webapi/ows/wcs/config.py
new file mode 100644
index 000000000..c3903660a
--- /dev/null
+++ b/xcube/webapi/ows/wcs/config.py
@@ -0,0 +1,43 @@
+# 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 xcube.util.jsonschema import JsonArraySchema
+from xcube.util.jsonschema import JsonObjectSchema
+from ...common.schemas import IDENTIFIER_SCHEMA
+from ...common.schemas import STRING_SCHEMA
+
+WCS_SCHEMA = JsonObjectSchema(
+ properties=dict(
+ Name=IDENTIFIER_SCHEMA,
+ Description=STRING_SCHEMA,
+ Label=STRING_SCHEMA,
+ Keywords=JsonArraySchema(items=STRING_SCHEMA),
+ ),
+ required=['Name'],
+ additional_properties=False
+)
+
+CONFIG_SCHEMA = JsonObjectSchema(
+ properties=dict(
+ WebCoverageService=WCS_SCHEMA,
+ ),
+ additional_properties=True
+)
diff --git a/xcube/webapi/ows/wcs/context.py b/xcube/webapi/ows/wcs/context.py
new file mode 100644
index 000000000..c6f7ca1ae
--- /dev/null
+++ b/xcube/webapi/ows/wcs/context.py
@@ -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 xcube.server.api import Context
+from xcube.webapi.common.context import ResourcesContext
+from ...datasets.context import DatasetsContext
+
+
+class WcsContext(ResourcesContext):
+
+ def __init__(self, server_ctx: Context):
+ super().__init__(server_ctx)
+ self._datasets_ctx = server_ctx.get_api_ctx('datasets',
+ cls=DatasetsContext)
+
+ @property
+ def datasets_ctx(self) -> DatasetsContext:
+ return self._datasets_ctx
diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py
new file mode 100644
index 000000000..0c2be8b2b
--- /dev/null
+++ b/xcube/webapi/ows/wcs/controllers.py
@@ -0,0 +1,502 @@
+# The MIT License (MIT)
+# Copyright (c) 2021/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 warnings
+from typing import Dict, List, Any, Union
+
+import numpy as np
+import xarray as xr
+
+from xcube.core.tile import BBox
+from xcube.core.tile import compute_tiles
+from xcube.server.api import ApiError
+from xcube.webapi.common.xml import Document
+from xcube.webapi.common.xml import Element
+from xcube.webapi.datasets.context import DatasetConfig
+from xcube.webapi.ows.wcs.context import WcsContext
+from xcube.webapi.ows.wmts.controllers import get_crs84_bbox
+
+WCS_VERSION = '1.0.0'
+VALID_CRS_LIST = ['EPSG:4326']
+
+
+# VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857']
+
+
+class CoverageRequest:
+ coverage = None
+ crs = None
+ bbox = None
+ time = None
+ width = None
+ height = None
+ format = None
+ resx = None
+ resy = None
+ interpolation = None
+ parameter = None
+ exceptions = None
+
+ def __init__(self, req: Dict[str, Any]):
+ if 'COVERAGE' in req:
+ self.coverage = req['COVERAGE']
+ if 'CRS' in req:
+ self.crs = req['CRS']
+ if 'BBOX' in req:
+ self.bbox = req['BBOX']
+ if 'TIME' in req:
+ self.time = req['TIME']
+ if 'WIDTH' in req:
+ self.width = req['WIDTH']
+ if 'HEIGHT' in req:
+ self.height = req['HEIGHT']
+ if 'FORMAT' in req:
+ self.format = req['FORMAT']
+ if 'RESX' in req:
+ self.resx = req['RESX']
+ if 'RESY' in req:
+ self.resy = req['RESY']
+ if 'INTERPOLATION' in req:
+ self.interpolation = req['INTERPOLATION']
+ if 'PARAMETER' in req:
+ self.parameter = req['PARAMETER']
+ if 'EXCEPTIONS' in req:
+ self.exceptions = req['EXCEPTIONS']
+
+
+def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str:
+ """
+ Get WCSCapabilities.xml according to
+ https://schemas.opengis.net/wcs/1.0.0/.
+
+ :param ctx: server context
+ :param base_url: the request base URL
+ :return: XML plain text in UTF-8 encoding
+ """
+ element = _get_capabilities_element(ctx, base_url)
+ document = Document(element)
+ return document.to_xml(indent=4)
+
+
+def get_describe_coverage_xml(ctx: WcsContext,
+ coverages: List[str] = None) -> str:
+ element = _get_describe_element(ctx, coverages)
+ document = Document(element)
+ return document.to_xml(indent=4)
+
+
+def get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset:
+ dataset_config = _get_dataset_config(ctx, req)
+ ds_name = dataset_config['Identifier']
+ ml_dataset = ctx.datasets_ctx.get_ml_dataset(ds_name)
+
+ # TODO (forman): compute optimal level for RES_X/RES_Y
+
+ bbox: Union[BBox, None]
+ try:
+ # noinspection PyTypeChecker
+ bbox = tuple(float(v) for v in req.bbox.split(','))
+ except ValueError:
+ bbox = None
+ if not bbox or len(bbox) != 4:
+ raise ApiError.BadRequest('invalid bbox')
+
+ ds_name = dataset_config['Identifier']
+ var_name = req.coverage.replace(ds_name + '.', '')
+ tile_size = int(req.width), int(req.height)
+ dataset = compute_tiles(ml_dataset, var_name, bbox, req.crs,
+ tile_size=tile_size, as_dataset=True)
+ dataset = dataset.rename_dims({'x': 'longitude', 'y': 'latitude'})
+ dataset = dataset.rename_vars({'x': 'longitude', 'y': 'latitude'})
+ dataset.longitude.attrs['standard_name'] = 'longitude'
+ dataset.latitude.attrs['standard_name'] = 'latitude'
+ dataset = dataset.drop_vars('crs')
+ return dataset
+
+
+def _get_dataset_config(ctx: WcsContext, req: CoverageRequest) \
+ -> DatasetConfig:
+ # TODO: too much computation here, precompute mapping and store in
+ # WCS context object, so the dataset config can be looked up:
+ #
+ # class WcsContext(...):
+ # ...
+ # def get_dataset_config(self, coverage: str):
+ # ds_config = self.coverage_to_ds_config.get(coverage)
+ # if ds_config is None:
+ # raise ApiError.BadRequest(f'unknown coverage {coverage!r}')
+ # return ds_config
+ #
+ for dataset_config in ctx.datasets_ctx.get_dataset_configs():
+ ds_name = dataset_config['Identifier']
+ ds = ctx.datasets_ctx.get_dataset(ds_name)
+
+ var_names = sorted(ds.data_vars)
+ for var_name in var_names:
+ qualified_var_name = f'{ds_name}.{var_name}'
+ if req.coverage == qualified_var_name:
+ return dataset_config
+ raise RuntimeError('Should never come here. Contact the developers.')
+
+
+# noinspection HttpUrlsUsage
+def _get_capabilities_element(ctx: WcsContext,
+ base_url: str) -> Element:
+ service_element = _get_service_element(ctx)
+ capability_element = _get_capability_element(base_url)
+ content_element = Element('ContentMetadata')
+
+ band_infos = _extract_band_infos(ctx)
+ for var_name in band_infos.keys():
+ content_element.add(Element('CoverageOfferingBrief', elements=[
+ Element('name', text=var_name),
+ Element('label', text=band_infos[var_name].label),
+ Element('lonLatEnvelope', elements=[
+ Element('gml:pos', text=f'{band_infos[var_name].bbox[0]}'
+ f' {band_infos[var_name].bbox[1]}'),
+ Element('gml:pos', text=f'{band_infos[var_name].bbox[2]}'
+ f' {band_infos[var_name].bbox[3]}')
+ ])
+ ]))
+
+ return Element(
+ 'WCS_Capabilities',
+ attrs={
+ 'xmlns': "http://www.opengis.net/wcs",
+ 'xmlns:gml': "http://www.opengis.net/gml",
+ 'xmlns:xlink': "http://www.w3.org/1999/xlink",
+ 'version': WCS_VERSION,
+ },
+ elements=[
+ service_element,
+ capability_element,
+ content_element
+ ]
+ )
+
+
+def _get_service_element(ctx: WcsContext) -> Element:
+ service_provider = ctx.config.get('ServiceProvider', {})
+ wcs_metadata = ctx.config.get('WebCoverageService', {})
+
+ def _get_sp_value(path):
+ v = None
+ node = service_provider
+ for k in path:
+ if not isinstance(node, dict) or k not in node:
+ return ''
+ v = node[k]
+ node = v
+ return str(v) if v is not None else ''
+
+ def _get_individual_name() -> str:
+ individual_name = _get_sp_value(['ServiceContact', 'IndividualName'])
+ if not individual_name:
+ return ''
+ individual_name = tuple(individual_name.split(' ').__reversed__())
+ return '{}, {}'.format(*individual_name)
+
+ element = Element('Service', elements=[
+ Element('description',
+ text=wcs_metadata.get('Description',
+ 'xcube WCS 1.0 API')),
+ Element('name',
+ text=wcs_metadata.get('Name', 'xcube WCS')),
+ Element('label',
+ text=wcs_metadata.get('Label', 'xcube WCS')),
+ Element('keywords', elements=[
+ Element('keyword', text=k) for k in wcs_metadata.get('Keywords',
+ [])
+ ]),
+ Element('responsibleParty', elements=[
+ Element('individualName',
+ text=_get_individual_name()),
+ Element('organisationName',
+ text=_get_sp_value(['ProviderName'])),
+ Element('positionName',
+ text=_get_sp_value(['ServiceContact',
+ 'PositionName'])),
+ Element('contactInfo', elements=[
+ Element('phone', elements=[
+ Element('voice',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Phone',
+ 'Voice'])),
+ Element('facsimile',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Phone',
+ 'Facsimile'])),
+ ]),
+ Element('address', elements=[
+ Element('deliveryPoint',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Address',
+ 'DeliveryPoint'])),
+ Element('city',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Address',
+ 'City'])),
+ Element('administrativeArea',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Address',
+ 'AdministrativeArea'])),
+ Element('postalCode',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Address',
+ 'PostalCode'])),
+ Element('country',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Address',
+ 'Country'])),
+ Element('electronicMailAddress',
+ text=_get_sp_value(['ServiceContact',
+ 'ContactInfo',
+ 'Address',
+ 'ElectronicMailAddress'])),
+ ]),
+ Element('onlineResource', attrs={
+ 'xlink:href': _get_sp_value(['ProviderSite'])})
+ ]),
+ ]),
+ Element('fees', text='NONE'),
+ Element('accessConstraints', text='NONE')
+ ])
+ return element
+
+
+def _get_capability_element(base_url: str) -> Element:
+ get_capabilities_url = f'{base_url}/wcs/kvp?service=WCS&' \
+ f'version=1.0.0&request=GetCapabilities'
+ describe_url = f'{base_url}/wcs/kvp?service=WCS&version=1.0.0&' \
+ f'request=DescribeCoverage'
+ get_url = f'{base_url}/wcs/kvp?service=WCS&version=1.0.0&' \
+ f'request=GetCoverage'
+ return Element('Capability', elements=[
+ Element('Request', elements=[
+ Element('GetCapabilities', elements=[
+ Element('DCPType', elements=[
+ Element('HTTP', elements=[
+ Element('Get', elements=[
+ Element('OnlineResource',
+ attrs={'xlink:href': get_capabilities_url})
+ ])
+ ])
+ ])
+ ]),
+ Element('DescribeCoverage', elements=[
+ Element('DCPType', elements=[
+ Element('HTTP', elements=[
+ Element('Get', elements=[
+ Element('OnlineResource',
+ attrs={'xlink:href': describe_url})
+ ])
+ ])
+ ])
+ ]),
+ Element('GetCoverage', elements=[
+ Element('DCPType', elements=[
+ Element('HTTP', elements=[
+ Element('Get', elements=[
+ Element('OnlineResource',
+ attrs={'xlink:href': get_url})
+ ])
+ ])
+ ])
+ ]),
+ ]),
+ Element('Exception', elements=[
+ Element('Format', text='application/x-ogc-wcs')
+ ])
+ ])
+
+
+def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \
+ -> Element:
+ coverage_elements = []
+
+ band_infos = _extract_band_infos(ctx, coverages)
+ for var_name in band_infos.keys():
+ coverage_elements.append(Element('CoverageOffering', elements=[
+ Element('description', text=band_infos[var_name].label),
+ Element('name', text=var_name),
+ Element('label', text=band_infos[var_name].label),
+ Element('lonLatEnvelope', elements=[
+ Element('gml:pos', text=f'{band_infos[var_name].bbox[0]} '
+ f'{band_infos[var_name].bbox[1]}'),
+ Element('gml:pos', text=f'{band_infos[var_name].bbox[2]} '
+ f'{band_infos[var_name].bbox[3]}')
+ ]),
+ Element('domainSet', elements=[
+ Element('spatialDomain', elements=[
+ Element('gml:Envelope', attrs={'srsName': 'EPSG:4326'},
+ elements=[
+ Element('gml:pos',
+ text=f'{band_infos[var_name].bbox[0]} '
+ f'{band_infos[var_name].bbox[1]}'),
+ Element('gml:pos',
+ text=f'{band_infos[var_name].bbox[2]} '
+ f'{band_infos[var_name].bbox[3]}')
+ ]),
+ Element('gml:RectifiedGrid',
+ attrs={'dimension': '2', 'srsName': 'EPSG:4326'},
+ elements=[
+ Element('gml:limits', elements=[
+ Element('gml:GridEnvelope', elements=[
+ Element('gml:low', text='0 0'),
+ Element('gml:high', text=
+ f'{band_infos[var_name].width} '
+ f'{band_infos[var_name].height}')
+ ])
+ ]),
+ Element('gml:axisName', text='lon'),
+ Element('gml:axisName', text='lat'),
+ Element('gml:origin', elements=[
+ Element('gml:pos', text=
+ f'{band_infos[var_name].bbox[0]} '
+ f'{band_infos[var_name].bbox[1]}')
+ ]),
+ Element('gml:offsetVector', text='0.0 0.0'),
+ Element('gml:offsetVector', text='0.0 0.0')
+ ])
+ ]),
+ Element('temporalDomain', elements=[
+ Element('gml:timePosition', text=time_step)
+ for time_step in band_infos[var_name].time_steps])
+ ]),
+ Element('rangeSet', elements=[
+ Element('RangeSet', elements=[
+ Element('name', text=var_name),
+ Element('label', text=band_infos[var_name].label),
+ Element('axisDescription', elements=[
+ Element('AxisDescription', elements=[
+ Element('name', text='Band'),
+ Element('label', text='Band'),
+ Element('values', elements=[
+ Element('singleValue', text='1')
+ ])
+ ])
+ ])
+ ])
+ ]),
+ Element('supportedCRSs', elements=[
+ Element('requestResponseCRSs', text='EPSG:4326')
+ # todo - find out why this does not work with qgis
+ # Element('requestResponseCRSs',
+ # text=','.join(VALID_CRS_LIST))
+ ]),
+ Element('supportedFormats', elements=[
+ Element('formats', text=f) for f in _get_formats_list()
+ ]),
+ Element('supportedInterpolations',
+ attrs=dict(default='nearest neighbor'),
+ elements=[
+ # Respect BBOX only
+ Element('interpolationMethod',
+ text='none'),
+ # Respect BBOX and WIDTH,HEIGHT or RESX,RESY
+ Element('interpolationMethod',
+ text='nearest neighbor'),
+ ]),
+ ]))
+
+ return Element(
+ 'CoverageDescription',
+ attrs={
+ 'xmlns': "http://www.opengis.net/wcs",
+ 'xmlns:gml': "http://www.opengis.net/gml",
+ 'version': WCS_VERSION,
+ },
+ elements=coverage_elements
+ )
+
+
+def _get_formats_list() -> List[str]:
+ # We currently only support NetCDF, because
+ # 1. QGIS understands them
+ # 2. response can be a single file
+ return ['netcdf']
+
+
+class BandInfo:
+
+ def __init__(self, var_name: str, label: str,
+ bbox: tuple[float, float, float, float],
+ time_steps: list[str], width, height):
+ self.var_name = var_name
+ self.label = label
+ self.bbox = bbox
+ self.min = np.nan
+ self.max = np.nan
+ self.time_steps = time_steps
+ self.height = height
+ self.width = width
+
+
+def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None) \
+ -> Dict[str, BandInfo]:
+ band_infos = {}
+ for dataset_config in ctx.datasets_ctx.get_dataset_configs():
+ ds_name = dataset_config['Identifier']
+ ml_dataset = ctx.datasets_ctx.get_ml_dataset(ds_name)
+ grid_mapping = ml_dataset.grid_mapping
+ ds = ml_dataset.base_dataset
+
+ try:
+ bbox = get_crs84_bbox(grid_mapping)
+ except ValueError:
+ warnings.warn(f'cannot compute geographical'
+ f' bounds for dataset {ds_name}, ignoring it')
+ continue
+
+ x_name, y_name = grid_mapping.xy_dim_names
+
+ var_names = sorted(ds.data_vars)
+ for var_name in var_names:
+ qualified_var_name = f'{ds_name}.{var_name}'
+ if coverages and qualified_var_name not in coverages:
+ continue
+ var = ds[var_name]
+
+ label = var.long_name if hasattr(var, 'long_name') else var_name
+ label += f' (from {ds_name})'
+ is_spatial_var = var.ndim >= 2 \
+ and var.dims[-1] == x_name \
+ and var.dims[-2] == y_name
+ if not is_spatial_var:
+ continue
+
+ is_temporal_var = var.ndim >= 3
+ time_steps = None
+ if is_temporal_var:
+ time_steps = [f'{str(d)[:19]}Z' for d in var.time.values]
+
+ band_info = BandInfo(qualified_var_name, label, bbox, time_steps,
+ grid_mapping.width, grid_mapping.height)
+ band_infos[f'{ds_name}.{var_name}'] = band_info
+
+ return band_infos
diff --git a/xcube/webapi/ows/wcs/res/__init__.py b/xcube/webapi/ows/wcs/res/__init__.py
new file mode 100644
index 000000000..2f1eda68b
--- /dev/null
+++ b/xcube/webapi/ows/wcs/res/__init__.py
@@ -0,0 +1,20 @@
+# The MIT License (MIT)
+# Copyright (c) 2021/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.
diff --git a/xcube/webapi/ows/wcs/res/wcsCapabilities.xsd b/xcube/webapi/ows/wcs/res/wcsCapabilities.xsd
new file mode 100644
index 000000000..6e30afb9f
--- /dev/null
+++ b/xcube/webapi/ows/wcs/res/wcsCapabilities.xsd
@@ -0,0 +1,698 @@
+
+
+
+
+ wcsCapabilities.xsd v1.0.2 2010-02-01
+ This schema defines the Capabilities operation request and reply XML
+ elements and types used by an OGC Web Coverage Service (WCS). This schema with the
+ schemas it uses are believed to be GML Application Schemas.
+
+ WCS is an OGC Standard.
+ Copyright (c) 2003,2010 Open Geospatial Consortium.
+ To obtain additional rights of use, visit http://www.opengeospatial.org/legal/ .
+
+ AEW 03/07/22 Changes made: Changed element name "Section" to "section" in
+ GetCapabilities Added documentation elements in GetCapabilities,
+ CapabilitiesSectionType, ContentMetadata, AbstractDescriptionBaseType,
+ AbstractDescriptionType Expanded documentation of WCS_CapabilitiesType Moved
+ documentation from Service to ServiceType Edited documentation of OnlineResourceType,
+ electronicMailAddress in AddressType Expanded separator comment before ContentMetadata
+ elements and types Added separator comment before CoverageOfferingBrief Moved
+ metadataLink from AbstractDescriptionType to AbstractDescriptionBaseType
+ in CoverageOfferingBrief, replaced boundedBy with a new lonLatBoundingBox
+ (added to owsBase.xsd); and made temporal domain of type TimeSequenceType.
+ AEW 03/07/29 Changes made: Changed optionality and documentation os "fees"
+ and accessConstraints" elements in ServiceType Changed reference to gml:description to
+ reference to (wcs:)description in AbstractDescriptionBaseType
+ AEW 03/07/30 Changes made: Added "version" and "updateSequence" attributes
+ to ServiceType, WCSCapabilityType, and ContentMetadata
+ JDE 03/07/30 - Added "version" and "updateSequence" attributes to
+ WCS_Capabilities type 03/08/27 - Made "version" attribute required
+ AEW 04/07/13 Changes made: Added declaration of the "xlink" namespace. In
+ wcs:AbstractDescriptionType, added the elements wcs:description and wcs:name, and the
+ attribute "gml:id" with use="prohibited".
+ AEW 05/07/18 Changes made: Changed documentation of updateSequence
+ attribute in GetCapabilities element, WCS_CapabilitiesType, ServiceType,
+ WCSCapabilityType, and ContentMetadata element. Added documentation of all enumeration
+ values in CapabilitiesSectionType. Added documentation of WCSCapabilityType,
+ DCPTypeType, CoverageOfferingBriefType, and AbstractDescriptionType.
+ JDE 2005/08/31 Changes made: AbstractDescriptionBaseType: made
+ metadataLink repeatable
+
+
+
+
+
+
+
+
+
+ Request to a WCS to perform the GetCapabilities operation. In this XML
+ encoding, no "request" parameter is included, since the element name specifies the
+ specific operation.
+
+
+
+
+
+
+
+
+
+ Service metadata (Capabilities) document version, having
+ values that are "increased" whenever any change is made in service metadata
+ document. Values are selected by each server, and are always opaque to
+ clients. When omitted or not supported by server, server shall return latest
+ complete service metadata document.
+
+
+
+
+
+
+
+ Identification of desired part of full Capabilities XML document to be
+ returned.
+
+
+
+
+ TBD.
+
+
+
+
+ TBD.
+
+
+
+
+ TBD.
+
+
+
+
+ TBD.
+
+
+
+
+
+
+
+
+
+ Metadata for a WCS server, also known as Capabilities document. Reply
+ from a WCS that performed the GetCapabilities operation.
+
+
+
+
+
+
+
+
+
+ Service metadata (Capabilities) document version, having values
+ that are "increased" whenever any change is made in service metadata document.
+ Values are selected by each server, and are always opaque to clients. When
+ supported by server, server shall return this attribute.
+
+
+
+
+
+
+
+
+ A minimal, human readable rescription of the service.
+
+
+
+
+
+
+
+
+
+ A text string identifying any fees imposed by the
+ service provider. The keyword NONE shall be used to mean no fees.
+
+
+
+
+
+ A text string identifying any access constraints
+ imposed by the service provider. The keyword NONE shall be used to
+ mean no access constraints are imposed.
+
+
+
+
+
+
+ Service metadata (Capabilities) document version, having
+ values that are "increased" whenever any change is made in service
+ metadata document. Values are selected by each server, and are always
+ opaque to clients. When supported by server, server shall return this
+ attribute.
+
+
+
+
+
+
+
+
+ Identification of, and means of communication with, person(s) and
+ organizations associated with the server.
+
+
+
+
+
+
+ Name of the responsible person-surname, given name,
+ title separated by a delimiter.
+
+
+
+
+
+
+ Name of the responsible organizationt.
+
+
+
+
+
+ Role or position of the responsible person.
+
+
+
+
+
+ Address of the responsible party.
+
+
+
+
+
+
+
+ Information required to enable contact with the responsible person
+ and/or organization.
+
+
+
+
+ Telephone numbers at which the organization or individual may
+ becontacted.
+
+
+
+
+ Physical and email address at which the organization or
+ individualmay be contacted.
+
+
+
+
+ On-line information that can be used to contact the individual
+ ororganization.
+
+
+
+
+
+
+
+ Reference to on-line resource from which data can be obtained.
+
+
+
+
+
+
+
+ Telephone numbers for contacting the responsible individual or
+ organization.
+
+
+
+
+ Telephone number by which individuals can speak to the
+ responsible organization or individual.
+
+
+
+
+ Telephone number of a facsimile machine for the
+ responsibleorganization or individual.
+
+
+
+
+
+
+
+ Location of the responsible individual or organization.
+
+
+
+
+
+ Address line for the location (as described in ISO 11180,
+ Annex A).
+
+
+
+
+ City of the location.
+
+
+
+
+ State ot province of the location.
+
+
+
+
+ ZIP or other postal code.
+
+
+
+
+ Country of the physical address.
+
+
+
+
+ Address of the electronic mailbox of the responsible
+ organization or individual.
+
+
+
+
+
+
+
+
+ XML encoded WCS GetCapabilities operation response. The Capabilities
+ document provides clients with service metadata about a specific service instance,
+ including metadata about the coverages served.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Service metadata document version, having values that are
+ "increased" whenever any change is made in service metadata document. Values are
+ selected by each server, and are always opaque to clients. When not supported by
+ server, server shall not return this attribute.
+
+
+
+
+
+
+ Connect point URLs for the HTTP Distributed Computing Platform (DCP).
+ Normally, only one Get and/or one Post is included in this element. More than one
+ Get and/or Post is allowed to support including alternative URLs for uses such as
+ load balancing or backup.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unordered list of brief descriptions of all coverages avaialble from
+ this WCS, or a reference to another service from which this information is
+ available.
+
+
+
+
+
+
+
+
+
+ Service metadata document version, having values that are
+ "increased" whenever any change is made in service metadata document. Values
+ are selected by each server, and are always opaque to clients. When not
+ supported by server, server shall not return this attribute.
+
+
+
+
+
+
+
+
+
+
+ Brief description of one coverage avaialble from a WCS.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Description of a WCS object.
+
+
+
+
+
+
+
+
+
+
+
+
+ Human-readable descriptive information for the object it is included
+ within.
+
+
+
+
+
+
+
+
+ Short human-readable label for this object, for human
+ interface display.
+
+
+
+
+
+
+
+
+
+ Identifier for the object, normally a descriptive name.
+ For WCS use, removed optional CodeSpace attribute from gml:name.
+
+
+
+
+
+ Contains a simple text description of the object.
+ For WCS use, removed optional AssociationAttributeGroup from gml:description.
+
+
+
+
+
+ Unordered list of one or more commonly used or formalised word(s) or phrase(s) used to describe the subject. When needed, the optional "type" can name the type of the associated list of keywords that shall all have the same type. Also when needed, the codeSpace attribute of that "type" can also reference the type name authority and/or thesaurus. (Largely based on MD_Keywords class in ISO 19115.)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ For WCS use, LonLatEnvelopeBaseType restricts gml:Envelope to the WGS84 geographic CRS with Longitude preceding Latitude and both using decimal degrees only. If included, height values are third and use metre units.
+ Envelope defines an extent using a pair of positions defining opposite corners in arbitrary dimensions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines spatial extent by extending LonLatEnvelope with an optional time position pair.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ An ordered sequence of time positions or intervals. The time positions and periods shall be ordered from the oldest to the newest.
+
+
+
+
+
+
+
+
+
+
+
+ This is a variation of the GML TimePeriod, which allows the beginning and end of a time-period to be expressed in short-form inline using the begin/endPosition element, which allows an identifiable TimeInstant to be defined simultaneously with using it, or by reference, using xlinks on the begin/end elements.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refers to a metadata package that contains metadata properties for an object. The metadataType attribute indicates the type of metadata referred to.
+
+
+
+
+
+
+
+
+ This metadata uses a profile of ISO TC211’s Geospatial Metadata Standard 19115.
+
+
+
+
+ This metadata uses a profile of the US FGDC Content Standard for Digital Geospatial Metadata.
+
+
+
+
+ This metadata uses some other metadata standard(s) and/or no standard.
+
+
+
+
+
+
+
+
+
+
+
+ Refers to a metadata package that contains metadata properties for an object.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unordered list of data transfer formats supported.
+
+
+
+
+
+
+ Identifiers of one format in which the data is stored.
+
+
+
+
+
+
+ Identifiers of one or more formats in which coverage content can be retrieved. The codeSpace optional attribute can reference the semantic of the format identifiers.
+
+
+
+
+
+
+
+ Unordered list(s) of identifiers of Coordinate Reference Systems (CRSs) supported in server operation requests and responses.
+
+
+
+
+
+ Unordered list of identifiers of the CRSs in which the server can both accept requests and deliver responses for this data. These CRSs should include the native CRSs defined below.
+
+
+
+
+
+ Unordered list of identifiers of the CRSs in which the server can accept requests for this data. These CRSs should include the native CRSs defined below.
+
+
+
+
+ Unordered list of identifiers of the CRSs in which the server can deliver responses for this data. These CRSs should include the native CRSs defined below.
+
+
+
+
+
+
+ Unordered list of identifiers of the CRSs in which the server stores this data, that is, the CRS(s) in which data can be obtained without any distortion or degradation.
+
+
+
+
+
+
+
+
+
+ Unordered list of interpolation methods supported.
+
+
+
+
+
+
+
+
+
+
+
+ Codes that identify interpolation methods. The meanings of these codes are defined in Annex B of ISO 19123: Geographic information — Schema for coverage geometry and functions.
+
+
+
+
+
+
+
+
+
+ No interpolation.
+
+
+
+
+
diff --git a/xcube/webapi/ows/wcs/res/wcsDescribe.xsd b/xcube/webapi/ows/wcs/res/wcsDescribe.xsd
new file mode 100644
index 000000000..6cd7042be
--- /dev/null
+++ b/xcube/webapi/ows/wcs/res/wcsDescribe.xsd
@@ -0,0 +1,1495 @@
+
+
+
+
+ describeCoverage.xsd v1.0.2 2010-02-01
+ This schema defines the DescribeCoverage operation
+ request and reply XML elements and types, used by an OGC Web
+ Coverage Service (WCS).
+
+
+ WCS is an OGC Standard.
+ Copyright (c) 2003,2010 Open Geospatial Consortium.
+ To obtain additional rights of use, visit
+ http://www.opengeospatial.org/legal/ .
+
+ JDE 2003-07-24 changes:
+ Changed first and third "include" below to "import" so as to bring
+ more than one namespace
+ Made spatialDomain a global element so as to substitute for it in
+ GetCoverage spatialSubset
+
+ JDE 2003-07-28 - Made temporalSubset of type
+ TimeSequenceType (from owsBase.xsd, uses gml:timePosition).
+
+ AEW 03/07/29 Changes made:
+ Edited documentation of some elements and types
+
+ AEW 03/07/30 Changes made:
+ Corrrected defintion of temporalDomain element, makiing first
+ letter of name lower case
+
+ AEW 03/08/01 Changes made:
+ Edited documentation of many types
+
+ JDE 03/08/27 Changes made:
+ Made DescribeCoverage/@service and DescribeCoverage/@version
+ required
+ Added CoverageDescription/@version (required) and
+ CoverageDescription/@updateSequence (optional)
+
+ AEW 04/07/14 Changes made:
+ Changed two "import" statement for the "wcs" namespace to "include"
+ statements.
+ Added "import" statement for the "gml" namespace used in
+ gml4wcs.xsd.
+
+ AEW 05/07/15 Changes made:
+ Changed documentation of updateSequence attribute in
+ CoverageDescription element
+
+
+
+
+
+
+
+
+ Request to a WCS to perform the DescribeCoverage
+ operation. In this XML encoding, no "request" parameter is
+ included, since the element name specifies the specific
+ operation.
+
+
+
+
+
+
+ Name or identifier of this coverage.
+ The same name value shall not be used for any other
+ coverages available from the same server. A client
+ can obtain this name by a prior GetCapabilities
+ request, or possibly from a third-party source. If
+ this element is omitted, the server may return
+ descriptions of every coverage offering available,
+ or return a service exception.
+
+
+
+
+
+
+
+
+
+
+
+ Reply from a WCS that performed the
+ DescribeCoverage operation, containing one or more full
+ coverage offering descriptions.
+
+
+
+
+
+
+
+
+
+ Service metadata (Capabilities) document
+ version, having values that are "increased" whenever
+ any change is made in service metadata document. Values
+ are selected by each server, and are always opaque to
+ clients.
+
+
+
+
+
+
+
+
+
+
+ Full description of one coverage available from a
+ WCS instance.
+
+
+
+
+
+
+
+
+
+
+
+ Specifies whether and how the
+ server can interpolate coverage values over the
+ spatial domain, when a GetCoverage request
+ requires resampling, reprojection, or other
+ generalization. If supportedInterpolations is
+ absent or empty with no default, then clients
+ should assume nearest-neighbor interpolation.
+ If the only interpolation method listed is
+ ‘none’, clients can only retrieve coverages
+ from this layer in its native CRS and at its
+ native resolution.
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines the spatial-temporal domain set of a
+ coverage offering. The domainSet shall include a SpatialDomain
+ (describing the spatial locations for which coverages can be
+ requested), a TemporalDomain (describing the time instants or
+ inter-vals for which coverages can be requested), or both.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines the spatial domain of a coverage
+ offering. A server shall describe the spatial domain by its
+ edges, using one or more gml:Envelope elements. The
+ gml:EnvelopeWithTimePeriod element may be used in place of
+ gml:Envelope, to add the time bounds of the coverage offering.
+ Each of these elements describes a bounding box defined by two
+ points in space (or two positions in space and two in time).
+ This bounding box could simply duplicate the information in the
+ lonLatEnvelope of CoverageOfferingBrief; but the intent is to
+ describe the locations in more detail (e.g., in several
+ different CRSs, or several rectangular areas instead of one
+ overall bounding box).
+
+ In addition, a server can describe the internal grid structure
+ of a coverage offering, using a gml:Grid (or gml:RectifiedGrid)
+ in addition to a gml:Envelope. This element can help clients
+ assess the fitness of the gridded data for their use (e.g. its
+ native resolution, inferred from the offsetVector of a
+ gml:RectifiedGrid), and to formulate grid coverage requests
+ expressed in the internal grid coordinate reference system.
+
+ Finally, a server can describe the spatial domain by means of a
+ (repeatable) gml:Polygon, representing the polygon(s) covered
+ by the coverage spatial domain. This is particularly useful for
+ areas that are poorly approximated by a gml:Envelope (such as
+ satellite image swaths, island groups, other non-convex areas).
+
+
+
+
+
+
+
+
+
+
+
+ Defines the temporal domain of a coverage
+ offering, that is, the times for which valid data are
+ available. The times shall to be ordered from the oldest to the
+ newest.
+
+
+
+
+
+
+ GML property containing one RangeSet GML
+ object.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines the properties (categories, measures, or
+ values) assigned to each location in the domain. Any such
+ property may be a scalar (numeric or text) value, such as
+ population density, or a compound (vector or tensor) value,
+ such as incomes by race, or radiances by wavelength. The
+ semantic of the range set is typically an observable and is
+ referenced by a URI. A rangeSet also has a reference system
+ that is reffered by the URI in the refSys attribute. The refSys
+ is either qualitative (classification) or quantitative (uom).
+ The three attributes can be included either here and in each
+ axisDescription. If included in both places, the values in the
+ axisDescription over-ride those included in the RangeSet.
+
+
+
+
+
+
+
+ Defines a range provided by a
+ coverage. Multiple occurences are used for
+ compound observations, to descibe an additional
+ parameter (that is, an independent variable
+ besides space and time), plus the valid values
+ of this parameter (which GetCoverage requests
+ can use to select subsets of a coverage
+ offering).
+
+
+
+
+
+ Values used when valid values are
+ not available. (The coverage encoding may
+ specify a fixed value for null (e.g. “–99999”
+ or “N/A”), but often the choice is up to the
+ provider and must be communicated to the client
+ outside of the coverage itself.)
+
+
+
+
+
+
+
+ Pointer to the reference system in
+ which values are expressed. This attribute shall be
+ included either here or in each
+ AxisDescriptionType.
+
+
+
+
+
+ Short human-readable label denoting
+ the reference system, for human interface display.
+ This attribute shall be included either here or in
+ each AxisDescriptionType.
+
+
+
+
+
+
+
+
+
+ GML property containing one AxisDescription GML
+ object.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Description of a measured or observed quantity,
+ and list of the “valid” quantity values (values for which
+ measurements are available or “by which” aggregate values are
+ available). The semantic is the URI of the quantity (for
+ example observable or mathematical variable). The refSys
+ attribute is a URI to a reference system, and the refSysLabel
+ is the label used by client to refer the reference system.
+
+
+
+
+
+
+
+ The type and value constraints
+ for the values of this axis.
+
+
+
+
+
+
+
+
+ Ordered
+ sequence of the parameter
+ value(s) that the server
+ will use for GetCoverage
+ requests which omit a
+ constraint on this
+ parameter axis.
+ (GetCoverage requests
+ against a coverage offering
+ whose AxisDescription has
+ no default must specify a
+ valid constraint for this
+ parameter.)
+
+
+
+
+
+
+
+
+
+
+
+
+ Pointer to the reference system in
+ which values are expressed. This attribute shall be
+ included either here or in RangeSetType.
+
+
+
+
+
+ Short human-readable label denoting
+ the reference system, for human interface display.
+ This attribute shall be included either here or in
+ RangeSetType.
+
+
+
+
+
+
+
+
+ Request to a WCS to perform the GetCapabilities
+ operation. In this XML
+ encoding, no "request" parameter is included, since the element
+ name specifies the
+ specific operation.
+
+
+
+
+
+
+
+
+
+
+ Service metadata (Capabilities) document
+ version, having
+ values that are "increased" whenever any change is made
+ in service metadata
+ document. Values are selected by each server, and are
+ always opaque to
+ clients. When omitted or not supported by server,
+ server shall return latest
+ complete service metadata document.
+
+
+
+
+
+
+
+
+ Identification of desired part of full
+ Capabilities XML document to be
+ returned.
+
+
+
+
+
+ TBD.
+
+
+
+
+ TBD.
+
+
+
+
+ TBD.
+
+
+
+
+ TBD.
+
+
+
+
+
+
+
+
+
+ Metadata for a WCS server, also known as
+ Capabilities document. Reply
+ from a WCS that performed the GetCapabilities operation.
+
+
+
+
+
+
+
+
+
+
+ Service metadata (Capabilities) document
+ version, having values
+ that are "increased" whenever any change is made in service
+ metadata document.
+ Values are selected by each server, and are always opaque
+ to clients. When
+ supported by server, server shall return this attribute.
+
+
+
+
+
+
+
+
+
+ A minimal, human readable rescription of the
+ service.
+
+
+
+
+
+
+
+
+
+ A text string identifying any
+ fees imposed by the
+ service provider. The keyword NONE shall be
+ used to mean no fees.
+
+
+
+
+
+ A text string identifying any
+ access constraints
+ imposed by the service provider. The keyword
+ NONE shall be used to
+ mean no access constraints are imposed.
+
+
+
+
+
+
+
+ Service metadata (Capabilities)
+ document version, having
+ values that are "increased" whenever any change is
+ made in service
+ metadata document. Values are selected by each
+ server, and are always
+ opaque to clients. When supported by server, server
+ shall return this
+ attribute.
+
+
+
+
+
+
+
+
+
+ Identification of, and means of communication
+ with, person(s) and
+ organizations associated with the server.
+
+
+
+
+
+
+
+ Name of the responsible
+ person-surname, given name,
+ title separated by a delimiter.
+
+
+
+
+
+
+
+ Name of the responsible
+ organizationt.
+
+
+
+
+
+
+ Role or position of the responsible
+ person.
+
+
+
+
+
+ Address of the responsible party.
+
+
+
+
+
+
+
+
+ Information required to enable contact with the
+ responsible person
+ and/or organization.
+
+
+
+
+
+ Telephone numbers at which the
+ organization or individual may
+ becontacted.
+
+
+
+
+
+ Physical and email address at which the
+ organization or
+ individualmay be contacted.
+
+
+
+
+
+ On-line information that can be used to
+ contact the individual
+ ororganization.
+
+
+
+
+
+
+
+
+ Reference to on-line resource from which data can
+ be obtained.
+
+
+
+
+
+
+
+ Telephone numbers for contacting the responsible
+ individual or
+ organization.
+
+
+
+
+
+ Telephone number by which individuals can
+ speak to the
+ responsible organization or individual.
+
+
+
+
+
+ Telephone number of a facsimile machine
+ for the
+ responsibleorganization or individual.
+
+
+
+
+
+
+
+
+ Location of the responsible individual or
+ organization.
+
+
+
+
+
+ Address line for the location (as
+ described in ISO 11180,
+ Annex A).
+
+
+
+
+
+ City of the location.
+
+
+
+
+ State ot province of the location.
+
+
+
+
+
+ ZIP or other postal code.
+
+
+
+
+
+ Country of the physical address.
+
+
+
+
+
+ Address of the electronic mailbox of the
+ responsible
+ organization or individual.
+
+
+
+
+
+
+
+
+
+ XML encoded WCS GetCapabilities operation
+ response. The Capabilities
+ document provides clients with service metadata about a
+ specific service instance,
+ including metadata about the coverages served.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Service metadata document version, having
+ values that are
+ "increased" whenever any change is made in service metadata
+ document. Values are
+ selected by each server, and are always opaque to clients.
+ When not supported by
+ server, server shall not return this attribute.
+
+
+
+
+
+
+
+ Connect point URLs for the HTTP Distributed
+ Computing Platform (DCP).
+ Normally, only one Get and/or one Post is included in this
+ element. More than one
+ Get and/or Post is allowed to support including alternative
+ URLs for uses such as
+ load balancing or backup.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unordered list of brief descriptions of all
+ coverages avaialble from
+ this WCS, or a reference to another service from which this
+ information is
+ available.
+
+
+
+
+
+
+
+
+
+
+ Service metadata document version, having
+ values that are
+ "increased" whenever any change is made in service
+ metadata document. Values
+ are selected by each server, and are always opaque to
+ clients. When not
+ supported by server, server shall not return this
+ attribute.
+
+
+
+
+
+
+
+
+
+
+ Brief description of one coverage avaialble from
+ a WCS.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Description of a WCS object.
+
+
+
+
+
+
+
+
+
+
+
+
+ Human-readable descriptive information for the
+ object it is included
+ within.
+
+
+
+
+
+
+
+
+
+ Short human-readable label for
+ this object, for human
+ interface display.
+
+
+
+
+
+
+
+
+
+
+ Identifier for the object, normally a descriptive
+ name.
+
+ For WCS use, removed optional CodeSpace attribute
+ from gml:name.
+
+
+
+
+
+
+ Contains a simple text description of the
+ object.
+
+ For WCS use, removed optional
+ AssociationAttributeGroup from gml:description.
+
+
+
+
+
+
+ Unordered list of one or more commonly used or
+ formalised word(s) or phrase(s) used to describe the subject.
+ When needed, the optional "type" can name the type of the
+ associated list of keywords that shall all have the same type.
+ Also when needed, the codeSpace attribute of that "type" can
+ also reference the type name authority and/or thesaurus.
+ (Largely based on MD_Keywords class in ISO 19115.)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ For WCS use, LonLatEnvelopeBaseType restricts
+ gml:Envelope to the WGS84 geographic CRS with Longitude
+ preceding Latitude and both using decimal degrees only. If
+ included, height values are third and use metre units.
+
+ Envelope defines an extent using a pair of
+ positions defining opposite corners in arbitrary dimensions.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Defines spatial extent by extending
+ LonLatEnvelope with an optional time position pair.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ An ordered sequence of time positions or
+ intervals. The time positions and periods shall be ordered from
+ the oldest to the newest.
+
+
+
+
+
+
+
+
+
+
+
+
+ This is a variation of the GML TimePeriod, which
+ allows the beginning and end of a time-period to be expressed
+ in short-form inline using the begin/endPosition element, which
+ allows an identifiable TimeInstant to be defined simultaneously
+ with using it, or by reference, using xlinks on the begin/end
+ elements.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refers to a metadata package that contains
+ metadata properties for an object. The metadataType attribute
+ indicates the type of metadata referred to.
+
+
+
+
+
+
+
+
+
+ This metadata uses a
+ profile of ISO TC211’s Geospatial
+ Metadata Standard 19115.
+
+
+
+
+
+ This metadata uses a
+ profile of the US FGDC Content Standard
+ for Digital Geospatial Metadata.
+
+
+
+
+
+ This metadata uses some
+ other metadata standard(s) and/or no
+ standard.
+
+
+
+
+
+
+
+
+
+
+
+
+ Refers to a metadata package that contains
+ metadata properties for an object.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unordered list of data transfer formats
+ supported.
+
+
+
+
+
+
+
+ Identifiers of one format in which the data
+ is stored.
+
+
+
+
+
+
+
+ Identifiers of one or more formats in which
+ coverage content can be retrieved. The codeSpace optional
+ attribute can reference the semantic of the format identifiers.
+
+
+
+
+
+
+
+
+ Unordered list(s) of identifiers of Coordinate
+ Reference Systems (CRSs) supported in server operation requests
+ and responses.
+
+
+
+
+
+
+ Unordered list of identifiers of the
+ CRSs in which the server can both accept requests
+ and deliver responses for this data. These CRSs
+ should include the native CRSs defined below.
+
+
+
+
+
+
+ Unordered list of identifiers of
+ the CRSs in which the server can accept
+ requests for this data. These CRSs should
+ include the native CRSs defined below.
+
+
+
+
+
+ Unordered list of identifiers of
+ the CRSs in which the server can deliver
+ responses for this data. These CRSs should
+ include the native CRSs defined below.
+
+
+
+
+
+
+
+ Unordered list of identifiers of the CRSs
+ in which the server stores this data, that is, the
+ CRS(s) in which data can be obtained without any
+ distortion or degradation.
+
+
+
+
+
+
+
+
+
+
+ Unordered list of interpolation methods
+ supported.
+
+
+
+
+
+
+
+
+
+
+
+
+ Codes that identify interpolation methods. The
+ meanings of these codes are defined in Annex B of ISO 19123:
+ Geographic information — Schema for coverage geometry and
+ functions.
+
+
+
+
+
+
+
+
+
+
+ No interpolation.
+
+
+
+
+
+
+ List of all the valid values and/or intervals of
+ values for this variable. For numeric variables, signed values
+ shall be ordered from negative infinity to positive infinity.
+ For intervals, the type and semantic attributes are inherited
+ by children elements, but can be superceded here.
+
+
+
+
+
+
+ Should be included if the data type
+ is not xs:string, and the valueEnumBaseType does
+ not include any "interval" elements that include
+ this attribute.
+
+
+
+
+
+ Should be included if the semantics
+ or meaning is not clearly specified elsewhere, and
+ the valueEnumBaseType does not include any
+ "interval" elements that include this attribute.
+
+
+
+
+
+
+
+
+
+ List of all the valid values and/or ranges of
+ values for this variable. For numeric variables, signed values
+ shall be ordered from negative infinity to positive infinity.
+ For intervals, the "type" and "semantic" attributes are
+ inherited by children elements, but can be superceded by them.
+
+
+
+
+
+
+
+
+
+
+ A single value for a quantity.
+
+
+
+
+
+
+
+ An interval of values of a numeric quantity. This
+ interval can be continuous or discrete, defined by a fixed
+ spacing between adjacent valid values. Note that the "type" and
+ "semantic" attributes for min/max and "res" may be different
+ (timeInstant and duration).
+
+
+
+
+
+
+
+ The regular distance or spacing
+ between the allowed values in this interval.
+ Shall be included when the allowed values are
+ NOT continuous in this interval. Shall not be
+ included when the allowed values are continuous
+ in this interval.
+
+
+
+
+
+
+
+
+
+
+ The range of an interval. If the "min" or "max"
+ element is not included, there is no value limit in that
+ direction. Inclusion of the specified minimum and maximum
+ values in the range shall be defined by the "closure". (The
+ interval can be bounded or semi-bounded with different
+ closures.) The data type and the semantic of the values are
+ inherited by children and may be superceded by them. This range
+ may be qualitative, i.e., nominal (age range) or qualitative
+ (percentage) meaning that a value between min/max can be
+ queried.
+
+
+
+
+
+ Minimum value of this numeric
+ parameter.
+
+
+
+
+
+ Maximum value of this numeric
+ parameter.
+
+
+
+
+
+
+ Can be omitted when the datatype of values in
+ this interval is xs:string, or the "type" attribute is
+ included in an enclosing element.
+
+
+
+
+
+ Can be omitted when the semantics or meaning
+ of values in this interval is clearly specified elsewhere,
+ or the "semantic" attribute is included in an enclosing
+ element.
+
+
+
+
+
+ What does this attribute mean? Is it useful
+ and not redundant? When should this attribute be included
+ or omitted? TBD.
+
+
+
+
+
+ Shall be included unless the default value
+ applies.
+
+
+
+
+
+
+
+ Specifies which of the minimum and maximum values
+ are included in the range. Note that plus and minus infinity
+ are considered closed bounds.
+
+
+
+
+
+
+ The specified minimum and maximum
+ values are included in this range.
+
+
+
+
+
+ The specified minimum and maximum
+ values are NOT included in this range.
+
+
+
+
+
+ The specified minimum value is NOT
+ included in this range, and the specified maximum
+ value IS included in this range.
+
+
+
+
+
+ The specified minimum value IS
+ included in this range, and the specified maximum
+ value is NOT included in this range.
+
+
+
+
+
+
+
+
+
+ A single value for a variable, encoded as a
+ string. This type can be used for one value, for a spacing
+ between allowed values, or for the default value of a
+ parameter. The "type" attribute indicates the datatype of this
+ value (default is a string). The value for a typed literal is
+ found by applying the datatype mapping associated with the
+ datatype URI to the lexical form string.
+
+
+
+
+
+
+ Should be included unless the
+ datatype is xs:string, or this "type" attribute is
+ included in an enclosing element.
+
+
+
+
+
+
+
+
+
+ Datatype of a typed literal value. This URI
+ typically references XSD simple types. It has the same semantic
+ as rdf:datatype.
+
+
+
+
+
+
+ Definition of the semantics or meaning of the
+ values in the XML element it belongs to. The value of this
+ "semantic" attribute can be a RDF Property or Class of a
+ taxonomy or ontology.
+
+
+
+
diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py
new file mode 100644
index 000000000..77f2772d5
--- /dev/null
+++ b/xcube/webapi/ows/wcs/routes.py
@@ -0,0 +1,232 @@
+# 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 os
+import tempfile
+
+from xarray import Dataset
+
+from xcube.constants import LOG
+from xcube.server.api import ApiError
+from xcube.server.api import ApiHandler
+from .api import api
+from .context import WcsContext
+from .controllers import CoverageRequest
+from .controllers import get_coverage
+from .controllers import get_describe_coverage_xml
+from .controllers import get_wcs_capabilities_xml
+
+WCS_VERSION = '1.0.0'
+
+# Number of bytes to read and write at once
+IO_CHUNK_SIZE = 4 * 1024 * 1024
+
+
+@api.route('/wcs/1.0.0/WCSCapabilities.xml')
+class WcsCapabilitiesXmlHandler(ApiHandler[WcsContext]):
+ @api.operation(operationId='getWcsCapabilities',
+ summary='Gets the WCS capabilities as XML document')
+ async def get(self):
+ self.request.make_query_lower_case()
+ capabilities = await self.ctx.run_in_executor(
+ None,
+ get_wcs_capabilities_xml,
+ self.ctx,
+ self.request.reverse_base_url
+ )
+ self.response.set_header('Content-Type', 'application/xml')
+ await self.response.finish(capabilities)
+
+
+@api.route('/wcs/kvp')
+class WcsKvpHandler(ApiHandler[WcsContext]):
+ @api.operation(operationId='invokeWcsMethodFromKvp',
+ summary='Invokes the WCS by key-value pairs')
+ async def get(self):
+ self.request.make_query_lower_case()
+ service = self.request.get_query_arg('service', default='WCS')
+ if service != 'WCS':
+ raise ApiError.BadRequest(
+ 'value for "service" parameter must be "WCS"'
+ )
+ LOG.debug(self.request.url)
+ request = self.request.get_query_arg('request')
+ if request == "GetCapabilities":
+ await self._do_get_capabilities()
+ elif request == 'DescribeCoverage':
+ await self._do_describe_coverage()
+ elif request == "GetCoverage":
+ await self._do_get_coverage()
+ else:
+ raise ApiError.BadRequest(
+ f'invalid request type "{request}"'
+ )
+
+ async def _do_get_coverage(self):
+ wcs_version = self.request.get_query_arg('version',
+ default=WCS_VERSION)
+ if wcs_version != WCS_VERSION:
+ raise ApiError.BadRequest(
+ f'value for "version" parameter must be "{WCS_VERSION}"'
+ )
+ coverage = self.request.get_query_arg('coverage')
+ if not coverage:
+ raise ApiError.BadRequest(
+ 'missing query argument "coverage"'
+ )
+ request_crs = self.request.get_query_arg('crs')
+ if not request_crs:
+ raise ApiError.BadRequest(
+ 'missing query argument "crs"'
+ )
+ response_crs = self.request.get_query_arg('response_crs',
+ default=request_crs)
+ time = self.request.get_query_arg('time')
+ if not time:
+ raise ApiError.BadRequest(
+ 'missing value for query argument "time"'
+ )
+ # QGIS specific hack!
+ time = time.replace('Z', '')
+ file_format = self.request.get_query_arg('format',
+ default='geotiff')
+ file_format = file_format.lower()
+ if file_format not in ("geotiff", "netcdf"):
+ raise ApiError.BadRequest(
+ f'value for "format" not supported: "{file_format}"'
+ )
+ bbox = self.request.get_query_arg('bbox',
+ default='-180,90,180,-90')
+ width = self.request.get_query_arg('width')
+ height = self.request.get_query_arg('height')
+ resx = self.request.get_query_arg('resx')
+ resy = self.request.get_query_arg('resy')
+ cov_req = CoverageRequest({
+ 'COVERAGE': coverage,
+ 'CRS': response_crs,
+ 'TIME': time,
+ 'BBOX': bbox,
+ 'FORMAT': file_format,
+ 'WIDTH': width,
+ 'HEIGHT': height,
+ 'RESX': resx,
+ 'RESY': resy
+ })
+
+ try:
+ cube = await self.ctx.run_in_executor(
+ None,
+ get_coverage,
+ self.ctx,
+ cov_req
+ )
+ except ValueError as e:
+ # TODO: too broad error message, must name invalid parameter
+ raise ApiError.BadRequest(f'{e}') from e
+
+ self.response.set_header('Content-Type', 'application/x-netcdf4')
+
+ if file_format == 'netcdf':
+ temp_file_path = await self.ctx.run_in_executor(
+ None, self._write_netcdf, cube
+ )
+ else:
+ temp_file_path = await self.ctx.run_in_executor(
+ None, self._write_geotiff, cube
+ )
+
+ with open(temp_file_path, 'rb') as tf:
+ while True:
+ chunk = tf.read(IO_CHUNK_SIZE)
+ if chunk is None or len(chunk) == 0:
+ break
+ try:
+ self.response.write(chunk)
+ except (OSError, IOError) as e:
+ raise ApiError.InternalServerError(
+ f'failed writing to {temp_file_path}: {e}'
+ ) from e
+
+ await self.response.finish()
+
+ try:
+ os.remove(temp_file_path)
+ except (OSError, IOError) as e:
+ LOG.error(f'Failed to remove'
+ f' temporary file {temp_file_path}: {e}',
+ exc_info=True)
+
+ async def _do_describe_coverage(self):
+ wcs_version = self.request.get_query_arg('version',
+ default=WCS_VERSION)
+ if wcs_version != WCS_VERSION:
+ raise ApiError.BadRequest(
+ f'value for "version" parameter must be "{WCS_VERSION}"'
+ )
+ coverages = self.request.get_query_arg("coverage")
+ if coverages:
+ coverages = coverages.split(',')
+ describe_coverage_xml = await self.ctx.run_in_executor(
+ None,
+ get_describe_coverage_xml,
+ self.ctx,
+ coverages
+ )
+ self.response.set_header('Content-Type', 'application/xml')
+ await self.response.finish(describe_coverage_xml)
+
+ async def _do_get_capabilities(self):
+ wcs_version = self.request.get_query_arg(
+ "version", default=WCS_VERSION
+ )
+ if wcs_version != WCS_VERSION:
+ raise ApiError.BadRequest(
+ f'value for "version" parameter must be "{WCS_VERSION}"'
+ )
+ capabilities_xml = await self.ctx.run_in_executor(
+ None,
+ get_wcs_capabilities_xml,
+ self.ctx,
+ self.request.reverse_base_url
+ )
+ self.response.set_header('Content-Type', 'application/xml')
+ await self.response.finish(capabilities_xml)
+
+ @staticmethod
+ def _write_geotiff(dataset: Dataset) -> str:
+ with tempfile.NamedTemporaryFile(prefix='xcube-wcs-',
+ suffix='.tif',
+ delete=False) as tf:
+ dataset = dataset.squeeze('time')
+ dataset.rio.to_raster(tf.name)
+ return tf.name
+
+ @staticmethod
+ def _write_netcdf(dataset: Dataset) -> str:
+ with tempfile.NamedTemporaryFile(prefix='xcube-wcs-',
+ suffix='.nc',
+ delete=False) as tf:
+ dataset.to_netcdf(path=tf.name, mode='w')
+ return tf.name
+
+
+def _query_to_dict(request):
+ return {k: v[0] for k, v in request.query.items()}