mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-13 00:03:09 -04:00
Server OAPIF datetime support
This commit is contained in:
parent
b2e7121493
commit
6112da45f6
@ -1904,6 +1904,18 @@ A set of attributes that are not advertised in WFS requests with QGIS server.
|
||||
void setExcludeAttributesWfs( const QSet<QString> &att );
|
||||
%Docstring
|
||||
A set of attributes that are not advertised in WFS requests with QGIS server.
|
||||
%End
|
||||
|
||||
QSet<QString> includeAttributesOapifTemporalFilters() const;
|
||||
%Docstring
|
||||
Returns the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
|
||||
\since: QGIS 3.12
|
||||
%End
|
||||
|
||||
void setIncludeAttributesOapifTemporalFilters( const QSet<QString> &att );
|
||||
%Docstring
|
||||
Sets the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
|
||||
\since: QGIS 3.12
|
||||
%End
|
||||
|
||||
virtual bool deleteAttribute( int attr );
|
||||
|
@ -35,6 +35,41 @@ Parses a comma separated ``bbox`` into a (possibly empty) :py:class:`QgsRectangl
|
||||
Z values (i.e. a 6 elements bbox) are silently discarded
|
||||
%End
|
||||
|
||||
struct TemporalDateInterval
|
||||
{
|
||||
QDate begin;
|
||||
QDate end;
|
||||
};
|
||||
|
||||
struct TemporalDateTimeInterval
|
||||
{
|
||||
QDateTime begin;
|
||||
QDateTime end;
|
||||
};
|
||||
|
||||
static TemporalDateInterval parseTemporalDateInterval( const QString &interval ) throw( QgsServerApiBadRequestException );
|
||||
%Docstring
|
||||
Parse a date time ``interval`` and returns a TemporalInterval
|
||||
|
||||
:raises QgsServerApiBadRequestException: if interval cannot be parsed
|
||||
%End
|
||||
static TemporalDateTimeInterval parseTemporalDateTimeInterval( const QString &interval ) throw( QgsServerApiBadRequestException );
|
||||
|
||||
|
||||
|
||||
static QgsExpression temporalFilterExpression( const QgsVectorLayer *layer, const QString &interval );
|
||||
%Docstring
|
||||
Parse the ``interval`` and constructs a (possibily invalid) temporal filter expression for the given ``layer``
|
||||
|
||||
Syntax:
|
||||
|
||||
interval-closed = date-time "/" date-time
|
||||
interval-open-start = [".."] "/" date-time
|
||||
interval-open-end = date-time "/" [".."]
|
||||
interval = interval-closed / interval-open-start / interval-open-end
|
||||
datetime = date-time / interval
|
||||
%End
|
||||
|
||||
|
||||
static QgsCoordinateReferenceSystem parseCrs( const QString &bboxCrs );
|
||||
%Docstring
|
||||
|
@ -60,8 +60,16 @@ QgsSourceFieldsProperties::QgsSourceFieldsProperties( QgsVectorLayer *layer, QWi
|
||||
mFieldsList->setHorizontalHeaderItem( AttrLengthCol, new QTableWidgetItem( tr( "Length" ) ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrPrecCol, new QTableWidgetItem( tr( "Precision" ) ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrCommentCol, new QTableWidgetItem( tr( "Comment" ) ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrWMSCol, new QTableWidgetItem( QStringLiteral( "WMS" ) ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrWFSCol, new QTableWidgetItem( QStringLiteral( "WFS" ) ) );
|
||||
const auto wmsWi = new QTableWidgetItem( QStringLiteral( "WMS" ) );
|
||||
wmsWi->setToolTip( tr( "Defines if this field is available in QGIS Server WMS service" ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrWMSCol, wmsWi );
|
||||
const auto wfsWi = new QTableWidgetItem( QStringLiteral( "WFS" ) );
|
||||
wfsWi->setToolTip( tr( "Defines if this field is available in QGIS Server WFS (and OAPIF) service" ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrWFSCol, wfsWi );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrAliasCol, new QTableWidgetItem( tr( "Alias" ) ) );
|
||||
const auto oapifWi = new QTableWidgetItem( QStringLiteral( "Temporal OAPIF" ) );
|
||||
oapifWi->setToolTip( tr( "Defines if this field will be used for temporal filtering in QGIS Server OAPIF service" ) );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrOapifDateTimeCol, oapifWi );
|
||||
mFieldsList->setHorizontalHeaderItem( AttrAliasCol, new QTableWidgetItem( tr( "Alias" ) ) );
|
||||
|
||||
mFieldsList->setSortingEnabled( true );
|
||||
@ -265,6 +273,23 @@ void QgsSourceFieldsProperties::setRow( int row, int idx, const QgsField &field
|
||||
wfsAttrItem->setCheckState( mLayer->excludeAttributesWfs().contains( field.name() ) ? Qt::Unchecked : Qt::Checked );
|
||||
wfsAttrItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable );
|
||||
mFieldsList->setItem( row, AttrWFSCol, wfsAttrItem );
|
||||
|
||||
// OAPIF temporal
|
||||
QTableWidgetItem *oapifAttrItem = new QTableWidgetItem();
|
||||
// ok, in theory, we could support any field type that
|
||||
// can contain something convertible to a date/datetime, but
|
||||
// let's keep it simple for now
|
||||
if ( field.isDateOrTime() )
|
||||
{
|
||||
oapifAttrItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable );
|
||||
oapifAttrItem->setCheckState( mLayer->includeAttributesOapifTemporalFilters().contains( field.name() ) ? Qt::Checked : Qt::Unchecked );
|
||||
}
|
||||
else
|
||||
{
|
||||
oapifAttrItem->setFlags( oapifAttrItem->flags() & ~Qt::ItemIsSelectable & ~Qt::ItemIsEnabled & ~Qt::ItemIsUserCheckable );
|
||||
}
|
||||
mFieldsList->setItem( row, AttrOapifDateTimeCol, oapifAttrItem );
|
||||
|
||||
}
|
||||
|
||||
bool QgsSourceFieldsProperties::addAttribute( const QgsField &field )
|
||||
@ -286,7 +311,7 @@ bool QgsSourceFieldsProperties::addAttribute( const QgsField &field )
|
||||
|
||||
void QgsSourceFieldsProperties::apply()
|
||||
{
|
||||
QSet<QString> excludeAttributesWMS, excludeAttributesWFS;
|
||||
QSet<QString> excludeAttributesWMS, excludeAttributesWFS, includeTemporalFilters;
|
||||
|
||||
for ( int i = 0; i < mFieldsList->rowCount(); i++ )
|
||||
{
|
||||
@ -298,11 +323,16 @@ void QgsSourceFieldsProperties::apply()
|
||||
{
|
||||
excludeAttributesWFS.insert( mFieldsList->item( i, AttrNameCol )->text() );
|
||||
}
|
||||
if ( mFieldsList->item( i, AttrOapifDateTimeCol )->checkState() == Qt::Checked )
|
||||
{
|
||||
includeTemporalFilters.insert( mFieldsList->item( i, AttrNameCol )->text() );
|
||||
}
|
||||
}
|
||||
|
||||
mLayer->setExcludeAttributesWms( excludeAttributesWMS );
|
||||
mLayer->setExcludeAttributesWfs( excludeAttributesWFS );
|
||||
|
||||
// Note: this is a whitelist unlike the previous two!
|
||||
mLayer->setIncludeAttributesOapifTemporalFilters( includeTemporalFilters );
|
||||
}
|
||||
|
||||
//SLOTS
|
||||
|
@ -79,6 +79,7 @@ class APP_EXPORT QgsSourceFieldsProperties : public QWidget, private Ui_QgsSourc
|
||||
AttrCommentCol,
|
||||
AttrWMSCol,
|
||||
AttrWFSCol,
|
||||
AttrOapifDateTimeCol, //! OAPIF (WFS3) datetime filter column
|
||||
AttrColCount,
|
||||
};
|
||||
|
||||
|
@ -2192,6 +2192,17 @@ bool QgsVectorLayer::readSymbology( const QDomNode &layerNode, QString &errorMes
|
||||
}
|
||||
}
|
||||
|
||||
mIncludeAttributesOapifTemporalFilters.clear();
|
||||
QDomNode includeOapifTemporalFiltersNode = layerNode.namedItem( QStringLiteral( "includeAttributesOapifTemporalFilters" ) );
|
||||
if ( !excludeWFSNode.isNull() )
|
||||
{
|
||||
QDomNodeList attributeNodeList = includeOapifTemporalFiltersNode.toElement().elementsByTagName( QStringLiteral( "attribute" ) );
|
||||
for ( int i = 0; i < attributeNodeList.size(); ++i )
|
||||
{
|
||||
mIncludeAttributesOapifTemporalFilters.insert( attributeNodeList.at( i ).toElement().text() );
|
||||
}
|
||||
}
|
||||
|
||||
// Load editor widget configuration
|
||||
QDomElement widgetsElem = layerNode.namedItem( QStringLiteral( "fieldConfiguration" ) ).toElement();
|
||||
QDomNodeList fieldConfigurationElementList = widgetsElem.elementsByTagName( QStringLiteral( "field" ) );
|
||||
@ -2505,6 +2516,18 @@ bool QgsVectorLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString
|
||||
}
|
||||
node.appendChild( excludeWFSElem );
|
||||
|
||||
//include attributes OAPIF
|
||||
QDomElement includeOapifElem = doc.createElement( QStringLiteral( "includeAttributesOapifTemporalFilters" ) );
|
||||
QSet<QString>::const_iterator attOapifTemporalIt = mIncludeAttributesOapifTemporalFilters.constBegin();
|
||||
for ( ; attOapifTemporalIt != mIncludeAttributesOapifTemporalFilters.constEnd(); ++attOapifTemporalIt )
|
||||
{
|
||||
QDomElement attrElem = doc.createElement( QStringLiteral( "attribute" ) );
|
||||
QDomText attrText = doc.createTextNode( *attOapifTemporalIt );
|
||||
attrElem.appendChild( attrText );
|
||||
includeOapifElem.appendChild( attrElem );
|
||||
}
|
||||
node.appendChild( includeOapifElem );
|
||||
|
||||
//default expressions
|
||||
QDomElement defaultsElem = doc.createElement( QStringLiteral( "defaults" ) );
|
||||
for ( const QgsField &field : mFields )
|
||||
|
@ -1775,6 +1775,18 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
|
||||
*/
|
||||
void setExcludeAttributesWfs( const QSet<QString> &att ) { mExcludeAttributesWFS = att; }
|
||||
|
||||
/**
|
||||
* Returns the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
|
||||
* \since: QGIS 3.12
|
||||
*/
|
||||
QSet<QString> includeAttributesOapifTemporalFilters() const { return mIncludeAttributesOapifTemporalFilters; }
|
||||
|
||||
/**
|
||||
* Sets the attributes that are used for temporal filtering with QGIS server OAPIF (WFS3).
|
||||
* \since: QGIS 3.12
|
||||
*/
|
||||
void setIncludeAttributesOapifTemporalFilters( const QSet<QString> &att ) { mIncludeAttributesOapifTemporalFilters = att; }
|
||||
|
||||
/**
|
||||
* Deletes an attribute field (but does not commit it).
|
||||
*
|
||||
@ -2728,6 +2740,9 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
|
||||
//! Attributes which are not published in WFS
|
||||
QSet<QString> mExcludeAttributesWFS;
|
||||
|
||||
//! Attributes which are used for OAPIF temporal filtering
|
||||
QSet<QString> mIncludeAttributesOapifTemporalFilters;
|
||||
|
||||
//! Geometry type as defined in enum WkbType (qgis.h)
|
||||
QgsWkbTypes::Type mWkbType = QgsWkbTypes::Unknown;
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "qgsserverprojectutils.h"
|
||||
#include "qgsmessagelog.h"
|
||||
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <QUrl>
|
||||
@ -61,6 +62,199 @@ QgsRectangle QgsServerApiUtils::parseBbox( const QString &bbox )
|
||||
return QgsRectangle();
|
||||
}
|
||||
|
||||
|
||||
template<typename T, class T2> T QgsServerApiUtils::parseTemporalInterval( const QString &interval )
|
||||
{
|
||||
auto parseDate = [ ]( const QString & date ) -> T2
|
||||
{
|
||||
T2 result;
|
||||
if ( date == QStringLiteral( ".." ) || date.isEmpty() )
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
T2 result { T2::fromString( date, Qt::DateFormat::ISODate ) };
|
||||
if ( !result.isValid() )
|
||||
{
|
||||
throw QgsServerApiBadRequestException( QStringLiteral( "%1 is not a valid date/datetime." ).arg( date ) );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
const QStringList parts { interval.split( '/' ) };
|
||||
return { parseDate( parts[0] ), parseDate( parts[1] ) };
|
||||
|
||||
}
|
||||
|
||||
QgsServerApiUtils::TemporalDateInterval QgsServerApiUtils::parseTemporalDateInterval( const QString &interval )
|
||||
{
|
||||
return QgsServerApiUtils::parseTemporalInterval<QgsServerApiUtils::TemporalDateInterval, QDate>( interval );
|
||||
}
|
||||
|
||||
QgsServerApiUtils::TemporalDateTimeInterval QgsServerApiUtils::parseTemporalDateTimeInterval( const QString &interval )
|
||||
{
|
||||
return QgsServerApiUtils::parseTemporalInterval<QgsServerApiUtils::TemporalDateTimeInterval, QDateTime>( interval );
|
||||
}
|
||||
|
||||
|
||||
QgsExpression QgsServerApiUtils::temporalFilterExpression( const QgsVectorLayer *layer, const QString &interval )
|
||||
{
|
||||
QgsExpression expression;
|
||||
QStringList conditions;
|
||||
|
||||
// Is it an interval?
|
||||
if ( interval.contains( '/' ) )
|
||||
{
|
||||
// Try date first
|
||||
try
|
||||
{
|
||||
TemporalDateInterval dateInterval { QgsServerApiUtils::parseTemporalDateInterval( interval ) };
|
||||
for ( const auto &fieldName : layer->includeAttributesOapifTemporalFilters() )
|
||||
{
|
||||
int fieldIdx { layer->fields().lookupField( fieldName ) };
|
||||
if ( fieldIdx < 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const QgsField field { layer->fields().at( fieldIdx ) };
|
||||
QString fieldValue;
|
||||
if ( field.type() == QVariant::Date )
|
||||
{
|
||||
fieldValue = QStringLiteral( R"raw("%1")raw" ).arg( fieldName );
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldValue = QStringLiteral( R"raw(to_date( "%1" ))raw" ).arg( fieldName );
|
||||
}
|
||||
|
||||
if ( ! dateInterval.begin.isValid( ) && ! dateInterval.end.isValid( ) )
|
||||
{
|
||||
// Nothing to do here: log?
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( dateInterval.begin.isValid( ) )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw(%1 >= to_date('%2'))raw" )
|
||||
.arg( fieldValue )
|
||||
.arg( dateInterval.begin.toString( Qt::DateFormat::ISODate ) ) );
|
||||
|
||||
}
|
||||
if ( dateInterval.end.isValid( ) )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw(%1 <= to_date('%2'))raw" )
|
||||
.arg( fieldValue )
|
||||
.arg( dateInterval.end.toString( Qt::DateFormat::ISODate ) ) );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch ( QgsServerApiBadRequestException & ) // try datetime
|
||||
{
|
||||
TemporalDateTimeInterval dateTimeInterval { QgsServerApiUtils::parseTemporalDateTimeInterval( interval ) };
|
||||
for ( const auto &fieldName : layer->includeAttributesOapifTemporalFilters() )
|
||||
{
|
||||
int fieldIdx { layer->fields().lookupField( fieldName ) };
|
||||
if ( fieldIdx < 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const QgsField field { layer->fields().at( fieldIdx ) };
|
||||
QString fieldValue;
|
||||
if ( field.type() == QVariant::Date )
|
||||
{
|
||||
fieldValue = QStringLiteral( R"raw("%1")raw" ).arg( fieldName );
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldValue = QStringLiteral( R"raw(to_datetime( "%1" ))raw" ).arg( fieldName );
|
||||
}
|
||||
|
||||
if ( ! dateTimeInterval.begin.isValid( ) && ! dateTimeInterval.end.isValid( ) )
|
||||
{
|
||||
// Nothing to do here: log?
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( dateTimeInterval.begin.isValid( ) )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw(%1 >= to_datetime('%2'))raw" )
|
||||
.arg( fieldValue )
|
||||
.arg( dateTimeInterval.begin.toString( Qt::DateFormat::ISODate ) ) );
|
||||
|
||||
}
|
||||
if ( dateTimeInterval.end.isValid( ) )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw(%1 <= to_datetime('%2'))raw" )
|
||||
.arg( fieldValue )
|
||||
.arg( dateTimeInterval.end.toString( Qt::DateFormat::ISODate ) ) );
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else // plain value
|
||||
{
|
||||
for ( const auto &fieldName : layer->includeAttributesOapifTemporalFilters() )
|
||||
{
|
||||
int fieldIdx { layer->fields().lookupField( fieldName ) };
|
||||
if ( fieldIdx < 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const QgsField field { layer->fields().at( fieldIdx ) };
|
||||
|
||||
if ( field.type() == QVariant::Date )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw("%1" = to_date('%2'))raw" )
|
||||
.arg( fieldName )
|
||||
.arg( interval ) );
|
||||
}
|
||||
else if ( field.type() == QVariant::DateTime )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw("%1" = to_datetime('%2'))raw" )
|
||||
.arg( fieldName )
|
||||
.arg( interval ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Guess the type from input
|
||||
QDateTime dateTime { QDateTime::fromString( interval, Qt::DateFormat::ISODate )};
|
||||
if ( dateTime.isValid() )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw(to_datetime("%1") = to_datetime('%2'))raw" )
|
||||
.arg( fieldName )
|
||||
.arg( interval ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
QDate date { QDate::fromString( interval, Qt::DateFormat::ISODate )};
|
||||
if ( date.isValid() )
|
||||
{
|
||||
conditions.push_back( QStringLiteral( R"raw(to_date("%1") = to_date('%2'))raw" )
|
||||
.arg( fieldName )
|
||||
.arg( interval ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing done here, log?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( ! conditions.isEmpty() )
|
||||
{
|
||||
expression.setExpression( conditions.join( QStringLiteral( " AND " ) ) );
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
json QgsServerApiUtils::layerExtent( const QgsVectorLayer *layer )
|
||||
{
|
||||
auto extent { layer->extent() };
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "qgsproject.h"
|
||||
#include "qgsserverprojectutils.h"
|
||||
#include "qgsserverapicontext.h"
|
||||
#include "qgsserverexception.h"
|
||||
|
||||
#ifdef HAVE_SERVER_PYTHON_PLUGINS
|
||||
#include "qgsaccesscontrol.h"
|
||||
@ -58,6 +59,44 @@ class SERVER_EXPORT QgsServerApiUtils
|
||||
*/
|
||||
static QgsRectangle parseBbox( const QString &bbox );
|
||||
|
||||
struct TemporalDateInterval
|
||||
{
|
||||
QDate begin;
|
||||
QDate end;
|
||||
};
|
||||
|
||||
struct TemporalDateTimeInterval
|
||||
{
|
||||
QDateTime begin;
|
||||
QDateTime end;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a date time \a interval and returns a TemporalInterval
|
||||
*
|
||||
* \throws QgsServerApiBadRequestException if interval cannot be parsed
|
||||
*/
|
||||
static TemporalDateInterval parseTemporalDateInterval( const QString &interval ) SIP_THROW( QgsServerApiBadRequestException );
|
||||
static TemporalDateTimeInterval parseTemporalDateTimeInterval( const QString &interval ) SIP_THROW( QgsServerApiBadRequestException );
|
||||
|
||||
///@cond PRIVATE
|
||||
template<typename T, class T2> static T parseTemporalInterval( const QString &interval ) SIP_SKIP;
|
||||
/// @endcond
|
||||
|
||||
|
||||
/**
|
||||
* Parse the \a interval and constructs a (possibily invalid) temporal filter expression for the given \a layer
|
||||
*
|
||||
* Syntax:
|
||||
*
|
||||
* interval-closed = date-time "/" date-time
|
||||
* interval-open-start = [".."] "/" date-time
|
||||
* interval-open-end = date-time "/" [".."]
|
||||
* interval = interval-closed / interval-open-start / interval-open-end
|
||||
* datetime = date-time / interval
|
||||
*/
|
||||
static QgsExpression temporalFilterExpression( const QgsVectorLayer *layer, const QString &interval );
|
||||
|
||||
/**
|
||||
* layerExtent returns json array with [xMin,yMin,xMax,yMax] CRS84 extent for the given \a layer
|
||||
* FIXME: the OpenAPI swagger docs say that it is inverted axis order: West, north, east, south edges of the spatial extent.
|
||||
|
@ -756,6 +756,20 @@ QList<QgsServerQueryStringParameter> QgsWfs3CollectionsItemsHandler::parameters(
|
||||
} );
|
||||
params.push_back( limit );
|
||||
|
||||
|
||||
// datetime
|
||||
QgsServerQueryStringParameter datetime { QStringLiteral( "datetime" ), false,
|
||||
QgsServerQueryStringParameter::Type::Integer,
|
||||
QStringLiteral( "Date time filter" ),
|
||||
};
|
||||
datetime.setCustomValidator( [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
|
||||
{
|
||||
// TODO
|
||||
return true;
|
||||
} );
|
||||
params.push_back( datetime );
|
||||
|
||||
|
||||
// Offset
|
||||
QgsServerQueryStringParameter offset { QStringLiteral( "offset" ), false,
|
||||
QgsServerQueryStringParameter::Type::Integer,
|
||||
@ -856,7 +870,7 @@ json QgsWfs3CollectionsItemsHandler::schema( const QgsServerApiContext &context
|
||||
{ "$ref", "#/components/parameters/resultType" },
|
||||
{ "$ref", "#/components/parameters/bbox" },
|
||||
{ "$ref", "#/components/parameters/bbox-crs" },
|
||||
// TODO: {{ "$ref", "#/components/parameters/time" }},
|
||||
{ "$ref", "#/components/parameters/time" },
|
||||
}
|
||||
};
|
||||
|
||||
@ -1025,15 +1039,25 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
|
||||
// so we do our own paging with "offset")
|
||||
const qlonglong offset { params.value( QStringLiteral( "offset" ) ).toLongLong( &ok ) };
|
||||
|
||||
// TODO: make the max limit configurable
|
||||
const qlonglong limit { params.value( QStringLiteral( "limit" ) ).toLongLong( &ok ) };
|
||||
|
||||
// TODO: implement time
|
||||
const QString time { context.request()->queryParameter( QStringLiteral( "time" ) ) };
|
||||
if ( ! time.isEmpty() )
|
||||
QString filterExpression;
|
||||
QStringList expressions;
|
||||
|
||||
/*/ datetime
|
||||
const QString datetime { context.request()->queryParameter( QStringLiteral( "datetime" ) ) };
|
||||
if ( ! datetime.isEmpty() )
|
||||
{
|
||||
throw QgsServerApiNotImplementedException( QStringLiteral( "Time filter is not implemented" ) ) ;
|
||||
}
|
||||
const QgsExpression timeExpression { QgsServerApiUtils::temporalFilterExpression( mapLayer, datetime ) };
|
||||
if ( ! timeExpression.isValid() )
|
||||
{
|
||||
throw QgsServerApiBadRequestException( QStringLiteral( "Invalid datetime filter expression: %1 " ).arg( datetime ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
expressions.push_back( timeExpression.expression() );
|
||||
}
|
||||
}*/
|
||||
|
||||
// Inputs are valid, process request
|
||||
QgsFeatureRequest featureRequest = filteredRequest( mapLayer, context );
|
||||
@ -1044,10 +1068,9 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
|
||||
featureRequest.setFilterRect( ct.transform( filterRect ) );
|
||||
}
|
||||
|
||||
QString filterExpression;
|
||||
if ( ! attrFilters.isEmpty() )
|
||||
{
|
||||
QStringList expressions;
|
||||
|
||||
if ( featureRequest.filterExpression() && ! featureRequest.filterExpression()->expression().isEmpty() )
|
||||
{
|
||||
expressions.push_back( featureRequest.filterExpression()->expression() );
|
||||
@ -1066,10 +1089,12 @@ void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &c
|
||||
expressions.push_back( QStringLiteral( "\"%1\" = '%2'" ).arg( it.key() ).arg( it.value() ) );
|
||||
}
|
||||
}
|
||||
filterExpression = expressions.join( QStringLiteral( " AND " ) );
|
||||
featureRequest.setFilterExpression( filterExpression );
|
||||
}
|
||||
|
||||
// Join all expression filters
|
||||
filterExpression = expressions.join( QStringLiteral( " AND " ) );
|
||||
featureRequest.setFilterExpression( filterExpression );
|
||||
|
||||
// WFS3 core specs only serves 4326
|
||||
featureRequest.setDestinationCrs( crs, context.project()->transformContext() );
|
||||
// Add offset to limit because paging is not supported by QgsFeatureRequest
|
||||
|
@ -92,7 +92,7 @@ class QgsServerAPIUtilsTest(QgsServerTestBase):
|
||||
def test_append_path(self):
|
||||
|
||||
path = QgsServerApiUtils.appendMapParameter('/wfs3', QtCore.QUrl('https://www.qgis.org/wfs3?MAP=/some/path'))
|
||||
self.assertEquals(path, '/wfs3?MAP=/some/path')
|
||||
self.assertEqual(path, '/wfs3?MAP=/some/path')
|
||||
|
||||
|
||||
class API(QgsServerApi):
|
||||
@ -141,6 +141,15 @@ class QgsServerAPITestBase(QgsServerTestBase):
|
||||
result = bytes(response.body()).decode('utf8') if reference_file.endswith('html') else self.dump(response)
|
||||
path = unitTestDataPath('qgis_server') + '/api/' + reference_file
|
||||
if self.regeregenerate_api_reference:
|
||||
# Try to change timestamp
|
||||
try:
|
||||
content = result.split('\n')
|
||||
j = ''.join(content[content.index('') + 1:])
|
||||
j = json.loads(j)
|
||||
j['timeStamp'] = '2019-07-05T12:27:07Z'
|
||||
result = '\n'.join(content[:2]) + '\n' + json.dumps(j, ensure_ascii=False, indent=2)
|
||||
except:
|
||||
pass
|
||||
f = open(path.encode('utf8'), 'w+', encoding='utf8')
|
||||
f.write(result)
|
||||
f.close()
|
||||
@ -462,6 +471,55 @@ class QgsServerAPITest(QgsServerAPITestBase):
|
||||
response = self.compareApi(request, project, 'test_wfs3_collections_items_layer1_with_short_name_eq_tw_star.json')
|
||||
self.assertEqual(response.statusCode(), 200)
|
||||
|
||||
def test_wfs3_time_filters(self):
|
||||
"""Test datetime filters"""
|
||||
|
||||
project = QgsProject()
|
||||
project.read(unitTestDataPath('qgis_server') + '/test_project_api_timefilters.qgs')
|
||||
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/points/items?name=lus*')
|
||||
response = self.compareApi(request, project, 'test_wfs3_collections_items_points_eq_lus.json')
|
||||
self.assertEqual(response.statusCode(), 200)
|
||||
|
||||
# Prepare 3 projects with all three options: "created", "updated", both.
|
||||
tmpDir = QtCore.QTemporaryDir()
|
||||
layer = list(project.mapLayers().values())[0]
|
||||
layer.setIncludeAttributesOapifTemporalFilters(['created'])
|
||||
created_path = os.path.join(tmpDir.path(), 'test_project_api_timefilters_created.qgs')
|
||||
project.write(created_path)
|
||||
layer.setIncludeAttributesOapifTemporalFilters(['updated'])
|
||||
updated_path = os.path.join(tmpDir.path(), 'test_project_api_timefilters_updated.qgs')
|
||||
project.write(updated_path)
|
||||
layer.setIncludeAttributesOapifTemporalFilters(['updated', 'created'])
|
||||
both_path = os.path.join(tmpDir.path(), 'test_project_api_timefilters_both.qgs')
|
||||
project.write(both_path)
|
||||
|
||||
# Test data:
|
||||
#wkt_geom fid name created updated
|
||||
#Point (7.30355493642693343 44.82162158126364915) 2 bricherasio 2019-05-05 2020-05-05T05:05:05.000
|
||||
#Point (7.2500747591236081 44.81342128741047048) 1 luserna 2019-01-01 2020-01-01T10:10:10.000
|
||||
|
||||
# What to test:
|
||||
#interval-closed = date-time "/" date-time
|
||||
#interval-open-start = [".."] "/" date-time
|
||||
#interval-open-end = date-time "/" [".."]
|
||||
#interval = interval-closed / interval-open-start / interval-open-end
|
||||
#datetime = date-time / interval
|
||||
|
||||
def _date_tester(project_path, expected, unexpected):
|
||||
# Test "created" date field exact
|
||||
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/points/items?datetime=2019-05-05')
|
||||
response = QgsBufferServerResponse()
|
||||
project.read(project_path)
|
||||
self.server.handleRequest(request, response, project)
|
||||
body = bytes(response.body()).decode('utf8')
|
||||
for exp in expected:
|
||||
self.assertTrue(exp in body)
|
||||
for unexp in unexpected:
|
||||
self.assertFalse(unexp in body)
|
||||
|
||||
# Test "created" date field exact
|
||||
_date_tester(created_path, ['bricherasio'], ['luserna'])
|
||||
|
||||
def test_wfs3_excluded_attributes(self):
|
||||
"""Test excluded attributes"""
|
||||
project = QgsProject()
|
||||
@ -706,10 +764,10 @@ class QgsServerOgcAPITest(QgsServerAPITestBase):
|
||||
self.assertEqual(str(ex.exception), 'Template not found: handlerThree.html')
|
||||
|
||||
# Define a template path
|
||||
dir = QtCore.QTemporaryDir()
|
||||
with open(dir.path() + '/handlerThree.html', 'w+') as f:
|
||||
tmpDir = QtCore.QTemporaryDir()
|
||||
with open(tmpDir.path() + '/handlerThree.html', 'w+') as f:
|
||||
f.write("Hello world")
|
||||
h3.templatePathOverride = dir.path() + '/handlerThree.html'
|
||||
h3.templatePathOverride = tmpDir.path() + '/handlerThree.html'
|
||||
ctx.response().clear()
|
||||
api.executeRequest(ctx)
|
||||
self.assertEqual(bytes(ctx.response().data()), b"Hello world")
|
||||
|
@ -1328,5 +1328,6 @@ Content-Type: application/openapi+json;version=3.0
|
||||
"description": "Access to data (features).",
|
||||
"name": "Features"
|
||||
}
|
||||
]
|
||||
],
|
||||
"timeStamp": "2019-07-05T12:27:07Z"
|
||||
}
|
@ -19,5 +19,5 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"timeStamp": "2019-10-16T13:53:56Z"
|
||||
}
|
||||
"timeStamp": "2019-07-05T12:27:07Z"
|
||||
}
|
@ -27,6 +27,6 @@ Content-Type: application/geo+json
|
||||
"id": 1,
|
||||
"utf8nameè": "one èé"
|
||||
},
|
||||
"timeStamp": "2019-10-16T13:53:57Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "Feature"
|
||||
}
|
||||
}
|
@ -35,6 +35,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 1,
|
||||
"numberReturned": 1,
|
||||
"timeStamp": "2019-10-16T13:53:57Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -35,6 +35,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 1,
|
||||
"numberReturned": 1,
|
||||
"timeStamp": "2019-10-16T13:53:57Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -67,6 +67,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 3,
|
||||
"numberReturned": 3,
|
||||
"timeStamp": "2019-10-16T13:53:55Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -35,6 +35,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 1,
|
||||
"numberReturned": 1,
|
||||
"timeStamp": "2019-10-16T13:53:55Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -51,6 +51,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 2,
|
||||
"numberReturned": 2,
|
||||
"timeStamp": "2019-10-16T13:53:55Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -67,6 +67,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 3,
|
||||
"numberReturned": 3,
|
||||
"timeStamp": "2019-10-16T13:53:55Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -42,6 +42,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 3,
|
||||
"numberReturned": 1,
|
||||
"timeStamp": "2019-10-16T13:53:55Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -49,6 +49,6 @@ Content-Type: application/geo+json
|
||||
],
|
||||
"numberMatched": 3,
|
||||
"numberReturned": 1,
|
||||
"timeStamp": "2019-10-16T13:53:55Z",
|
||||
"timeStamp": "2019-07-05T12:27:07Z",
|
||||
"type": "FeatureCollection"
|
||||
}
|
||||
}
|
@ -10,19 +10,16 @@ Content-Type: application/json
|
||||
],
|
||||
"description": "A test vector layer with unicode òà",
|
||||
"extent": {
|
||||
"spatial": {
|
||||
"bbox": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
],
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
||||
}
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
|
||||
"spatial": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "testlayer èé",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json",
|
||||
@ -37,6 +34,7 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"name": "testlayer èé",
|
||||
"title": "A test vector layer èé"
|
||||
},
|
||||
{
|
||||
@ -47,19 +45,16 @@ Content-Type: application/json
|
||||
],
|
||||
"description": "A Layer1 with an abstract",
|
||||
"extent": {
|
||||
"spatial": {
|
||||
"bbox": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
],
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
||||
}
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
|
||||
"spatial": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "layer1_with_short_name",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/items.json",
|
||||
@ -74,6 +69,7 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"name": "layer1_with_short_name",
|
||||
"title": "A Layer1 with a short name"
|
||||
},
|
||||
{
|
||||
@ -84,19 +80,16 @@ Content-Type: application/json
|
||||
],
|
||||
"description": "A test vector layer with unicode òà",
|
||||
"extent": {
|
||||
"spatial": {
|
||||
"bbox": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
],
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
||||
}
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
|
||||
"spatial": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "exclude_attribute",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://server.qgis.org/wfs3/collections/exclude_attribute/items.json",
|
||||
@ -111,6 +104,7 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"name": "exclude_attribute",
|
||||
"title": "A test vector layer exclude attrs"
|
||||
},
|
||||
{
|
||||
@ -121,19 +115,16 @@ Content-Type: application/json
|
||||
],
|
||||
"description": "A test vector layer with aliases",
|
||||
"extent": {
|
||||
"spatial": {
|
||||
"bbox": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
],
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
||||
}
|
||||
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
|
||||
"spatial": [
|
||||
[
|
||||
8.203459307036344,
|
||||
44.90139483904469,
|
||||
8.203546993993488,
|
||||
44.901482526001836
|
||||
]
|
||||
]
|
||||
},
|
||||
"id": "fields_alias",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://server.qgis.org/wfs3/collections/fields_alias/items.json",
|
||||
@ -148,6 +139,7 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"name": "fields_alias",
|
||||
"title": "A test vector layer with aliases"
|
||||
}
|
||||
],
|
||||
@ -170,5 +162,5 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"timeStamp": "2019-10-16T13:53:56Z"
|
||||
}
|
||||
"timeStamp": "2019-07-05T12:27:07Z"
|
||||
}
|
@ -22,5 +22,5 @@ Content-Type: application/json
|
||||
"type": "text/html"
|
||||
}
|
||||
],
|
||||
"timeStamp": "2019-10-16T13:53:57Z"
|
||||
}
|
||||
"timeStamp": "2019-07-05T12:27:07Z"
|
||||
}
|
@ -33,5 +33,5 @@ Content-Type: application/json
|
||||
"type": "application/openapi+json;version=3.0"
|
||||
}
|
||||
],
|
||||
"timeStamp": "2019-10-16T13:53:57Z"
|
||||
}
|
||||
"timeStamp": "2019-07-05T12:27:07Z"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user