From 358c8147227a436713597ab59a29731734ddf863 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Sun, 20 Oct 2019 16:31:36 +0200 Subject: [PATCH] Server OAPIF temporal extent --- .../auto_generated/qgsserverapiutils.sip.in | 20 ++++++ resources/server/api/ogc/schema.json | 34 +++++++++ src/server/qgsserverapiutils.cpp | 66 ++++++++++++++++++ src/server/qgsserverapiutils.h | 38 ++++++++++ src/server/services/wfs3/qgswfs3handlers.cpp | 13 ++++ tests/src/python/test_qgsserver_api.py | 40 +++++++++++ .../api/test_wfs3_api_project.json | 34 +++++++++ ...st_wfs3_collection_points_timefilters.json | 60 ++++++++++++++++ .../test_wfs3_collection_testlayer_èé.json | 4 ++ .../api/test_wfs3_collections_project.json | 16 +++++ .../test_project_api_timefilters.gpkg | Bin 102400 -> 102400 bytes .../test_project_api_timefilters.qgs | 16 +++-- 12 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 tests/testdata/qgis_server/api/test_wfs3_collection_points_timefilters.json diff --git a/python/server/auto_generated/qgsserverapiutils.sip.in b/python/server/auto_generated/qgsserverapiutils.sip.in index 020fd003309..5e515d37bee 100644 --- a/python/server/auto_generated/qgsserverapiutils.sip.in +++ b/python/server/auto_generated/qgsserverapiutils.sip.in @@ -72,6 +72,26 @@ datetime = date-time / interval %End + + static QVariantList temporalExtentList( const QgsVectorLayer *layer ) /PyName=temporalExtent/; +%Docstring +temporalExtent returns a json array with an array of [min, max] temporal extent for the given ``layer``. +In case multiple temporal dimensions are available in the layer, a union of all dimensions is returned. + +From specifications: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/extent.yaml + +One or more time intervals that describe the temporal extent of the dataset. +The value `null` is supported and indicates an open time interval. + +In the Core only a single time interval is supported. Extensions may support +multiple intervals. If multiple intervals are provided, the union of the +intervals describes the temporal extent. + +:return: An array of intervals + +.. versionadded:: 3.12 +%End + static QgsCoordinateReferenceSystem parseCrs( const QString &bboxCrs ); %Docstring Parses the CRS URI ``bboxCrs`` (example: "http://www.opengis.net/def/crs/OGC/1.3/CRS84") into a QGIS CRS object diff --git a/resources/server/api/ogc/schema.json b/resources/server/api/ogc/schema.json index d811b704afb..967c8d51662 100644 --- a/resources/server/api/ogc/schema.json +++ b/resources/server/api/ogc/schema.json @@ -188,6 +188,40 @@ "items" : { "type" : "number" } + }, + "temporal" : { + "description" : "The temporal extent of the features in the collection.", + "type" : "object", + "properties" : { + "interval" : { + "description" : "One or more time intervals that describe the temporal extent of the dataset.\nThe value `null` is supported and indicates an open time interval.\nIn the Core only a single time interval is supported. Extensions may support multiple intervals. If multiple intervals are provided, the union of the intervals describes the temporal extent.", + "type": "array", + "minItems": 1, + "items" : { + "description" : "Begin and end times of the time interval. The timestamps\nare in the coordinate reference system specified in `trs`. By default\nthis is the Gregorian calendar.", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type" : "string", + "format" : "date-time", + "nullable" : true, + "example" : [ + "2011-11-11T12:22:11Z", + null + ], + "trs" : { + "description": "Coordinate reference system of the coordinates in the temporal extent\n(property `interval`). The default reference system is the Gregorian calendar.\nIn the Core this is the only supported temporal reference system.\nExtensions may support additional temporal reference systems and add\nadditional enum values.", + "type": "string", + "enum" : [ + "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" + ], + "default" : "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" + } + } + } + } + } } } }, diff --git a/src/server/qgsserverapiutils.cpp b/src/server/qgsserverapiutils.cpp index 684ba6a2e0d..7b4da71c4d3 100644 --- a/src/server/qgsserverapiutils.cpp +++ b/src/server/qgsserverapiutils.cpp @@ -415,6 +415,72 @@ json QgsServerApiUtils::layerExtent( const QgsVectorLayer *layer ) return {{ extent.xMinimum(), extent.yMinimum(), extent.xMaximum(), extent.yMaximum() }}; } +json QgsServerApiUtils::temporalExtent( const QgsVectorLayer *layer ) +{ + // Helper to get min/max from a dimension + auto range = [ & ]( const QgsVectorLayerServerProperties::WmsDimensionInfo & dimInfo ) -> QgsDateTimeRange + { + QgsDateTimeRange result; + // min + int fieldIdx { layer->fields().lookupField( dimInfo.fieldName )}; + if ( fieldIdx < 0 ) + { + return result; + } + QDateTime min { layer->minimumValue( fieldIdx ).toDateTime() }; + QDateTime max { layer->maximumValue( fieldIdx ).toDateTime() }; + if ( ! dimInfo.endFieldName.isEmpty() ) + { + fieldIdx = layer->fields().lookupField( dimInfo.endFieldName ); + if ( fieldIdx >= 0 ) + { + QDateTime minEnd { layer->minimumValue( fieldIdx ).toDateTime() }; + QDateTime maxEnd { layer->maximumValue( fieldIdx ).toDateTime() }; + if ( minEnd.isValid() ) + { + min = std::min( min, layer->minimumValue( fieldIdx ).toDateTime() ); + } + if ( maxEnd.isValid() ) + { + max = std::max( max, layer->maximumValue( fieldIdx ).toDateTime() ); + } + } + } + return { min, max }; + }; + + const QList dimensions { QgsServerApiUtils::temporalDimensions( layer ) }; + if ( dimensions.isEmpty() ) + { + return nullptr; + } + else + { + QgsDateTimeRange extent; + for ( const auto &dimension : dimensions ) + { + // Get min/max for dimension + extent.extend( range( dimension ) ); + } + json ret = json::array(); + const QString beginVal { extent.begin().toString( Qt::DateFormat::ISODate ) }; + const QString endVal { extent.end().toString( Qt::DateFormat::ISODate ) }; + ret.push_back( + { + beginVal.isEmpty() ? nullptr : beginVal.toStdString(), + endVal.isEmpty() ? nullptr : endVal.toStdString() + } ); + return ret; + } +} + +QVariantList QgsServerApiUtils::temporalExtentList( const QgsVectorLayer *layer ) SIP_PYNAME( temporalExtent ) +{ + QVariantList list; + list.push_back( QgsJsonUtils::parseArray( QString::fromStdString( temporalExtent( layer )[0].dump() ) ) ); + return list; +} + QgsCoordinateReferenceSystem QgsServerApiUtils::parseCrs( const QString &bboxCrs ) { QgsCoordinateReferenceSystem crs; diff --git a/src/server/qgsserverapiutils.h b/src/server/qgsserverapiutils.h index 279d558393c..fd8fd5f2737 100644 --- a/src/server/qgsserverapiutils.h +++ b/src/server/qgsserverapiutils.h @@ -29,6 +29,7 @@ #include "qgsserverexception.h" #include "qgsvectorlayerserverproperties.h" #include "qgsrange.h" +#include "qgsjsonutils.h" #ifdef HAVE_SERVER_PYTHON_PLUGINS #include "qgsaccesscontrol.h" @@ -105,6 +106,43 @@ class SERVER_EXPORT QgsServerApiUtils */ static json layerExtent( const QgsVectorLayer *layer ) SIP_SKIP; + /** + * temporalExtent returns a json array with an array of [min, max] temporal extent for the given \a layer. + * In case multiple temporal dimensions are available in the layer, a union of all dimensions is returned. + * + * From specifications: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/extent.yaml + * + * One or more time intervals that describe the temporal extent of the dataset. + * The value `null` is supported and indicates an open time interval. + * + * In the Core only a single time interval is supported. Extensions may support + * multiple intervals. If multiple intervals are provided, the union of the + * intervals describes the temporal extent. + * + * \return An array of intervals + * \note not available in Python bindings + * \since QGIS 3.12 + */ + static json temporalExtent( const QgsVectorLayer *layer ) SIP_SKIP; + + /** + * temporalExtent returns a json array with an array of [min, max] temporal extent for the given \a layer. + * In case multiple temporal dimensions are available in the layer, a union of all dimensions is returned. + * + * From specifications: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/extent.yaml + * + * One or more time intervals that describe the temporal extent of the dataset. + * The value `null` is supported and indicates an open time interval. + * + * In the Core only a single time interval is supported. Extensions may support + * multiple intervals. If multiple intervals are provided, the union of the + * intervals describes the temporal extent. + * + * \return An array of intervals + * \since QGIS 3.12 + */ + static QVariantList temporalExtentList( const QgsVectorLayer *layer ) SIP_PYNAME( temporalExtent ); + /** * Parses the CRS URI \a bboxCrs (example: "http://www.opengis.net/def/crs/OGC/1.3/CRS84") into a QGIS CRS object */ diff --git a/src/server/services/wfs3/qgswfs3handlers.cpp b/src/server/services/wfs3/qgswfs3handlers.cpp index 43fbb1f515d..76cbf0ba5a5 100644 --- a/src/server/services/wfs3/qgswfs3handlers.cpp +++ b/src/server/services/wfs3/qgswfs3handlers.cpp @@ -494,6 +494,12 @@ void QgsWfs3CollectionsHandler::handleRequest( const QgsServerApiContext &contex "spatial", { { "bbox", QgsServerApiUtils::layerExtent( layer ) }, { "crs", "http://www.opengis.net/def/crs/OGC/1.3/CRS84" }, + }, + }, + { + "temporal", { + { "interval", QgsServerApiUtils::temporalExtent( layer ) }, + { "trs", "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" }, } } } @@ -524,6 +530,7 @@ void QgsWfs3CollectionsHandler::handleRequest( const QgsServerApiContext &contex } ); } } + json navigation = json::array(); const QUrl url { context.request()->url() }; navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 1 ).toStdString() }} ) ; @@ -660,6 +667,12 @@ void QgsWfs3DescribeCollectionHandler::handleRequest( const QgsServerApiContext { "bbox", QgsServerApiUtils::layerExtent( mapLayer ) }, { "crs", "http://www.opengis.net/def/crs/OGC/1.3/CRS84" }, } + }, + { + "temporal", { + { "interval", QgsServerApiUtils::temporalExtent( mapLayer ) }, + { "trs", "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" }, + } } } }, diff --git a/tests/src/python/test_qgsserver_api.py b/tests/src/python/test_qgsserver_api.py index 3cf9ce1075d..314483d7341 100644 --- a/tests/src/python/test_qgsserver_api.py +++ b/tests/src/python/test_qgsserver_api.py @@ -94,6 +94,39 @@ class QgsServerAPIUtilsTest(QgsServerTestBase): path = QgsServerApiUtils.appendMapParameter('/wfs3', QtCore.QUrl('https://www.qgis.org/wfs3?MAP=/some/path')) self.assertEqual(path, '/wfs3?MAP=/some/path') + def test_temporal_extent(self): + + project = QgsProject() + base_path = unitTestDataPath('qgis_server') + '/test_project_api_timefilters.qgs' + project.read(base_path) + + layer = list(project.mapLayers().values())[0] + + layer.serverProperties().removeWmsDimension('date') + layer.serverProperties().removeWmsDimension('time') + self.assertTrue(layer.serverProperties().addWmsDimension(QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated_string'))) + self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [['2010-01-01T01:01:01', '2020-01-01T01:01:01']]) + + layer.serverProperties().removeWmsDimension('date') + layer.serverProperties().removeWmsDimension('time') + self.assertTrue(layer.serverProperties().addWmsDimension(QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created'))) + self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [['2010-01-01T00:00:00', '2019-01-01T00:00:00']]) + + layer.serverProperties().removeWmsDimension('date') + layer.serverProperties().removeWmsDimension('time') + self.assertTrue(layer.serverProperties().addWmsDimension(QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'created_string'))) + self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [['2010-01-01T00:00:00', '2019-01-01T00:00:00']]) + + layer.serverProperties().removeWmsDimension('date') + layer.serverProperties().removeWmsDimension('time') + self.assertTrue(layer.serverProperties().addWmsDimension(QgsVectorLayerServerProperties.WmsDimensionInfo('time', 'updated'))) + self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [['2010-01-01T01:01:01Z', '2022-01-01T01:01:01Z']]) + + layer.serverProperties().removeWmsDimension('date') + layer.serverProperties().removeWmsDimension('time') + self.assertTrue(layer.serverProperties().addWmsDimension(QgsVectorLayerServerProperties.WmsDimensionInfo('date', 'begin', 'end'))) + self.assertEqual(QgsServerApiUtils.temporalExtent(layer), [['2010-01-01T00:00:00', '2022-01-01T00:00:00']]) + class API(QgsServerApi): @@ -359,6 +392,13 @@ class QgsServerAPITest(QgsServerAPITestBase): request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé') self.compareApi(request, project, 'test_wfs3_collection_testlayer_èé.json') + def test_wfs3_collection_temporal_extent_json(self): + """Test collection with timefilter""" + project = QgsProject() + project.read(unitTestDataPath('qgis_server') + '/test_project_api_timefilters.qgs') + request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/points') + self.compareApi(request, project, 'test_wfs3_collection_points_timefilters.json') + def test_wfs3_collection_html(self): """Test WFS3 API collection""" project = QgsProject() diff --git a/tests/testdata/qgis_server/api/test_wfs3_api_project.json b/tests/testdata/qgis_server/api/test_wfs3_api_project.json index ba02ebaabeb..676f224fe5a 100644 --- a/tests/testdata/qgis_server/api/test_wfs3_api_project.json +++ b/tests/testdata/qgis_server/api/test_wfs3_api_project.json @@ -280,6 +280,40 @@ Content-Type: application/openapi+json;version=3.0 "maxItems": 6, "minItems": 4, "type": "array" + }, + "temporal": { + "description": "The temporal extent of the features in the collection.", + "properties": { + "interval": { + "description": "One or more time intervals that describe the temporal extent of the dataset.\nThe value `null` is supported and indicates an open time interval.\nIn the Core only a single time interval is supported. Extensions may support multiple intervals. If multiple intervals are provided, the union of the intervals describes the temporal extent.", + "items": { + "description": "Begin and end times of the time interval. The timestamps\nare in the coordinate reference system specified in `trs`. By default\nthis is the Gregorian calendar.", + "items": { + "example": [ + "2011-11-11T12:22:11Z", + null + ], + "format": "date-time", + "nullable": true, + "trs": { + "default": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", + "description": "Coordinate reference system of the coordinates in the temporal extent\n(property `interval`). The default reference system is the Gregorian calendar.\nIn the Core this is the only supported temporal reference system.\nExtensions may support additional temporal reference systems and add\nadditional enum values.", + "enum": [ + "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" + ], + "type": "string" + }, + "type": "string" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" } }, "required": [ diff --git a/tests/testdata/qgis_server/api/test_wfs3_collection_points_timefilters.json b/tests/testdata/qgis_server/api/test_wfs3_collection_points_timefilters.json new file mode 100644 index 00000000000..5481d7fb7de --- /dev/null +++ b/tests/testdata/qgis_server/api/test_wfs3_collection_points_timefilters.json @@ -0,0 +1,60 @@ +Content-Type: application/json + +{ + "crs": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "http://www.opengis.net/def/crs/EPSG/9.6.2/4326", + "http://www.opengis.net/def/crs/EPSG/9.6.2/3857" + ], + "extent": { + "spatial": { + "bbox": [ + [ + 7.15826, + 44.7977, + 7.30356, + 44.8216 + ] + ], + "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": [ + [ + "2010-01-01T00:00:00", + "2022-01-01T01:01:01Z" + ] + ], + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" + } + }, + "id": "points", + "links": [ + { + "href": "http://server.qgis.org/wfs3/collections/points.json", + "rel": "self", + "title": "Feature collection as JSON", + "type": "application/json" + }, + { + "href": "http://server.qgis.org/wfs3/collections/points.html", + "rel": "alternate", + "title": "Feature collection as HTML", + "type": "text/html" + }, + { + "href": "http://server.qgis.org/wfs3/collections/points/items.json", + "rel": "items", + "title": "points", + "type": "application/json" + }, + { + "href": "http://server.qgis.org/wfs3/collections/points/items.html", + "rel": "items", + "title": "points", + "type": "text/html" + } + ], + "timeStamp": "2019-10-20T16:29:01Z", + "title": "points" +} \ No newline at end of file diff --git a/tests/testdata/qgis_server/api/test_wfs3_collection_testlayer_èé.json b/tests/testdata/qgis_server/api/test_wfs3_collection_testlayer_èé.json index 2dacbd499ac..3a505ba8b89 100644 --- a/tests/testdata/qgis_server/api/test_wfs3_collection_testlayer_èé.json +++ b/tests/testdata/qgis_server/api/test_wfs3_collection_testlayer_èé.json @@ -17,6 +17,10 @@ Content-Type: application/json ] ], "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": null, + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" } }, "id": "testlayer èé", diff --git a/tests/testdata/qgis_server/api/test_wfs3_collections_project.json b/tests/testdata/qgis_server/api/test_wfs3_collections_project.json index f801a87345c..df56eaebd34 100644 --- a/tests/testdata/qgis_server/api/test_wfs3_collections_project.json +++ b/tests/testdata/qgis_server/api/test_wfs3_collections_project.json @@ -20,6 +20,10 @@ Content-Type: application/json ] ], "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": null, + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" } }, "id": "testlayer èé", @@ -57,6 +61,10 @@ Content-Type: application/json ] ], "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": null, + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" } }, "id": "layer1_with_short_name", @@ -94,6 +102,10 @@ Content-Type: application/json ] ], "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": null, + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" } }, "id": "exclude_attribute", @@ -131,6 +143,10 @@ Content-Type: application/json ] ], "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": null, + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" } }, "id": "fields_alias", diff --git a/tests/testdata/qgis_server/test_project_api_timefilters.gpkg b/tests/testdata/qgis_server/test_project_api_timefilters.gpkg index aa24bda4e91c2c46a4c7b26c6a36134add679087..013406a6207e360024315aedf117a7943218d5c1 100644 GIT binary patch delta 22 ecmZozz}B#UZGtr8hlw)Ij2{{kwk9wx=m!8}a0u`K delta 22 ecmZozz}B#UZGtr8n~5^cjBgqfwk9wx=m!8}90=$D diff --git a/tests/testdata/qgis_server/test_project_api_timefilters.qgs b/tests/testdata/qgis_server/test_project_api_timefilters.qgs index cec0df22588..2efe4121a84 100644 --- a/tests/testdata/qgis_server/test_project_api_timefilters.qgs +++ b/tests/testdata/qgis_server/test_project_api_timefilters.qgs @@ -68,10 +68,10 @@ - 7.15825748443603516 - 44.79768753051757813 - 7.30355501174926758 - 44.82162857055664063 + 7.15826000000000029 + 44.79769999999999897 + 7.30356000000000005 + 44.82159999999999656 points_47ad3bc8_35bd_4392_8994_2dc5ff04be60 ./test_project_api_timefilters.gpkg|layername=points @@ -119,7 +119,7 @@ - true + false @@ -187,7 +187,9 @@ - + + name + @@ -328,7 +330,7 @@