Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[OAPIF provider] Add support for filtering on feature properties (OGC…
… API Features Part 1 - /rec/core/fc-filters)

This uses the /api endpoint to get the list of queryable items.
  • Loading branch information
rouault authored and nyalldawson committed Apr 24, 2023
1 parent cce5cc2 commit 8727529
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 14 deletions.
113 changes: 113 additions & 0 deletions src/providers/wfs/oapif/qgsoapifapirequest.cpp
Expand Up @@ -47,6 +47,26 @@ QString QgsOapifApiRequest::errorMessageWithReason( const QString &reason )
return tr( "Download of API page failed: %1" ).arg( reason );
}

// j must be the root element
// ref is something like "#/components/parameters/limitFeatures_VegetationSrf"
static json resolveRef( const json &j, const std::string &ref )
{
if ( ref.compare( 0, 2, "#/" ) != 0 )
return json();
const auto subPaths = QString::fromStdString( ref.substr( 2 ) ).split( QLatin1Char( '/' ) );
json ret = j;
for ( const auto &subPath : subPaths )
{
if ( !ret.is_object() )
return json();
const auto subJIter = ret.find( subPath.toStdString() );
if ( subJIter == ret.end() )
return json();
ret = *subJIter;
}
return ret;
}

void QgsOapifApiRequest::processReply()
{
if ( mErrorCode != QgsBaseNetworkRequest::NoError )
Expand Down Expand Up @@ -119,6 +139,99 @@ void QgsOapifApiRequest::processReply()
}
}

if ( j.is_object() && j.contains( "paths" ) )
{
const auto paths = j["paths"];
if ( paths.is_object() )
{
for ( const auto& [key, val] : paths.items() )
{
const char *prefix = "/collections/";
const char *suffix = "/items";
if ( key.size() > strlen( prefix ) + strlen( suffix ) &&
key.compare( 0, strlen( prefix ), prefix ) == 0 &&
key.compare( key.size() - strlen( suffix ), std::string::npos, suffix ) == 0 )
{
const std::string collection = key.substr(
strlen( prefix ), key.size() - strlen( prefix ) - strlen( suffix ) );
if ( val.is_object() && val.contains( "get" ) )
{
const auto get = val["get"];
if ( get.is_object() && get.contains( "parameters" ) )
{
const auto parameters = get["parameters"];
if ( parameters.is_array() )
{
CollectionProperties collectionProperties;
for ( const auto &parameter : parameters )
{
if ( parameter.is_object() )
{
json parameterResolved;
if ( parameter.contains( "$ref" ) )
{
const auto ref = parameter["$ref"];
if ( ref.is_string() )
{
const auto refStr = ref.get<std::string>();
parameterResolved = resolveRef( j, refStr );
}
}
else
{
parameterResolved = parameter;
}
if ( parameterResolved.is_object() &&
parameterResolved.contains( "name" ) &&
parameterResolved.contains( "in" ) &&
parameterResolved.contains( "style" ) &&
parameterResolved.contains( "explode" ) &&
parameterResolved.contains( "schema" ) )
{
const auto jName = parameterResolved["name"];
const auto jIn = parameterResolved["in"];
const auto jStyle = parameterResolved["style"];
const auto jExplode = parameterResolved["explode"];
const auto jSchema = parameterResolved["schema"];
if ( jName.is_string() && jIn.is_string() &&
jStyle.is_string() && jExplode.is_boolean() &&
jSchema.is_object() && jSchema.contains( "type" ) )
{
const auto name = jName.get<std::string>();
const auto in = jIn.get<std::string>();
const auto style = jStyle.get<std::string>();
const bool explode = jExplode.get<bool>();
const auto jSchemaType = jSchema["type"];
if ( in == "query" &&
style == "form" &&
!explode &&
jSchemaType.is_string() &&
name != "crs" &&
name != "bbox" && name != "bbox-crs" &&
name != "filter" && name != "filter-lang" &&
name != "filter-crs" && name != "datetime" &&
name != "limit" )
{
SimpleQueryable queryable;
queryable.mType = QString::fromStdString( jSchemaType.get<std::string>() );
collectionProperties.mSimpleQueryables[QString::fromStdString( name )] = queryable;
}
}
}
}
}
if ( !collectionProperties.mSimpleQueryables.isEmpty() )
{
mCollectionProperties[QString::fromStdString( collection )] = collectionProperties;
}
}
}
}
}
}
}
}

if ( j.is_object() && j.contains( "info" ) )
{
const auto info = j["info"];
Expand Down
19 changes: 19 additions & 0 deletions src/providers/wfs/oapif/qgsoapifapirequest.h
Expand Up @@ -52,6 +52,23 @@ class QgsOapifApiRequest : public QgsBaseNetworkRequest
//! Return metadata (mostly contact info)
const QgsAbstractMetadataBase &metadata() const { return mMetadata; }

//! Describes a simple queryable parameter.
struct SimpleQueryable
{
// type as in a JSON schema: "string", "integer", "number", etc.
QString mType;
};

//! Describes the properties of a collection.
struct CollectionProperties
{
// Map of simple queryables items (that is as query parameters). The key of the map is a queryable name.
QMap<QString, SimpleQueryable> mSimpleQueryables;
};

//! Get collection properties. The key of the map is a collection name.
const QMap<QString, CollectionProperties> &collectionProperties() const { return mCollectionProperties; }

signals:
//! emitted when the capabilities have been fully parsed, or an error occurred */
void gotResponse();
Expand All @@ -71,6 +88,8 @@ class QgsOapifApiRequest : public QgsBaseNetworkRequest

QgsLayerMetadata mMetadata;

QMap<QString, CollectionProperties> mCollectionProperties;

ApplicationLevelError mAppLevelError = ApplicationLevelError::NoError;

};
Expand Down
2 changes: 1 addition & 1 deletion src/providers/wfs/oapif/qgsoapifitemsrequest.cpp
Expand Up @@ -38,7 +38,7 @@ QgsOapifItemsRequest::QgsOapifItemsRequest( const QgsDataSourceUri &baseUri, con

bool QgsOapifItemsRequest::request( bool synchronous, bool forceRefresh )
{
if ( !sendGET( QUrl( mUrl ), QString( "application/geo+json, application/json" ), synchronous, forceRefresh ) )
if ( !sendGET( QUrl::fromEncoded( mUrl.toLatin1() ), QString( "application/geo+json, application/json" ), synchronous, forceRefresh ) )
{
emit gotResponse();
return false;
Expand Down
58 changes: 58 additions & 0 deletions src/providers/wfs/oapif/qgsoapifprovider.cpp
Expand Up @@ -32,6 +32,7 @@

#include <algorithm>
#include <QIcon>
#include <QUrlQuery>

const QString QgsOapifProvider::OAPIF_PROVIDER_KEY = QStringLiteral( "OAPIF" );
const QString QgsOapifProvider::OAPIF_PROVIDER_DESCRIPTION = QStringLiteral( "OGC API - Features data provider" );
Expand Down Expand Up @@ -97,6 +98,13 @@ bool QgsOapifProvider::init()
if ( apiRequest.errorCode() != QgsBaseNetworkRequest::NoError )
return false;

const auto &collectionProperties = apiRequest.collectionProperties();
const auto thisCollPropertiesIter = collectionProperties.find( mShared->mURI.typeName() );
if ( thisCollPropertiesIter != collectionProperties.end() )
{
mShared->mSimpleQueryables = thisCollPropertiesIter->mSimpleQueryables;
}

mShared->mServerMaxFeatures = apiRequest.maxLimit();

if ( mShared->mURI.maxNumFeatures() > 0 && mShared->mServerMaxFeatures > 0 && !mShared->mURI.pagingEnabled() )
Expand Down Expand Up @@ -765,6 +773,7 @@ QgsOapifSharedData *QgsOapifSharedData::clone() const
copy->mServerFilter = mServerFilter;
copy->mFoundIdTopLevel = mFoundIdTopLevel;
copy->mFoundIdInProperties = mFoundIdInProperties;
copy->mSimpleQueryables = mSimpleQueryables;
QgsBackgroundCachedSharedData::copyStateToClone( copy );

return copy;
Expand Down Expand Up @@ -804,6 +813,13 @@ static bool isDateTimeField( const QgsFields &fields, const QString &fieldName )
return false;
}

static QString getEncodedQueryParam( const QString &key, const QString &value )
{
QUrlQuery query;
query.addQueryItem( key, value );
return query.toString( QUrl::FullyEncoded );
}

static void collectTopLevelAndNodes( const QgsExpressionNode *node,
std::vector<const QgsExpressionNode *> &topAndNodes )
{
Expand Down Expand Up @@ -832,6 +848,7 @@ QString QgsOapifSharedData::translateNodeToServer(
QDateTime maxDate;
QString minDateStr;
QString maxDateStr;
QStringList equalityComparisons;
bool hasTranslatedParts = false;
for ( size_t i = 0; i < topAndNodes.size(); /* do not increment here */ )
{
Expand Down Expand Up @@ -872,6 +889,40 @@ QString QgsOapifSharedData::translateNodeToServer(
}
}
}
else if ( op == QgsExpressionNodeBinaryOperator::boEQ &&
mFields.indexOf( left->name() ) >= 0 )
{
// Filtering based on Part 1 /rec/core/fc-filters recommendation.
const auto iter = mSimpleQueryables.find( left->name() );
if ( iter != mSimpleQueryables.end() )
{
if ( iter->mType == QLatin1String( "string" ) &&
right->value().type() == QVariant::String )
{
equalityComparisons << getEncodedQueryParam( left->name(), right->value().toString() );
removeMe = true;
}
else if ( ( iter->mType == QLatin1String( "integer" ) ||
iter->mType == QLatin1String( "number" ) ) &&
right->value().type() == QVariant::Int )
{
equalityComparisons << getEncodedQueryParam( left->name(), QString::number( right->value().toInt() ) );
removeMe = true;
}
else if ( iter->mType == QLatin1String( "number" ) &&
right->value().type() == QVariant::Double )
{
equalityComparisons << getEncodedQueryParam( left->name(), QString::number( right->value().toDouble() ) );
removeMe = true;
}
else if ( iter->mType == QLatin1String( "boolean" ) &&
right->value().type() == QVariant::Bool )
{
equalityComparisons << getEncodedQueryParam( left->name(), right->value().toBool() ? QLatin1String( "true" ) : QLatin1String( "false" ) );
removeMe = true;
}
}
}
}
}
if ( removeMe )
Expand Down Expand Up @@ -906,6 +957,13 @@ QString QgsOapifSharedData::translateNodeToServer(
ret = QStringLiteral( "datetime=0000-01-01T00:00:00Z%2F" ) + maxDateStr;
}

for ( const QString &equalityComparison : equalityComparisons )
{
if ( !ret.isEmpty() )
ret += QLatin1Char( '&' );
ret += equalityComparison;
}

if ( !hasTranslatedParts )
{
untranslatedPart = rootNode->dump();
Expand Down
4 changes: 4 additions & 0 deletions src/providers/wfs/oapif/qgsoapifprovider.h
Expand Up @@ -23,6 +23,7 @@
#include "qgsvectordataprovider.h"
#include "qgsbackgroundcachedshareddata.h"
#include "qgswfsdatasourceuri.h"
#include "qgsoapifapirequest.h"
#include "qgsoapifitemsrequest.h"

#include "qgsprovidermetadata.h"
Expand Down Expand Up @@ -215,6 +216,9 @@ class QgsOapifSharedData final: public QObject, public QgsBackgroundCachedShared
//! Set if an "id" is present in the "properties" object of features
bool mFoundIdInProperties = false;

// Map of simple queryables items (that is as query parameters). The key of the map is a queryable name.
QMap<QString, QgsOapifApiRequest::SimpleQueryable> mSimpleQueryables;

//! Append extra query parameters if needed
QString appendExtraQueryParameters( const QString &url ) const;

Expand Down
15 changes: 12 additions & 3 deletions src/providers/wfs/qgsbasenetworkrequest.cpp
Expand Up @@ -108,9 +108,18 @@ bool QgsBaseNetworkRequest::sendGET( const QUrl &url, const QString &acceptHeade
if ( modifiedUrl.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
{
// Just for testing with local files instead of http:// resources
QString modifiedUrlString = modifiedUrl.toString();
// Qt5 does URL encoding from some reason (of the FILTER parameter for example)
modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
QString modifiedUrlString;

if ( modifiedUrl.toString().contains( QLatin1String( "fake_qgis_http_endpoint_encoded_query" ) ) )
{
// Get encoded representation (used by test_provider_oapif.py testSimpleQueryableFiltering())
modifiedUrlString = modifiedUrl.toEncoded();
}
else
{
// Get representation with percent decoding (easier for WFS filtering)
modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrl.toString().toUtf8() );
}

if ( !acceptHeader.isEmpty() )
{
Expand Down

0 comments on commit 8727529

Please sign in to comment.