From dc6082bf9a2b0deb8e8471f2c122b518831546ae Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 1 Aug 2022 14:38:47 +0200 Subject: [PATCH 01/43] duplicated wmts -> wcs --- test/webapi/ows/wcs/__init__.py | 20 + .../ows/wcs/res/WMTSCapabilities-CRS84.xml | 637 ++++++++++++++++++ .../ows/wcs/res/WMTSCapabilities-OSM.xml | 637 ++++++++++++++++++ test/webapi/ows/wcs/test_controller.py | 336 +++++++++ test/webapi/ows/wcs/test_routes.py | 213 ++++++ 5 files changed, 1843 insertions(+) create mode 100644 test/webapi/ows/wcs/__init__.py create mode 100644 test/webapi/ows/wcs/res/WMTSCapabilities-CRS84.xml create mode 100644 test/webapi/ows/wcs/res/WMTSCapabilities-OSM.xml create mode 100644 test/webapi/ows/wcs/test_controller.py create mode 100644 test/webapi/ows/wcs/test_routes.py 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/res/WMTSCapabilities-CRS84.xml b/test/webapi/ows/wcs/res/WMTSCapabilities-CRS84.xml new file mode 100644 index 000000000..c143a5732 --- /dev/null +++ b/test/webapi/ows/wcs/res/WMTSCapabilities-CRS84.xml @@ -0,0 +1,637 @@ + + + + xcube WMTS + Web Map Tile Service (WMTS) for xcube-conformant data cubes + + tile + tile matrix set + map + + OGC WMTS + 1.0.0 + none + none + + + Brockmann Consult GmbH + + + Norman Fomferra + Senior Software Engineer + + + +49 4152 889 303 + +49 4152 889 330 + + + HZG / GITZ + Geesthacht + Herzogtum Lauenburg + 21502 + Germany + norman.fomferra@brockmann-consult.de + + + + + + + + + + + + KVP + + + + + + + REST + + + + + + + + + + + + + KVP + + + + + + + REST + + + + + + + + + + demo.c2rcc_flags + demo/C2RCC quality flags + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.conc_chl + demo/Chlorophyll concentration + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.conc_tsm + demo/Total suspended matter dry weight concentration + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.kd489 + demo/Irradiance attenuation coefficient at 489 nm + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.quality_flags + demo/Classification and quality flags + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo-1w.c2rcc_flags + demo-1w/c2rcc_flags + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.c2rcc_flags_stdev + demo-1w/c2rcc_flags_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_chl + demo-1w/conc_chl + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_chl_stdev + demo-1w/conc_chl_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_tsm + demo-1w/conc_tsm + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_tsm_stdev + demo-1w/conc_tsm_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.kd489 + demo-1w/kd489 + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.kd489_stdev + demo-1w/kd489_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.quality_flags + demo-1w/quality_flags + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.quality_flags_stdev + demo-1w/quality_flags_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldCRS84Quad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + WorldCRS84Quad + CRS84 for the World + urn:ogc:def:crs:OGC:1.3:CRS84 + + -180 -90 + 180 90 + + urn:ogc:def:wkss:OGC:1.0:GoogleCRS84Quad + + 7 + 2183915.093862179 + -180 90 + 256 + 256 + 256 + 128 + + + 8 + 1091957.5469310896 + -180 90 + 256 + 256 + 512 + 256 + + + 9 + 545978.7734655448 + -180 90 + 256 + 256 + 1024 + 512 + + + + + + demo + xcube-server Demonstration L2C Cube + + + demo.c2rcc_flags + demo/C2RCC quality flags + + demo.c2rcc_flags + + + demo.conc_chl + demo/Chlorophyll concentration + + demo.conc_chl + + + demo.conc_tsm + demo/Total suspended matter dry weight concentration + + demo.conc_tsm + + + demo.kd489 + demo/Irradiance attenuation coefficient at 489 nm + + demo.kd489 + + + demo.quality_flags + demo/Classification and quality flags + + demo.quality_flags + + + + demo-1w + xcube-server Demonstration L3 Cube + + + demo-1w.c2rcc_flags + demo-1w/c2rcc_flags + + demo-1w.c2rcc_flags + + + demo-1w.c2rcc_flags_stdev + demo-1w/c2rcc_flags_stdev + + demo-1w.c2rcc_flags_stdev + + + demo-1w.conc_chl + demo-1w/conc_chl + + demo-1w.conc_chl + + + demo-1w.conc_chl_stdev + demo-1w/conc_chl_stdev + + demo-1w.conc_chl_stdev + + + demo-1w.conc_tsm + demo-1w/conc_tsm + + demo-1w.conc_tsm + + + demo-1w.conc_tsm_stdev + demo-1w/conc_tsm_stdev + + demo-1w.conc_tsm_stdev + + + demo-1w.kd489 + demo-1w/kd489 + + demo-1w.kd489 + + + demo-1w.kd489_stdev + demo-1w/kd489_stdev + + demo-1w.kd489_stdev + + + demo-1w.quality_flags + demo-1w/quality_flags + + demo-1w.quality_flags + + + demo-1w.quality_flags_stdev + demo-1w/quality_flags_stdev + + demo-1w.quality_flags_stdev + + + + + diff --git a/test/webapi/ows/wcs/res/WMTSCapabilities-OSM.xml b/test/webapi/ows/wcs/res/WMTSCapabilities-OSM.xml new file mode 100644 index 000000000..4acdd925f --- /dev/null +++ b/test/webapi/ows/wcs/res/WMTSCapabilities-OSM.xml @@ -0,0 +1,637 @@ + + + + xcube WMTS + Web Map Tile Service (WMTS) for xcube-conformant data cubes + + tile + tile matrix set + map + + OGC WMTS + 1.0.0 + none + none + + + Brockmann Consult GmbH + + + Norman Fomferra + Senior Software Engineer + + + +49 4152 889 303 + +49 4152 889 330 + + + HZG / GITZ + Geesthacht + Herzogtum Lauenburg + 21502 + Germany + norman.fomferra@brockmann-consult.de + + + + + + + + + + + + KVP + + + + + + + REST + + + + + + + + + + + + + KVP + + + + + + + REST + + + + + + + + + + demo.c2rcc_flags + demo/C2RCC quality flags + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.conc_chl + demo/Chlorophyll concentration + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.conc_tsm + demo/Total suspended matter dry weight concentration + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.kd489 + demo/Irradiance attenuation coefficient at 489 nm + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo.quality_flags + demo/Classification and quality flags + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 + 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 + 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 + 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 + 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 + + + + demo-1w.c2rcc_flags + demo-1w/c2rcc_flags + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.c2rcc_flags_stdev + demo-1w/c2rcc_flags_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_chl + demo-1w/conc_chl + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_chl_stdev + demo-1w/conc_chl_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_tsm + demo-1w/conc_tsm + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.conc_tsm_stdev + demo-1w/conc_tsm_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.kd489 + demo-1w/kd489 + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.kd489_stdev + demo-1w/kd489_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.quality_flags + demo-1w/quality_flags + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + demo-1w.quality_flags_stdev + demo-1w/quality_flags_stdev + + + 0 50 + 5 52.5 + + + image/png + + WorldWebMercatorQuad + + + + time + time + ISO8601 + current + true + 2017-01-22T00:00:00.000000000 + 2017-01-29T00:00:00.000000000 + 2017-02-05T00:00:00.000000000 + + + + WorldWebMercatorQuad + Google Maps Compatible for the World + urn:ogc:def:crs:EPSG::3857 + + -20037508.3427892 -20037508.3427892 + 20037508.3427892 20037508.3427892 + + urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible + + 8 + 2183915.0938621787 + -20037508.3427892 20037508.3427892 + 256 + 256 + 256 + 256 + + + 9 + 1091957.5469310894 + -20037508.3427892 20037508.3427892 + 256 + 256 + 512 + 512 + + + 10 + 545978.7734655447 + -20037508.3427892 20037508.3427892 + 256 + 256 + 1024 + 1024 + + + + + + demo + xcube-server Demonstration L2C Cube + + + demo.c2rcc_flags + demo/C2RCC quality flags + + demo.c2rcc_flags + + + demo.conc_chl + demo/Chlorophyll concentration + + demo.conc_chl + + + demo.conc_tsm + demo/Total suspended matter dry weight concentration + + demo.conc_tsm + + + demo.kd489 + demo/Irradiance attenuation coefficient at 489 nm + + demo.kd489 + + + demo.quality_flags + demo/Classification and quality flags + + demo.quality_flags + + + + demo-1w + xcube-server Demonstration L3 Cube + + + demo-1w.c2rcc_flags + demo-1w/c2rcc_flags + + demo-1w.c2rcc_flags + + + demo-1w.c2rcc_flags_stdev + demo-1w/c2rcc_flags_stdev + + demo-1w.c2rcc_flags_stdev + + + demo-1w.conc_chl + demo-1w/conc_chl + + demo-1w.conc_chl + + + demo-1w.conc_chl_stdev + demo-1w/conc_chl_stdev + + demo-1w.conc_chl_stdev + + + demo-1w.conc_tsm + demo-1w/conc_tsm + + demo-1w.conc_tsm + + + demo-1w.conc_tsm_stdev + demo-1w/conc_tsm_stdev + + demo-1w.conc_tsm_stdev + + + demo-1w.kd489 + demo-1w/kd489 + + demo-1w.kd489 + + + demo-1w.kd489_stdev + demo-1w/kd489_stdev + + demo-1w.kd489_stdev + + + demo-1w.quality_flags + demo-1w/quality_flags + + demo-1w.quality_flags + + + demo-1w.quality_flags_stdev + demo-1w/quality_flags_stdev + + demo-1w.quality_flags_stdev + + + + + diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py new file mode 100644 index 000000000..31f1a31ce --- /dev/null +++ b/test/webapi/ows/wcs/test_controller.py @@ -0,0 +1,336 @@ +# 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 unittest + +import pyproj + +from test.webapi.helpers import get_api_ctx +from test.webapi.helpers import get_server +from xcube.core.gridmapping import GridMapping +from xcube.core.tilingscheme import TilingScheme +from xcube.webapi.ows.wmts.context import WmtsContext +from xcube.webapi.ows.wmts.controllers import ( + get_operations_metadata_element, + get_service_identification_element, + get_service_provider_element, + get_tile_matrix_set_crs84_element, + get_tile_matrix_set_web_mercator_element, + get_wmts_capabilities_xml, + get_crs84_bbox, + WMTS_CRS84_TMS_ID, + WMTS_WEB_MERCATOR_TMS_ID +) + + +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() + self.wmts_ctx = get_api_ctx('ows.wmts', WmtsContext) + + 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() + actual_xml = get_wmts_capabilities_xml(self.wmts_ctx, + 'http://bibo', + tms_id=WMTS_CRS84_TMS_ID) + # Do not delete, useful for debugging + print(80 * '=') + print(actual_xml) + print(80 * '=') + self.assertEqual(expected_xml, actual_xml) + + 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() + actual_xml = get_wmts_capabilities_xml(self.wmts_ctx, + 'http://bibo', + WMTS_WEB_MERCATOR_TMS_ID) + # Do not delete, useful for debugging + print(80 * '=') + print(actual_xml) + print(80 * '=') + self.assertEqual(expected_xml, actual_xml) + + +class WmtsControllerXmlGenTest(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + + server = get_server() + config = dict(server.ctx.config) + config.update( + ServiceProvider=dict( + ProviderName='Bibo', + ServiceContact=dict( + PositionName='Boss', + ContactInfo=dict( + Address=dict( + City='NYC' + ), + ) + ), + ) + ) + server.update(config) + self.wmts_ctx = server.ctx.get_api_ctx('ows.wmts', cls=WmtsContext) + + def test_get_service_provider(self): + element = get_service_provider_element(self.wmts_ctx) + self.assertEqual( + '\n' + ' Bibo\n' + ' \n' + ' \n' + ' \n' + ' Boss\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' NYC\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '', + element.to_xml(indent=2) + ) + + def test_get_operations_metadata(self): + element = get_operations_metadata_element(self.wmts_ctx, + 'https://bibo', + WMTS_WEB_MERCATOR_TMS_ID) + self.assertEqual( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' KVP\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' REST\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' KVP\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' REST\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '', + element.to_xml(indent=2) + ) + + def test_get_tile_matrix_set_crs84_element(self): + tiling_scheme = TilingScheme.GEOGRAPHIC.derive( + min_level=0, + max_level=2 + ) + element = get_tile_matrix_set_crs84_element(tiling_scheme) + self.assertEqual( + '\n' + ' WorldCRS84Quad\n' + ' CRS84 for the World\n' + ' urn:ogc:def:crs:OGC:1.3:CRS84' + '\n' + ' \n' + ' -180 -90\n' + ' 180 90\n' + ' \n' + ' urn:ogc:def:wkss:OGC:1.0:GoogleCRS84Quad' + '\n' + ' \n' + ' 0\n' + ' 279541132.01435894\n' + ' -180 90\n' + ' 256\n' + ' 256\n' + ' 2\n' + ' 1\n' + ' \n' + ' \n' + ' 1\n' + ' 139770566.00717947\n' + ' -180 90\n' + ' 256\n' + ' 256\n' + ' 4\n' + ' 2\n' + ' \n' + ' \n' + ' 2\n' + ' 69885283.00358973\n' + ' -180 90\n' + ' 256\n' + ' 256\n' + ' 8\n' + ' 4\n' + ' \n' + '', + element.to_xml(indent=2) + ) + + def test_get_tile_matrix_set_web_mercator_element(self): + tiling_scheme = TilingScheme.WEB_MERCATOR.derive( + min_level=0, + max_level=2 + ) + element = get_tile_matrix_set_web_mercator_element(tiling_scheme) + self.assertEqual( + ('\n' + ' WorldWebMercatorQuad\n' + ' Google Maps Compatible for the World\n' + ' ' + 'urn:ogc:def:crs:EPSG::3857\n' + ' \n' + ' ' + '-20037508.3427892 -20037508.3427892\n' + ' ' + '20037508.3427892 20037508.3427892\n' + ' \n' + ' ' + '' + 'urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible' + '\n' + ' \n' + ' 0\n' + ' 559082264.0287178\n' + ' ' + '-20037508.3427892 20037508.3427892\n' + ' 256\n' + ' 256\n' + ' 1\n' + ' 1\n' + ' \n' + ' \n' + ' 1\n' + ' 279541132.0143589\n' + ' ' + '-20037508.3427892 20037508.3427892\n' + ' 256\n' + ' 256\n' + ' 2\n' + ' 2\n' + ' \n' + ' \n' + ' 2\n' + ' 139770566.00717944\n' + ' ' + '-20037508.3427892 20037508.3427892\n' + ' 256\n' + ' 256\n' + ' 4\n' + ' 4\n' + ' \n' + ''), + element.to_xml(indent=2) + ) + + def test_service_identification_element(self): + element = get_service_identification_element() + self.assertEqual( + ( + '\n' + ' xcube WMTS\n' + ' Web Map Tile Service (WMTS)' + ' for xcube-conformant data cubes\n' + ' \n' + ' tile\n' + ' tile matrix set\n' + ' map\n' + ' \n' + ' OGC WMTS\n' + ' 1.0.0\n' + ' none\n' + ' none\n' + '' + ), + element.to_xml(indent=2) + ) + + +class WmtsCrs84BboxTest(unittest.TestCase): + def test_get_crs84_bbox_ok(self): + t = pyproj.Transformer.from_crs('EPSG:4326', 'EPSG:3035', + always_xy=True) + p0 = t.transform(10, 50) + + gm = GridMapping.regular((100, 100), + p0, + (1000., 1000.), + pyproj.CRS.from_string('EPSG:3035')) + bbox = get_crs84_bbox(gm) + self.assertIsInstance(bbox, tuple) + self.assertEqual(4, len(bbox)) + self.assertAlmostEqual(10.0, bbox[0]) + self.assertAlmostEqual(49.991462404901604, bbox[1]) + self.assertAlmostEqual(11.421266448415976, bbox[2]) + self.assertAlmostEqual(50.8990377520989, bbox[3]) + + def test_get_crs84_bbox_fail(self): + # construct impossible GridMapping + gm = GridMapping.regular((100, 100), + (100000000, -1000000), + (1000., 1000.), + pyproj.CRS.from_string('EPSG:3035')) + with self.assertRaises(ValueError): + get_crs84_bbox(gm) diff --git a/test/webapi/ows/wcs/test_routes.py b/test/webapi/ows/wcs/test_routes.py new file mode 100644 index 000000000..51eea4883 --- /dev/null +++ b/test/webapi/ows/wcs/test_routes.py @@ -0,0 +1,213 @@ +# The MIT License (MIT) +# Copyright (c) 2022 by the xcube team and contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from ...helpers import RoutesTestCase + + +class WmtsRoutesTest(RoutesTestCase): + + def test_fetch_wmts_kvp_capabilities(self): + response = self.fetch('/wmts/kvp' + '?SERVICE=WMTS' + '&VERSION=1.0.0' + '&REQUEST=GetCapabilities') + self.assertResponseOK(response) + + response = self.fetch('/wmts/kvp' + '?service=WMTS' + '&version=1.0.0' + '&request=GetCapabilities') + self.assertResponseOK(response) + + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.0.0' + '&Request=GetCapabilities') + self.assertResponseOK(response) + + response = self.fetch('/wmts/kvp' + '?VERSION=1.0.0&REQUEST=GetCapabilities') + self.assertBadRequestResponse( + response, + expected_message='value for "service" parameter must be "WMTS"' + ) + + response = self.fetch('/wmts/kvp' + '?SERVICE=WMS' + 'VERSION=1.0.0' + '&REQUEST=GetCapabilities') + self.assertBadRequestResponse( + response, + expected_message='value for "service" parameter must be "WMTS"' + ) + + def test_fetch_wmts_kvp_tile(self): + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.0.0' + '&Request=GetTile' + '&Format=image/png' + '&Style=Default' + '&Layer=demo.conc_chl' + '&TileMatrixSet=WorldCRS84Quad' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0') + self.assertResponseOK(response) + + # issue #132 by Dirk + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.0.0' + '&Request=GetTile' + '&Format=image/png' + '&Style=Default' + '&Layer=demo.conc_chl' + '&TileMatrixSet=WorldWebMercatorQuad' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0' + '&Time=2017-01-25T09%3A35%3A50') + self.assertResponseOK(response) + + # issue #132 by Dirk + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.0.0' + '&Request=GetTile' + '&Format=image/png' + '&Style=Default' + '&Layer=demo.conc_chl' + '&TileMatrixSet=WorldWebMercatorQuad' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0' + '&Time=2017-01-25T09%3A35%3A50%2F2017-01-25T10%3A20%3A15') + self.assertResponseOK(response) + + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.0.0' + '&Request=GetTile' + '&Format=image/jpg' + '&Style=Default' + '&Layer=demo.conc_chl' + '&TileMatrixSet=WorldCRS84Quad' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0') + self.assertBadRequestResponse( + response, + 'value for "format" parameter must be "image/png"' + ) + + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.1.0' + '&Request=GetTile' + '&Format=image/png' + '&Style=Default' + '&Layer=demo.conc_chl' + '&TileMatrixSet=WorldCRS84Quad' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0') + self.assertBadRequestResponse( + response, + 'value for "version" parameter must be "1.0.0"' + ) + + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Request=GetTile' + '&Version=1.0.0' + '&Format=image/png' + '&Style=Default' + '&Layer=conc_chl' + '&TileMatrixSet=WorldCRS84Quad' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0') + self.assertBadRequestResponse( + response, + 'value for "layer" parameter must be "."' + ) + + response = self.fetch('/wmts/kvp' + '?Service=WMTS' + '&Version=1.0.0' + '&Request=GetTile' + '&Format=image/png' + '&Style=Default' + '&Layer=demo.conc_chl' + '&TileMatrixSet=TileGrid_2000_1000' + '&TileMatrix=0' + '&TileRow=0' + '&TileCol=0') + self.assertBadRequestResponse( + response, + 'value for "tilematrixset" parameter must' + ' be one of (\'WorldCRS84Quad\', \'WorldWebMercatorQuad\')' + ) + + def test_fetch_wmts_capabilities(self): + response = self.fetch( + '/wmts/1.0.0/WMTSCapabilities.xml') + self.assertResponseOK(response) + + def test_fetch_wmts_tile(self): + response = self.fetch( + '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' + ) + self.assertResponseOK(response) + + def test_fetch_wmts_tile_geo(self): + response = self.fetch( + '/wmts/1.0.0/tile/demo/conc_chl/WorldCRS84Quad/0/0/0.png' + ) + self.assertResponseOK(response) + + def test_fetch_wmts_tile_mercator(self): + response = self.fetch( + '/wmts/1.0.0/tile/demo/conc_chl/WorldWebMercatorQuad/0/0/0.png' + ) + self.assertResponseOK(response) + + def test_fetch_wmts_tile_with_params(self): + response = self.fetch( + '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' + '?time=current&cbar=jet' + ) + self.assertResponseOK(response) + + def test_fetch_wmts_tile_with_params_geo(self): + response = self.fetch( + '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' + '?time=current&cbar=jet&TileMatrixSet=WorldCRS84Quad' + ) + self.assertResponseOK(response) + + def test_fetch_wmts_tile_with_params_mercator(self): + response = self.fetch( + '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' + '?time=current&cbar=jet&TileMatrixSet=WorldWebMercatorQuad' + ) + self.assertResponseOK(response) From f54a4d3f6672c80cbc67266594d09c9b82d959d5 Mon Sep 17 00:00:00 2001 From: Tonio Fincke Date: Thu, 28 Jul 2022 15:23:15 +0200 Subject: [PATCH 02/43] limit werkzeug version (cherry picked from commit 55c433a549fc771b758a678bb3cc2ae094fa691f) --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 6abbd5135..9ce35018d 100644 --- a/environment.yml +++ b/environment.yml @@ -41,6 +41,7 @@ dependencies: - shapely >=1.6 - tornado >=6.0 - urllib3 >=1.26 + - werkzeug < 2.2 - xarray >=0.19 - zarr >=2.8 # Required by Coiled From 26e5a3b17739ebd18e0d38d782ef0cb2aa4355b5 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 5 Aug 2022 15:08:50 +0200 Subject: [PATCH 03/43] extracted ows test utility method --- .../ows/res/WCSCapabilities_minimum.xml | 55 ++ .../{wcs => }/res/WMTSCapabilities-CRS84.xml | 0 .../{wcs => }/res/WMTSCapabilities-OSM.xml | 0 test/webapi/ows/test_utils.py | 26 + test/webapi/ows/wcs/test_controller.py | 336 --------- .../ows/wmts/res/WMTSCapabilities-CRS84.xml | 637 ---------------- .../ows/wmts/res/WMTSCapabilities-OSM.xml | 637 ---------------- test/webapi/ows/wmts/test_controller.py | 7 +- xcube/webapi/ows/wcs/res/wcsCapabilities.xsd | 698 ++++++++++++++++++ 9 files changed, 780 insertions(+), 1616 deletions(-) create mode 100644 test/webapi/ows/res/WCSCapabilities_minimum.xml rename test/webapi/ows/{wcs => }/res/WMTSCapabilities-CRS84.xml (100%) rename test/webapi/ows/{wcs => }/res/WMTSCapabilities-OSM.xml (100%) create mode 100644 test/webapi/ows/test_utils.py delete mode 100644 test/webapi/ows/wcs/test_controller.py delete mode 100644 test/webapi/ows/wmts/res/WMTSCapabilities-CRS84.xml delete mode 100644 test/webapi/ows/wmts/res/WMTSCapabilities-OSM.xml create mode 100644 xcube/webapi/ows/wcs/res/wcsCapabilities.xsd diff --git a/test/webapi/ows/res/WCSCapabilities_minimum.xml b/test/webapi/ows/res/WCSCapabilities_minimum.xml new file mode 100644 index 000000000..1b48bcb54 --- /dev/null +++ b/test/webapi/ows/res/WCSCapabilities_minimum.xml @@ -0,0 +1,55 @@ + + + + + xcube WCS server + xcube-WCS + xcube-WCS + + + NONE + NONE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + application/x-ogc-wcs + + + + + + \ No newline at end of file diff --git a/test/webapi/ows/wcs/res/WMTSCapabilities-CRS84.xml b/test/webapi/ows/res/WMTSCapabilities-CRS84.xml similarity index 100% rename from test/webapi/ows/wcs/res/WMTSCapabilities-CRS84.xml rename to test/webapi/ows/res/WMTSCapabilities-CRS84.xml diff --git a/test/webapi/ows/wcs/res/WMTSCapabilities-OSM.xml b/test/webapi/ows/res/WMTSCapabilities-OSM.xml similarity index 100% rename from test/webapi/ows/wcs/res/WMTSCapabilities-OSM.xml rename to test/webapi/ows/res/WMTSCapabilities-OSM.xml diff --git a/test/webapi/ows/test_utils.py b/test/webapi/ows/test_utils.py new file mode 100644 index 000000000..809788447 --- /dev/null +++ b/test/webapi/ows/test_utils.py @@ -0,0 +1,26 @@ +# 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 os + + +def get_test_res_path(path: str) -> str: + return os.path.normpath(os.path.join(os.path.dirname(__file__), + 'res', path)) diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py deleted file mode 100644 index 31f1a31ce..000000000 --- a/test/webapi/ows/wcs/test_controller.py +++ /dev/null @@ -1,336 +0,0 @@ -# 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 unittest - -import pyproj - -from test.webapi.helpers import get_api_ctx -from test.webapi.helpers import get_server -from xcube.core.gridmapping import GridMapping -from xcube.core.tilingscheme import TilingScheme -from xcube.webapi.ows.wmts.context import WmtsContext -from xcube.webapi.ows.wmts.controllers import ( - get_operations_metadata_element, - get_service_identification_element, - get_service_provider_element, - get_tile_matrix_set_crs84_element, - get_tile_matrix_set_web_mercator_element, - get_wmts_capabilities_xml, - get_crs84_bbox, - WMTS_CRS84_TMS_ID, - WMTS_WEB_MERCATOR_TMS_ID -) - - -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() - self.wmts_ctx = get_api_ctx('ows.wmts', WmtsContext) - - 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() - actual_xml = get_wmts_capabilities_xml(self.wmts_ctx, - 'http://bibo', - tms_id=WMTS_CRS84_TMS_ID) - # Do not delete, useful for debugging - print(80 * '=') - print(actual_xml) - print(80 * '=') - self.assertEqual(expected_xml, actual_xml) - - 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() - actual_xml = get_wmts_capabilities_xml(self.wmts_ctx, - 'http://bibo', - WMTS_WEB_MERCATOR_TMS_ID) - # Do not delete, useful for debugging - print(80 * '=') - print(actual_xml) - print(80 * '=') - self.assertEqual(expected_xml, actual_xml) - - -class WmtsControllerXmlGenTest(unittest.TestCase): - def setUp(self) -> None: - super().setUp() - - server = get_server() - config = dict(server.ctx.config) - config.update( - ServiceProvider=dict( - ProviderName='Bibo', - ServiceContact=dict( - PositionName='Boss', - ContactInfo=dict( - Address=dict( - City='NYC' - ), - ) - ), - ) - ) - server.update(config) - self.wmts_ctx = server.ctx.get_api_ctx('ows.wmts', cls=WmtsContext) - - def test_get_service_provider(self): - element = get_service_provider_element(self.wmts_ctx) - self.assertEqual( - '\n' - ' Bibo\n' - ' \n' - ' \n' - ' \n' - ' Boss\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' NYC\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - '', - element.to_xml(indent=2) - ) - - def test_get_operations_metadata(self): - element = get_operations_metadata_element(self.wmts_ctx, - 'https://bibo', - WMTS_WEB_MERCATOR_TMS_ID) - self.assertEqual( - '\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' KVP\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' REST\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' KVP\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' REST\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - '', - element.to_xml(indent=2) - ) - - def test_get_tile_matrix_set_crs84_element(self): - tiling_scheme = TilingScheme.GEOGRAPHIC.derive( - min_level=0, - max_level=2 - ) - element = get_tile_matrix_set_crs84_element(tiling_scheme) - self.assertEqual( - '\n' - ' WorldCRS84Quad\n' - ' CRS84 for the World\n' - ' urn:ogc:def:crs:OGC:1.3:CRS84' - '\n' - ' \n' - ' -180 -90\n' - ' 180 90\n' - ' \n' - ' urn:ogc:def:wkss:OGC:1.0:GoogleCRS84Quad' - '\n' - ' \n' - ' 0\n' - ' 279541132.01435894\n' - ' -180 90\n' - ' 256\n' - ' 256\n' - ' 2\n' - ' 1\n' - ' \n' - ' \n' - ' 1\n' - ' 139770566.00717947\n' - ' -180 90\n' - ' 256\n' - ' 256\n' - ' 4\n' - ' 2\n' - ' \n' - ' \n' - ' 2\n' - ' 69885283.00358973\n' - ' -180 90\n' - ' 256\n' - ' 256\n' - ' 8\n' - ' 4\n' - ' \n' - '', - element.to_xml(indent=2) - ) - - def test_get_tile_matrix_set_web_mercator_element(self): - tiling_scheme = TilingScheme.WEB_MERCATOR.derive( - min_level=0, - max_level=2 - ) - element = get_tile_matrix_set_web_mercator_element(tiling_scheme) - self.assertEqual( - ('\n' - ' WorldWebMercatorQuad\n' - ' Google Maps Compatible for the World\n' - ' ' - 'urn:ogc:def:crs:EPSG::3857\n' - ' \n' - ' ' - '-20037508.3427892 -20037508.3427892\n' - ' ' - '20037508.3427892 20037508.3427892\n' - ' \n' - ' ' - '' - 'urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible' - '\n' - ' \n' - ' 0\n' - ' 559082264.0287178\n' - ' ' - '-20037508.3427892 20037508.3427892\n' - ' 256\n' - ' 256\n' - ' 1\n' - ' 1\n' - ' \n' - ' \n' - ' 1\n' - ' 279541132.0143589\n' - ' ' - '-20037508.3427892 20037508.3427892\n' - ' 256\n' - ' 256\n' - ' 2\n' - ' 2\n' - ' \n' - ' \n' - ' 2\n' - ' 139770566.00717944\n' - ' ' - '-20037508.3427892 20037508.3427892\n' - ' 256\n' - ' 256\n' - ' 4\n' - ' 4\n' - ' \n' - ''), - element.to_xml(indent=2) - ) - - def test_service_identification_element(self): - element = get_service_identification_element() - self.assertEqual( - ( - '\n' - ' xcube WMTS\n' - ' Web Map Tile Service (WMTS)' - ' for xcube-conformant data cubes\n' - ' \n' - ' tile\n' - ' tile matrix set\n' - ' map\n' - ' \n' - ' OGC WMTS\n' - ' 1.0.0\n' - ' none\n' - ' none\n' - '' - ), - element.to_xml(indent=2) - ) - - -class WmtsCrs84BboxTest(unittest.TestCase): - def test_get_crs84_bbox_ok(self): - t = pyproj.Transformer.from_crs('EPSG:4326', 'EPSG:3035', - always_xy=True) - p0 = t.transform(10, 50) - - gm = GridMapping.regular((100, 100), - p0, - (1000., 1000.), - pyproj.CRS.from_string('EPSG:3035')) - bbox = get_crs84_bbox(gm) - self.assertIsInstance(bbox, tuple) - self.assertEqual(4, len(bbox)) - self.assertAlmostEqual(10.0, bbox[0]) - self.assertAlmostEqual(49.991462404901604, bbox[1]) - self.assertAlmostEqual(11.421266448415976, bbox[2]) - self.assertAlmostEqual(50.8990377520989, bbox[3]) - - def test_get_crs84_bbox_fail(self): - # construct impossible GridMapping - gm = GridMapping.regular((100, 100), - (100000000, -1000000), - (1000., 1000.), - pyproj.CRS.from_string('EPSG:3035')) - with self.assertRaises(ValueError): - get_crs84_bbox(gm) diff --git a/test/webapi/ows/wmts/res/WMTSCapabilities-CRS84.xml b/test/webapi/ows/wmts/res/WMTSCapabilities-CRS84.xml deleted file mode 100644 index c143a5732..000000000 --- a/test/webapi/ows/wmts/res/WMTSCapabilities-CRS84.xml +++ /dev/null @@ -1,637 +0,0 @@ - - - - xcube WMTS - Web Map Tile Service (WMTS) for xcube-conformant data cubes - - tile - tile matrix set - map - - OGC WMTS - 1.0.0 - none - none - - - Brockmann Consult GmbH - - - Norman Fomferra - Senior Software Engineer - - - +49 4152 889 303 - +49 4152 889 330 - - - HZG / GITZ - Geesthacht - Herzogtum Lauenburg - 21502 - Germany - norman.fomferra@brockmann-consult.de - - - - - - - - - - - - KVP - - - - - - - REST - - - - - - - - - - - - - KVP - - - - - - - REST - - - - - - - - - - demo.c2rcc_flags - demo/C2RCC quality flags - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.conc_chl - demo/Chlorophyll concentration - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.conc_tsm - demo/Total suspended matter dry weight concentration - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.kd489 - demo/Irradiance attenuation coefficient at 489 nm - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.quality_flags - demo/Classification and quality flags - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo-1w.c2rcc_flags - demo-1w/c2rcc_flags - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.c2rcc_flags_stdev - demo-1w/c2rcc_flags_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_chl - demo-1w/conc_chl - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_chl_stdev - demo-1w/conc_chl_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_tsm - demo-1w/conc_tsm - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_tsm_stdev - demo-1w/conc_tsm_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.kd489 - demo-1w/kd489 - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.kd489_stdev - demo-1w/kd489_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.quality_flags - demo-1w/quality_flags - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.quality_flags_stdev - demo-1w/quality_flags_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldCRS84Quad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - WorldCRS84Quad - CRS84 for the World - urn:ogc:def:crs:OGC:1.3:CRS84 - - -180 -90 - 180 90 - - urn:ogc:def:wkss:OGC:1.0:GoogleCRS84Quad - - 7 - 2183915.093862179 - -180 90 - 256 - 256 - 256 - 128 - - - 8 - 1091957.5469310896 - -180 90 - 256 - 256 - 512 - 256 - - - 9 - 545978.7734655448 - -180 90 - 256 - 256 - 1024 - 512 - - - - - - demo - xcube-server Demonstration L2C Cube - - - demo.c2rcc_flags - demo/C2RCC quality flags - - demo.c2rcc_flags - - - demo.conc_chl - demo/Chlorophyll concentration - - demo.conc_chl - - - demo.conc_tsm - demo/Total suspended matter dry weight concentration - - demo.conc_tsm - - - demo.kd489 - demo/Irradiance attenuation coefficient at 489 nm - - demo.kd489 - - - demo.quality_flags - demo/Classification and quality flags - - demo.quality_flags - - - - demo-1w - xcube-server Demonstration L3 Cube - - - demo-1w.c2rcc_flags - demo-1w/c2rcc_flags - - demo-1w.c2rcc_flags - - - demo-1w.c2rcc_flags_stdev - demo-1w/c2rcc_flags_stdev - - demo-1w.c2rcc_flags_stdev - - - demo-1w.conc_chl - demo-1w/conc_chl - - demo-1w.conc_chl - - - demo-1w.conc_chl_stdev - demo-1w/conc_chl_stdev - - demo-1w.conc_chl_stdev - - - demo-1w.conc_tsm - demo-1w/conc_tsm - - demo-1w.conc_tsm - - - demo-1w.conc_tsm_stdev - demo-1w/conc_tsm_stdev - - demo-1w.conc_tsm_stdev - - - demo-1w.kd489 - demo-1w/kd489 - - demo-1w.kd489 - - - demo-1w.kd489_stdev - demo-1w/kd489_stdev - - demo-1w.kd489_stdev - - - demo-1w.quality_flags - demo-1w/quality_flags - - demo-1w.quality_flags - - - demo-1w.quality_flags_stdev - demo-1w/quality_flags_stdev - - demo-1w.quality_flags_stdev - - - - - diff --git a/test/webapi/ows/wmts/res/WMTSCapabilities-OSM.xml b/test/webapi/ows/wmts/res/WMTSCapabilities-OSM.xml deleted file mode 100644 index 4acdd925f..000000000 --- a/test/webapi/ows/wmts/res/WMTSCapabilities-OSM.xml +++ /dev/null @@ -1,637 +0,0 @@ - - - - xcube WMTS - Web Map Tile Service (WMTS) for xcube-conformant data cubes - - tile - tile matrix set - map - - OGC WMTS - 1.0.0 - none - none - - - Brockmann Consult GmbH - - - Norman Fomferra - Senior Software Engineer - - - +49 4152 889 303 - +49 4152 889 330 - - - HZG / GITZ - Geesthacht - Herzogtum Lauenburg - 21502 - Germany - norman.fomferra@brockmann-consult.de - - - - - - - - - - - - KVP - - - - - - - REST - - - - - - - - - - - - - KVP - - - - - - - REST - - - - - - - - - - demo.c2rcc_flags - demo/C2RCC quality flags - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.conc_chl - demo/Chlorophyll concentration - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.conc_tsm - demo/Total suspended matter dry weight concentration - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.kd489 - demo/Irradiance attenuation coefficient at 489 nm - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo.quality_flags - demo/Classification and quality flags - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-16T10:09:05.396602624/2017-01-16T10:09:38.271909120 - 2017-01-25T09:35:32.619965696/2017-01-25T09:36:09.500161024 - 2017-01-26T10:50:13.978930176/2017-01-26T10:50:19.393455616 - 2017-01-28T09:57:52.162121984/2017-01-28T09:58:30.538650112 - 2017-01-30T10:46:29.566899712/2017-01-30T10:46:38.106885120 - - - - demo-1w.c2rcc_flags - demo-1w/c2rcc_flags - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.c2rcc_flags_stdev - demo-1w/c2rcc_flags_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_chl - demo-1w/conc_chl - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_chl_stdev - demo-1w/conc_chl_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_tsm - demo-1w/conc_tsm - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.conc_tsm_stdev - demo-1w/conc_tsm_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.kd489 - demo-1w/kd489 - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.kd489_stdev - demo-1w/kd489_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.quality_flags - demo-1w/quality_flags - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - demo-1w.quality_flags_stdev - demo-1w/quality_flags_stdev - - - 0 50 - 5 52.5 - - - image/png - - WorldWebMercatorQuad - - - - time - time - ISO8601 - current - true - 2017-01-22T00:00:00.000000000 - 2017-01-29T00:00:00.000000000 - 2017-02-05T00:00:00.000000000 - - - - WorldWebMercatorQuad - Google Maps Compatible for the World - urn:ogc:def:crs:EPSG::3857 - - -20037508.3427892 -20037508.3427892 - 20037508.3427892 20037508.3427892 - - urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible - - 8 - 2183915.0938621787 - -20037508.3427892 20037508.3427892 - 256 - 256 - 256 - 256 - - - 9 - 1091957.5469310894 - -20037508.3427892 20037508.3427892 - 256 - 256 - 512 - 512 - - - 10 - 545978.7734655447 - -20037508.3427892 20037508.3427892 - 256 - 256 - 1024 - 1024 - - - - - - demo - xcube-server Demonstration L2C Cube - - - demo.c2rcc_flags - demo/C2RCC quality flags - - demo.c2rcc_flags - - - demo.conc_chl - demo/Chlorophyll concentration - - demo.conc_chl - - - demo.conc_tsm - demo/Total suspended matter dry weight concentration - - demo.conc_tsm - - - demo.kd489 - demo/Irradiance attenuation coefficient at 489 nm - - demo.kd489 - - - demo.quality_flags - demo/Classification and quality flags - - demo.quality_flags - - - - demo-1w - xcube-server Demonstration L3 Cube - - - demo-1w.c2rcc_flags - demo-1w/c2rcc_flags - - demo-1w.c2rcc_flags - - - demo-1w.c2rcc_flags_stdev - demo-1w/c2rcc_flags_stdev - - demo-1w.c2rcc_flags_stdev - - - demo-1w.conc_chl - demo-1w/conc_chl - - demo-1w.conc_chl - - - demo-1w.conc_chl_stdev - demo-1w/conc_chl_stdev - - demo-1w.conc_chl_stdev - - - demo-1w.conc_tsm - demo-1w/conc_tsm - - demo-1w.conc_tsm - - - demo-1w.conc_tsm_stdev - demo-1w/conc_tsm_stdev - - demo-1w.conc_tsm_stdev - - - demo-1w.kd489 - demo-1w/kd489 - - demo-1w.kd489 - - - demo-1w.kd489_stdev - demo-1w/kd489_stdev - - demo-1w.kd489_stdev - - - demo-1w.quality_flags - demo-1w/quality_flags - - demo-1w.quality_flags - - - demo-1w.quality_flags_stdev - demo-1w/quality_flags_stdev - - demo-1w.quality_flags_stdev - - - - - diff --git a/test/webapi/ows/wmts/test_controller.py b/test/webapi/ows/wmts/test_controller.py index 31f1a31ce..31e8f6a01 100644 --- a/test/webapi/ows/wmts/test_controller.py +++ b/test/webapi/ows/wmts/test_controller.py @@ -19,13 +19,13 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -import os import unittest import pyproj from test.webapi.helpers import get_api_ctx from test.webapi.helpers import get_server +from test.webapi.ows.test_utils import get_test_res_path from xcube.core.gridmapping import GridMapping from xcube.core.tilingscheme import TilingScheme from xcube.webapi.ows.wmts.context import WmtsContext @@ -42,11 +42,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() 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. + + + + + From 3d4af0b1419835ffde1373c2cdb7a38273099869 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 5 Aug 2022 15:41:13 +0200 Subject: [PATCH 04/43] running first WCS test --- environment.yml | 1 + examples/serve/demo/config-serve2.yml | 20 ++- test/webapi/ows/res/__init__.py | 20 +++ test/webapi/ows/wcs/test_routes.py | 213 ------------------------ test/webapi/ows/wcs/test_validation.py | 19 +++ test/webapi/ows/wmts/test_controller.py | 11 +- xcube/webapi/ows/wcs/__init__.py | 22 +++ xcube/webapi/ows/wcs/api.py | 30 ++++ xcube/webapi/ows/wcs/context.py | 45 +++++ xcube/webapi/ows/wcs/res/__init__.py | 20 +++ xcube/webapi/ows/wcs/routes.py | 32 ++++ 11 files changed, 209 insertions(+), 224 deletions(-) create mode 100644 test/webapi/ows/res/__init__.py delete mode 100644 test/webapi/ows/wcs/test_routes.py create mode 100644 test/webapi/ows/wcs/test_validation.py create mode 100644 xcube/webapi/ows/wcs/__init__.py create mode 100644 xcube/webapi/ows/wcs/api.py create mode 100644 xcube/webapi/ows/wcs/context.py create mode 100644 xcube/webapi/ows/wcs/res/__init__.py create mode 100644 xcube/webapi/ows/wcs/routes.py diff --git a/environment.yml b/environment.yml index 9ce35018d..91f808806 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-serve2.yml b/examples/serve/demo/config-serve2.yml index 65d3a9590..dd93563df 100644 --- a/examples/serve/demo/config-serve2.yml +++ b/examples/serve/demo/config-serve2.yml @@ -1,13 +1,21 @@ dataset_cache_size: 100M -data_stores: - - store_id: file - store_params: +DataStores: + - Identifier: test + StoreId: file + StoreParams: root: examples/serve/demo - datasets: - - data_id: cube-1-250-250.levels - - data_id: cube-5-100-200.zarr + Datasets: + - Path: "cube-1-250-250.zarr" + Identifier: "Cube 1" + Style: "default" + - Path: "cube-5-100-200.zarr" + Identifier: "Cube 5" + Style: "default" + - Path: "*.levels" + Style: "default" + # - store_id: s3 # store_params: # root: xcube-examples 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/wcs/test_routes.py b/test/webapi/ows/wcs/test_routes.py deleted file mode 100644 index 51eea4883..000000000 --- a/test/webapi/ows/wcs/test_routes.py +++ /dev/null @@ -1,213 +0,0 @@ -# The MIT License (MIT) -# Copyright (c) 2022 by the xcube team and contributors -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from ...helpers import RoutesTestCase - - -class WmtsRoutesTest(RoutesTestCase): - - def test_fetch_wmts_kvp_capabilities(self): - response = self.fetch('/wmts/kvp' - '?SERVICE=WMTS' - '&VERSION=1.0.0' - '&REQUEST=GetCapabilities') - self.assertResponseOK(response) - - response = self.fetch('/wmts/kvp' - '?service=WMTS' - '&version=1.0.0' - '&request=GetCapabilities') - self.assertResponseOK(response) - - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.0.0' - '&Request=GetCapabilities') - self.assertResponseOK(response) - - response = self.fetch('/wmts/kvp' - '?VERSION=1.0.0&REQUEST=GetCapabilities') - self.assertBadRequestResponse( - response, - expected_message='value for "service" parameter must be "WMTS"' - ) - - response = self.fetch('/wmts/kvp' - '?SERVICE=WMS' - 'VERSION=1.0.0' - '&REQUEST=GetCapabilities') - self.assertBadRequestResponse( - response, - expected_message='value for "service" parameter must be "WMTS"' - ) - - def test_fetch_wmts_kvp_tile(self): - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.0.0' - '&Request=GetTile' - '&Format=image/png' - '&Style=Default' - '&Layer=demo.conc_chl' - '&TileMatrixSet=WorldCRS84Quad' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0') - self.assertResponseOK(response) - - # issue #132 by Dirk - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.0.0' - '&Request=GetTile' - '&Format=image/png' - '&Style=Default' - '&Layer=demo.conc_chl' - '&TileMatrixSet=WorldWebMercatorQuad' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0' - '&Time=2017-01-25T09%3A35%3A50') - self.assertResponseOK(response) - - # issue #132 by Dirk - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.0.0' - '&Request=GetTile' - '&Format=image/png' - '&Style=Default' - '&Layer=demo.conc_chl' - '&TileMatrixSet=WorldWebMercatorQuad' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0' - '&Time=2017-01-25T09%3A35%3A50%2F2017-01-25T10%3A20%3A15') - self.assertResponseOK(response) - - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.0.0' - '&Request=GetTile' - '&Format=image/jpg' - '&Style=Default' - '&Layer=demo.conc_chl' - '&TileMatrixSet=WorldCRS84Quad' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0') - self.assertBadRequestResponse( - response, - 'value for "format" parameter must be "image/png"' - ) - - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.1.0' - '&Request=GetTile' - '&Format=image/png' - '&Style=Default' - '&Layer=demo.conc_chl' - '&TileMatrixSet=WorldCRS84Quad' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0') - self.assertBadRequestResponse( - response, - 'value for "version" parameter must be "1.0.0"' - ) - - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Request=GetTile' - '&Version=1.0.0' - '&Format=image/png' - '&Style=Default' - '&Layer=conc_chl' - '&TileMatrixSet=WorldCRS84Quad' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0') - self.assertBadRequestResponse( - response, - 'value for "layer" parameter must be "."' - ) - - response = self.fetch('/wmts/kvp' - '?Service=WMTS' - '&Version=1.0.0' - '&Request=GetTile' - '&Format=image/png' - '&Style=Default' - '&Layer=demo.conc_chl' - '&TileMatrixSet=TileGrid_2000_1000' - '&TileMatrix=0' - '&TileRow=0' - '&TileCol=0') - self.assertBadRequestResponse( - response, - 'value for "tilematrixset" parameter must' - ' be one of (\'WorldCRS84Quad\', \'WorldWebMercatorQuad\')' - ) - - def test_fetch_wmts_capabilities(self): - response = self.fetch( - '/wmts/1.0.0/WMTSCapabilities.xml') - self.assertResponseOK(response) - - def test_fetch_wmts_tile(self): - response = self.fetch( - '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' - ) - self.assertResponseOK(response) - - def test_fetch_wmts_tile_geo(self): - response = self.fetch( - '/wmts/1.0.0/tile/demo/conc_chl/WorldCRS84Quad/0/0/0.png' - ) - self.assertResponseOK(response) - - def test_fetch_wmts_tile_mercator(self): - response = self.fetch( - '/wmts/1.0.0/tile/demo/conc_chl/WorldWebMercatorQuad/0/0/0.png' - ) - self.assertResponseOK(response) - - def test_fetch_wmts_tile_with_params(self): - response = self.fetch( - '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' - '?time=current&cbar=jet' - ) - self.assertResponseOK(response) - - def test_fetch_wmts_tile_with_params_geo(self): - response = self.fetch( - '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' - '?time=current&cbar=jet&TileMatrixSet=WorldCRS84Quad' - ) - self.assertResponseOK(response) - - def test_fetch_wmts_tile_with_params_mercator(self): - response = self.fetch( - '/wmts/1.0.0/tile/demo/conc_chl/0/0/0.png' - '?time=current&cbar=jet&TileMatrixSet=WorldWebMercatorQuad' - ) - self.assertResponseOK(response) diff --git a/test/webapi/ows/wcs/test_validation.py b/test/webapi/ows/wcs/test_validation.py new file mode 100644 index 000000000..958983a25 --- /dev/null +++ b/test/webapi/ows/wcs/test_validation.py @@ -0,0 +1,19 @@ +import unittest + +from lxml import etree + +from xcube.webapi.ows.wcs import res +from test.webapi.ows import res as test_res +import importlib.resources as resources + + +class ValidationTest(unittest.TestCase): + + # noinspection PyMethodMayBeStatic + def test_validate_minimum(self): + xml_text = resources.read_text(test_res, 'WCSCapabilities_minimum.xml') + schema_text = resources.read_text(res, 'wcsCapabilities.xsd') + + xml = etree.fromstring(xml_text) + xsd = etree.XMLSchema(etree.fromstring(schema_text)) + xsd.assertValid(xml) diff --git a/test/webapi/ows/wmts/test_controller.py b/test/webapi/ows/wmts/test_controller.py index 31e8f6a01..c3a457c7c 100644 --- a/test/webapi/ows/wmts/test_controller.py +++ b/test/webapi/ows/wmts/test_controller.py @@ -20,12 +20,13 @@ # DEALINGS IN THE SOFTWARE. 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.test_utils import get_test_res_path +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 @@ -49,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) @@ -62,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/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..3e3a19272 --- /dev/null +++ b/xcube/webapi/ows/wcs/api.py @@ -0,0 +1,30 @@ +# 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 .context import WcsContext + +api = Api( + 'ows.wcs', + description='xcube OGC WCS API', + required_apis=['tiles', 'datasets'], + create_ctx=WcsContext +) diff --git a/xcube/webapi/ows/wcs/context.py b/xcube/webapi/ows/wcs/context.py new file mode 100644 index 000000000..64ce2ab63 --- /dev/null +++ b/xcube/webapi/ows/wcs/context.py @@ -0,0 +1,45 @@ +# 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.resctx import ResourcesContext +from ...datasets.context import DatasetsContext +from ...tiles.context import TilesContext + + +class WcsContext(ResourcesContext): + _feature_index: int = 0 + + def __init__(self, server_ctx: Context): + super().__init__(server_ctx) + self._tiles_ctx = server_ctx.get_api_ctx('tiles', + cls=TilesContext) + self._datasets_ctx = server_ctx.get_api_ctx('datasets', + cls=DatasetsContext) + + @property + def tiles_ctx(self) -> TilesContext: + return self._tiles_ctx + + @property + def datasets_ctx(self) -> DatasetsContext: + return self._datasets_ctx 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/routes.py b/xcube/webapi/ows/wcs/routes.py new file mode 100644 index 000000000..20f86006d --- /dev/null +++ b/xcube/webapi/ows/wcs/routes.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 ApiHandler +from .api import api +from .context import WcsContext + + +@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): + pass From 7987ecb41dbd397b457dbcacb101c8ccea6e7c92 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 6 Aug 2022 00:10:45 +0200 Subject: [PATCH 05/43] implemented get capabilities within controller --- test/webapi/ows/res/WCSCapabilities.xml | 196 ++++++++++++++ .../ows/res/WCSCapabilities_minimum.xml | 55 ---- test/webapi/ows/test_utils.py | 26 -- test/webapi/ows/wcs/test_controller.py | 51 ++++ test/webapi/ows/wcs/test_validation.py | 19 -- test/webapi/res/test/config.yml | 5 +- xcube/plugin.py | 1 + xcube/webapi/ows/wcs/controllers.py | 240 ++++++++++++++++++ 8 files changed, 492 insertions(+), 101 deletions(-) create mode 100644 test/webapi/ows/res/WCSCapabilities.xml delete mode 100644 test/webapi/ows/res/WCSCapabilities_minimum.xml delete mode 100644 test/webapi/ows/test_utils.py create mode 100644 test/webapi/ows/wcs/test_controller.py delete mode 100644 test/webapi/ows/wcs/test_validation.py create mode 100644 xcube/webapi/ows/wcs/controllers.py diff --git a/test/webapi/ows/res/WCSCapabilities.xml b/test/webapi/ows/res/WCSCapabilities.xml new file mode 100644 index 000000000..b5d105f8b --- /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 + + + + + c2rcc_flags + + + 0 50 + 5 52.5 + + + + conc_chl + + + 0 50 + 5 52.5 + + + + conc_tsm + + + 0 50 + 5 52.5 + + + + kd489 + + + 0 50 + 5 52.5 + + + + quality_flags + + + 0 50 + 5 52.5 + + + + c2rcc_flags + + + 0 50 + 5 52.5 + + + + c2rcc_flags_stdev + + + 0 50 + 5 52.5 + + + + conc_chl + + + 0 50 + 5 52.5 + + + + conc_chl_stdev + + + 0 50 + 5 52.5 + + + + conc_tsm + + + 0 50 + 5 52.5 + + + + conc_tsm_stdev + + + 0 50 + 5 52.5 + + + + kd489 + + + 0 50 + 5 52.5 + + + + kd489_stdev + + + 0 50 + 5 52.5 + + + + quality_flags + + + 0 50 + 5 52.5 + + + + quality_flags_stdev + + + 0 50 + 5 52.5 + + + +
\ No newline at end of file diff --git a/test/webapi/ows/res/WCSCapabilities_minimum.xml b/test/webapi/ows/res/WCSCapabilities_minimum.xml deleted file mode 100644 index 1b48bcb54..000000000 --- a/test/webapi/ows/res/WCSCapabilities_minimum.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - xcube WCS server - xcube-WCS - xcube-WCS - - - NONE - NONE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - application/x-ogc-wcs - - - - - - \ No newline at end of file diff --git a/test/webapi/ows/test_utils.py b/test/webapi/ows/test_utils.py deleted file mode 100644 index 809788447..000000000 --- a/test/webapi/ows/test_utils.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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 os - - -def get_test_res_path(path: str) -> str: - return os.path.normpath(os.path.join(os.path.dirname(__file__), - 'res', path)) diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py new file mode 100644 index 000000000..bfdf538a0 --- /dev/null +++ b/test/webapi/ows/wcs/test_controller.py @@ -0,0 +1,51 @@ +import unittest +from importlib import resources as resources + +from lxml import etree +import xml.etree.ElementTree as ElementTree + +from test.webapi.helpers import get_api_ctx +from test.webapi.ows import res as test_res +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 + + +class ControllerTest(unittest.TestCase): + + def setUp(self) -> None: + super().setUp() + self.wcs_ctx = get_api_ctx('ows.wcs', WcsContext) + + # noinspection PyMethodMayBeStatic + def test_get_capabilities(self): + expected_xml = resources.read_text(test_res, 'WCSCapabilities.xml') + actual_xml = get_wcs_capabilities_xml( + self.wcs_ctx, 'https://xcube.brockmann-consult.de/wcs/kvp' + ) + + self.maxDiff = None + # Do not delete, useful for debugging + print(80 * '=') + print(actual_xml) + print(80 * '=') + + actual_xml = self.strip_whitespace(actual_xml) + expected_xml = self.strip_whitespace(expected_xml) + + 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 test_how_to_validate(self): + xml_text = resources.read_text(test_res, 'WCSCapabilities.xml') + xml = etree.fromstring(xml_text) + + schema_text = resources.read_text(res, 'wcsCapabilities.xsd') + xsd = etree.XMLSchema(etree.fromstring(schema_text)) + xsd.assertValid(xml) diff --git a/test/webapi/ows/wcs/test_validation.py b/test/webapi/ows/wcs/test_validation.py deleted file mode 100644 index 958983a25..000000000 --- a/test/webapi/ows/wcs/test_validation.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest - -from lxml import etree - -from xcube.webapi.ows.wcs import res -from test.webapi.ows import res as test_res -import importlib.resources as resources - - -class ValidationTest(unittest.TestCase): - - # noinspection PyMethodMayBeStatic - def test_validate_minimum(self): - xml_text = resources.read_text(test_res, 'WCSCapabilities_minimum.xml') - schema_text = resources.read_text(res, 'wcsCapabilities.xsd') - - xml = etree.fromstring(xml_text) - xsd = etree.XMLSchema(etree.fromstring(schema_text)) - xsd.assertValid(xml) diff --git a/test/webapi/res/test/config.yml b/test/webapi/res/test/config.yml index 7216c1391..9b00712a4 100644 --- a/test/webapi/res/test/config.yml +++ b/test/webapi/res/test/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/plugin.py b/xcube/plugin.py index 696d91f85..65ac98946 100644 --- a/xcube/plugin.py +++ b/xcube/plugin.py @@ -228,6 +228,7 @@ def _register_server_apis(ext_registry: extension.ExtensionRegistry): 'tiles', 'timeseries', 'ows.wmts', + 'ows.wcs', 's3', ] for api_name in server_api_names: diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py new file mode 100644 index 000000000..648984526 --- /dev/null +++ b/xcube/webapi/ows/wcs/controllers.py @@ -0,0 +1,240 @@ +# 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 xcube.webapi.ows.wcs.context import WcsContext +from xcube.webapi.ows.wmts.controllers import get_crs84_bbox + +from xcube.webapi.xml import Document +from xcube.webapi.xml import Element + +WCS_VERSION = '1.0.0' + + +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) + + +# 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') + + 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: + var = ds[var_name] + + label = var.long_name if hasattr(var, 'long_name') else var_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 + + content_element.add(Element('CoverageOfferingBrief', elements=[ + Element('name', text=var_name), + Element('label', text=label), + Element('lonLatEnvelope', elements=[ + Element('gml:pos', text=f'{bbox[0]}' + f' {bbox[1]}'), + Element('gml:pos', text=f'{bbox[2]}' + f' {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') + + def _get_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(): + individual_name = _get_value(['ServiceContact', 'IndividualName']) + individual_name = tuple(individual_name.split(' ').__reversed__()) + return '{}, {}'.format(*individual_name) + + element = Element('Service', elements=[ + Element('description', + text=_get_value(['WCS-description'])), + Element('name', + text=_get_value(['WCS-name'])), + Element('label', + text=_get_value(['WCS-label'])), + Element('keywords', elements=[ + Element('keyword', text=k) for k in service_provider['keywords'] + ]), + Element('responsibleParty', elements=[ + Element('individualName', + text=_get_individual_name()), + Element('organisationName', + text=_get_value(['ProviderName'])), + Element('positionName', + text=_get_value(['ServiceContact', + 'PositionName'])), + Element('contactInfo', elements=[ + Element('phone', elements=[ + Element('voice', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Phone', + 'Voice'])), + Element('facsimile', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Phone', + 'Facsimile'])), + ]), + Element('address', elements=[ + Element('deliveryPoint', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Address', + 'DeliveryPoint'])), + Element('city', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Address', + 'City'])), + Element('administrativeArea', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Address', + 'AdministrativeArea'])), + Element('postalCode', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Address', + 'PostalCode'])), + Element('country', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Address', + 'Country'])), + Element('electronicMailAddress', + text=_get_value(['ServiceContact', + 'ContactInfo', + 'Address', + 'ElectronicMailAddress'])), + ]), + Element('onlineResource', attrs={ + 'xlink:href': _get_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}?service=WCS&version=1.0.0&' \ + f'request=GetCapabilities' + describe_url = f'{base_url}?service=WCS&version=1.0.0&' \ + f'request=DescribeCoverage' + get_url = f'{base_url}?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') + ]) + ]) From ebf4daf2a2f54a8d520a801cfc4505e2abde9370 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 9 Aug 2022 00:19:21 +0200 Subject: [PATCH 06/43] started dev of describe coverage endpoint --- test/webapi/ows/res/WCSDescribe.xml | 45 + test/webapi/ows/res/describe_example.xml | 1984 ++++++++++++++++++++++ test/webapi/ows/wcs/test_controller.py | 32 +- xcube/webapi/ows/wcs/controllers.py | 174 +- xcube/webapi/ows/wcs/res/wcsDescribe.xsd | 1495 ++++++++++++++++ 5 files changed, 3678 insertions(+), 52 deletions(-) create mode 100644 test/webapi/ows/res/WCSDescribe.xml create mode 100644 test/webapi/ows/res/describe_example.xml create mode 100644 xcube/webapi/ows/wcs/res/wcsDescribe.xsd diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml new file mode 100644 index 000000000..58e3812b9 --- /dev/null +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -0,0 +1,45 @@ + + + + c2rcc_flags + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + c2rcc_flags + + + + Band + + + + 2147483652,0 + 2147532813,0 + + + + + + + + EPSG:4326 + + + zarr netcdf4 csv + + + \ No newline at end of file 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/test_controller.py b/test/webapi/ows/wcs/test_controller.py index bfdf538a0..15b29c36a 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -9,31 +9,44 @@ 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 +from xcube.webapi.ows.wcs.controllers import get_capabilities_xml +from xcube.webapi.ows.wcs.controllers import get_describe_xml +# noinspection PyMethodMayBeStatic class ControllerTest(unittest.TestCase): def setUp(self) -> None: super().setUp() self.wcs_ctx = get_api_ctx('ows.wcs', WcsContext) - # noinspection PyMethodMayBeStatic def test_get_capabilities(self): - expected_xml = resources.read_text(test_res, 'WCSCapabilities.xml') - actual_xml = get_wcs_capabilities_xml( + actual_xml = get_capabilities_xml( self.wcs_ctx, 'https://xcube.brockmann-consult.de/wcs/kvp' ) + self.check_xml(actual_xml, 'WCSCapabilities.xml', + 'wcsCapabilities.xsd') + + def test_describe_coverage(self): + actual_xml = get_describe_xml(self.wcs_ctx) + self.check_xml(actual_xml, 'WCSDescribe.xml', 'wcsDescribe.xsd') + + 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 @@ -42,10 +55,13 @@ def strip_whitespace(xml: str) -> str: return ElementTree.canonicalize(ElementTree.tostring(root), strip_text=True) - def test_how_to_validate(self): - xml_text = resources.read_text(test_res, 'WCSCapabilities.xml') + 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, 'wcsCapabilities.xsd') + schema_text = resources.read_text(res, xsd_resource) xsd = etree.XMLSchema(etree.fromstring(schema_text)) - xsd.assertValid(xml) + xsd.assertValid(xml) # fail if xml is invalid + return True diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 648984526..dd6b842d0 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -20,6 +20,8 @@ # DEALINGS IN THE SOFTWARE. import warnings +from typing import List + from xcube.webapi.ows.wcs.context import WcsContext from xcube.webapi.ows.wmts.controllers import get_crs84_bbox @@ -29,7 +31,7 @@ WCS_VERSION = '1.0.0' -def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str: +def get_capabilities_xml(ctx: WcsContext, base_url: str) -> str: """ Get WCSCapabilities.xml according to https://schemas.opengis.net/wcs/1.0.0/. @@ -38,54 +40,37 @@ def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str: :param base_url: the request base URL :return: XML plain text in UTF-8 encoding """ - element = get_capabilities_element(ctx, base_url) + element = _get_capabilities_element(ctx, base_url) document = Document(element) return document.to_xml(indent=4) -# 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') - - 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 +def get_describe_xml(ctx: WcsContext) -> str: + # possible formats are shown on cli with xcube gen --info + element = _get_describe_element(ctx) + document = Document(element) + return document.to_xml(indent=4) - var_names = sorted(ds.data_vars) - for var_name in var_names: - var = ds[var_name] - label = var.long_name if hasattr(var, 'long_name') else var_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 +# 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') - content_element.add(Element('CoverageOfferingBrief', elements=[ - Element('name', text=var_name), - Element('label', text=label), - Element('lonLatEnvelope', elements=[ - Element('gml:pos', text=f'{bbox[0]}' - f' {bbox[1]}'), - Element('gml:pos', text=f'{bbox[2]}' - f' {bbox[3]}') - ]) - ])) + band_infos = _extract_band_infos(ctx) + for band_info in band_infos: + content_element.add(Element('CoverageOfferingBrief', elements=[ + Element('name', text=band_info.var_name), + Element('label', text=band_info.label), + Element('lonLatEnvelope', elements=[ + Element('gml:pos', text=f'{band_info.bbox[0]}' + f' {band_info.bbox[1]}'), + Element('gml:pos', text=f'{band_info.bbox[2]}' + f' {band_info.bbox[3]}') + ]) + ])) return Element( 'WCS_Capabilities', @@ -103,7 +88,7 @@ def get_capabilities_element(ctx: WcsContext, ) -def get_service_element(ctx: WcsContext) -> Element: +def _get_service_element(ctx: WcsContext) -> Element: service_provider = ctx.config.get('ServiceProvider') def _get_value(path): @@ -194,7 +179,7 @@ def _get_individual_name(): return element -def get_capability_element(base_url: str) -> Element: +def _get_capability_element(base_url: str) -> Element: get_capabilities_url = f'{base_url}?service=WCS&version=1.0.0&' \ f'request=GetCapabilities' describe_url = f'{base_url}?service=WCS&version=1.0.0&' \ @@ -238,3 +223,104 @@ def get_capability_element(base_url: str) -> Element: Element('Format', text='application/x-ogc-wcs') ]) ]) + + +# noinspection HttpUrlsUsage +def _get_describe_element(ctx: WcsContext) -> Element: + coverage_elements = [] + + band_infos = _extract_band_infos(ctx) + for band_info in band_infos: + coverage_elements.append(Element('CoverageOffering', elements=[ + Element('name', text=band_info.var_name), + Element('label', text=band_info.label), + Element('lonLatEnvelope', elements=[ + Element('gml:pos', text=f'{band_info.bbox[0]} ' + f'{band_info.bbox[1]}'), + Element('gml:pos', text=f'{band_info.bbox[2]} ' + f'{band_info.bbox[3]}') + ]), + Element('domainSet', elements=[ + Element('spatialDomain', elements=[ + Element('gml:Envelope', elements=[ + Element('gml:pos', text=f'{band_info.bbox[0]} ' + f'{band_info.bbox[1]}'), + Element('gml:pos', text=f'{band_info.bbox[2]} ' + f'{band_info.bbox[3]}') + ]) + ]) + ]), + Element('rangeSet', elements=[ + Element('name', text=band_info.var_name), + Element('label', text=band_info.label), + Element('axisDescription', elements=[ + Element('AxisDescription', elements=[ + Element('name', text='Band'), + Element('label', text='Band'), + Element('values', elements=[ + Element('interval', elements=[ + Element('min', text='5'), + Element('max', text='2') + ]) + ]), + ]) + ]) + ]), + Element('supportedCRSs', elements=[ + Element('requestResponseCRSs', text='SomeCRS, fixme') + ]), + Element('supportedFormats', elements=[ + Element('formats', text='the list of formats') + ]) + ])) + + return Element( + 'CoverageDescription', + attrs={ + 'xmlns': "http://www.opengis.net/wcs", + 'xmlns:gml': "http://www.opengis.net/gml", + 'version': WCS_VERSION, + }, + elements=coverage_elements + ) + + +class BandInfo: + + def __init__(self, var_name: str, label: str, + bbox: tuple[float, float, float, float]): + self.label = label + self.var_name = var_name + self.bbox = bbox + + +def _extract_band_infos(ctx: WcsContext) -> List[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: + var = ds[var_name] + + label = var.long_name if hasattr(var, 'long_name') else var_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 + + band_infos.append(BandInfo(var_name, label, bbox)) + return band_infos 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. + + + + From e87277895810cc406710da504a398a96af200e7b Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 9 Aug 2022 14:29:00 +0200 Subject: [PATCH 07/43] finished dev of describe coverage endpoint --- test/webapi/ows/res/WCSCapabilities.xml | 30 +- test/webapi/ows/res/WCSDescribe.xml | 602 +++++++++++++++++++++++- xcube/webapi/ows/wcs/controllers.py | 104 ++-- 3 files changed, 676 insertions(+), 60 deletions(-) diff --git a/test/webapi/ows/res/WCSCapabilities.xml b/test/webapi/ows/res/WCSCapabilities.xml index b5d105f8b..ffb41ae60 100644 --- a/test/webapi/ows/res/WCSCapabilities.xml +++ b/test/webapi/ows/res/WCSCapabilities.xml @@ -73,7 +73,7 @@ - c2rcc_flags + demo.c2rcc_flags 0 50 @@ -81,7 +81,7 @@ - conc_chl + demo.conc_chl 0 50 @@ -89,7 +89,7 @@ - conc_tsm + demo.conc_tsm 0 50 @@ -97,7 +97,7 @@ - kd489 + demo.kd489 0 50 @@ -105,7 +105,7 @@ - quality_flags + demo.quality_flags 0 50 @@ -113,7 +113,7 @@ - c2rcc_flags + demo-1w.c2rcc_flags 0 50 @@ -121,7 +121,7 @@ - c2rcc_flags_stdev + demo-1w.c2rcc_flags_stdev 0 50 @@ -129,7 +129,7 @@ - conc_chl + demo-1w.conc_chl 0 50 @@ -137,7 +137,7 @@ - conc_chl_stdev + demo-1w.conc_chl_stdev 0 50 @@ -145,7 +145,7 @@ - conc_tsm + demo-1w.conc_tsm 0 50 @@ -153,7 +153,7 @@ - conc_tsm_stdev + demo-1w.conc_tsm_stdev 0 50 @@ -161,7 +161,7 @@ - kd489 + demo-1w.kd489 0 50 @@ -169,7 +169,7 @@ - kd489_stdev + demo-1w.kd489_stdev 0 50 @@ -177,7 +177,7 @@ - quality_flags + demo-1w.quality_flags 0 50 @@ -185,7 +185,7 @@ - quality_flags_stdev + demo-1w.quality_flags_stdev 0 50 diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml index 58e3812b9..cbeebadc9 100644 --- a/test/webapi/ows/res/WCSDescribe.xml +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -3,7 +3,7 @@ xmlns:gml="http://www.opengis.net/gml" version="1.0.0"> - c2rcc_flags + demo.c2rcc_flags 0 50 @@ -19,7 +19,7 @@ - c2rcc_flags + demo.c2rcc_flags @@ -27,8 +27,8 @@ - 2147483652,0 - 2147532813,0 + 2147483648.0000 + 2147532813.0000 @@ -36,10 +36,600 @@ - EPSG:4326 + EPSG:4326 EPSG:3857 - zarr netcdf4 csv + zarr + netcdf4 + csv + + + + demo.conc_chl + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo.conc_chl + + + + Band + + + + 0.0001 + 22.4421 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo.conc_tsm + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo.conc_tsm + + + + Band + + + + 0.0155 + 166.5728 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo.kd489 + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo.kd489 + + + + Band + + + + 0.0224 + 7.0847 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo.quality_flags + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo.quality_flags + + + + Band + + + + 8388608.0000 + 4169138176.0000 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.c2rcc_flags + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.c2rcc_flags + + + + Band + + + + 2147483648.0000 + 2147532813.0000 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.c2rcc_flags_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.c2rcc_flags_stdev + + + + Band + + + + 0.0000 + 24576.0000 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.conc_chl + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.conc_chl + + + + Band + + + + 0.0001 + 22.4421 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.conc_chl_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.conc_chl_stdev + + + + Band + + + + 0.0000 + 10.3862 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.conc_tsm + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.conc_tsm + + + + Band + + + + 0.0155 + 166.5728 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.conc_tsm_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.conc_tsm_stdev + + + + Band + + + + 0.0000 + 71.6995 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.kd489 + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.kd489 + + + + Band + + + + 0.0224 + 7.0847 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.kd489_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.kd489_stdev + + + + Band + + + + 0.0000 + 2.7701 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.quality_flags + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.quality_flags + + + + Band + + + + 8388608.0000 + 4169138176.0000 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.quality_flags_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.quality_flags_stdev + + + + Band + + + + 0.0000 + 1883242496.0000 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv \ No newline at end of file diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index dd6b842d0..c7ed7f483 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -19,12 +19,14 @@ # 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 -from typing import List +import numpy as np +from xcube.constants import EXTENSION_POINT_DATASET_IOS +from xcube.util.plugin import get_extension_registry from xcube.webapi.ows.wcs.context import WcsContext from xcube.webapi.ows.wmts.controllers import get_crs84_bbox - from xcube.webapi.xml import Document from xcube.webapi.xml import Element @@ -60,15 +62,15 @@ def _get_capabilities_element(ctx: WcsContext, content_element = Element('ContentMetadata') band_infos = _extract_band_infos(ctx) - for band_info in band_infos: + for var_name in band_infos.keys(): content_element.add(Element('CoverageOfferingBrief', elements=[ - Element('name', text=band_info.var_name), - Element('label', text=band_info.label), + Element('name', text=var_name), + Element('label', text=band_infos[var_name].label), Element('lonLatEnvelope', elements=[ - Element('gml:pos', text=f'{band_info.bbox[0]}' - f' {band_info.bbox[1]}'), - Element('gml:pos', text=f'{band_info.bbox[2]}' - f' {band_info.bbox[3]}') + 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]}') ]) ])) @@ -229,48 +231,54 @@ def _get_capability_element(base_url: str) -> Element: def _get_describe_element(ctx: WcsContext) -> Element: coverage_elements = [] - band_infos = _extract_band_infos(ctx) - for band_info in band_infos: + band_infos = _extract_band_infos(ctx, True) + for var_name in band_infos.keys(): coverage_elements.append(Element('CoverageOffering', elements=[ - Element('name', text=band_info.var_name), - Element('label', text=band_info.label), + Element('name', text=var_name), + Element('label', text=band_infos[var_name].label), Element('lonLatEnvelope', elements=[ - Element('gml:pos', text=f'{band_info.bbox[0]} ' - f'{band_info.bbox[1]}'), - Element('gml:pos', text=f'{band_info.bbox[2]} ' - f'{band_info.bbox[3]}') + 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', elements=[ - Element('gml:pos', text=f'{band_info.bbox[0]} ' - f'{band_info.bbox[1]}'), - Element('gml:pos', text=f'{band_info.bbox[2]} ' - f'{band_info.bbox[3]}') + 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('rangeSet', elements=[ - Element('name', text=band_info.var_name), - Element('label', text=band_info.label), - Element('axisDescription', elements=[ - Element('AxisDescription', elements=[ - Element('name', text='Band'), - Element('label', text='Band'), - Element('values', elements=[ - Element('interval', elements=[ - Element('min', text='5'), - Element('max', text='2') - ]) - ]), + 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('interval', elements=[ + Element('min', text= + f'{band_infos[var_name].min:0.4f}'), + Element('max', text= + f'{band_infos[var_name].max:0.4f}') + ]) + ]), + ]) ]) ]) ]), Element('supportedCRSs', elements=[ - Element('requestResponseCRSs', text='SomeCRS, fixme') + Element('requestResponseCRSs', text='EPSG:4326 EPSG:3857') ]), Element('supportedFormats', elements=[ - Element('formats', text='the list of formats') + Element('formats', text=f) for f in _get_formats_list() ]) ])) @@ -285,17 +293,28 @@ def _get_describe_element(ctx: WcsContext) -> Element: ) +def _get_formats_list() -> List[str]: + dsio_extensions = get_extension_registry().find_extensions( + EXTENSION_POINT_DATASET_IOS, + lambda e: 'w' in e.metadata.get('modes', set()) + ) + return [ext.name for ext in dsio_extensions if not ext.name == 'mem'] + + class BandInfo: def __init__(self, var_name: str, label: str, bbox: tuple[float, float, float, float]): - self.label = label self.var_name = var_name + self.label = label self.bbox = bbox + self.min = np.nan + self.max = np.nan -def _extract_band_infos(ctx: WcsContext) -> List[BandInfo]: - band_infos = [] +def _extract_band_infos(ctx: WcsContext, full: bool = False) \ + -> 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) @@ -322,5 +341,12 @@ def _extract_band_infos(ctx: WcsContext) -> List[BandInfo]: if not is_spatial_var: continue - band_infos.append(BandInfo(var_name, label, bbox)) + band_info = BandInfo(f'{ds_name}.{var_name}', label, bbox) + if full: + nn_values = var.values[~np.isnan(var.values)] + band_info.min = nn_values.min() + band_info.max = nn_values.max() + + band_infos[f'{ds_name}.{var_name}'] = band_info + return band_infos From 60869ded621883324c8479ef62f37976db14f414 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 9 Aug 2022 14:42:41 +0200 Subject: [PATCH 08/43] allow to filter describe coverages --- test/webapi/ows/res/WCSDescribe_subset.xml | 131 +++++++++++++++++++++ test/webapi/ows/wcs/test_controller.py | 9 ++ xcube/webapi/ows/wcs/controllers.py | 19 +-- 3 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 test/webapi/ows/res/WCSDescribe_subset.xml diff --git a/test/webapi/ows/res/WCSDescribe_subset.xml b/test/webapi/ows/res/WCSDescribe_subset.xml new file mode 100644 index 000000000..4a90f27be --- /dev/null +++ b/test/webapi/ows/res/WCSDescribe_subset.xml @@ -0,0 +1,131 @@ + + + + demo.conc_chl + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo.conc_chl + + + + Band + + + + 0.0001 + 22.4421 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.kd489_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.kd489_stdev + + + + Band + + + + 0.0000 + 2.7701 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + + demo-1w.quality_flags_stdev + + + 0 50 + 5 52.5 + + + + + 0 50 + 5 52.5 + + + + + + demo-1w.quality_flags_stdev + + + + Band + + + + 0.0000 + 1883242496.0000 + + + + + + + + EPSG:4326 EPSG:3857 + + + zarr + netcdf4 + csv + + + \ No newline at end of file diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index 15b29c36a..71364ada4 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -32,6 +32,15 @@ def test_describe_coverage(self): actual_xml = get_describe_xml(self.wcs_ctx) self.check_xml(actual_xml, 'WCSDescribe.xml', 'wcsDescribe.xsd') + def test_describe_coverage_subset(self): + actual_xml = get_describe_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 check_xml(self, actual_xml, expected_xml_resource, xsd): self.maxDiff = None # Do not delete, useful for debugging diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index c7ed7f483..141be3cf0 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -47,9 +47,8 @@ def get_capabilities_xml(ctx: WcsContext, base_url: str) -> str: return document.to_xml(indent=4) -def get_describe_xml(ctx: WcsContext) -> str: - # possible formats are shown on cli with xcube gen --info - element = _get_describe_element(ctx) +def get_describe_xml(ctx: WcsContext, coverages: List[str] = None) -> str: + element = _get_describe_element(ctx, coverages) document = Document(element) return document.to_xml(indent=4) @@ -228,10 +227,11 @@ def _get_capability_element(base_url: str) -> Element: # noinspection HttpUrlsUsage -def _get_describe_element(ctx: WcsContext) -> Element: +def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ + -> Element: coverage_elements = [] - band_infos = _extract_band_infos(ctx, True) + band_infos = _extract_band_infos(ctx, coverages, True) for var_name in band_infos.keys(): coverage_elements.append(Element('CoverageOffering', elements=[ Element('name', text=var_name), @@ -312,8 +312,8 @@ def __init__(self, var_name: str, label: str, self.max = np.nan -def _extract_band_infos(ctx: WcsContext, full: bool = False) \ - -> Dict[str, BandInfo]: +def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, + full: bool = False) -> Dict[str, BandInfo]: band_infos = {} for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] @@ -332,6 +332,9 @@ def _extract_band_infos(ctx: WcsContext, full: bool = False) \ 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 @@ -341,7 +344,7 @@ def _extract_band_infos(ctx: WcsContext, full: bool = False) \ if not is_spatial_var: continue - band_info = BandInfo(f'{ds_name}.{var_name}', label, bbox) + band_info = BandInfo(qualified_var_name, label, bbox) if full: nn_values = var.values[~np.isnan(var.values)] band_info.min = nn_values.min() From 9870bc03cc965fccfd202802c1df1717cef6744a Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 10 Aug 2022 00:59:49 +0200 Subject: [PATCH 09/43] preparing GetCoverage endpoint; first step: validating user requests --- test/webapi/ows/wcs/test_controller.py | 233 ++++++++++++++++++++++++- xcube/webapi/ows/wcs/controllers.py | 94 +++++++++- 2 files changed, 319 insertions(+), 8 deletions(-) diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index 71364ada4..6035e1814 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -1,15 +1,15 @@ import unittest +import xml.etree.ElementTree as ElementTree from importlib import resources as resources from lxml import etree -import xml.etree.ElementTree as ElementTree from test.webapi.helpers import get_api_ctx from test.webapi.ows import res as test_res from xcube.webapi.ows.wcs import res from xcube.webapi.ows.wcs.context import WcsContext - -from xcube.webapi.ows.wcs.controllers import get_capabilities_xml +from xcube.webapi.ows.wcs.controllers import get_capabilities_xml, \ + validate_coverage_req from xcube.webapi.ows.wcs.controllers import get_describe_xml @@ -41,6 +41,233 @@ def test_describe_coverage_subset(self): ]) self.check_xml(actual_xml, 'WCSDescribe_subset.xml', 'wcsDescribe.xsd') + def test_validate_coverage_request(self): + # request is fine + validate_coverage_req({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'BBOX': '1 51 4 52', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + + # TIME given in addition to BBOX -> fine + validate_coverage_req({ + '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': 'zarr' + }, self.wcs_ctx) + + # COVERAGE is missing -> expect a failure + try: + validate_coverage_req({ + 'CRS': 'EPSG:4326', + 'BBOX': '1 51 4 52', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'invalid_coverage!', + 'CRS': 'EPSG:4326', + 'BBOX': '1 51 4 52', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'TIME': '2020-01-28', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + + # use invalid TIME format -> expect a failure + try: + validate_coverage_req({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'TIME': '20201208', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'TIME': '2020-01-28', + 'RESX': 23.56, + 'RESY': 23.56, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + + # PARAMETER is given -> expect a failure (not yet supported) + try: + validate_coverage_req({ + 'COVERAGE': 'demo.conc_chl', + 'PARAMETER': 'I expect nothing from using this parameter', + 'CRS': 'EPSG:4326', + 'TIME': '2020-12-08', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'BBOX': '-10,3,-5,4', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'BBOX': '-10 3 -5 4', + 'WIDTH': 200, + 'RESY': 156.45, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'BBOX': '-10 3 -5 4', + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'BBOX': '-10 3 -5 4', + 'WIDTH': 200, + 'HEIGHT': 200, + 'RESX': 200, + 'RESY': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'INTERPOLATION': 'Farest Neighbor', + 'CRS': 'EPSG:4326', + 'TIME': '2020-12-08', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'EXCEPTIONS': 'Farest Neighbor', + 'CRS': 'EPSG:4326', + 'TIME': '2020-12-08', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }, self.wcs_ctx) + 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({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'TIME': '2020-12-08', + 'WIDTH': 200, + 'HEIGHT': 200, + }, self.wcs_ctx) + self.fail('Classified invalid request as valid.') + except ValueError as e: + self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' + 'netcdf4, csv', str(e)) + + # FORMAT is invalid -> expect a failure + try: + validate_coverage_req({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'TIME': '2020-12-08', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'MettCDF' + }, self.wcs_ctx) + self.fail('Classified invalid request as valid.') + except ValueError as e: + self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' + 'netcdf4, csv', str(e)) + def check_xml(self, actual_xml, expected_xml_resource, xsd): self.maxDiff = None # Do not delete, useful for debugging diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 141be3cf0..96062f87b 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -18,10 +18,12 @@ # 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 datetime import datetime +import numpy as np +import re import warnings -from typing import Dict, List -import numpy as np +from typing import Dict, List, Any from xcube.constants import EXTENSION_POINT_DATASET_IOS from xcube.util.plugin import get_extension_registry @@ -31,6 +33,9 @@ from xcube.webapi.xml import Element WCS_VERSION = '1.0.0' +VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857'] + +CoverageRequest = Dict[str, Any] def get_capabilities_xml(ctx: WcsContext, base_url: str) -> str: @@ -53,6 +58,85 @@ def get_describe_xml(ctx: WcsContext, coverages: List[str] = None) -> str: return document.to_xml(indent=4) +def validate_coverage_req(req: CoverageRequest, ctx: WcsContext): + if 'COVERAGE' in req and is_valid_coverage(req['COVERAGE'], ctx) \ + and 'CRS' in req and is_valid_crs(req['CRS']) \ + and (('BBOX' in req and is_valid_bbox(req['BBOX'])) or + ('TIME' in req and is_valid_time(req['TIME']))) \ + and (is_valid_bbox(req['BBOX']) if 'BBOX' in req else True) \ + and (is_valid_time(req['TIME']) if 'TIME' in req else True) \ + and (('WIDTH' in req and 'HEIGHT' in req) or + ('RESX' in req and 'RESY' in req)) \ + and (('WIDTH' in req and 'RESX' not in req) or + ('WIDTH' in req and 'RESY' not in req) or + ('HEIGHT' in req and 'RESX' not in req) or + ('HEIGHT' in req and 'RESY' not in req) or + ('RESX' in req and 'WIDTH' not in req) or + ('RESX' in req and 'HEIGHT' not in req) or + ('RESY' in req and 'WIDTH' not in req) or + ('RESY' in req and 'HEIGHT' not in req)) \ + and 'FORMAT' in req and is_valid_format(req['FORMAT']) \ + and 'PARAMETER' not in req \ + and 'INTERPOLATION' not in req \ + and 'EXCEPTIONS' not in req: + return + elif 'COVERAGE' not in req or not is_valid_coverage(req['COVERAGE'], ctx): + raise ValueError('No valid value for parameter COVERAGE provided. ' + 'COVERAGE must be a variable name prefixed with ' + 'its dataset name. Example: my_dataset.my_var') + elif 'PARAMETER' in req: + raise ValueError('PARAMETER not yet supported') + elif 'INTERPOLATION' in req: + raise ValueError('INTERPOLATION not yet supported') + elif 'EXCEPTIONS' in req: + raise ValueError('EXCEPTIONS not yet supported') + elif (('WIDTH' in req and 'HEIGHT' not in req) or + ('HEIGHT' in req and 'WIDTH' not in req) or + ('RESX' in req and 'RESY' not in req) or + ('RESY' in req and 'RESX' not in req) or + ('WIDTH' in req and 'RESX' in req or 'RESY' in req) or + ('HEIGHT' in req and 'RESX' in req or 'RESY' in req)): + raise ValueError('Either both WIDTH and HEIGHT, or both RESX and RESY ' + 'must be provided.') + elif 'FORMAT' not in req or not is_valid_format(req['FORMAT']): + raise ValueError('FORMAT wrong or missing. Must be one of ' + + ', '.join(_get_formats_list())) + elif True: + raise ValueError('Reason unclear, fix me') + + +def is_valid_coverage(coverage: str, ctx: WcsContext) -> bool: + band_infos = _extract_band_infos(ctx, [coverage]) + if band_infos: + return True + return False + + +def is_valid_crs(crs: str) -> bool: + return crs in VALID_CRS_LIST + + +def is_valid_bbox(bbox: str) -> bool: + bbox_regex = re.compile(r'-?\d{1,3} -?\d{1,2} -?\d{1,3} -?\d{1,2}') + if not bbox_regex.match(bbox): + raise ValueError('BBOX must be given as `minx miny maxx maxy`') + return True + + +def is_valid_format(format_req: str) -> bool: + return format_req in _get_formats_list() + + +def is_valid_time(time: str) -> bool: + try: + datetime.fromisoformat(time) + except ValueError: + raise ValueError('TIME value must be given in the format' + '\'YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]]' + '[+HH:MM[:SS[.ffffff]]]]\'') + return True + + # noinspection HttpUrlsUsage def _get_capabilities_element(ctx: WcsContext, base_url: str) -> Element: @@ -275,7 +359,7 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ ]) ]), Element('supportedCRSs', elements=[ - Element('requestResponseCRSs', text='EPSG:4326 EPSG:3857') + Element('requestResponseCRSs', text=' '.join(VALID_CRS_LIST)) ]), Element('supportedFormats', elements=[ Element('formats', text=f) for f in _get_formats_list() @@ -294,11 +378,11 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ def _get_formats_list() -> List[str]: - dsio_extensions = get_extension_registry().find_extensions( + formats = get_extension_registry().find_extensions( EXTENSION_POINT_DATASET_IOS, lambda e: 'w' in e.metadata.get('modes', set()) ) - return [ext.name for ext in dsio_extensions if not ext.name == 'mem'] + return [ext.name for ext in formats if not ext.name == 'mem'] class BandInfo: From 9a78aee0011c6fcf8b8e65e60eca2c23a2a214bb Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 11 Aug 2022 09:34:32 +0200 Subject: [PATCH 10/43] preparing GetCoverage endpoint, intermediate --- test/webapi/ows/wcs/test_controller.py | 109 +++++++++----- xcube/webapi/ows/wcs/controllers.py | 193 ++++++++++++++++++++----- 2 files changed, 233 insertions(+), 69 deletions(-) diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index 6035e1814..79aababe6 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -9,7 +9,8 @@ from xcube.webapi.ows.wcs import res from xcube.webapi.ows.wcs.context import WcsContext from xcube.webapi.ows.wcs.controllers import get_capabilities_xml, \ - validate_coverage_req + _validate_coverage_req, CoverageRequest, get_coverage, \ + translate_to_generator_request from xcube.webapi.ows.wcs.controllers import get_describe_xml @@ -43,17 +44,17 @@ def test_describe_coverage_subset(self): def test_validate_coverage_request(self): # request is fine - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) # TIME given in addition to BBOX -> fine - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', @@ -61,17 +62,17 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) # COVERAGE is missing -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('No valid value for parameter COVERAGE provided. ' @@ -81,14 +82,14 @@ def test_validate_coverage_request(self): # COVERAGE is given but not found -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'invalid_coverage!', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('No valid value for parameter COVERAGE provided. ' @@ -97,25 +98,25 @@ def test_validate_coverage_request(self): str(e)) # TIME is used instead of BBOX -> fine - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '2020-01-28', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) # use invalid TIME format -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '20201208', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('TIME value must be given in the format' @@ -124,18 +125,18 @@ def test_validate_coverage_request(self): str(e)) # RESX and RESY are given instead of W/H -> fine - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '2020-01-28', 'RESX': 23.56, 'RESY': 23.56, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) # PARAMETER is given -> expect a failure (not yet supported) try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'PARAMETER': 'I expect nothing from using this parameter', 'CRS': 'EPSG:4326', @@ -143,21 +144,21 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) 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({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10,3,-5,4', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('BBOX must be given as `minx miny maxx maxy`', @@ -165,14 +166,14 @@ def test_validate_coverage_request(self): # WIDTH, but not HEIGHT is given -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10 3 -5 4', 'WIDTH': 200, 'RESY': 156.45, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and ' @@ -180,13 +181,13 @@ def test_validate_coverage_request(self): # HEIGHT, but not WIDTH is given -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10 3 -5 4', 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and ' @@ -194,7 +195,7 @@ def test_validate_coverage_request(self): # WIDTH and HEIGHT and RESX and RESY are given -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10 3 -5 4', @@ -203,7 +204,7 @@ def test_validate_coverage_request(self): 'RESX': 200, 'RESY': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and ' @@ -211,7 +212,7 @@ def test_validate_coverage_request(self): # INTERPOLATION is given -> expect a failure (not yet supported) try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'INTERPOLATION': 'Farest Neighbor', 'CRS': 'EPSG:4326', @@ -219,35 +220,35 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) 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({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', - 'EXCEPTIONS': 'Farest Neighbor', + 'EXCEPTIONS': 'exceptions', 'CRS': 'EPSG:4326', 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }, self.wcs_ctx) + }), self.wcs_ctx) 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({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' @@ -255,19 +256,59 @@ def test_validate_coverage_request(self): # FORMAT is invalid -> expect a failure try: - validate_coverage_req({ + _validate_coverage_req(CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'MettCDF' - }, self.wcs_ctx) + }), self.wcs_ctx) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' 'netcdf4, csv', 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': 'zarr' + }) + get_coverage(coverage_request, self.wcs_ctx) + + def test_translate_requests(self): + coverage_request = CoverageRequest({ + 'COVERAGE': 'demo.conc_chl', + 'CRS': 'EPSG:4326', + 'BBOX': '1 51 4 52', + 'WIDTH': 200, + 'HEIGHT': 200, + 'FORMAT': 'zarr' + }) + gen_req = translate_to_generator_request(coverage_request) + self.assertEqual('request_json = { \ + "input_config": { \ + "store_id": "file", \ + "store_params": { \ + "root": "../../serve/demo" \ + }, \ + "data_id": "cube.nc" \ + }, \ + "cube_config": {}, \ + "output_config": { \ + "store_id": "file", \ + "store_params": { \ + "root": "." \ + }, \ + "replace": True, \ + "data_id": "cube.zarr" \ + }' + '}', gen_req) + def check_xml(self, actual_xml, expected_xml_resource, xsd): self.maxDiff = None # Do not delete, useful for debugging diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 96062f87b..0d6b59916 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -19,12 +19,13 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. from datetime import datetime +from io import BufferedIOBase +from typing import Dict, List, Any, Union, Optional, Tuple + import numpy as np import re import warnings -from typing import Dict, List, Any - from xcube.constants import EXTENSION_POINT_DATASET_IOS from xcube.util.plugin import get_extension_registry from xcube.webapi.ows.wcs.context import WcsContext @@ -35,7 +36,46 @@ WCS_VERSION = '1.0.0' VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857'] -CoverageRequest = Dict[str, Any] + +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_capabilities_xml(ctx: WcsContext, base_url: str) -> str: @@ -58,47 +98,130 @@ def get_describe_xml(ctx: WcsContext, coverages: List[str] = None) -> str: return document.to_xml(indent=4) -def validate_coverage_req(req: CoverageRequest, ctx: WcsContext): - if 'COVERAGE' in req and is_valid_coverage(req['COVERAGE'], ctx) \ - and 'CRS' in req and is_valid_crs(req['CRS']) \ - and (('BBOX' in req and is_valid_bbox(req['BBOX'])) or - ('TIME' in req and is_valid_time(req['TIME']))) \ - and (is_valid_bbox(req['BBOX']) if 'BBOX' in req else True) \ - and (is_valid_time(req['TIME']) if 'TIME' in req else True) \ - and (('WIDTH' in req and 'HEIGHT' in req) or - ('RESX' in req and 'RESY' in req)) \ - and (('WIDTH' in req and 'RESX' not in req) or - ('WIDTH' in req and 'RESY' not in req) or - ('HEIGHT' in req and 'RESX' not in req) or - ('HEIGHT' in req and 'RESY' not in req) or - ('RESX' in req and 'WIDTH' not in req) or - ('RESX' in req and 'HEIGHT' not in req) or - ('RESY' in req and 'WIDTH' not in req) or - ('RESY' in req and 'HEIGHT' not in req)) \ - and 'FORMAT' in req and is_valid_format(req['FORMAT']) \ - and 'PARAMETER' not in req \ - and 'INTERPOLATION' not in req \ - and 'EXCEPTIONS' not in req: +def translate_to_generator_request(req: CoverageRequest) -> str: + pass + + +def get_coverage(req: CoverageRequest, ctx: WcsContext) -> BufferedIOBase: + from xcube.core.gen import gen + _validate_coverage_req(req, ctx) + + gen_req = translate_to_generator_request(req) + + input_path = _get_input_path(req, ctx) + output_region = _get_output_region(req) + output_variable = [(req.coverage[req.coverage.index('.') + 1:], {})] + # [('conc_chl', None)] + + from xcube.core.gen2 import CubeGenerator + + + + dsios = get_extension_registry().find_components( + EXTENSION_POINT_DATASET_IOS) + for dataset_io in dsios: + if dataset_io.name.lower() == 'mem': + break + print(dataset_io) + return None + +''' + + # xcube gen only works with time dim of length 1 + # so need to ask for time constraint in WCS request + # then use raster closest in time + # or don't do that because it's wrong + # todo - set output dir, will be created + gen_status = gen.gen_cube([input_path], + output_region=output_region, + output_variables=output_variable, + output_writer_name='mem' + ) + print(str(gen_status)) +''' + + +def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: + if not req.bbox: + return None + + output_region = [] + for v in req.bbox.split(' '): + output_region.append(float(v)) + return tuple(output_region) + + +def _get_input_path(req: CoverageRequest, ctx: WcsContext) -> str: + 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: + path = dataset_config['Path'] + break + store_instance_id = dataset_config['StoreInstanceId'] + store = ctx.datasets_ctx.get_data_store_pool().\ + get_store(store_instance_id) + return store.root + '/' + path + raise RuntimeError('Should never come here. Contact the developers.') + + +def _validate_coverage_req(req: CoverageRequest, ctx: WcsContext): + def _has_no_invalid_bbox() -> bool: + if req.bbox: + return is_valid_bbox(req.bbox) + else: + return True + + def _has_no_invalid_time() -> bool: + if req.time: + return is_valid_time(req.time) + else: + return True + + if req.coverage and is_valid_coverage(req.coverage, ctx) \ + and req.crs and is_valid_crs(req.crs) \ + and ((req.bbox and is_valid_bbox(req.bbox)) or + (req.time and is_valid_time(req.time))) \ + and _has_no_invalid_bbox \ + and _has_no_invalid_time() \ + and ((req.width and req.height) or + (req.resx and req.resy)) \ + and ((req.width and not req.resx) or + (req.width and not req.resy) or + (req.height and not req.resx) or + (req.height and not req.resy) or + (req.resx and not req.width) or + (req.resx and not req.height) or + (req.resy and not req.width) or + (req.resy and not req.height)) \ + and req.format and is_valid_format(req.format) \ + and not req.parameter \ + and not req.interpolation \ + and not req.exceptions: return - elif 'COVERAGE' not in req or not is_valid_coverage(req['COVERAGE'], ctx): + elif not req.coverage or not is_valid_coverage(req.coverage, ctx): raise ValueError('No valid value for parameter COVERAGE provided. ' 'COVERAGE must be a variable name prefixed with ' 'its dataset name. Example: my_dataset.my_var') - elif 'PARAMETER' in req: + elif req.parameter: raise ValueError('PARAMETER not yet supported') - elif 'INTERPOLATION' in req: + elif req.interpolation: raise ValueError('INTERPOLATION not yet supported') - elif 'EXCEPTIONS' in req: + elif req.exceptions: raise ValueError('EXCEPTIONS not yet supported') - elif (('WIDTH' in req and 'HEIGHT' not in req) or - ('HEIGHT' in req and 'WIDTH' not in req) or - ('RESX' in req and 'RESY' not in req) or - ('RESY' in req and 'RESX' not in req) or - ('WIDTH' in req and 'RESX' in req or 'RESY' in req) or - ('HEIGHT' in req and 'RESX' in req or 'RESY' in req)): + elif ((req.width and not req.height) or + (req.height and not req.width) or + (req.resx and not req.resy) or + (req.resy and not req.resx) or + (req.width and req.resx or req.resy) or + (req.height and req.resx or req.resy)): raise ValueError('Either both WIDTH and HEIGHT, or both RESX and RESY ' 'must be provided.') - elif 'FORMAT' not in req or not is_valid_format(req['FORMAT']): + elif not req.format or not is_valid_format(req.format): raise ValueError('FORMAT wrong or missing. Must be one of ' + ', '.join(_get_formats_list())) elif True: From 567d158b1c3b732aeb18dd212fb9a28deb558508 Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 11 Aug 2022 17:21:23 +0200 Subject: [PATCH 11/43] intermediate --- test/webapi/ows/wcs/test_controller.py | 2 +- xcube/webapi/ows/wcs/controllers.py | 65 +++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index 79aababe6..f28302bef 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -280,7 +280,7 @@ def test_get_coverage(self): }) get_coverage(coverage_request, self.wcs_ctx) - def test_translate_requests(self): + def test_translate_request(self): coverage_request = CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 0d6b59916..1a34688b9 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -27,6 +27,7 @@ import warnings from xcube.constants import EXTENSION_POINT_DATASET_IOS +from xcube.core.gen2 import CubeGenerator from xcube.util.plugin import get_extension_registry from xcube.webapi.ows.wcs.context import WcsContext from xcube.webapi.ows.wmts.controllers import get_crs84_bbox @@ -104,19 +105,55 @@ def translate_to_generator_request(req: CoverageRequest) -> str: def get_coverage(req: CoverageRequest, ctx: WcsContext) -> BufferedIOBase: from xcube.core.gen import gen - _validate_coverage_req(req, ctx) + # _validate_coverage_req(req, ctx) - gen_req = translate_to_generator_request(req) + # gen_req = translate_to_generator_request(req) - input_path = _get_input_path(req, ctx) - output_region = _get_output_region(req) - output_variable = [(req.coverage[req.coverage.index('.') + 1:], {})] + # input_path = _get_input_path(req, ctx) + # store_id = _get_input_store_id(req, ctx) + # output_region = _get_output_region(req) + # output_variable = [(req.coverage[req.coverage.index('.') + 1:], {})] # [('conc_chl', None)] - from xcube.core.gen2 import CubeGenerator + from xcube.core.gen2.local.generator import LocalCubeGenerator + from xcube.core.gen2 import CubeGeneratorRequest + req = CubeGeneratorRequest.from_dict( + {'input_config': { + 'store_id': 'file', + 'store_params': { + 'root': '../../../../examples/serve/demo' + }, + 'data_id': 'cube.nc' + }, 'cube_config': { + + }, + "output_config": { + "store_id": "memory", + "replace": True, + "data_id": "mem_cube" + } + }) + gen = CubeGenerator.new() + + # cube = LocalCubeGenerator(raise_on_error=True, verbosity=4).generate_cube(request=req) + cube = gen.generate_cube(request=req) + cube_id = cube.result.data_id + + import xarray as xr + + import xcube.core.store.storepool as sp + instance = sp.get_data_store_instance('memory') + cube_id = list(instance.store.get_data_ids())[0] + cube = instance.store.open_data(cube_id) + # take a look at open_params as 2nd arg to open_data + check_me_out = instance.store.get_open_data_params_schema(cube_id) + #cube2 = xr.open_zarr(cube_id) + #cube = xr.open_dataset(cube_id, backend_kwargs={'group': '\\'}) + print(cube) + dsios = get_extension_registry().find_components( EXTENSION_POINT_DATASET_IOS) for dataset_io in dsios: @@ -125,6 +162,7 @@ def get_coverage(req: CoverageRequest, ctx: WcsContext) -> BufferedIOBase: print(dataset_io) return None + ''' # xcube gen only works with time dim of length 1 @@ -163,12 +201,25 @@ def _get_input_path(req: CoverageRequest, ctx: WcsContext) -> str: path = dataset_config['Path'] break store_instance_id = dataset_config['StoreInstanceId'] - store = ctx.datasets_ctx.get_data_store_pool().\ + store = ctx.datasets_ctx.get_data_store_pool(). \ get_store(store_instance_id) return store.root + '/' + path raise RuntimeError('Should never come here. Contact the developers.') +def _get_input_store_id(req: CoverageRequest, ctx: WcsContext) -> str: + 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['StoreInstanceId'] + raise RuntimeError('Should never come here. Contact the developers.') + + def _validate_coverage_req(req: CoverageRequest, ctx: WcsContext): def _has_no_invalid_bbox() -> bool: if req.bbox: From 59949182345a14b528018fe5c99b2244a87db6de Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 13 Aug 2022 00:53:35 +0200 Subject: [PATCH 12/43] first version of GetCoverage implemented in the backend. Next: create routes for the endpoints. --- test/webapi/ows/wcs/test_controller.py | 60 +++++++---- xcube/webapi/ows/wcs/controllers.py | 141 ++++++++++++------------- 2 files changed, 108 insertions(+), 93 deletions(-) diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index f28302bef..087796146 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -6,6 +6,7 @@ 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_capabilities_xml, \ @@ -19,6 +20,7 @@ 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): @@ -278,36 +280,52 @@ def test_get_coverage(self): 'HEIGHT': 200, 'FORMAT': 'zarr' }) - get_coverage(coverage_request, self.wcs_ctx) + cube = get_coverage(coverage_request, self.wcs_ctx) + 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': 'demo.conc_chl', + 'COVERAGE': f'{coverage}', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' }) - gen_req = translate_to_generator_request(coverage_request) - self.assertEqual('request_json = { \ - "input_config": { \ - "store_id": "file", \ - "store_params": { \ - "root": "../../serve/demo" \ - }, \ - "data_id": "cube.nc" \ - }, \ - "cube_config": {}, \ - "output_config": { \ - "store_id": "file", \ - "store_params": { \ - "root": "." \ - }, \ - "replace": True, \ - "data_id": "cube.zarr" \ - }' - '}', gen_req) + gen_req = translate_to_generator_request(coverage_request, + self.wcs_ctx) + # todo - put generic data store here + 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' + } + }) + self.assertDictEqual(expected.to_dict(), gen_req.to_dict()) def check_xml(self, actual_xml, expected_xml_resource, xsd): self.maxDiff = None diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 1a34688b9..00925fd07 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -18,16 +18,20 @@ # 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 re +import warnings from datetime import datetime -from io import BufferedIOBase -from typing import Dict, List, Any, Union, Optional, Tuple +from typing import Dict, List, Any, Optional import numpy as np -import re -import warnings +from xarray import Dataset +import xcube.core.store.storepool as sp from xcube.constants import EXTENSION_POINT_DATASET_IOS -from xcube.core.gen2 import CubeGenerator +from xcube.core.gen2 import CubeGenerator, OutputConfig +from xcube.core.gen2 import CubeGeneratorRequest +from xcube.core.gen2.local.writer import CubeWriter +from xcube.core.gridmapping import GridMapping from xcube.util.plugin import get_extension_registry from xcube.webapi.ows.wcs.context import WcsContext from xcube.webapi.ows.wmts.controllers import get_crs84_bbox @@ -99,84 +103,64 @@ def get_describe_xml(ctx: WcsContext, coverages: List[str] = None) -> str: return document.to_xml(indent=4) -def translate_to_generator_request(req: CoverageRequest) -> str: - pass - - -def get_coverage(req: CoverageRequest, ctx: WcsContext) -> BufferedIOBase: - from xcube.core.gen import gen - # _validate_coverage_req(req, ctx) - - # gen_req = translate_to_generator_request(req) - - # input_path = _get_input_path(req, ctx) - # store_id = _get_input_store_id(req, ctx) - # output_region = _get_output_region(req) - # output_variable = [(req.coverage[req.coverage.index('.') + 1:], {})] - # [('conc_chl', None)] - - from xcube.core.gen2.local.generator import LocalCubeGenerator - from xcube.core.gen2 import CubeGeneratorRequest - req = CubeGeneratorRequest.from_dict( - {'input_config': { - 'store_id': 'file', - 'store_params': { - 'root': '../../../../examples/serve/demo' +def translate_to_generator_request(req: CoverageRequest, ctx: WcsContext) \ + -> CubeGeneratorRequest: + data_id = _get_input_data_id(req, ctx) + bbox = [] + for v in req.bbox.split(' '): + bbox.append(float(v)) + + return CubeGeneratorRequest.from_dict( + { + # todo - put generic data store here + 'input_config': { + 'store_id': 'file', + 'store_params': { + 'root': '../../../../examples/serve/demo' + }, + 'data_id': f'{data_id}' }, - 'data_id': 'cube.nc' - }, 'cube_config': { - - }, - "output_config": { - "store_id": "memory", - "replace": True, - "data_id": "mem_cube" + 'cube_config': { + 'variable_names': [f'{req.coverage}'.split('.')[-1]], + 'crs': f'{req.crs}', + 'bbox': tuple(bbox) + }, + 'output_config': { + 'store_id': 'memory', + 'replace': True, + 'data_id': f'{req.coverage}.zarr', } - }) - gen = CubeGenerator.new() - - # cube = LocalCubeGenerator(raise_on_error=True, verbosity=4).generate_cube(request=req) - cube = gen.generate_cube(request=req) - cube_id = cube.result.data_id + } + ) - import xarray as xr - import xcube.core.store.storepool as sp - instance = sp.get_data_store_instance('memory') - cube_id = list(instance.store.get_data_ids())[0] - cube = instance.store.open_data(cube_id) - # take a look at open_params as 2nd arg to open_data - check_me_out = instance.store.get_open_data_params_schema(cube_id) +def get_coverage(req: CoverageRequest, ctx: WcsContext) -> Dataset: + _validate_coverage_req(req, ctx) + gen_req = translate_to_generator_request(req, ctx) + gen = CubeGenerator.new() + result = gen.generate_cube(request=gen_req) + if not result.status == 'ok': + raise ValueError(f'Failed to generate cube: {result.message}') - #cube2 = xr.open_zarr(cube_id) - #cube = xr.open_dataset(cube_id, backend_kwargs={'group': '\\'}) - print(cube) + memory_store = sp.get_data_store_instance('memory') + cube_id = list(memory_store.store.get_data_ids())[0] + cube = memory_store.store.open_data(cube_id) - dsios = get_extension_registry().find_components( - EXTENSION_POINT_DATASET_IOS) - for dataset_io in dsios: - if dataset_io.name.lower() == 'mem': - break - print(dataset_io) - return None + #_write_debug_output(cube) + return cube -''' - # xcube gen only works with time dim of length 1 - # so need to ask for time constraint in WCS request - # then use raster closest in time - # or don't do that because it's wrong - # todo - set output dir, will be created - gen_status = gen.gen_cube([input_path], - output_region=output_region, - output_variables=output_variable, - output_writer_name='mem' - ) - print(str(gen_status)) -''' +def _write_debug_output(cube): + history = str(cube.history[0]) + del cube.attrs['history'] + cube['history'] = history + cw = CubeWriter(OutputConfig('file', + writer_id='dataset:netcdf:file', + data_id='/../../../test_cube.nc')) + cw.write_cube(cube, GridMapping.from_dataset(cube)) def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: @@ -189,6 +173,19 @@ def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: return tuple(output_region) +def _get_input_data_id(req: CoverageRequest, ctx: WcsContext) -> str: + 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['Path'] + raise RuntimeError('Should never come here. Contact the developers.') + + def _get_input_path(req: CoverageRequest, ctx: WcsContext) -> str: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] From 5b2d834f4e1bead717b2705c0d8e3022235df455 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 13 Sep 2022 21:48:45 +0200 Subject: [PATCH 13/43] integrated some NF comments --- examples/serve/demo/config-serve2.yml | 6 -- test/webapi/ows/wcs/test_controller.py | 75 ++++++++++++------------ xcube/webapi/ows/wcs/controllers.py | 80 +++++++++++++------------- 3 files changed, 77 insertions(+), 84 deletions(-) diff --git a/examples/serve/demo/config-serve2.yml b/examples/serve/demo/config-serve2.yml index dd93563df..6e1256232 100644 --- a/examples/serve/demo/config-serve2.yml +++ b/examples/serve/demo/config-serve2.yml @@ -15,9 +15,3 @@ DataStores: Style: "default" - Path: "*.levels" Style: "default" - -# - store_id: s3 -# store_params: -# root: xcube-examples -# datasets: -# - data_id: OLCI-SNS-RAW-CUBE-2.zarr diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index 087796146..b73888acf 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -24,9 +24,8 @@ def setUp(self) -> None: self.wcs_ctx = get_api_ctx('ows.wcs', WcsContext) def test_get_capabilities(self): - actual_xml = get_capabilities_xml( - self.wcs_ctx, 'https://xcube.brockmann-consult.de/wcs/kvp' - ) + actual_xml = get_capabilities_xml(self.wcs_ctx, + 'https://xcube.brockmann-consult.de/wcs/kvp') self.check_xml(actual_xml, 'WCSCapabilities.xml', 'wcsCapabilities.xsd') @@ -46,17 +45,17 @@ def test_describe_coverage_subset(self): def test_validate_coverage_request(self): # request is fine - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) # TIME given in addition to BBOX -> fine - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', @@ -64,17 +63,17 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) # COVERAGE is missing -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('No valid value for parameter COVERAGE provided. ' @@ -84,14 +83,14 @@ def test_validate_coverage_request(self): # COVERAGE is given but not found -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'invalid_coverage!', 'CRS': 'EPSG:4326', 'BBOX': '1 51 4 52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('No valid value for parameter COVERAGE provided. ' @@ -100,25 +99,25 @@ def test_validate_coverage_request(self): str(e)) # TIME is used instead of BBOX -> fine - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '2020-01-28', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) # use invalid TIME format -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '20201208', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('TIME value must be given in the format' @@ -127,18 +126,18 @@ def test_validate_coverage_request(self): str(e)) # RESX and RESY are given instead of W/H -> fine - _validate_coverage_req(CoverageRequest({ + _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': 'zarr' - }), self.wcs_ctx) + })) # PARAMETER is given -> expect a failure (not yet supported) try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'PARAMETER': 'I expect nothing from using this parameter', 'CRS': 'EPSG:4326', @@ -146,21 +145,21 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) 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(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10,3,-5,4', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('BBOX must be given as `minx miny maxx maxy`', @@ -168,14 +167,14 @@ def test_validate_coverage_request(self): # WIDTH, but not HEIGHT is given -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10 3 -5 4', 'WIDTH': 200, 'RESY': 156.45, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and ' @@ -183,13 +182,13 @@ def test_validate_coverage_request(self): # HEIGHT, but not WIDTH is given -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10 3 -5 4', 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and ' @@ -197,7 +196,7 @@ def test_validate_coverage_request(self): # WIDTH and HEIGHT and RESX and RESY are given -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'BBOX': '-10 3 -5 4', @@ -206,7 +205,7 @@ def test_validate_coverage_request(self): 'RESX': 200, 'RESY': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('Either both WIDTH and HEIGHT, or both RESX and ' @@ -214,7 +213,7 @@ def test_validate_coverage_request(self): # INTERPOLATION is given -> expect a failure (not yet supported) try: - _validate_coverage_req(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'INTERPOLATION': 'Farest Neighbor', 'CRS': 'EPSG:4326', @@ -222,14 +221,14 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) 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(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'EXCEPTIONS': 'exceptions', 'CRS': 'EPSG:4326', @@ -237,20 +236,20 @@ def test_validate_coverage_request(self): 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' - }), self.wcs_ctx) + })) 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(CoverageRequest({ + _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, - }), self.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' @@ -258,14 +257,14 @@ def test_validate_coverage_request(self): # FORMAT is invalid -> expect a failure try: - _validate_coverage_req(CoverageRequest({ + _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.wcs_ctx) + })) self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' @@ -280,7 +279,7 @@ def test_get_coverage(self): 'HEIGHT': 200, 'FORMAT': 'zarr' }) - cube = get_coverage(coverage_request, self.wcs_ctx) + cube = get_coverage(self.wcs_ctx, coverage_request) self.assertIsNotNone(cube.coords) self.assertDictEqual( { @@ -304,8 +303,8 @@ def test_translate_request(self): 'HEIGHT': 200, 'FORMAT': 'zarr' }) - gen_req = translate_to_generator_request(coverage_request, - self.wcs_ctx) + gen_req = translate_to_generator_request(self.wcs_ctx, + coverage_request) # todo - put generic data store here expected = CubeGeneratorRequest.from_dict( {'input_config': { diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 00925fd07..46d7442d7 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -103,9 +103,9 @@ def get_describe_xml(ctx: WcsContext, coverages: List[str] = None) -> str: return document.to_xml(indent=4) -def translate_to_generator_request(req: CoverageRequest, ctx: WcsContext) \ +def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ -> CubeGeneratorRequest: - data_id = _get_input_data_id(req, ctx) + data_id = _get_input_data_id(ctx, req) bbox = [] for v in req.bbox.split(' '): bbox.append(float(v)) @@ -134,9 +134,9 @@ def translate_to_generator_request(req: CoverageRequest, ctx: WcsContext) \ ) -def get_coverage(req: CoverageRequest, ctx: WcsContext) -> Dataset: - _validate_coverage_req(req, ctx) - gen_req = translate_to_generator_request(req, ctx) +def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: + _validate_coverage_req(ctx, req) + gen_req = translate_to_generator_request(ctx, req) gen = CubeGenerator.new() @@ -148,7 +148,7 @@ def get_coverage(req: CoverageRequest, ctx: WcsContext) -> Dataset: cube_id = list(memory_store.store.get_data_ids())[0] cube = memory_store.store.open_data(cube_id) - #_write_debug_output(cube) + # _write_debug_output(cube) return cube @@ -173,7 +173,7 @@ def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: return tuple(output_region) -def _get_input_data_id(req: CoverageRequest, ctx: WcsContext) -> str: +def _get_input_data_id(ctx: WcsContext, req: CoverageRequest) -> str: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] ds = ctx.datasets_ctx.get_dataset(ds_name) @@ -186,7 +186,7 @@ def _get_input_data_id(req: CoverageRequest, ctx: WcsContext) -> str: raise RuntimeError('Should never come here. Contact the developers.') -def _get_input_path(req: CoverageRequest, ctx: WcsContext) -> str: +def _get_input_path(ctx: WcsContext, req: CoverageRequest) -> str: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] ds = ctx.datasets_ctx.get_dataset(ds_name) @@ -204,7 +204,7 @@ def _get_input_path(req: CoverageRequest, ctx: WcsContext) -> str: raise RuntimeError('Should never come here. Contact the developers.') -def _get_input_store_id(req: CoverageRequest, ctx: WcsContext) -> str: +def _get_input_store_id(ctx: WcsContext, req: CoverageRequest) -> str: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] ds = ctx.datasets_ctx.get_dataset(ds_name) @@ -217,41 +217,41 @@ def _get_input_store_id(req: CoverageRequest, ctx: WcsContext) -> str: raise RuntimeError('Should never come here. Contact the developers.') -def _validate_coverage_req(req: CoverageRequest, ctx: WcsContext): +def _validate_coverage_req(ctx: WcsContext, req: CoverageRequest): def _has_no_invalid_bbox() -> bool: if req.bbox: - return is_valid_bbox(req.bbox) + return _is_valid_bbox(req.bbox) else: return True def _has_no_invalid_time() -> bool: if req.time: - return is_valid_time(req.time) + return _is_valid_time(req.time) else: return True - if req.coverage and is_valid_coverage(req.coverage, ctx) \ - and req.crs and is_valid_crs(req.crs) \ - and ((req.bbox and is_valid_bbox(req.bbox)) or - (req.time and is_valid_time(req.time))) \ + if req.coverage and _is_valid_coverage(ctx, req.coverage) \ + and req.crs and _is_valid_crs(req.crs) \ + and ((req.bbox and _is_valid_bbox(req.bbox)) + or (req.time and _is_valid_time(req.time))) \ and _has_no_invalid_bbox \ and _has_no_invalid_time() \ - and ((req.width and req.height) or - (req.resx and req.resy)) \ - and ((req.width and not req.resx) or - (req.width and not req.resy) or - (req.height and not req.resx) or - (req.height and not req.resy) or - (req.resx and not req.width) or - (req.resx and not req.height) or - (req.resy and not req.width) or - (req.resy and not req.height)) \ - and req.format and is_valid_format(req.format) \ + and ((req.width and req.height) + or (req.resx and req.resy)) \ + and ((req.width and not req.resx) + or (req.width and not req.resy) + or (req.height and not req.resx) + or (req.height and not req.resy) + or (req.resx and not req.width) + or (req.resx and not req.height) + or (req.resy and not req.width) + or(req.resy and not req.height)) \ + and req.format and _is_valid_format(req.format) \ and not req.parameter \ and not req.interpolation \ and not req.exceptions: return - elif not req.coverage or not is_valid_coverage(req.coverage, ctx): + elif not req.coverage or not _is_valid_coverage(ctx, req.coverage): raise ValueError('No valid value for parameter COVERAGE provided. ' 'COVERAGE must be a variable name prefixed with ' 'its dataset name. Example: my_dataset.my_var') @@ -261,44 +261,44 @@ def _has_no_invalid_time() -> bool: raise ValueError('INTERPOLATION not yet supported') elif req.exceptions: raise ValueError('EXCEPTIONS not yet supported') - elif ((req.width and not req.height) or - (req.height and not req.width) or - (req.resx and not req.resy) or - (req.resy and not req.resx) or - (req.width and req.resx or req.resy) or - (req.height and req.resx or req.resy)): + elif ((req.width and not req.height) + or (req.height and not req.width) + or (req.resx and not req.resy) + or (req.resy and not req.resx) + or (req.width and req.resx or req.resy) + or (req.height and req.resx or req.resy)): raise ValueError('Either both WIDTH and HEIGHT, or both RESX and RESY ' 'must be provided.') - elif not req.format or not is_valid_format(req.format): + elif not req.format or not _is_valid_format(req.format): raise ValueError('FORMAT wrong or missing. Must be one of ' + ', '.join(_get_formats_list())) elif True: raise ValueError('Reason unclear, fix me') -def is_valid_coverage(coverage: str, ctx: WcsContext) -> bool: +def _is_valid_coverage(ctx: WcsContext, coverage: str) -> bool: band_infos = _extract_band_infos(ctx, [coverage]) if band_infos: return True return False -def is_valid_crs(crs: str) -> bool: +def _is_valid_crs(crs: str) -> bool: return crs in VALID_CRS_LIST -def is_valid_bbox(bbox: str) -> bool: +def _is_valid_bbox(bbox: str) -> bool: bbox_regex = re.compile(r'-?\d{1,3} -?\d{1,2} -?\d{1,3} -?\d{1,2}') if not bbox_regex.match(bbox): raise ValueError('BBOX must be given as `minx miny maxx maxy`') return True -def is_valid_format(format_req: str) -> bool: +def _is_valid_format(format_req: str) -> bool: return format_req in _get_formats_list() -def is_valid_time(time: str) -> bool: +def _is_valid_time(time: str) -> bool: try: datetime.fromisoformat(time) except ValueError: From 0a313abecfb7a0a142273da1b89e8738c3ad8857 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 13 Sep 2022 23:35:28 +0200 Subject: [PATCH 14/43] added time information to DescribeCoverage --- test/webapi/ows/res/WCSDescribe.xml | 140 ++++++++++++++++++++++++++++ xcube/webapi/ows/wcs/controllers.py | 19 +++- 2 files changed, 154 insertions(+), 5 deletions(-) diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml index cbeebadc9..7fcfe2945 100644 --- a/test/webapi/ows/res/WCSDescribe.xml +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -16,6 +16,18 @@ 5 52.5 + + 2017-01-16T10:09:21.834255872 + + 2017-01-25T09:35:51.060063488 + + 2017-01-26T10:50:16.686192896 + + 2017-01-28T09:58:11.350386176 + + 2017-01-30T10:46:33.836892416 + + @@ -58,6 +70,18 @@ 5 52.5 + + 2017-01-16T10:09:21.834255872 + + 2017-01-25T09:35:51.060063488 + + 2017-01-26T10:50:16.686192896 + + 2017-01-28T09:58:11.350386176 + + 2017-01-30T10:46:33.836892416 + + @@ -100,6 +124,18 @@ 5 52.5 + + 2017-01-16T10:09:21.834255872 + + 2017-01-25T09:35:51.060063488 + + 2017-01-26T10:50:16.686192896 + + 2017-01-28T09:58:11.350386176 + + 2017-01-30T10:46:33.836892416 + + @@ -142,6 +178,18 @@ 5 52.5 + + 2017-01-16T10:09:21.834255872 + + 2017-01-25T09:35:51.060063488 + + 2017-01-26T10:50:16.686192896 + + 2017-01-28T09:58:11.350386176 + + 2017-01-30T10:46:33.836892416 + + @@ -184,6 +232,18 @@ 5 52.5 + + 2017-01-16T10:09:21.834255872 + + 2017-01-25T09:35:51.060063488 + + 2017-01-26T10:50:16.686192896 + + 2017-01-28T09:58:11.350386176 + + 2017-01-30T10:46:33.836892416 + + @@ -226,6 +286,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -268,6 +336,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -310,6 +386,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -352,6 +436,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -394,6 +486,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -436,6 +536,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -478,6 +586,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -520,6 +636,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -562,6 +686,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + @@ -604,6 +736,14 @@ 5 52.5 + + 2017-01-22T00:00:00.000000000 + + 2017-01-29T00:00:00.000000000 + + 2017-02-05T00:00:00.000000000 + + diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 46d7442d7..86ff5be27 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -245,7 +245,7 @@ def _has_no_invalid_time() -> bool: or (req.resx and not req.width) or (req.resx and not req.height) or (req.resy and not req.width) - or(req.resy and not req.height)) \ + or (req.resy and not req.height)) \ and req.format and _is_valid_format(req.format) \ and not req.parameter \ and not req.interpolation \ @@ -481,7 +481,6 @@ def _get_capability_element(base_url: str) -> Element: ]) -# noinspection HttpUrlsUsage def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ -> Element: coverage_elements = [] @@ -507,7 +506,10 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ text=f'{band_infos[var_name].bbox[2]} ' f'{band_infos[var_name].bbox[3]}') ]) - ]) + ]), + 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=[ @@ -559,12 +561,14 @@ def _get_formats_list() -> List[str]: class BandInfo: def __init__(self, var_name: str, label: str, - bbox: tuple[float, float, float, float]): + bbox: tuple[float, float, float, float], + time_steps: list[str]): self.var_name = var_name self.label = label self.bbox = bbox self.min = np.nan self.max = np.nan + self.time_steps = time_steps def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, @@ -599,7 +603,12 @@ def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, if not is_spatial_var: continue - band_info = BandInfo(qualified_var_name, label, bbox) + is_temporal_var = var.ndim >= 3 + time_steps = None + if is_temporal_var: + time_steps = [str(d) for d in var.time.values] + + band_info = BandInfo(qualified_var_name, label, bbox, time_steps) if full: nn_values = var.values[~np.isnan(var.values)] band_info.min = nn_values.min() From 0d6235235bab138075c3844e1970d04400fff51e Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 15 Sep 2022 09:32:52 +0200 Subject: [PATCH 15/43] started to implement routes --- test/webapi/ows/res/WCSDescribe.xml | 110 ++++++++++---------- test/webapi/ows/res/WCSDescribe_subset.xml | 40 ++++++++ test/webapi/ows/wcs/test_controller.py | 12 +-- xcube/webapi/ows/wcs/controllers.py | 10 +- xcube/webapi/ows/wcs/routes.py | 113 ++++++++++++++++++++- 5 files changed, 219 insertions(+), 66 deletions(-) diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml index 7fcfe2945..6e8a12463 100644 --- a/test/webapi/ows/res/WCSDescribe.xml +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -17,15 +17,15 @@ - 2017-01-16T10:09:21.834255872 + 2017-01-16T10:09:21Z - 2017-01-25T09:35:51.060063488 + 2017-01-25T09:35:51Z - 2017-01-26T10:50:16.686192896 + 2017-01-26T10:50:16Z - 2017-01-28T09:58:11.350386176 + 2017-01-28T09:58:11Z - 2017-01-30T10:46:33.836892416 + 2017-01-30T10:46:33Z @@ -71,15 +71,15 @@ - 2017-01-16T10:09:21.834255872 + 2017-01-16T10:09:21Z - 2017-01-25T09:35:51.060063488 + 2017-01-25T09:35:51Z - 2017-01-26T10:50:16.686192896 + 2017-01-26T10:50:16Z - 2017-01-28T09:58:11.350386176 + 2017-01-28T09:58:11Z - 2017-01-30T10:46:33.836892416 + 2017-01-30T10:46:33Z @@ -125,15 +125,15 @@ - 2017-01-16T10:09:21.834255872 + 2017-01-16T10:09:21Z - 2017-01-25T09:35:51.060063488 + 2017-01-25T09:35:51Z - 2017-01-26T10:50:16.686192896 + 2017-01-26T10:50:16Z - 2017-01-28T09:58:11.350386176 + 2017-01-28T09:58:11Z - 2017-01-30T10:46:33.836892416 + 2017-01-30T10:46:33Z @@ -179,15 +179,15 @@ - 2017-01-16T10:09:21.834255872 + 2017-01-16T10:09:21Z - 2017-01-25T09:35:51.060063488 + 2017-01-25T09:35:51Z - 2017-01-26T10:50:16.686192896 + 2017-01-26T10:50:16Z - 2017-01-28T09:58:11.350386176 + 2017-01-28T09:58:11Z - 2017-01-30T10:46:33.836892416 + 2017-01-30T10:46:33Z @@ -233,15 +233,15 @@ - 2017-01-16T10:09:21.834255872 + 2017-01-16T10:09:21Z - 2017-01-25T09:35:51.060063488 + 2017-01-25T09:35:51Z - 2017-01-26T10:50:16.686192896 + 2017-01-26T10:50:16Z - 2017-01-28T09:58:11.350386176 + 2017-01-28T09:58:11Z - 2017-01-30T10:46:33.836892416 + 2017-01-30T10:46:33Z @@ -287,11 +287,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -337,11 +337,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -387,11 +387,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -437,11 +437,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -487,11 +487,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -537,11 +537,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -587,11 +587,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -637,11 +637,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -687,11 +687,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z @@ -737,11 +737,11 @@ - 2017-01-22T00:00:00.000000000 + 2017-01-22T00:00:00Z - 2017-01-29T00:00:00.000000000 + 2017-01-29T00:00:00Z - 2017-02-05T00:00:00.000000000 + 2017-02-05T00:00:00Z diff --git a/test/webapi/ows/res/WCSDescribe_subset.xml b/test/webapi/ows/res/WCSDescribe_subset.xml index 4a90f27be..8fb265e20 100644 --- a/test/webapi/ows/res/WCSDescribe_subset.xml +++ b/test/webapi/ows/res/WCSDescribe_subset.xml @@ -3,12 +3,16 @@ xmlns:gml="http://www.opengis.net/gml" version="1.0.0"> + Chlorophyll concentration demo.conc_chl 0 50 5 52.5 + + grid + @@ -16,6 +20,18 @@ 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 + + @@ -45,12 +61,16 @@ + kd489_stdev demo-1w.kd489_stdev 0 50 5 52.5 + + grid + @@ -58,6 +78,14 @@ 5 52.5 + + 2017-01-22T00:00:00Z + + 2017-01-29T00:00:00Z + + 2017-02-05T00:00:00Z + + @@ -87,12 +115,16 @@ + quality_flags_stdev demo-1w.quality_flags_stdev 0 50 5 52.5 + + grid + @@ -100,6 +132,14 @@ 5 52.5 + + 2017-01-22T00:00:00Z + + 2017-01-29T00:00:00Z + + 2017-02-05T00:00:00Z + + diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index b73888acf..f18fbec06 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -9,10 +9,10 @@ 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_capabilities_xml, \ +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_xml +from xcube.webapi.ows.wcs.controllers import get_describe_coverage_xml # noinspection PyMethodMayBeStatic @@ -24,19 +24,19 @@ def setUp(self) -> None: self.wcs_ctx = get_api_ctx('ows.wcs', WcsContext) def test_get_capabilities(self): - actual_xml = get_capabilities_xml(self.wcs_ctx, + actual_xml = get_wcs_capabilities_xml(self.wcs_ctx, 'https://xcube.brockmann-consult.de/wcs/kvp') self.check_xml(actual_xml, 'WCSCapabilities.xml', 'wcsCapabilities.xsd') def test_describe_coverage(self): - actual_xml = get_describe_xml(self.wcs_ctx) + 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_xml(self.wcs_ctx, - [ + actual_xml = get_describe_coverage_xml(self.wcs_ctx, + [ 'demo-1w.quality_flags_stdev', 'demo-1w.kd489_stdev', 'demo.conc_chl' diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 86ff5be27..328c80ed8 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -83,7 +83,7 @@ def __init__(self, req: Dict[str, Any]): self.exceptions = req['EXCEPTIONS'] -def get_capabilities_xml(ctx: WcsContext, base_url: str) -> str: +def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str: """ Get WCSCapabilities.xml according to https://schemas.opengis.net/wcs/1.0.0/. @@ -97,7 +97,7 @@ def get_capabilities_xml(ctx: WcsContext, base_url: str) -> str: return document.to_xml(indent=4) -def get_describe_xml(ctx: WcsContext, coverages: List[str] = None) -> str: +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) @@ -488,6 +488,7 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ band_infos = _extract_band_infos(ctx, coverages, True) 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=[ @@ -496,6 +497,9 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ Element('gml:pos', text=f'{band_infos[var_name].bbox[2]} ' f'{band_infos[var_name].bbox[3]}') ]), + Element('keywords', elements=[ + Element('keyword', text='grid') + ]), Element('domainSet', elements=[ Element('spatialDomain', elements=[ Element('gml:Envelope', elements=[ @@ -606,7 +610,7 @@ def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, is_temporal_var = var.ndim >= 3 time_steps = None if is_temporal_var: - time_steps = [str(d) for d in var.time.values] + time_steps = [f'{str(d)[:19]}Z' for d in var.time.values] band_info = BandInfo(qualified_var_name, label, bbox, time_steps) if full: diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index 20f86006d..9a9353c29 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -19,9 +19,13 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from xcube.server.api import ApiHandler +from xcube.server.api import ApiHandler, ApiError from .api import api from .context import WcsContext +from .controllers import get_wcs_capabilities_xml, get_describe_coverage_xml, \ + get_coverage, translate_to_generator_request, CoverageRequest + +WCS_VERSION = '1.0.0' @api.route('/wcs/1.0.0/WCSCapabilities.xml') @@ -29,4 +33,109 @@ class WcsCapabilitiesXmlHandler(ApiHandler[WcsContext]): @api.operation(operationId='getWcsCapabilities', summary='Gets the WCS capabilities as XML document') async def get(self): - pass + self.request.make_query_lower_case() + capabilities = await self.ctx.run_in_executor( + None, + get_wcs_capabilities_xml, + self.ctx, + self.request.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"' + ) + request = self.request.get_query_arg('request') + if request is None: + request = self.request.get_query_arg('REQUEST') + if request == "GetCapabilities": + 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.base_url + ) + self.response.set_header('Content-Type', 'application/xml') + await self.response.finish(capabilities_xml) + + elif request == 'DescribeCoverage': + 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 not coverages: + 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) + elif request == "GetCoverage": + 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( + f'missing query argument "coverage"' + ) + request_crs = self.request.get_query_arg('crs') + if not request_crs: + raise ApiError.BadRequest( + f'missing query argument "crs"' + ) + response_crs = self.request.get_query_arg('response_crs', + default=request_crs) + time = self.request.get_query_arg('time') + + cov_req = CoverageRequest({ + 'COVERAGE': coverage, + 'CRS': response_crs, + 'TIME': time + }) + gen_request = translate_to_generator_request(self.ctx, cov_req) + coverage = await self.ctx.run_in_executor( + None, + get_coverage, + self.ctx, + gen_request + ) + # self.response.set_header('Content-Type', 'application/xml') + await self.response.finish(get_coverage) + else: + raise ApiError.BadRequest( + f'invalid request type "{request}"' + ) + + +def _query_to_dict(request): + return {k: v[0] for k, v in request.query.items()} From 88c836e534f102b698ca3904820a61ab4251af7b Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 15 Sep 2022 14:15:52 +0200 Subject: [PATCH 16/43] added todo --- xcube/server/webservers/tornado.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xcube/server/webservers/tornado.py b/xcube/server/webservers/tornado.py index 805e96374..9731b0cf6 100644 --- a/xcube/server/webservers/tornado.py +++ b/xcube/server/webservers/tornado.py @@ -355,6 +355,7 @@ def __init__(self, request: tornado.httputil.HTTPServerRequest): # 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 From 3fd42907df4bb5d9ae69d180f7acf21e0284fe09 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 16 Sep 2022 22:05:57 +0200 Subject: [PATCH 17/43] ctd. WCS dev --- test/webapi/ows/res/WCSDescribe.xml | 45 ++++++++++++------ test/webapi/ows/res/WCSDescribe_subset.xml | 15 ++---- test/webapi/ows/wcs/test_controller.py | 38 ++++++++------- xcube/webapi/ows/wcs/controllers.py | 55 ++++++++++++++-------- xcube/webapi/ows/wcs/routes.py | 31 +++++++----- 5 files changed, 109 insertions(+), 75 deletions(-) diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml index 6e8a12463..df246223f 100644 --- a/test/webapi/ows/res/WCSDescribe.xml +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -3,6 +3,7 @@ xmlns:gml="http://www.opengis.net/gml" version="1.0.0"> + C2RCC quality flags demo.c2rcc_flags @@ -48,7 +49,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -57,6 +58,7 @@ + Chlorophyll concentration demo.conc_chl @@ -102,7 +104,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -111,6 +113,7 @@ + Total suspended matter dry weight concentration demo.conc_tsm @@ -156,7 +159,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -165,6 +168,7 @@ + Irradiance attenuation coefficient at 489 nm demo.kd489 @@ -210,7 +214,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -219,6 +223,7 @@ + Classification and quality flags demo.quality_flags @@ -264,7 +269,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -273,6 +278,7 @@ + c2rcc_flags demo-1w.c2rcc_flags @@ -314,7 +320,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -323,6 +329,7 @@ + c2rcc_flags_stdev demo-1w.c2rcc_flags_stdev @@ -364,7 +371,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -373,6 +380,7 @@ + conc_chl demo-1w.conc_chl @@ -414,7 +422,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -423,6 +431,7 @@ + conc_chl_stdev demo-1w.conc_chl_stdev @@ -464,7 +473,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -473,6 +482,7 @@ + conc_tsm demo-1w.conc_tsm @@ -514,7 +524,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -523,6 +533,7 @@ + conc_tsm_stdev demo-1w.conc_tsm_stdev @@ -564,7 +575,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -573,6 +584,7 @@ + kd489 demo-1w.kd489 @@ -614,7 +626,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -623,6 +635,7 @@ + kd489_stdev demo-1w.kd489_stdev @@ -664,7 +677,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -673,6 +686,7 @@ + quality_flags demo-1w.quality_flags @@ -714,7 +728,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -723,6 +737,7 @@ + quality_flags_stdev demo-1w.quality_flags_stdev @@ -764,7 +779,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr diff --git a/test/webapi/ows/res/WCSDescribe_subset.xml b/test/webapi/ows/res/WCSDescribe_subset.xml index 8fb265e20..7c0f09663 100644 --- a/test/webapi/ows/res/WCSDescribe_subset.xml +++ b/test/webapi/ows/res/WCSDescribe_subset.xml @@ -10,9 +10,6 @@ 0 50 5 52.5 - - grid - @@ -52,7 +49,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -68,9 +65,6 @@ 0 50 5 52.5 - - grid - @@ -106,7 +100,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr @@ -122,9 +116,6 @@ 0 50 5 52.5 - - grid - @@ -160,7 +151,7 @@ - EPSG:4326 EPSG:3857 + EPSG:4326 zarr diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index f18fbec06..700669d62 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -24,8 +24,8 @@ def setUp(self) -> 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/wcs/kvp') + actual_xml = get_wcs_capabilities_xml( + self.wcs_ctx, 'https://xcube.brockmann-consult.de') self.check_xml(actual_xml, 'WCSCapabilities.xml', 'wcsCapabilities.xsd') @@ -48,7 +48,7 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '1 51 4 52', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' @@ -58,7 +58,7 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '1 51 4 52', + 'BBOX': '1,51,4,52', 'TIME': '2017-01-28 20:23:55.123456', 'WIDTH': 200, 'HEIGHT': 200, @@ -69,7 +69,7 @@ def test_validate_coverage_request(self): try: _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'CRS': 'EPSG:4326', - 'BBOX': '1 51 4 52', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' @@ -86,7 +86,7 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'invalid_coverage!', 'CRS': 'EPSG:4326', - 'BBOX': '1 51 4 52', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' @@ -155,14 +155,14 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '-10,3,-5,4', + 'BBOX': '-10 3 -5 4', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' })) self.fail('Classified invalid request as valid.') except ValueError as e: - self.assertEqual('BBOX must be given as `minx miny maxx maxy`', + self.assertEqual('BBOX must be given as `minx,miny,maxx,maxy`', str(e)) # WIDTH, but not HEIGHT is given -> expect a failure @@ -170,7 +170,7 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '-10 3 -5 4', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'RESY': 156.45, 'FORMAT': 'zarr' @@ -185,7 +185,7 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '-10 3 -5 4', + 'BBOX': '1,51,4,52', 'HEIGHT': 200, 'FORMAT': 'zarr' })) @@ -199,7 +199,7 @@ def test_validate_coverage_request(self): _validate_coverage_req(self.wcs_ctx, CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '-10 3 -5 4', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, 'RESX': 200, @@ -253,7 +253,7 @@ def test_validate_coverage_request(self): self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' - 'netcdf4, csv', str(e)) + 'netcdf4, csv. Was: None', str(e)) # FORMAT is invalid -> expect a failure try: @@ -268,13 +268,13 @@ def test_validate_coverage_request(self): self.fail('Classified invalid request as valid.') except ValueError as e: self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' - 'netcdf4, csv', str(e)) + 'netcdf4, csv. Was: MettCDF', str(e)) def test_get_coverage(self): coverage_request = CoverageRequest({ 'COVERAGE': 'demo.conc_chl', 'CRS': 'EPSG:4326', - 'BBOX': '1 51 4 52', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' @@ -298,14 +298,13 @@ def test_translate_request(self): coverage_request = CoverageRequest({ 'COVERAGE': f'{coverage}', 'CRS': 'EPSG:4326', - 'BBOX': '1 51 4 52', + 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, 'FORMAT': 'zarr' }) gen_req = translate_to_generator_request(self.wcs_ctx, coverage_request) - # todo - put generic data store here expected = CubeGeneratorRequest.from_dict( {'input_config': { 'store_id': 'file', @@ -324,7 +323,12 @@ def test_translate_request(self): 'data_id': f'{coverage}.zarr' } }) - self.assertDictEqual(expected.to_dict(), gen_req.to_dict()) + 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 diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 328c80ed8..6c4c6c3b0 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -39,7 +39,8 @@ from xcube.webapi.xml import Element WCS_VERSION = '1.0.0' -VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857'] +VALID_CRS_LIST = ['EPSG:4326'] +#VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857'] class CoverageRequest: @@ -107,16 +108,20 @@ def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ -> CubeGeneratorRequest: data_id = _get_input_data_id(ctx, req) bbox = [] - for v in req.bbox.split(' '): + for v in req.bbox.split(','): bbox.append(float(v)) + store_pool = ctx.datasets_ctx.get_data_store_pool() + store_id = _get_store_id(store_pool, data_id) + datastore_root = store_pool.get_store_config(store_id).store_params['root'] + store_config_id = store_pool.get_store_config(store_id).store_id + return CubeGeneratorRequest.from_dict( { - # todo - put generic data store here 'input_config': { - 'store_id': 'file', + 'store_id': store_config_id, 'store_params': { - 'root': '../../../../examples/serve/demo' + 'root': datastore_root }, 'data_id': f'{data_id}' }, @@ -163,6 +168,14 @@ def _write_debug_output(cube): cw.write_cube(cube, GridMapping.from_dataset(cube)) +def _get_store_id(store_pool: sp.DataStorePool, data_id: str) -> str: + for store_id in store_pool.store_instance_ids: + current_store = store_pool.get_store(store_id) + if current_store.has_data(data_id): + return store_id + raise ValueError(f'{data_id} not found in any available data store') + + def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: if not req.bbox: return None @@ -261,7 +274,8 @@ def _has_no_invalid_time() -> bool: raise ValueError('INTERPOLATION not yet supported') elif req.exceptions: raise ValueError('EXCEPTIONS not yet supported') - elif ((req.width and not req.height) + elif ((not req.width and not req.height and not req.resy and not req.resy) + or (req.width and not req.height) or (req.height and not req.width) or (req.resx and not req.resy) or (req.resy and not req.resx) @@ -270,9 +284,10 @@ def _has_no_invalid_time() -> bool: raise ValueError('Either both WIDTH and HEIGHT, or both RESX and RESY ' 'must be provided.') elif not req.format or not _is_valid_format(req.format): - raise ValueError('FORMAT wrong or missing. Must be one of ' + - ', '.join(_get_formats_list())) - elif True: + raise ValueError('FORMAT wrong or missing. Must be one of ' + + ', '.join(_get_formats_list()) + + f'. Was: {req.format}') + else: raise ValueError('Reason unclear, fix me') @@ -288,9 +303,9 @@ def _is_valid_crs(crs: str) -> bool: def _is_valid_bbox(bbox: str) -> bool: - bbox_regex = re.compile(r'-?\d{1,3} -?\d{1,2} -?\d{1,3} -?\d{1,2}') - if not bbox_regex.match(bbox): - raise ValueError('BBOX must be given as `minx miny maxx maxy`') + values = bbox.split(',') + if not len(values) == 4: + raise ValueError('BBOX must be given as `minx,miny,maxx,maxy`') return True @@ -299,6 +314,7 @@ def _is_valid_format(format_req: str) -> bool: def _is_valid_time(time: str) -> bool: + # todo - change so that it works with qgis try: datetime.fromisoformat(time) except ValueError: @@ -436,11 +452,11 @@ def _get_individual_name(): def _get_capability_element(base_url: str) -> Element: - get_capabilities_url = f'{base_url}?service=WCS&version=1.0.0&' \ - f'request=GetCapabilities' - describe_url = f'{base_url}?service=WCS&version=1.0.0&' \ + get_capabilities_url = f'{base_url}/wcs/kvp?service=WCS&version=1.0.0'\ + f'&request=GetCapabilities' + describe_url = f'{base_url}/wcs/kvp?service=WCS&version=1.0.0&' \ f'request=DescribeCoverage' - get_url = f'{base_url}?service=WCS&version=1.0.0&' \ + get_url = f'{base_url}/wcs/kvp?service=WCS&version=1.0.0&' \ f'request=GetCoverage' return Element('Capability', elements=[ Element('Request', elements=[ @@ -497,9 +513,6 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ Element('gml:pos', text=f'{band_infos[var_name].bbox[2]} ' f'{band_infos[var_name].bbox[3]}') ]), - Element('keywords', elements=[ - Element('keyword', text='grid') - ]), Element('domainSet', elements=[ Element('spatialDomain', elements=[ Element('gml:Envelope', elements=[ @@ -536,7 +549,9 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ ]) ]), Element('supportedCRSs', elements=[ - Element('requestResponseCRSs', text=' '.join(VALID_CRS_LIST)) + 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() diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index 9a9353c29..d0fafce2d 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -18,7 +18,6 @@ # 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 ApiHandler, ApiError from .api import api from .context import WcsContext @@ -56,8 +55,6 @@ async def get(self): 'value for "service" parameter must be "WCS"' ) request = self.request.get_query_arg('request') - if request is None: - request = self.request.get_query_arg('REQUEST') if request == "GetCapabilities": wcs_version = self.request.get_query_arg( "version", default=WCS_VERSION @@ -83,8 +80,6 @@ async def get(self): f'value for "version" parameter must be "{WCS_VERSION}"' ) coverages = self.request.get_query_arg("coverage") - if not coverages: - coverages = self.request.get_query_arg("COVERAGE") if coverages: coverages = coverages.split(',') @@ -116,21 +111,35 @@ async def get(self): response_crs = self.request.get_query_arg('response_crs', default=request_crs) time = self.request.get_query_arg('time') + file_format = self.request.get_query_arg('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 + 'TIME': time, + 'BBOX': bbox, + 'FORMAT': file_format, + 'WIDTH': width, + 'HEIGHT': height, + 'RESX': resx, + 'RESY': resy }) - gen_request = translate_to_generator_request(self.ctx, cov_req) - coverage = await self.ctx.run_in_executor( + cube = await self.ctx.run_in_executor( None, get_coverage, self.ctx, - gen_request + cov_req ) - # self.response.set_header('Content-Type', 'application/xml') - await self.response.finish(get_coverage) + self.response.set_header('Content-Type', + 'application/octet-stream') + + await self.response.finish(cube) else: raise ApiError.BadRequest( f'invalid request type "{request}"' From 1feb9f5b8b183d2a2f03f2a605d9b8c6d666d7c0 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 20 Sep 2022 22:38:04 +0200 Subject: [PATCH 18/43] WCS shows images in QGIS --- test/webapi/ows/res/WCSDescribe.xml | 75 +++++++++------------ test/webapi/ows/res/WCSDescribe_subset.xml | 15 ++--- test/webapi/ows/wcs/test_controller.py | 41 ++++++------ xcube/webapi/ows/wcs/context.py | 2 +- xcube/webapi/ows/wcs/controllers.py | 35 +++++----- xcube/webapi/ows/wcs/routes.py | 76 ++++++++++++++++++++-- 6 files changed, 147 insertions(+), 97 deletions(-) diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml index df246223f..c1677c5cc 100644 --- a/test/webapi/ows/res/WCSDescribe.xml +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -52,9 +52,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -107,9 +106,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -162,9 +160,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -217,9 +214,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -272,9 +268,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -323,9 +318,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -374,9 +368,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -425,9 +418,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -476,9 +468,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -527,9 +518,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -578,9 +568,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -629,9 +618,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -680,9 +668,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -731,9 +718,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -782,9 +768,8 @@ EPSG:4326 - zarr - netcdf4 - csv + 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 index 7c0f09663..cc16e1b68 100644 --- a/test/webapi/ows/res/WCSDescribe_subset.xml +++ b/test/webapi/ows/res/WCSDescribe_subset.xml @@ -52,9 +52,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -103,9 +102,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 @@ -154,9 +152,8 @@ EPSG:4326 - zarr - netcdf4 - csv + GeoTIFF + NetCDF4 \ No newline at end of file diff --git a/test/webapi/ows/wcs/test_controller.py b/test/webapi/ows/wcs/test_controller.py index 700669d62..95b69d636 100644 --- a/test/webapi/ows/wcs/test_controller.py +++ b/test/webapi/ows/wcs/test_controller.py @@ -51,7 +51,7 @@ def test_validate_coverage_request(self): 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) # TIME given in addition to BBOX -> fine @@ -62,7 +62,7 @@ def test_validate_coverage_request(self): 'TIME': '2017-01-28 20:23:55.123456', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) # COVERAGE is missing -> expect a failure @@ -72,7 +72,7 @@ def test_validate_coverage_request(self): 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -89,7 +89,7 @@ def test_validate_coverage_request(self): 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -105,7 +105,7 @@ def test_validate_coverage_request(self): 'TIME': '2020-01-28', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) # use invalid TIME format -> expect a failure @@ -116,9 +116,10 @@ def test_validate_coverage_request(self): 'TIME': '20201208', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) - self.fail('Classified invalid request as valid.') + # 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]]]]' @@ -132,7 +133,7 @@ def test_validate_coverage_request(self): 'TIME': '2020-01-28', 'RESX': 23.56, 'RESY': 23.56, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) # PARAMETER is given -> expect a failure (not yet supported) @@ -144,7 +145,7 @@ def test_validate_coverage_request(self): 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -158,7 +159,7 @@ def test_validate_coverage_request(self): 'BBOX': '-10 3 -5 4', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -173,7 +174,7 @@ def test_validate_coverage_request(self): 'BBOX': '1,51,4,52', 'WIDTH': 200, 'RESY': 156.45, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -187,7 +188,7 @@ def test_validate_coverage_request(self): 'CRS': 'EPSG:4326', 'BBOX': '1,51,4,52', 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -204,7 +205,7 @@ def test_validate_coverage_request(self): 'HEIGHT': 200, 'RESX': 200, 'RESY': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -220,7 +221,7 @@ def test_validate_coverage_request(self): 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -235,7 +236,7 @@ def test_validate_coverage_request(self): 'TIME': '2020-12-08', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'NetCDF4' })) self.fail('Classified invalid request as valid.') except ValueError as e: @@ -252,8 +253,8 @@ def test_validate_coverage_request(self): })) self.fail('Classified invalid request as valid.') except ValueError as e: - self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' - 'netcdf4, csv. Was: None', str(e)) + self.assertEqual('FORMAT wrong or missing. Must be one of GeoTIFF,' + ' NetCDF4. Was: None', str(e)) # FORMAT is invalid -> expect a failure try: @@ -267,8 +268,8 @@ def test_validate_coverage_request(self): })) self.fail('Classified invalid request as valid.') except ValueError as e: - self.assertEqual('FORMAT wrong or missing. Must be one of zarr, ' - 'netcdf4, csv. Was: MettCDF', str(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({ @@ -277,7 +278,7 @@ def test_get_coverage(self): 'BBOX': '1,51,4,52', 'WIDTH': 200, 'HEIGHT': 200, - 'FORMAT': 'zarr' + 'FORMAT': 'GeoTIFF' }) cube = get_coverage(self.wcs_ctx, coverage_request) self.assertIsNotNone(cube.coords) diff --git a/xcube/webapi/ows/wcs/context.py b/xcube/webapi/ows/wcs/context.py index 64ce2ab63..f3e4b90e2 100644 --- a/xcube/webapi/ows/wcs/context.py +++ b/xcube/webapi/ows/wcs/context.py @@ -21,7 +21,7 @@ from xcube.server.api import Context -from xcube.webapi.resctx import ResourcesContext +from xcube.webapi.common.context import ResourcesContext from ...datasets.context import DatasetsContext from ...tiles.context import TilesContext diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 6c4c6c3b0..88ad8a2fa 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -18,29 +18,25 @@ # 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 re import warnings -from datetime import datetime from typing import Dict, List, Any, Optional import numpy as np from xarray import Dataset import xcube.core.store.storepool as sp -from xcube.constants import EXTENSION_POINT_DATASET_IOS from xcube.core.gen2 import CubeGenerator, OutputConfig from xcube.core.gen2 import CubeGeneratorRequest from xcube.core.gen2.local.writer import CubeWriter from xcube.core.gridmapping import GridMapping -from xcube.util.plugin import get_extension_registry from xcube.webapi.ows.wcs.context import WcsContext from xcube.webapi.ows.wmts.controllers import get_crs84_bbox -from xcube.webapi.xml import Document -from xcube.webapi.xml import Element +from xcube.webapi.common.xml import Document +from xcube.webapi.common.xml import Element WCS_VERSION = '1.0.0' VALID_CRS_LIST = ['EPSG:4326'] -#VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857'] +# VALID_CRS_LIST = ['EPSG:4326', 'EPSG:3857'] class CoverageRequest: @@ -98,7 +94,8 @@ def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str: return document.to_xml(indent=4) -def get_describe_coverage_xml(ctx: WcsContext, coverages: List[str] = None) -> str: +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) @@ -314,9 +311,10 @@ def _is_valid_format(format_req: str) -> bool: def _is_valid_time(time: str) -> bool: - # todo - change so that it works with qgis + # todo - change validation so that it makes sense but works with QGIS try: - datetime.fromisoformat(time) + pass + # datetime.fromisoformat(time) except ValueError: raise ValueError('TIME value must be given in the format' '\'YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]]' @@ -551,7 +549,7 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ 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('requestResponseCRSs', text=','.join(VALID_CRS_LIST)) ]), Element('supportedFormats', elements=[ Element('formats', text=f) for f in _get_formats_list() @@ -570,11 +568,16 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ def _get_formats_list() -> List[str]: - formats = get_extension_registry().find_extensions( - EXTENSION_POINT_DATASET_IOS, - lambda e: 'w' in e.metadata.get('modes', set()) - ) - return [ext.name for ext in formats if not ext.name == 'mem'] + # formats = get_extension_registry().find_extensions( + # EXTENSION_POINT_DATASET_IOS, + # lambda e: 'w' in e.metadata.get('modes', set()) + # ) + # return [ext.name for ext in formats if not ext.name == 'mem' + # or not ext.name == 'zarr'] + + # the code above is correct, but QGIS and WCS only support GeoTIFF or + # NetCDF, so we simply return these. + return ['GeoTIFF', 'NetCDF4'] class BandInfo: diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index d0fafce2d..bb386d69d 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -18,11 +18,18 @@ # 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 + +import dask.array +from tornado import iostream +from xarray import Dataset + from xcube.server.api import ApiHandler, ApiError from .api import api from .context import WcsContext from .controllers import get_wcs_capabilities_xml, get_describe_coverage_xml, \ - get_coverage, translate_to_generator_request, CoverageRequest + get_coverage, CoverageRequest WCS_VERSION = '1.0.0' @@ -71,7 +78,6 @@ async def get(self): ) self.response.set_header('Content-Type', 'application/xml') await self.response.finish(capabilities_xml) - elif request == 'DescribeCoverage': wcs_version = self.request.get_query_arg('version', default=WCS_VERSION) @@ -111,7 +117,10 @@ async def get(self): response_crs = self.request.get_query_arg('response_crs', default=request_crs) time = self.request.get_query_arg('time') - file_format = self.request.get_query_arg('format') + # QGIS specific hack! + time = time.replace('Z', '') + file_format = self.request.get_query_arg('format', + default='geotiff') bbox = self.request.get_query_arg('bbox', default='-180,90,180,-90') width = self.request.get_query_arg('width') @@ -136,15 +145,70 @@ async def get(self): self.ctx, cov_req ) - self.response.set_header('Content-Type', - 'application/octet-stream') - await self.response.finish(cube) + cube = await self.ctx.run_in_executor( + None, + self._clean_cube, + cube, + time + ) + + try: + if file_format.lower() == 'netcdf4': + temp_file_name = await self.ctx.run_in_executor( + None, self._write_netcdf, cube + ) + elif file_format.lower() == 'geotiff': + temp_file_name = await self.ctx.run_in_executor( + None, self._write_geotiff, cube + ) + else: + raise ValueError('Unsupported format: ' + file_format) + + self.response.set_header('Content-Type', + 'application/octet-stream') + + chunk_size = 1024 * 1024 + with open(temp_file_name, 'rb') as f: + while True: + chunk = f.read(chunk_size) + if chunk is None or len(chunk) == 0: + break + try: + self.response.write(chunk) + except iostream.StreamClosedError: + break + finally: + if 'temp_file_name' in locals(): + os.remove(temp_file_name) + await self.response.finish() else: raise ApiError.BadRequest( f'invalid request type "{request}"' ) + @staticmethod + def _write_geotiff(cube: Dataset) -> str: + temp_file_handle, temp_file_name = tempfile.mkstemp('.tif') + cube.rio.to_raster(temp_file_name) + os.close(temp_file_handle) + return temp_file_name + + @staticmethod + def _write_netcdf(cube: Dataset) -> str: + temp_file_handle, temp_file_name = tempfile.mkstemp('.nc') + cube.to_netcdf(temp_file_name, 'w') + os.close(temp_file_handle) + return temp_file_name + + @staticmethod + def _clean_cube(cube: Dataset, time: str) -> Dataset: + del cube.attrs['history'] + cube = cube.drop_vars(['lat_bnds', 'lon_bnds', 'time_bnds']) + cube = cube.sel(time=time) + cube = cube.reduce(dask.array.squeeze, dim='time') + return cube + def _query_to_dict(request): return {k: v[0] for k, v in request.query.items()} From 521044a721ee2980d49b7ffdea4aa5523aa14f84 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 20 Sep 2022 23:08:44 +0200 Subject: [PATCH 19/43] fix --- test/webapi/ows/res/WCSDescribe.xml | 30 +++++++++++----------- test/webapi/ows/res/WCSDescribe_subset.xml | 6 ++--- xcube/webapi/ows/wcs/controllers.py | 6 ++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/test/webapi/ows/res/WCSDescribe.xml b/test/webapi/ows/res/WCSDescribe.xml index c1677c5cc..22332ab8a 100644 --- a/test/webapi/ows/res/WCSDescribe.xml +++ b/test/webapi/ows/res/WCSDescribe.xml @@ -52,7 +52,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -106,7 +106,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -160,7 +160,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -214,7 +214,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -268,7 +268,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -318,7 +318,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -368,7 +368,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -418,7 +418,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -468,7 +468,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -518,7 +518,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -568,7 +568,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -618,7 +618,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -668,7 +668,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -718,7 +718,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -768,7 +768,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 diff --git a/test/webapi/ows/res/WCSDescribe_subset.xml b/test/webapi/ows/res/WCSDescribe_subset.xml index cc16e1b68..08cf80135 100644 --- a/test/webapi/ows/res/WCSDescribe_subset.xml +++ b/test/webapi/ows/res/WCSDescribe_subset.xml @@ -52,7 +52,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -102,7 +102,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 @@ -152,7 +152,7 @@ EPSG:4326 - GeoTIFF + geotiff NetCDF4 diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 88ad8a2fa..53ea95b33 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -146,8 +146,8 @@ def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: if not result.status == 'ok': raise ValueError(f'Failed to generate cube: {result.message}') + cube_id = result.result.data_id memory_store = sp.get_data_store_instance('memory') - cube_id = list(memory_store.store.get_data_ids())[0] cube = memory_store.store.open_data(cube_id) # _write_debug_output(cube) @@ -575,8 +575,8 @@ def _get_formats_list() -> List[str]: # return [ext.name for ext in formats if not ext.name == 'mem' # or not ext.name == 'zarr'] - # the code above is correct, but QGIS and WCS only support GeoTIFF or - # NetCDF, so we simply return these. + # the code above is correct, but the combination of QGIS and WCS only + # supports GeoTIFF or NetCDF, so we simply return these. return ['GeoTIFF', 'NetCDF4'] From 2f0c624b66c8cdd378ceb5945637c06081100e10 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 21 Sep 2022 17:45:07 +0200 Subject: [PATCH 20/43] Make WCS work with demo config --- examples/serve/demo/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/serve/demo/config.yml b/examples/serve/demo/config.yml index b2620a2ee..ab0d6972a 100644 --- a/examples/serve/demo/config.yml +++ b/examples/serve/demo/config.yml @@ -197,3 +197,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"] From ddd9b00cd1830b0688b1f4a7f22d9396d50ed76b Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 22 Sep 2022 11:52:54 +0200 Subject: [PATCH 21/43] Using time.process_time() instead of time.perf_counter() --- xcube/util/progress.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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): From 4243fce3446575d2a79f0e59bf6e4c3db9c67748 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 22 Sep 2022 12:30:11 +0200 Subject: [PATCH 22/43] Fixed import format; Refactored and fixed GET /wcs/kvp operation; Created own WCS config; Closing temp files in context manager; Removed tiles API requirement; Added code comments. --- examples/serve/demo/config.yml | 11 +- xcube/webapi/ows/wcs/api.py | 4 +- xcube/webapi/ows/wcs/config.py | 43 +++++ xcube/webapi/ows/wcs/context.py | 8 - xcube/webapi/ows/wcs/controllers.py | 10 +- xcube/webapi/ows/wcs/routes.py | 271 +++++++++++++++------------- 6 files changed, 210 insertions(+), 137 deletions(-) create mode 100644 xcube/webapi/ows/wcs/config.py diff --git a/examples/serve/demo/config.yml b/examples/serve/demo/config.yml index ab0d6972a..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" @@ -197,7 +204,3 @@ 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/webapi/ows/wcs/api.py b/xcube/webapi/ows/wcs/api.py index 3e3a19272..6ae6cf4c6 100644 --- a/xcube/webapi/ows/wcs/api.py +++ b/xcube/webapi/ows/wcs/api.py @@ -20,11 +20,13 @@ # 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'], + required_apis=['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 index f3e4b90e2..c6f7ca1ae 100644 --- a/xcube/webapi/ows/wcs/context.py +++ b/xcube/webapi/ows/wcs/context.py @@ -23,23 +23,15 @@ from xcube.server.api import Context from xcube.webapi.common.context import ResourcesContext from ...datasets.context import DatasetsContext -from ...tiles.context import TilesContext class WcsContext(ResourcesContext): - _feature_index: int = 0 def __init__(self, server_ctx: Context): super().__init__(server_ctx) - self._tiles_ctx = server_ctx.get_api_ctx('tiles', - cls=TilesContext) self._datasets_ctx = server_ctx.get_api_ctx('datasets', cls=DatasetsContext) - @property - def tiles_ctx(self) -> TilesContext: - return self._tiles_ctx - @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 index 53ea95b33..fafc0e989 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -18,6 +18,7 @@ # 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, Optional @@ -25,17 +26,20 @@ from xarray import Dataset import xcube.core.store.storepool as sp -from xcube.core.gen2 import CubeGenerator, OutputConfig +from xcube.core.gen2 import CubeGenerator from xcube.core.gen2 import CubeGeneratorRequest +from xcube.core.gen2 import OutputConfig from xcube.core.gen2.local.writer import CubeWriter from xcube.core.gridmapping import GridMapping -from xcube.webapi.ows.wcs.context import WcsContext -from xcube.webapi.ows.wmts.controllers import get_crs84_bbox from xcube.webapi.common.xml import Document from xcube.webapi.common.xml import Element +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'] diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index bb386d69d..efed2e93e 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -18,21 +18,28 @@ # 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 import dask.array -from tornado import iostream from xarray import Dataset -from xcube.server.api import ApiHandler, ApiError +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 get_wcs_capabilities_xml, get_describe_coverage_xml, \ - get_coverage, CoverageRequest +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]): @@ -63,149 +70,171 @@ async def get(self): ) request = self.request.get_query_arg('request') if request == "GetCapabilities": - wcs_version = self.request.get_query_arg( - "version", default=WCS_VERSION + 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}"' ) - 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.base_url + + 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}"' ) - self.response.set_header('Content-Type', 'application/xml') - await self.response.finish(capabilities_xml) - elif request == 'DescribeCoverage': - 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 + coverage = self.request.get_query_arg('coverage') + if not coverage: + raise ApiError.BadRequest( + 'missing query argument "coverage"' ) - self.response.set_header('Content-Type', 'application/xml') - await self.response.finish(describe_coverage_xml) - elif request == "GetCoverage": - 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( - f'missing query argument "coverage"' - ) - request_crs = self.request.get_query_arg('crs') - if not request_crs: - raise ApiError.BadRequest( - f'missing query argument "crs"' - ) - response_crs = self.request.get_query_arg('response_crs', - default=request_crs) - time = self.request.get_query_arg('time') - # QGIS specific hack! - time = time.replace('Z', '') - file_format = self.request.get_query_arg('format', - default='geotiff') - 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 - }) + 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') + # 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: + raise ApiError.BadRequest(f'{e}') from e - cube = await self.ctx.run_in_executor( - None, - self._clean_cube, - cube, - time - ) + cube = self._clean_cube(cube, time) + + self.response.set_header('Content-Type', + 'application/octet-stream') - try: - if file_format.lower() == 'netcdf4': - temp_file_name = await self.ctx.run_in_executor( - None, self._write_netcdf, cube - ) - elif file_format.lower() == 'geotiff': - temp_file_name = await self.ctx.run_in_executor( - None, self._write_geotiff, cube - ) - else: - raise ValueError('Unsupported format: ' + file_format) - - self.response.set_header('Content-Type', - 'application/octet-stream') - - chunk_size = 1024 * 1024 - with open(temp_file_name, 'rb') as f: - while True: - chunk = f.read(chunk_size) - if chunk is None or len(chunk) == 0: - break - try: - self.response.write(chunk) - except iostream.StreamClosedError: - break - finally: - if 'temp_file_name' in locals(): - os.remove(temp_file_name) - await self.response.finish() + if file_format == 'netcdf4': + 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'invalid request type "{request}"' + 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.base_url + ) + self.response.set_header('Content-Type', 'application/xml') + await self.response.finish(capabilities_xml) @staticmethod def _write_geotiff(cube: Dataset) -> str: - temp_file_handle, temp_file_name = tempfile.mkstemp('.tif') - cube.rio.to_raster(temp_file_name) - os.close(temp_file_handle) - return temp_file_name + with tempfile.NamedTemporaryFile(prefix='xcube-wcs-', + suffix='.tif', + delete=False) as tf: + cube.rio.to_raster(tf.name) + return tf.name @staticmethod def _write_netcdf(cube: Dataset) -> str: - temp_file_handle, temp_file_name = tempfile.mkstemp('.nc') - cube.to_netcdf(temp_file_name, 'w') - os.close(temp_file_handle) - return temp_file_name + with tempfile.NamedTemporaryFile(prefix='xcube-wcs-', + suffix='.nc', + delete=False) as tf: + cube.to_netcdf(path=tf.name, mode='w') + return tf.name @staticmethod def _clean_cube(cube: Dataset, time: str) -> Dataset: + # TODO (forman): FIXME: Cubes generated by gen2 + # have a non-JSON-serializable value in "history" attribute. del cube.attrs['history'] + # GeoTIFF doesn't like variables with non-spatial dimensions + # Here it is the dimension "bnds": cube = cube.drop_vars(['lat_bnds', 'lon_bnds', 'time_bnds']) + # Select desired time slice cube = cube.sel(time=time) + # Make the slice truly 2-D cube = cube.reduce(dask.array.squeeze, dim='time') return cube From 371a40b3e28d398446a76cbffb2ddb53e8880078 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 22 Sep 2022 12:38:49 +0200 Subject: [PATCH 23/43] Fixed import format; Refactored and fixed GET /wcs/kvp operation; Created own WCS config; Closing temp files in context manager; Removed tiles API requirement; Added code comments. --- xcube/webapi/ows/wcs/controllers.py | 39 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index fafc0e989..8106e6ed8 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -363,9 +363,10 @@ def _get_capabilities_element(ctx: WcsContext, def _get_service_element(ctx: WcsContext) -> Element: - service_provider = ctx.config.get('ServiceProvider') + service_provider = ctx.config.get('ServiceProvider', {}) + wcs_metadata = ctx.config.get('WebCoverageService', {}) - def _get_value(path): + def _get_sp_value(path): v = None node = service_provider for k in path: @@ -376,75 +377,77 @@ def _get_value(path): return str(v) if v is not None else '' def _get_individual_name(): - individual_name = _get_value(['ServiceContact', 'IndividualName']) + individual_name = _get_sp_value(['ServiceContact', 'IndividualName']) individual_name = tuple(individual_name.split(' ').__reversed__()) return '{}, {}'.format(*individual_name) element = Element('Service', elements=[ Element('description', - text=_get_value(['WCS-description'])), + text=wcs_metadata.get('Description', + 'xcube WCS 1.0 API')), Element('name', - text=_get_value(['WCS-name'])), + text=wcs_metadata.get('Name', 'xcube WCS')), Element('label', - text=_get_value(['WCS-label'])), + text=wcs_metadata.get('Label', 'xcube WCS')), Element('keywords', elements=[ - Element('keyword', text=k) for k in service_provider['keywords'] + Element('keyword', text=k) for k in wcs_metadata.get('Keywords', + []) ]), Element('responsibleParty', elements=[ Element('individualName', text=_get_individual_name()), Element('organisationName', - text=_get_value(['ProviderName'])), + text=_get_sp_value(['ProviderName'])), Element('positionName', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'PositionName'])), Element('contactInfo', elements=[ Element('phone', elements=[ Element('voice', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Phone', 'Voice'])), Element('facsimile', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Phone', 'Facsimile'])), ]), Element('address', elements=[ Element('deliveryPoint', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Address', 'DeliveryPoint'])), Element('city', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Address', 'City'])), Element('administrativeArea', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Address', 'AdministrativeArea'])), Element('postalCode', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Address', 'PostalCode'])), Element('country', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Address', 'Country'])), Element('electronicMailAddress', - text=_get_value(['ServiceContact', + text=_get_sp_value(['ServiceContact', 'ContactInfo', 'Address', 'ElectronicMailAddress'])), ]), Element('onlineResource', attrs={ - 'xlink:href': _get_value(['ProviderSite'])}) + 'xlink:href': _get_sp_value(['ProviderSite'])}) ]), ]), Element('fees', text='NONE'), From bbeaf82cda974b262cfbef683dd3497958e62ffd Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 22 Sep 2022 13:23:38 +0200 Subject: [PATCH 24/43] Only show duration, if total != None --- xcube/core/gen2/local/generator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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.' ) From 7ef8280c32dcc29d4425e584c04b2e0d57172924 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 22 Sep 2022 13:27:13 +0200 Subject: [PATCH 25/43] Fix format names; Expand var label. --- xcube/webapi/ows/wcs/controllers.py | 15 +++++---------- xcube/webapi/ows/wcs/routes.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 8106e6ed8..e1dc5f934 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -575,16 +575,10 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ def _get_formats_list() -> List[str]: - # formats = get_extension_registry().find_extensions( - # EXTENSION_POINT_DATASET_IOS, - # lambda e: 'w' in e.metadata.get('modes', set()) - # ) - # return [ext.name for ext in formats if not ext.name == 'mem' - # or not ext.name == 'zarr'] - - # the code above is correct, but the combination of QGIS and WCS only - # supports GeoTIFF or NetCDF, so we simply return these. - return ['GeoTIFF', 'NetCDF4'] + # We only support GeoTIFF or NetCDF, because + # 1. QGIS understands them + # 2. response can be a single file + return ['geotiff', 'netcdf4'] class BandInfo: @@ -626,6 +620,7 @@ def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, 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 diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index efed2e93e..c95bcac45 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -100,6 +100,10 @@ async def _do_get_coverage(self): 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', @@ -228,10 +232,12 @@ def _write_netcdf(cube: Dataset) -> str: def _clean_cube(cube: Dataset, time: str) -> Dataset: # TODO (forman): FIXME: Cubes generated by gen2 # have a non-JSON-serializable value in "history" attribute. - del cube.attrs['history'] + cube.attrs.pop('history', None) # GeoTIFF doesn't like variables with non-spatial dimensions # Here it is the dimension "bnds": - cube = cube.drop_vars(['lat_bnds', 'lon_bnds', 'time_bnds']) + for var_name in ['lat_bnds', 'lon_bnds', 'time_bnds']: + if var_name in cube: + cube = cube.drop_vars(var_name) # Select desired time slice cube = cube.sel(time=time) # Make the slice truly 2-D From 8a3750db40dd751cb465054c1d20f87d7fdec24a Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 22 Sep 2022 13:30:48 +0200 Subject: [PATCH 26/43] cube.sel(time=time, method='nearest') --- xcube/webapi/ows/wcs/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index c95bcac45..5060be114 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -239,7 +239,7 @@ def _clean_cube(cube: Dataset, time: str) -> Dataset: if var_name in cube: cube = cube.drop_vars(var_name) # Select desired time slice - cube = cube.sel(time=time) + cube = cube.sel(time=time, method='nearest') # Make the slice truly 2-D cube = cube.reduce(dask.array.squeeze, dim='time') return cube From 32926efd8c654de43fef68438ed4157078b28427 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 30 Sep 2022 14:36:43 +0200 Subject: [PATCH 27/43] netcdf4 --> netcdf --- xcube/webapi/ows/wcs/routes.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index 5060be114..ab7bbcdf6 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -146,7 +146,7 @@ async def _do_get_coverage(self): self.response.set_header('Content-Type', 'application/octet-stream') - if file_format == 'netcdf4': + if file_format == 'netcdf': temp_file_path = await self.ctx.run_in_executor( None, self._write_netcdf, cube ) @@ -239,10 +239,7 @@ def _clean_cube(cube: Dataset, time: str) -> Dataset: if var_name in cube: cube = cube.drop_vars(var_name) # Select desired time slice - cube = cube.sel(time=time, method='nearest') - # Make the slice truly 2-D - cube = cube.reduce(dask.array.squeeze, dim='time') - return cube + return cube.sel(time=time, method='nearest') def _query_to_dict(request): From d0d2de0d2d5d2d1a8e50ceb8056db94eaa3d465e Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 30 Sep 2022 14:37:04 +0200 Subject: [PATCH 28/43] Added supportedInterpolations --- xcube/webapi/ows/wcs/controllers.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index e1dc5f934..bc626cbbb 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -556,11 +556,22 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ 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('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( @@ -578,7 +589,7 @@ def _get_formats_list() -> List[str]: # We only support GeoTIFF or NetCDF, because # 1. QGIS understands them # 2. response can be a single file - return ['geotiff', 'netcdf4'] + return ['geotiff', 'netcdf'] class BandInfo: From 8a1d9eb37e88cba6942e119dc3bdecacab661102 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 30 Sep 2022 14:51:38 +0200 Subject: [PATCH 29/43] Make time optional --- xcube/webapi/ows/wcs/routes.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index ab7bbcdf6..a1cdb2df3 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -21,6 +21,7 @@ import os import tempfile +from typing import Optional import dask.array from xarray import Dataset @@ -213,33 +214,38 @@ async def _do_get_capabilities(self): await self.response.finish(capabilities_xml) @staticmethod - def _write_geotiff(cube: Dataset) -> str: + def _write_geotiff(dataset: Dataset) -> str: with tempfile.NamedTemporaryFile(prefix='xcube-wcs-', suffix='.tif', delete=False) as tf: - cube.rio.to_raster(tf.name) + dataset.rio.to_raster(tf.name) return tf.name @staticmethod - def _write_netcdf(cube: Dataset) -> str: + def _write_netcdf(dataset: Dataset) -> str: with tempfile.NamedTemporaryFile(prefix='xcube-wcs-', suffix='.nc', delete=False) as tf: - cube.to_netcdf(path=tf.name, mode='w') + dataset.to_netcdf(path=tf.name, mode='w') return tf.name @staticmethod - def _clean_cube(cube: Dataset, time: str) -> Dataset: + def _clean_cube(dataset: Dataset, time: Optional[str] = None) -> Dataset: # TODO (forman): FIXME: Cubes generated by gen2 # have a non-JSON-serializable value in "history" attribute. - cube.attrs.pop('history', None) + dataset.attrs.pop('history', None) # GeoTIFF doesn't like variables with non-spatial dimensions # Here it is the dimension "bnds": for var_name in ['lat_bnds', 'lon_bnds', 'time_bnds']: - if var_name in cube: - cube = cube.drop_vars(var_name) + if var_name in dataset: + dataset = dataset.drop_vars(var_name) # Select desired time slice - return cube.sel(time=time, method='nearest') + if 'time' in dataset.coords: + if time is None: + dataset = dataset.isel(time=-1) + else: + dataset = dataset.sel(time=time, method='nearest') + return dataset def _query_to_dict(request): From 12b2230c4916afb8c5d93ffd11b17618f11c6703 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 26 Oct 2022 20:56:58 +0200 Subject: [PATCH 30/43] fixes...still need cleanup and testing --- xcube/core/_tile2.py | 11 +++-- xcube/webapi/ows/wcs/controllers.py | 72 +++++++++++++++++++++++++---- xcube/webapi/ows/wcs/routes.py | 2 +- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/xcube/core/_tile2.py b/xcube/core/_tile2.py index b7b5a282e..fff709c03 100644 --- a/xcube/core/_tile2.py +++ b/xcube/core/_tile2.py @@ -151,8 +151,11 @@ def compute_tiles( tile_y_1d = np.linspace(tile_y_min + 0.5 * tile_res_y, tile_y_max - 0.5 * tile_res_y, tile_height) - tile_x_2d = np.tile(tile_x_1d, (tile_width, 1)) - tile_y_2d = np.tile(tile_y_1d, (tile_height, 1)).transpose() + tile_x_2d = np.tile(tile_x_1d, (tile_height, 1)) + tile_y_2d = np.tile(tile_y_1d, (tile_width, 1)).transpose() + + assert tile_x_2d.shape == (tile_height, tile_width) + assert tile_y_2d.shape == tile_x_2d.shape t_map_to_ds = ProjCache.INSTANCE.get_transformer( tile_crs, @@ -168,14 +171,14 @@ def compute_tiles( ds_x_n = tile_ds_x_2d[0, :] ds_y_n = tile_ds_y_2d[0, :] # South - ds_x_s = tile_ds_x_2d[tile_width - 1, :] + ds_x_s = tile_ds_x_2d[tile_height - 1, :] ds_y_s = tile_ds_y_2d[tile_height - 1, :] # West ds_x_w = tile_ds_x_2d[:, 0] ds_y_w = tile_ds_y_2d[:, 0] # East ds_x_e = tile_ds_x_2d[:, tile_width - 1] - ds_y_e = tile_ds_y_2d[:, tile_height - 1] + ds_y_e = tile_ds_y_2d[:, tile_width - 1] # Min ds_x_min = np.nanmin([np.nanmin(ds_x_n), np.nanmin(ds_x_s), np.nanmin(ds_x_w), np.nanmin(ds_x_e)]) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index bc626cbbb..3f07279fb 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -23,7 +23,7 @@ from typing import Dict, List, Any, Optional import numpy as np -from xarray import Dataset +from xarray import Dataset, DataArray import xcube.core.store.storepool as sp from xcube.core.gen2 import CubeGenerator @@ -31,8 +31,11 @@ from xcube.core.gen2 import OutputConfig from xcube.core.gen2.local.writer import CubeWriter from xcube.core.gridmapping import GridMapping +from xcube.core.mldataset import BaseMultiLevelDataset +from xcube.core._tile2 import compute_tiles 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 @@ -107,13 +110,13 @@ def get_describe_coverage_xml(ctx: WcsContext, def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ -> CubeGeneratorRequest: - data_id = _get_input_data_id(ctx, req) + data_id = _get_dataset_config(ctx, req)['Path'] bbox = [] for v in req.bbox.split(','): bbox.append(float(v)) store_pool = ctx.datasets_ctx.get_data_store_pool() - store_id = _get_store_id(store_pool, data_id) + store_id = _get_store_instance_id(store_pool, data_id) datastore_root = store_pool.get_store_config(store_id).store_params['root'] store_config_id = store_pool.get_store_config(store_id).store_id @@ -140,7 +143,7 @@ def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ ) -def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: +def __get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: _validate_coverage_req(ctx, req) gen_req = translate_to_generator_request(ctx, req) @@ -159,6 +162,54 @@ def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: return cube +def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: + dataset_config = _get_dataset_config(ctx, req) + data_id = dataset_config['Path'] + store_pool = ctx.datasets_ctx.get_data_store_pool() + store_instance_id = _get_store_instance_id(store_pool, data_id) + store_config = store_pool.get_store_config(store_instance_id) + datastore_root = store_config.store_params['root'] + store_config_id = store_config.store_id + store = store_pool.get_store(store_instance_id) + ds = store.open_data(data_id) + ml_dataset = BaseMultiLevelDataset(ds) + + xy_dim_names = ml_dataset.grid_mapping.xy_dim_names + + bbox = [] + for v in req.bbox.split(','): + bbox.append(float(v)) + + ds_name = dataset_config['Identifier'] + var_name = req.coverage.replace(ds_name + '.', '') + tile_size = (int(req.width), int(req.height)) + tile = compute_tiles(ml_dataset, var_name, tuple(bbox), req.crs, + tile_size=tile_size)[0] + + # tile = compute_tiles(ml_dataset, var_name, tuple(bbox), req.crs)[0] + + # lon = compute_tiles(ml_dataset, xy_dim_names[0], tuple(bbox), req.crs, + # tile_size=(int(req.width), int(req.height)))[0] + # lat = compute_tiles(ml_dataset, xy_dim_names[1], tuple(bbox), req.crs, + # tile_size=(int(req.width), int(req.height)))[0] + # spatial_ref = DataArray( + # 0, attrs=pyproj.CRS.from_string(req.crs).to_cf() + # ) + + if ml_dataset.grid_mapping.is_j_axis_up: + data = DataArray(tile, dims=['y', 'x']) + else: + data = DataArray(tile[::-1, :], dims=['y', 'x']) + + # lat_coord = DataArray(lat, dims=['lat']) + # lon_coord = DataArray(lon, dims=['lon']) + return Dataset( + data_vars=dict( + data=data + ), + ) + + def _write_debug_output(cube): history = str(cube.history[0]) del cube.attrs['history'] @@ -169,11 +220,11 @@ def _write_debug_output(cube): cw.write_cube(cube, GridMapping.from_dataset(cube)) -def _get_store_id(store_pool: sp.DataStorePool, data_id: str) -> str: - for store_id in store_pool.store_instance_ids: - current_store = store_pool.get_store(store_id) +def _get_store_instance_id(store_pool: sp.DataStorePool, data_id: str) -> str: + for store_instance_id in store_pool.store_instance_ids: + current_store = store_pool.get_store(store_instance_id) if current_store.has_data(data_id): - return store_id + return store_instance_id raise ValueError(f'{data_id} not found in any available data store') @@ -187,7 +238,8 @@ def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: return tuple(output_region) -def _get_input_data_id(ctx: WcsContext, req: CoverageRequest) -> str: +def _get_dataset_config(ctx: WcsContext, req: CoverageRequest) \ + -> DatasetConfig: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] ds = ctx.datasets_ctx.get_dataset(ds_name) @@ -196,7 +248,7 @@ def _get_input_data_id(ctx: WcsContext, req: CoverageRequest) -> str: for var_name in var_names: qualified_var_name = f'{ds_name}.{var_name}' if req.coverage == qualified_var_name: - return dataset_config['Path'] + return dataset_config raise RuntimeError('Should never come here. Contact the developers.') diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index a1cdb2df3..685d49c65 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -168,7 +168,7 @@ async def _do_get_coverage(self): f'failed writing to {temp_file_path}: {e}' ) from e - await self.response.finish() + await self.response.finish() try: os.remove(temp_file_path) From bbd7b3cf893397b3451b6fe335d054c117854ec5 Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 27 Oct 2022 15:30:56 +0200 Subject: [PATCH 31/43] simplified and fixed support for datasets computed on-the-fly --- xcube/webapi/ows/wcs/controllers.py | 34 +++++------------------------ 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 3f07279fb..935e1b776 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -164,17 +164,8 @@ def __get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: dataset_config = _get_dataset_config(ctx, req) - data_id = dataset_config['Path'] - store_pool = ctx.datasets_ctx.get_data_store_pool() - store_instance_id = _get_store_instance_id(store_pool, data_id) - store_config = store_pool.get_store_config(store_instance_id) - datastore_root = store_config.store_params['root'] - store_config_id = store_config.store_id - store = store_pool.get_store(store_instance_id) - ds = store.open_data(data_id) - ml_dataset = BaseMultiLevelDataset(ds) - - xy_dim_names = ml_dataset.grid_mapping.xy_dim_names + ds_name = dataset_config['Identifier'] + ds = ctx.datasets_ctx.get_ml_dataset(ds_name) bbox = [] for v in req.bbox.split(','): @@ -183,26 +174,13 @@ def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: ds_name = dataset_config['Identifier'] var_name = req.coverage.replace(ds_name + '.', '') tile_size = (int(req.width), int(req.height)) - tile = compute_tiles(ml_dataset, var_name, tuple(bbox), req.crs, + tile = compute_tiles(ds, var_name, tuple(bbox), req.crs, tile_size=tile_size)[0] - # tile = compute_tiles(ml_dataset, var_name, tuple(bbox), req.crs)[0] - - # lon = compute_tiles(ml_dataset, xy_dim_names[0], tuple(bbox), req.crs, - # tile_size=(int(req.width), int(req.height)))[0] - # lat = compute_tiles(ml_dataset, xy_dim_names[1], tuple(bbox), req.crs, - # tile_size=(int(req.width), int(req.height)))[0] - # spatial_ref = DataArray( - # 0, attrs=pyproj.CRS.from_string(req.crs).to_cf() - # ) - - if ml_dataset.grid_mapping.is_j_axis_up: - data = DataArray(tile, dims=['y', 'x']) - else: - data = DataArray(tile[::-1, :], dims=['y', 'x']) + if not ds.grid_mapping.is_j_axis_up: + tile = tile[::-1, :] - # lat_coord = DataArray(lat, dims=['lat']) - # lon_coord = DataArray(lon, dims=['lon']) + data = DataArray(tile, dims=['y', 'x']) return Dataset( data_vars=dict( data=data From 37f02e03a944d9d34ebbd6fb2ad5475dbfa7e732 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 28 Oct 2022 14:44:16 +0200 Subject: [PATCH 32/43] fixes --- xcube/webapi/ows/wcs/api.py | 2 +- xcube/webapi/ows/wcs/controllers.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/xcube/webapi/ows/wcs/api.py b/xcube/webapi/ows/wcs/api.py index 6ae6cf4c6..29d545cae 100644 --- a/xcube/webapi/ows/wcs/api.py +++ b/xcube/webapi/ows/wcs/api.py @@ -26,7 +26,7 @@ api = Api( 'ows.wcs', description='xcube OGC WCS API', - required_apis=['datasets'], + required_apis=['tiles', 'datasets'], config_schema=CONFIG_SCHEMA, create_ctx=WcsContext ) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 935e1b776..1f1ec0c96 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -406,8 +406,10 @@ def _get_sp_value(path): node = v return str(v) if v is not None else '' - def _get_individual_name(): + 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) From 76e74c1a8384c31193dad9ddc59ed98504d20d15 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 28 Oct 2022 22:15:58 +0200 Subject: [PATCH 33/43] removed memory-consuming and unnecessary min/max computation --- xcube/webapi/ows/wcs/controllers.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 1f1ec0c96..0c0c4d29e 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -538,7 +538,7 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ -> Element: coverage_elements = [] - band_infos = _extract_band_infos(ctx, coverages, True) + 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), @@ -572,15 +572,7 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ Element('axisDescription', elements=[ Element('AxisDescription', elements=[ Element('name', text='Band'), - Element('label', text='Band'), - Element('values', elements=[ - Element('interval', elements=[ - Element('min', text= - f'{band_infos[var_name].min:0.4f}'), - Element('max', text= - f'{band_infos[var_name].max:0.4f}') - ]) - ]), + Element('label', text='Band') ]) ]) ]) @@ -637,8 +629,8 @@ def __init__(self, var_name: str, label: str, self.time_steps = time_steps -def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, - full: bool = False) -> Dict[str, BandInfo]: +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'] @@ -676,11 +668,6 @@ def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None, time_steps = [f'{str(d)[:19]}Z' for d in var.time.values] band_info = BandInfo(qualified_var_name, label, bbox, time_steps) - if full: - nn_values = var.values[~np.isnan(var.values)] - band_info.min = nn_values.min() - band_info.max = nn_values.max() - band_infos[f'{ds_name}.{var_name}'] = band_info return band_infos From 154af249ad280c374fe3edcd8dad548e154690d1 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 4 Nov 2022 12:53:00 +0100 Subject: [PATCH 34/43] Using new compute_tiles(..., as_dataset=True) --- xcube/core/_tile2.py | 26 +++++++---- xcube/core/tile.py | 4 +- xcube/webapi/ows/wcs/controllers.py | 72 ++++++++++++++++++++--------- xcube/webapi/ows/wcs/routes.py | 2 +- 4 files changed, 70 insertions(+), 34 deletions(-) diff --git a/xcube/core/_tile2.py b/xcube/core/_tile2.py index 142f99d76..f1b38e4a7 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 = {} @@ -308,6 +311,7 @@ def _new_tile_dataset( 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, @@ -316,14 +320,18 @@ def _new_tile_dataset( 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 if is_j_axis_up else 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/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/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 0c0c4d29e..517cf1845 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -20,10 +20,10 @@ # DEALINGS IN THE SOFTWARE. import warnings -from typing import Dict, List, Any, Optional +from typing import Dict, List, Any, Optional, Union import numpy as np -from xarray import Dataset, DataArray +import xarray as xr import xcube.core.store.storepool as sp from xcube.core.gen2 import CubeGenerator @@ -31,8 +31,9 @@ from xcube.core.gen2 import OutputConfig from xcube.core.gen2.local.writer import CubeWriter from xcube.core.gridmapping import GridMapping -from xcube.core.mldataset import BaseMultiLevelDataset -from xcube.core._tile2 import compute_tiles +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 @@ -108,6 +109,7 @@ def get_describe_coverage_xml(ctx: WcsContext, return document.to_xml(indent=4) +# TODO: remove! def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ -> CubeGeneratorRequest: data_id = _get_dataset_config(ctx, req)['Path'] @@ -143,7 +145,8 @@ def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ ) -def __get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: +# TODO: remove! +def __get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: _validate_coverage_req(ctx, req) gen_req = translate_to_generator_request(ctx, req) @@ -162,32 +165,34 @@ def __get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: return cube -def get_coverage(ctx: WcsContext, req: CoverageRequest) -> Dataset: +def get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: dataset_config = _get_dataset_config(ctx, req) ds_name = dataset_config['Identifier'] - ds = ctx.datasets_ctx.get_ml_dataset(ds_name) + ml_dataset = ctx.datasets_ctx.get_ml_dataset(ds_name) - bbox = [] - for v in req.bbox.split(','): - bbox.append(float(v)) + # 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)) - tile = compute_tiles(ds, var_name, tuple(bbox), req.crs, - tile_size=tile_size)[0] - - if not ds.grid_mapping.is_j_axis_up: - tile = tile[::-1, :] - - data = DataArray(tile, dims=['y', 'x']) - return Dataset( - data_vars=dict( - data=data - ), - ) + tile_size = int(req.width), int(req.height) + return compute_tiles(ml_dataset, + var_name, + bbox, + req.crs, + tile_size=tile_size, + as_dataset=True) +# TODO: remove! def _write_debug_output(cube): history = str(cube.history[0]) del cube.attrs['history'] @@ -198,6 +203,7 @@ def _write_debug_output(cube): cw.write_cube(cube, GridMapping.from_dataset(cube)) +# TODO: remove! def _get_store_instance_id(store_pool: sp.DataStorePool, data_id: str) -> str: for store_instance_id in store_pool.store_instance_ids: current_store = store_pool.get_store(store_instance_id) @@ -206,6 +212,7 @@ def _get_store_instance_id(store_pool: sp.DataStorePool, data_id: str) -> str: raise ValueError(f'{data_id} not found in any available data store') +# TODO: remove! def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: if not req.bbox: return None @@ -218,6 +225,17 @@ def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: 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) @@ -230,6 +248,7 @@ def _get_dataset_config(ctx: WcsContext, req: CoverageRequest) \ raise RuntimeError('Should never come here. Contact the developers.') +# TODO: remove! def _get_input_path(ctx: WcsContext, req: CoverageRequest) -> str: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] @@ -248,6 +267,7 @@ def _get_input_path(ctx: WcsContext, req: CoverageRequest) -> str: raise RuntimeError('Should never come here. Contact the developers.') +# TODO: remove! def _get_input_store_id(ctx: WcsContext, req: CoverageRequest) -> str: for dataset_config in ctx.datasets_ctx.get_dataset_configs(): ds_name = dataset_config['Identifier'] @@ -261,6 +281,7 @@ def _get_input_store_id(ctx: WcsContext, req: CoverageRequest) -> str: raise RuntimeError('Should never come here. Contact the developers.') +# TODO: remove! def _validate_coverage_req(ctx: WcsContext, req: CoverageRequest): def _has_no_invalid_bbox() -> bool: if req.bbox: @@ -322,6 +343,7 @@ def _has_no_invalid_time() -> bool: raise ValueError('Reason unclear, fix me') +# TODO: remove! def _is_valid_coverage(ctx: WcsContext, coverage: str) -> bool: band_infos = _extract_band_infos(ctx, [coverage]) if band_infos: @@ -329,10 +351,12 @@ def _is_valid_coverage(ctx: WcsContext, coverage: str) -> bool: return False +# TODO: remove! def _is_valid_crs(crs: str) -> bool: return crs in VALID_CRS_LIST +# TODO: remove! def _is_valid_bbox(bbox: str) -> bool: values = bbox.split(',') if not len(values) == 4: @@ -340,10 +364,12 @@ def _is_valid_bbox(bbox: str) -> bool: return True +# TODO: remove! def _is_valid_format(format_req: str) -> bool: return format_req in _get_formats_list() +# TODO: remove! def _is_valid_time(time: str) -> bool: # todo - change validation so that it makes sense but works with QGIS try: diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index 685d49c65..af659bd71 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -23,7 +23,6 @@ import tempfile from typing import Optional -import dask.array from xarray import Dataset from xcube.constants import LOG @@ -140,6 +139,7 @@ async def _do_get_coverage(self): cov_req ) except ValueError as e: + # TODO: too broad error message, must name invalid parameter raise ApiError.BadRequest(f'{e}') from e cube = self._clean_cube(cube, time) From 911d78b1a4077374b4ad73dea95727b3082f4c6d Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 9 Nov 2022 15:13:18 +0100 Subject: [PATCH 35/43] made wcs qgis-ready --- xcube/core/_tile2.py | 7 +++---- xcube/webapi/ows/wcs/controllers.py | 4 ++-- xcube/webapi/ows/wcs/routes.py | 21 --------------------- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/xcube/core/_tile2.py b/xcube/core/_tile2.py index f1b38e4a7..63e8d6ac7 100644 --- a/xcube/core/_tile2.py +++ b/xcube/core/_tile2.py @@ -302,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, @@ -310,17 +310,16 @@ 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(y_coords if is_j_axis_up else y_coords[::-1], + y=xr.DataArray(y_coords[::-1], dims="y", attrs=dict( long_name="y coordinate of projection", diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 517cf1845..428851e6e 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -636,10 +636,10 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ def _get_formats_list() -> List[str]: - # We only support GeoTIFF or NetCDF, because + # We currently only support NetCDF, because # 1. QGIS understands them # 2. response can be a single file - return ['geotiff', 'netcdf'] + return ['netcdf'] class BandInfo: diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index af659bd71..584387d28 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -21,7 +21,6 @@ import os import tempfile -from typing import Optional from xarray import Dataset @@ -142,8 +141,6 @@ async def _do_get_coverage(self): # TODO: too broad error message, must name invalid parameter raise ApiError.BadRequest(f'{e}') from e - cube = self._clean_cube(cube, time) - self.response.set_header('Content-Type', 'application/octet-stream') @@ -229,24 +226,6 @@ def _write_netcdf(dataset: Dataset) -> str: dataset.to_netcdf(path=tf.name, mode='w') return tf.name - @staticmethod - def _clean_cube(dataset: Dataset, time: Optional[str] = None) -> Dataset: - # TODO (forman): FIXME: Cubes generated by gen2 - # have a non-JSON-serializable value in "history" attribute. - dataset.attrs.pop('history', None) - # GeoTIFF doesn't like variables with non-spatial dimensions - # Here it is the dimension "bnds": - for var_name in ['lat_bnds', 'lon_bnds', 'time_bnds']: - if var_name in dataset: - dataset = dataset.drop_vars(var_name) - # Select desired time slice - if 'time' in dataset.coords: - if time is None: - dataset = dataset.isel(time=-1) - else: - dataset = dataset.sel(time=time, method='nearest') - return dataset - def _query_to_dict(request): return {k: v[0] for k, v in request.query.items()} From 5a088561ccb7e9cd246ad436efcb8915c4ec258a Mon Sep 17 00:00:00 2001 From: thomas Date: Tue, 10 Jan 2023 17:15:21 +0100 Subject: [PATCH 36/43] added some debug output --- xcube/webapi/ows/wcs/controllers.py | 9 +++++++++ xcube/webapi/ows/wcs/routes.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 428851e6e..c849d5d60 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -87,6 +87,15 @@ def __init__(self, req: Dict[str, Any]): if 'EXCEPTIONS' in req: self.exceptions = req['EXCEPTIONS'] + def __repr__(self): + return f'Coverage: {self.coverage}\n' \ + f'CRS: {self.crs}\n' \ + f'BBOX: {self.bbox}\n' \ + f'Time: {self.time}\n' \ + f'Width: {self.width}\n' \ + f'Height: {self.height}\n' \ + f'Format: {self.format}' + def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str: """ diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index 584387d28..a09e2ce4c 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -67,6 +67,7 @@ async def get(self): 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() @@ -130,6 +131,8 @@ async def _do_get_coverage(self): 'RESY': resy }) + LOG.debug(str(cov_req)) + try: cube = await self.ctx.run_in_executor( None, From 5d291abb5a284be0bf49a7617bd9cd88f520b42e Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 23:12:23 +0100 Subject: [PATCH 37/43] cleanup and fixes --- xcube/webapi/ows/wcs/controllers.py | 308 ++++------------------------ 1 file changed, 37 insertions(+), 271 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index c849d5d60..29d69c073 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -20,17 +20,11 @@ # DEALINGS IN THE SOFTWARE. import warnings -from typing import Dict, List, Any, Optional, Union +from typing import Dict, List, Any, Union import numpy as np import xarray as xr -import xcube.core.store.storepool as sp -from xcube.core.gen2 import CubeGenerator -from xcube.core.gen2 import CubeGeneratorRequest -from xcube.core.gen2 import OutputConfig -from xcube.core.gen2.local.writer import CubeWriter -from xcube.core.gridmapping import GridMapping from xcube.core.tile import BBox from xcube.core.tile import compute_tiles from xcube.server.api import ApiError @@ -87,15 +81,6 @@ def __init__(self, req: Dict[str, Any]): if 'EXCEPTIONS' in req: self.exceptions = req['EXCEPTIONS'] - def __repr__(self): - return f'Coverage: {self.coverage}\n' \ - f'CRS: {self.crs}\n' \ - f'BBOX: {self.bbox}\n' \ - f'Time: {self.time}\n' \ - f'Width: {self.width}\n' \ - f'Height: {self.height}\n' \ - f'Format: {self.format}' - def get_wcs_capabilities_xml(ctx: WcsContext, base_url: str) -> str: """ @@ -118,62 +103,6 @@ def get_describe_coverage_xml(ctx: WcsContext, return document.to_xml(indent=4) -# TODO: remove! -def translate_to_generator_request(ctx: WcsContext, req: CoverageRequest) \ - -> CubeGeneratorRequest: - data_id = _get_dataset_config(ctx, req)['Path'] - bbox = [] - for v in req.bbox.split(','): - bbox.append(float(v)) - - store_pool = ctx.datasets_ctx.get_data_store_pool() - store_id = _get_store_instance_id(store_pool, data_id) - datastore_root = store_pool.get_store_config(store_id).store_params['root'] - store_config_id = store_pool.get_store_config(store_id).store_id - - return CubeGeneratorRequest.from_dict( - { - 'input_config': { - 'store_id': store_config_id, - 'store_params': { - 'root': datastore_root - }, - 'data_id': f'{data_id}' - }, - 'cube_config': { - 'variable_names': [f'{req.coverage}'.split('.')[-1]], - 'crs': f'{req.crs}', - 'bbox': tuple(bbox) - }, - 'output_config': { - 'store_id': 'memory', - 'replace': True, - 'data_id': f'{req.coverage}.zarr', - } - } - ) - - -# TODO: remove! -def __get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: - _validate_coverage_req(ctx, req) - gen_req = translate_to_generator_request(ctx, req) - - gen = CubeGenerator.new() - - result = gen.generate_cube(request=gen_req) - if not result.status == 'ok': - raise ValueError(f'Failed to generate cube: {result.message}') - - cube_id = result.result.data_id - memory_store = sp.get_data_store_instance('memory') - cube = memory_store.store.open_data(cube_id) - - # _write_debug_output(cube) - - return cube - - def get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: dataset_config = _get_dataset_config(ctx, req) ds_name = dataset_config['Identifier'] @@ -193,43 +122,11 @@ def get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: ds_name = dataset_config['Identifier'] var_name = req.coverage.replace(ds_name + '.', '') tile_size = int(req.width), int(req.height) - return compute_tiles(ml_dataset, - var_name, - bbox, - req.crs, - tile_size=tile_size, - as_dataset=True) - - -# TODO: remove! -def _write_debug_output(cube): - history = str(cube.history[0]) - del cube.attrs['history'] - cube['history'] = history - cw = CubeWriter(OutputConfig('file', - writer_id='dataset:netcdf:file', - data_id='/../../../test_cube.nc')) - cw.write_cube(cube, GridMapping.from_dataset(cube)) - - -# TODO: remove! -def _get_store_instance_id(store_pool: sp.DataStorePool, data_id: str) -> str: - for store_instance_id in store_pool.store_instance_ids: - current_store = store_pool.get_store(store_instance_id) - if current_store.has_data(data_id): - return store_instance_id - raise ValueError(f'{data_id} not found in any available data store') - - -# TODO: remove! -def _get_output_region(req: CoverageRequest) -> Optional[tuple[float, ...]]: - if not req.bbox: - return None - - output_region = [] - for v in req.bbox.split(' '): - output_region.append(float(v)) - return tuple(output_region) + dataset = compute_tiles(ml_dataset, var_name, bbox, req.crs, + tile_size=tile_size, as_dataset=True) + dataset = dataset.rename_dims({'x': 'lon', 'y': 'lat'}) + dataset = dataset.rename_vars({'x': 'lon', 'y': 'lat'}) + return dataset def _get_dataset_config(ctx: WcsContext, req: CoverageRequest) \ @@ -257,140 +154,6 @@ def _get_dataset_config(ctx: WcsContext, req: CoverageRequest) \ raise RuntimeError('Should never come here. Contact the developers.') -# TODO: remove! -def _get_input_path(ctx: WcsContext, req: CoverageRequest) -> str: - 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: - path = dataset_config['Path'] - break - store_instance_id = dataset_config['StoreInstanceId'] - store = ctx.datasets_ctx.get_data_store_pool(). \ - get_store(store_instance_id) - return store.root + '/' + path - raise RuntimeError('Should never come here. Contact the developers.') - - -# TODO: remove! -def _get_input_store_id(ctx: WcsContext, req: CoverageRequest) -> str: - 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['StoreInstanceId'] - raise RuntimeError('Should never come here. Contact the developers.') - - -# TODO: remove! -def _validate_coverage_req(ctx: WcsContext, req: CoverageRequest): - def _has_no_invalid_bbox() -> bool: - if req.bbox: - return _is_valid_bbox(req.bbox) - else: - return True - - def _has_no_invalid_time() -> bool: - if req.time: - return _is_valid_time(req.time) - else: - return True - - if req.coverage and _is_valid_coverage(ctx, req.coverage) \ - and req.crs and _is_valid_crs(req.crs) \ - and ((req.bbox and _is_valid_bbox(req.bbox)) - or (req.time and _is_valid_time(req.time))) \ - and _has_no_invalid_bbox \ - and _has_no_invalid_time() \ - and ((req.width and req.height) - or (req.resx and req.resy)) \ - and ((req.width and not req.resx) - or (req.width and not req.resy) - or (req.height and not req.resx) - or (req.height and not req.resy) - or (req.resx and not req.width) - or (req.resx and not req.height) - or (req.resy and not req.width) - or (req.resy and not req.height)) \ - and req.format and _is_valid_format(req.format) \ - and not req.parameter \ - and not req.interpolation \ - and not req.exceptions: - return - elif not req.coverage or not _is_valid_coverage(ctx, req.coverage): - raise ValueError('No valid value for parameter COVERAGE provided. ' - 'COVERAGE must be a variable name prefixed with ' - 'its dataset name. Example: my_dataset.my_var') - elif req.parameter: - raise ValueError('PARAMETER not yet supported') - elif req.interpolation: - raise ValueError('INTERPOLATION not yet supported') - elif req.exceptions: - raise ValueError('EXCEPTIONS not yet supported') - elif ((not req.width and not req.height and not req.resy and not req.resy) - or (req.width and not req.height) - or (req.height and not req.width) - or (req.resx and not req.resy) - or (req.resy and not req.resx) - or (req.width and req.resx or req.resy) - or (req.height and req.resx or req.resy)): - raise ValueError('Either both WIDTH and HEIGHT, or both RESX and RESY ' - 'must be provided.') - elif not req.format or not _is_valid_format(req.format): - raise ValueError('FORMAT wrong or missing. Must be one of ' - + ', '.join(_get_formats_list()) - + f'. Was: {req.format}') - else: - raise ValueError('Reason unclear, fix me') - - -# TODO: remove! -def _is_valid_coverage(ctx: WcsContext, coverage: str) -> bool: - band_infos = _extract_band_infos(ctx, [coverage]) - if band_infos: - return True - return False - - -# TODO: remove! -def _is_valid_crs(crs: str) -> bool: - return crs in VALID_CRS_LIST - - -# TODO: remove! -def _is_valid_bbox(bbox: str) -> bool: - values = bbox.split(',') - if not len(values) == 4: - raise ValueError('BBOX must be given as `minx,miny,maxx,maxy`') - return True - - -# TODO: remove! -def _is_valid_format(format_req: str) -> bool: - return format_req in _get_formats_list() - - -# TODO: remove! -def _is_valid_time(time: str) -> bool: - # todo - change validation so that it makes sense but works with QGIS - try: - pass - # datetime.fromisoformat(time) - except ValueError: - raise ValueError('TIME value must be given in the format' - '\'YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]]' - '[+HH:MM[:SS[.ffffff]]]]\'') - return True - - # noinspection HttpUrlsUsage def _get_capabilities_element(ctx: WcsContext, base_url: str) -> Element: @@ -467,51 +230,51 @@ def _get_individual_name() -> str: text=_get_sp_value(['ProviderName'])), Element('positionName', text=_get_sp_value(['ServiceContact', - 'PositionName'])), + 'PositionName'])), Element('contactInfo', elements=[ Element('phone', elements=[ Element('voice', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Phone', - 'Voice'])), + 'ContactInfo', + 'Phone', + 'Voice'])), Element('facsimile', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Phone', - 'Facsimile'])), + 'ContactInfo', + 'Phone', + 'Facsimile'])), ]), Element('address', elements=[ Element('deliveryPoint', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Address', - 'DeliveryPoint'])), + 'ContactInfo', + 'Address', + 'DeliveryPoint'])), Element('city', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Address', - 'City'])), + 'ContactInfo', + 'Address', + 'City'])), Element('administrativeArea', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Address', - 'AdministrativeArea'])), + 'ContactInfo', + 'Address', + 'AdministrativeArea'])), Element('postalCode', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Address', - 'PostalCode'])), + 'ContactInfo', + 'Address', + 'PostalCode'])), Element('country', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Address', - 'Country'])), + 'ContactInfo', + 'Address', + 'Country'])), Element('electronicMailAddress', text=_get_sp_value(['ServiceContact', - 'ContactInfo', - 'Address', - 'ElectronicMailAddress'])), + 'ContactInfo', + 'Address', + 'ElectronicMailAddress'])), ]), Element('onlineResource', attrs={ 'xlink:href': _get_sp_value(['ProviderSite'])}) @@ -524,8 +287,8 @@ def _get_individual_name() -> str: def _get_capability_element(base_url: str) -> Element: - get_capabilities_url = f'{base_url}/wcs/kvp?service=WCS&version=1.0.0'\ - f'&request=GetCapabilities' + 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&' \ @@ -607,7 +370,10 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ Element('axisDescription', elements=[ Element('AxisDescription', elements=[ Element('name', text='Band'), - Element('label', text='Band') + Element('label', text='Band'), + Element('values', elements=[ + Element('singleValue', text='1') + ]) ]) ]) ]) From 06c6e0cbdbf685fe9d895e50ccd7956119e762b8 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Jan 2023 23:21:31 +0100 Subject: [PATCH 38/43] more details, less debug output --- xcube/webapi/ows/wcs/controllers.py | 3 ++- xcube/webapi/ows/wcs/routes.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 29d69c073..6247f4b5a 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -350,7 +350,8 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ ]), Element('domainSet', elements=[ Element('spatialDomain', elements=[ - Element('gml:Envelope', 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]}'), diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index a09e2ce4c..49275a72e 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -131,8 +131,6 @@ async def _do_get_coverage(self): 'RESY': resy }) - LOG.debug(str(cov_req)) - try: cube = await self.ctx.run_in_executor( None, From 7f08c524cf58d9bea3fcf16ba58edc8930f97341 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 11 Jan 2023 19:10:46 +0100 Subject: [PATCH 39/43] more details in DescribeCoverage --- xcube/webapi/ows/wcs/controllers.py | 24 ++++++++++++++++++++++-- xcube/webapi/ows/wcs/routes.py | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 6247f4b5a..cea0015a4 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -358,7 +358,27 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ 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=[ + # to do - handle negative values! + Element('gml:low', text='0 0'), + Element('gml:high', text= + f'{band_infos[var_name].bbox[2] - band_infos[var_name].bbox[0]} ' + f'{band_infos[var_name].bbox[3] - band_infos[var_name].bbox[1]}') + ]) + ]), + 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]} {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) @@ -415,7 +435,7 @@ 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'] + return ['netcdf', 'GeoTIFF'] class BandInfo: diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index 49275a72e..d9079e0fe 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -142,8 +142,7 @@ async def _do_get_coverage(self): # TODO: too broad error message, must name invalid parameter raise ApiError.BadRequest(f'{e}') from e - self.response.set_header('Content-Type', - 'application/octet-stream') + self.response.set_header('Content-Type', 'application/x-netcdf4') if file_format == 'netcdf': temp_file_path = await self.ctx.run_in_executor( @@ -216,6 +215,7 @@ 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 From 58b7359858f8789bd2dadc082e8c39143ddc1452 Mon Sep 17 00:00:00 2001 From: thomas Date: Thu, 12 Jan 2023 11:37:36 +0100 Subject: [PATCH 40/43] renaming standard names and dropping crs --- xcube/webapi/ows/wcs/controllers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index cea0015a4..1256857c8 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -124,8 +124,11 @@ def get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: 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': 'lon', 'y': 'lat'}) - dataset = dataset.rename_vars({'x': 'lon', 'y': 'lat'}) + 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 From c1defb378119240d955a13e370a66febaa4a88fc Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 13 Jan 2023 16:17:25 +0100 Subject: [PATCH 41/43] test --- xcube/webapi/ows/wcs/controllers.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index cea0015a4..8d29ba301 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -126,6 +126,7 @@ def get_coverage(ctx: WcsContext, req: CoverageRequest) -> xr.Dataset: tile_size=tile_size, as_dataset=True) dataset = dataset.rename_dims({'x': 'lon', 'y': 'lat'}) dataset = dataset.rename_vars({'x': 'lon', 'y': 'lat'}) + return dataset @@ -367,8 +368,8 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ # to do - handle negative values! Element('gml:low', text='0 0'), Element('gml:high', text= - f'{band_infos[var_name].bbox[2] - band_infos[var_name].bbox[0]} ' - f'{band_infos[var_name].bbox[3] - band_infos[var_name].bbox[1]}') + f'{band_infos[var_name].width} ' + f'{band_infos[var_name].height}') ]) ]), Element('gml:axisName', text='lon'), @@ -442,7 +443,9 @@ class BandInfo: def __init__(self, var_name: str, label: str, bbox: tuple[float, float, float, float], - time_steps: list[str]): + time_steps: list[str], width, height): + self.height = height + self.width = width self.var_name = var_name self.label = label self.bbox = bbox @@ -489,7 +492,11 @@ def _extract_band_infos(ctx: WcsContext, coverages: List[str] = 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) + width = grid_mapping.width + height = grid_mapping.height + + band_info = BandInfo(qualified_var_name, label, bbox, time_steps, + width, height) band_infos[f'{ds_name}.{var_name}'] = band_info return band_infos From 86571d9c4cc22fa5c2451c1096b8aac57ce05e95 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 13 Jan 2023 16:24:56 +0100 Subject: [PATCH 42/43] cleanup after fix --- xcube/webapi/ows/wcs/controllers.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/xcube/webapi/ows/wcs/controllers.py b/xcube/webapi/ows/wcs/controllers.py index 69d4a8d6b..0c2be8b2b 100644 --- a/xcube/webapi/ows/wcs/controllers.py +++ b/xcube/webapi/ows/wcs/controllers.py @@ -367,7 +367,6 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ elements=[ Element('gml:limits', elements=[ Element('gml:GridEnvelope', elements=[ - # to do - handle negative values! Element('gml:low', text='0 0'), Element('gml:high', text= f'{band_infos[var_name].width} ' @@ -377,7 +376,9 @@ def _get_describe_element(ctx: WcsContext, coverages: List[str] = None) \ 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]} {band_infos[var_name].bbox[1]}') + 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') @@ -438,7 +439,7 @@ 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', 'GeoTIFF'] + return ['netcdf'] class BandInfo: @@ -446,14 +447,14 @@ class BandInfo: def __init__(self, var_name: str, label: str, bbox: tuple[float, float, float, float], time_steps: list[str], width, height): - self.height = height - self.width = width 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) \ @@ -494,11 +495,8 @@ def _extract_band_infos(ctx: WcsContext, coverages: List[str] = None) \ if is_temporal_var: time_steps = [f'{str(d)[:19]}Z' for d in var.time.values] - width = grid_mapping.width - height = grid_mapping.height - band_info = BandInfo(qualified_var_name, label, bbox, time_steps, - width, height) + grid_mapping.width, grid_mapping.height) band_infos[f'{ds_name}.{var_name}'] = band_info return band_infos From 58c4fdb0f18059ae27998f78c44ff944b99de30f Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 31 Jan 2023 13:00:15 +0100 Subject: [PATCH 43/43] adapted to xcube server changes --- xcube/webapi/ows/wcs/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcube/webapi/ows/wcs/routes.py b/xcube/webapi/ows/wcs/routes.py index d9079e0fe..77f2772d5 100644 --- a/xcube/webapi/ows/wcs/routes.py +++ b/xcube/webapi/ows/wcs/routes.py @@ -50,7 +50,7 @@ async def get(self): None, get_wcs_capabilities_xml, self.ctx, - self.request.base_url + self.request.reverse_base_url ) self.response.set_header('Content-Type', 'application/xml') await self.response.finish(capabilities) @@ -205,7 +205,7 @@ async def _do_get_capabilities(self): None, get_wcs_capabilities_xml, self.ctx, - self.request.base_url + self.request.reverse_base_url ) self.response.set_header('Content-Type', 'application/xml') await self.response.finish(capabilities_xml)