Server OAPIF temporal extent

This commit is contained in:
Alessandro Pasotti 2019-10-20 16:31:36 +02:00
parent c9df6aee25
commit 358c814722
12 changed files with 334 additions and 7 deletions

View File

@ -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

View File

@ -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"
}
}
}
}
}
}
}
},

View File

@ -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<QDateTime>( min, layer->minimumValue( fieldIdx ).toDateTime() );
}
if ( maxEnd.isValid() )
{
max = std::max<QDateTime>( max, layer->maximumValue( fieldIdx ).toDateTime() );
}
}
}
return { min, max };
};
const QList<QgsVectorLayerServerProperties::WmsDimensionInfo> 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;

View File

@ -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
*/

View File

@ -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" },
}
}
}
},

View File

@ -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()

View File

@ -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": [

View File

@ -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"
}

View File

@ -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 èé",

View File

@ -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",

View File

@ -68,10 +68,10 @@
<projectlayers>
<maplayer minScale="1e+08" simplifyLocal="1" autoRefreshEnabled="0" maxScale="0" autoRefreshTime="0" wkbType="Point" labelsEnabled="0" simplifyDrawingTol="1.2" geometry="Point" readOnly="0" styleCategories="AllStyleCategories" simplifyAlgorithm="0" type="vector" refreshOnNotifyEnabled="0" simplifyDrawingHints="0" hasScaleBasedVisibilityFlag="0" refreshOnNotifyMessage="" simplifyMaxScale="1">
<extent>
<xmin>7.15825748443603516</xmin>
<ymin>44.79768753051757813</ymin>
<xmax>7.30355501174926758</xmax>
<ymax>44.82162857055664063</ymax>
<xmin>7.15826000000000029</xmin>
<ymin>44.79769999999999897</ymin>
<xmax>7.30356000000000005</xmax>
<ymax>44.82159999999999656</ymax>
</extent>
<id>points_47ad3bc8_35bd_4392_8994_2dc5ff04be60</id>
<datasource>./test_project_api_timefilters.gpkg|layername=points</datasource>
@ -119,7 +119,7 @@
<description></description>
<projectionacronym></projectionacronym>
<ellipsoidacronym></ellipsoidacronym>
<geographicflag>true</geographicflag>
<geographicflag>false</geographicflag>
</spatialrefsys>
</crs>
<extent>
@ -187,7 +187,9 @@
<sizescale/>
</renderer-v2>
<customproperties>
<property key="dualview/previewExpressions" value="name"/>
<property key="dualview/previewExpressions">
<value>name</value>
</property>
<property key="embeddedWidgets/count" value="0"/>
<property key="variableNames"/>
<property key="variableValues"/>
@ -328,7 +330,7 @@
<column type="field" name="updated" hidden="0" width="497"/>
<column type="field" name="begin" hidden="0" width="-1"/>
<column type="field" name="end" hidden="0" width="-1"/>
<column type="field" name="updated_string" hidden="0" width="-1"/>
<column type="field" name="updated_string" hidden="0" width="371"/>
<column type="field" name="created_string" hidden="0" width="-1"/>
</columns>
</attributetableconfig>