From ecfcf6ced12417c702840b54ef5534bd9b0b9a2b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 26 Mar 2024 10:43:24 +1000 Subject: [PATCH] Use a more flexible API for handling SensorThings expansions This allows us to control the sort order and limit for each expansion, and gives us more flexibility in future to eg handle per expansion filter strings --- .../sensorthings/qgssensorthingsutils.sip.in | 162 ++++++++- .../sensorthings/qgssensorthingsutils.sip.in | 162 ++++++++- scripts/spell_check/spelling.dat | 2 +- .../sensorthings/qgssensorthingsprovider.cpp | 48 ++- .../qgssensorthingsshareddata.cpp | 50 ++- .../sensorthings/qgssensorthingsshareddata.h | 3 +- .../sensorthings/qgssensorthingsutils.cpp | 336 +++++++++++++++--- .../sensorthings/qgssensorthingsutils.h | 165 ++++++++- src/core/qgsapplication.cpp | 2 + .../src/python/test_provider_sensorthings.py | 236 ++++++++++-- 10 files changed, 1043 insertions(+), 123 deletions(-) diff --git a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index 918f04c579d..b1d31c38a38 100644 --- a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -46,6 +46,13 @@ Returns :py:class:`Qgis`.SensorThingsEntity.Invalid if the string could not be c %Docstring Converts a SensorThings entity set to a SensorThings entity set string. +.. versionadded:: 3.38 +%End + + static QStringList propertiesForEntityType( Qgis::SensorThingsEntity type ); +%Docstring +Returns the SensorThings properties which correspond to a specified entity ``type``. + .. versionadded:: 3.38 %End @@ -114,13 +121,166 @@ and entity ``type``. This method will block while network requests are made to the server. %End - static QList< QList< Qgis::SensorThingsEntity > > expandableTargets( Qgis::SensorThingsEntity type ); + static QList< Qgis::SensorThingsEntity > expandableTargets( Qgis::SensorThingsEntity type ); %Docstring Returns a list of permissible expand targets for a given base entity ``type``. .. versionadded:: 3.38 %End + static QString asQueryString( const QList< QgsSensorThingsExpansionDefinition > &expansions ); +%Docstring +Returns a list of ``expansions`` as a valid SensorThings API query string, eg "$expand=Locations($orderby=id desc;$top=3;$expand=Datastreams($top=101))". + +.. versionadded:: 3.38 +%End + +}; + + +class QgsSensorThingsExpansionDefinition +{ +%Docstring(signature="appended") +Encapsulates information about how relationships in a SensorThings API service should be expanded. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgssensorthingsutils.h" +%End + public: + + QgsSensorThingsExpansionDefinition( Qgis::SensorThingsEntity childEntity = Qgis::SensorThingsEntity::Invalid, + const QString &orderBy = QString(), + Qt::SortOrder sortOrder = Qt::SortOrder::AscendingOrder, + int limit = QgsSensorThingsUtils::DEFAULT_EXPANSION_LIMIT ); +%Docstring +Constructor for QgsSensorThingsExpansionDefinition, targeting the specified child entity type. +%End + + bool isValid() const; +%Docstring +Returns ``True`` if the definition is valid. +%End + + Qgis::SensorThingsEntity childEntity() const; +%Docstring +Returns the target child entity which should be expanded. + +.. seealso:: :py:func:`setChildEntity` +%End + + void setChildEntity( Qgis::SensorThingsEntity entity ); +%Docstring +Sets the target child ``entity`` which should be expanded. + +.. seealso:: :py:func:`childEntity` +%End + + QString orderBy() const; +%Docstring +Returns the field name to order the expanded child entities by. + +.. seealso:: :py:func:`sortOrder` + +.. seealso:: :py:func:`setOrderBy` +%End + + void setOrderBy( const QString &field ); +%Docstring +Sets the ``field`` name to order the expanded child entities by. + +.. seealso:: :py:func:`orderBy` + +.. seealso:: :py:func:`setSortOrder` +%End + + Qt::SortOrder sortOrder() const; +%Docstring +Returns the sort order for the expanded child entities. + +.. seealso:: :py:func:`orderBy` + +.. seealso:: :py:func:`setSortOrder` +%End + + void setSortOrder( Qt::SortOrder order ); +%Docstring +Sets the sort order for the expanded child entities. + +.. seealso:: :py:func:`setOrderBy` + +.. seealso:: :py:func:`sortOrder` +%End + + int limit() const; +%Docstring +Returns the limit on the number of child features to fetch. + +Returns -1 if no limit is defined. + +.. seealso:: :py:func:`setLimit` +%End + + void setLimit( int limit ); +%Docstring +Sets the ``limit`` on the number of child features to fetch. + +Set to -1 if no limit is desired. + +.. seealso:: :py:func:`limit` +%End + + QString toString() const; +%Docstring +Returns a string encapsulation of the expansion definition. + +.. seealso:: :py:func:`fromString` +%End + + static QgsSensorThingsExpansionDefinition fromString( const QString &string ); +%Docstring +Returns a QgsSensorThingsExpansionDefinition from a string representation. + +.. seealso:: :py:func:`toString` +%End + + QString asQueryString( const QStringList &additionalOptions = QStringList() ) const; +%Docstring +Returns the expansion as a valid SensorThings API query string, eg "$expand=Observations($orderby=phenomenonTime desc;$top=10)". + +Optionally a list of additional query options can be specified for the expansion. +%End + + bool operator==( const QgsSensorThingsExpansionDefinition &other ) const; + bool operator!=( const QgsSensorThingsExpansionDefinition &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + if ( !sipCpp->isValid() ) + { + sipRes = PyUnicode_FromString( "" ); + return; + } + + QString innerDefinition; + if ( !sipCpp->orderBy().isEmpty() ) + { + innerDefinition = QStringLiteral( "by %1 (%2)" ).arg( sipCpp->orderBy(), sipCpp->sortOrder() == Qt::SortOrder::AscendingOrder ? QStringLiteral( "asc" ) : QStringLiteral( "desc" ) ); + } + if ( sipCpp->limit() >= 0 ) + { + if ( !innerDefinition.isEmpty() ) + innerDefinition = QStringLiteral( "%1, limit %2" ).arg( innerDefinition ).arg( sipCpp->limit() ); + else + innerDefinition = QStringLiteral( "limit %1" ).arg( sipCpp->limit() ); + } + + QString str = QStringLiteral( "" ).arg( qgsEnumValueToKey( sipCpp->childEntity() ), innerDefinition.isEmpty() ? QString() : ( QStringLiteral( " " ) + innerDefinition ) ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + }; /************************************************************************ diff --git a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index 918f04c579d..b1d31c38a38 100644 --- a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -46,6 +46,13 @@ Returns :py:class:`Qgis`.SensorThingsEntity.Invalid if the string could not be c %Docstring Converts a SensorThings entity set to a SensorThings entity set string. +.. versionadded:: 3.38 +%End + + static QStringList propertiesForEntityType( Qgis::SensorThingsEntity type ); +%Docstring +Returns the SensorThings properties which correspond to a specified entity ``type``. + .. versionadded:: 3.38 %End @@ -114,13 +121,166 @@ and entity ``type``. This method will block while network requests are made to the server. %End - static QList< QList< Qgis::SensorThingsEntity > > expandableTargets( Qgis::SensorThingsEntity type ); + static QList< Qgis::SensorThingsEntity > expandableTargets( Qgis::SensorThingsEntity type ); %Docstring Returns a list of permissible expand targets for a given base entity ``type``. .. versionadded:: 3.38 %End + static QString asQueryString( const QList< QgsSensorThingsExpansionDefinition > &expansions ); +%Docstring +Returns a list of ``expansions`` as a valid SensorThings API query string, eg "$expand=Locations($orderby=id desc;$top=3;$expand=Datastreams($top=101))". + +.. versionadded:: 3.38 +%End + +}; + + +class QgsSensorThingsExpansionDefinition +{ +%Docstring(signature="appended") +Encapsulates information about how relationships in a SensorThings API service should be expanded. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgssensorthingsutils.h" +%End + public: + + QgsSensorThingsExpansionDefinition( Qgis::SensorThingsEntity childEntity = Qgis::SensorThingsEntity::Invalid, + const QString &orderBy = QString(), + Qt::SortOrder sortOrder = Qt::SortOrder::AscendingOrder, + int limit = QgsSensorThingsUtils::DEFAULT_EXPANSION_LIMIT ); +%Docstring +Constructor for QgsSensorThingsExpansionDefinition, targeting the specified child entity type. +%End + + bool isValid() const; +%Docstring +Returns ``True`` if the definition is valid. +%End + + Qgis::SensorThingsEntity childEntity() const; +%Docstring +Returns the target child entity which should be expanded. + +.. seealso:: :py:func:`setChildEntity` +%End + + void setChildEntity( Qgis::SensorThingsEntity entity ); +%Docstring +Sets the target child ``entity`` which should be expanded. + +.. seealso:: :py:func:`childEntity` +%End + + QString orderBy() const; +%Docstring +Returns the field name to order the expanded child entities by. + +.. seealso:: :py:func:`sortOrder` + +.. seealso:: :py:func:`setOrderBy` +%End + + void setOrderBy( const QString &field ); +%Docstring +Sets the ``field`` name to order the expanded child entities by. + +.. seealso:: :py:func:`orderBy` + +.. seealso:: :py:func:`setSortOrder` +%End + + Qt::SortOrder sortOrder() const; +%Docstring +Returns the sort order for the expanded child entities. + +.. seealso:: :py:func:`orderBy` + +.. seealso:: :py:func:`setSortOrder` +%End + + void setSortOrder( Qt::SortOrder order ); +%Docstring +Sets the sort order for the expanded child entities. + +.. seealso:: :py:func:`setOrderBy` + +.. seealso:: :py:func:`sortOrder` +%End + + int limit() const; +%Docstring +Returns the limit on the number of child features to fetch. + +Returns -1 if no limit is defined. + +.. seealso:: :py:func:`setLimit` +%End + + void setLimit( int limit ); +%Docstring +Sets the ``limit`` on the number of child features to fetch. + +Set to -1 if no limit is desired. + +.. seealso:: :py:func:`limit` +%End + + QString toString() const; +%Docstring +Returns a string encapsulation of the expansion definition. + +.. seealso:: :py:func:`fromString` +%End + + static QgsSensorThingsExpansionDefinition fromString( const QString &string ); +%Docstring +Returns a QgsSensorThingsExpansionDefinition from a string representation. + +.. seealso:: :py:func:`toString` +%End + + QString asQueryString( const QStringList &additionalOptions = QStringList() ) const; +%Docstring +Returns the expansion as a valid SensorThings API query string, eg "$expand=Observations($orderby=phenomenonTime desc;$top=10)". + +Optionally a list of additional query options can be specified for the expansion. +%End + + bool operator==( const QgsSensorThingsExpansionDefinition &other ) const; + bool operator!=( const QgsSensorThingsExpansionDefinition &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + if ( !sipCpp->isValid() ) + { + sipRes = PyUnicode_FromString( "" ); + return; + } + + QString innerDefinition; + if ( !sipCpp->orderBy().isEmpty() ) + { + innerDefinition = QStringLiteral( "by %1 (%2)" ).arg( sipCpp->orderBy(), sipCpp->sortOrder() == Qt::SortOrder::AscendingOrder ? QStringLiteral( "asc" ) : QStringLiteral( "desc" ) ); + } + if ( sipCpp->limit() >= 0 ) + { + if ( !innerDefinition.isEmpty() ) + innerDefinition = QStringLiteral( "%1, limit %2" ).arg( innerDefinition ).arg( sipCpp->limit() ); + else + innerDefinition = QStringLiteral( "limit %1" ).arg( sipCpp->limit() ); + } + + QString str = QStringLiteral( "" ).arg( qgsEnumValueToKey( sipCpp->childEntity() ), innerDefinition.isEmpty() ? QString() : ( QStringLiteral( " " ) + innerDefinition ) ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + }; /************************************************************************ diff --git a/scripts/spell_check/spelling.dat b/scripts/spell_check/spelling.dat index 10dad32b93f..fb711d4ae67 100644 --- a/scripts/spell_check/spelling.dat +++ b/scripts/spell_check/spelling.dat @@ -3285,7 +3285,7 @@ geometricians:geometers geomtry:geometry gerat:great get's:gets -geting:getting +geting:getting:* Ghandi:Gandhi gived:given glight:flight diff --git a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp index b585a4517f3..6783b6ae7cc 100644 --- a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp @@ -377,9 +377,23 @@ QVariantMap QgsSensorThingsProviderMetadata::decodeUri( const QString &uri ) con if ( entity != Qgis::SensorThingsEntity::Invalid ) components.insert( QStringLiteral( "entity" ), qgsEnumValueToKey( entity ) ); - const QString expandToParam = dsUri.param( QStringLiteral( "expandTo" ) ); + const QStringList expandToParam = dsUri.param( QStringLiteral( "expandTo" ) ).split( ';', Qt::SkipEmptyParts ); if ( !expandToParam.isEmpty() ) - components.insert( QStringLiteral( "expandTo" ), expandToParam.split( ',' ) ); + { + QVariantList expandParts; + for ( const QString &expandString : expandToParam ) + { + const QgsSensorThingsExpansionDefinition definition = QgsSensorThingsExpansionDefinition::fromString( expandString ); + if ( definition.isValid() ) + { + expandParts.append( QVariant::fromValue( definition ) ); + } + } + if ( !expandParts.isEmpty() ) + { + components.insert( QStringLiteral( "expandTo" ), expandParts ); + } + } bool ok = false; const int maxPageSizeParam = dsUri.param( QStringLiteral( "pageSize" ) ).toInt( &ok ); @@ -394,12 +408,6 @@ QVariantMap QgsSensorThingsProviderMetadata::decodeUri( const QString &uri ) con { components.insert( QStringLiteral( "featureLimit" ), featureLimitParam ); } - ok = false; - const int expansionLimitParam = dsUri.param( QStringLiteral( "expansionLimit" ) ).toInt( &ok ); - if ( ok ) - { - components.insert( QStringLiteral( "expansionLimit" ), expansionLimitParam ); - } switch ( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) { @@ -476,9 +484,23 @@ QString QgsSensorThingsProviderMetadata::encodeUri( const QVariantMap &parts ) c qgsEnumValueToKey( entity ) ); } - const QStringList expandToParam = parts.value( QStringLiteral( "expandTo" ) ).toStringList(); + const QVariantList expandToParam = parts.value( QStringLiteral( "expandTo" ) ).toList(); if ( !expandToParam.isEmpty() ) - dsUri.setParam( QStringLiteral( "expandTo" ), expandToParam.join( ',' ) ); + { + QStringList expandToStringList; + for ( const QVariant &expansion : expandToParam ) + { + const QgsSensorThingsExpansionDefinition expansionDefinition = expansion.value< QgsSensorThingsExpansionDefinition >(); + if ( !expansionDefinition.isValid() ) + continue; + + expandToStringList.append( expansionDefinition.toString() ); + } + if ( !expandToStringList.isEmpty() ) + { + dsUri.setParam( QStringLiteral( "expandTo" ), expandToStringList.join( ';' ) ); + } + } bool ok = false; const int maxPageSizeParam = parts.value( QStringLiteral( "pageSize" ) ).toInt( &ok ); @@ -493,12 +515,6 @@ QString QgsSensorThingsProviderMetadata::encodeUri( const QVariantMap &parts ) c { dsUri.setParam( QStringLiteral( "featureLimit" ), QString::number( featureLimitParam ) ); } - ok = false; - const int expansionLimitParam = parts.value( QStringLiteral( "expansionLimit" ) ).toInt( &ok ); - if ( ok ) - { - dsUri.setParam( QStringLiteral( "expansionLimit" ), QString::number( expansionLimitParam ) ); - } const QString geometryType = parts.value( QStringLiteral( "geometryType" ) ).toString(); if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 ) diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index a56f9cbf9ed..1febdce73f6 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -34,28 +34,21 @@ QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri ) const QVariantMap uriParts = QgsSensorThingsProviderMetadata().decodeUri( uri ); mEntityType = qgsEnumKeyToValue( uriParts.value( QStringLiteral( "entity" ) ).toString(), Qgis::SensorThingsEntity::Invalid ); - const QStringList expandTo = uriParts.value( QStringLiteral( "expandTo" ) ).toStringList(); - QStringList expandQueryParts; - for ( const QString &expand : expandTo ) + const QVariantList expandTo = uriParts.value( QStringLiteral( "expandTo" ) ).toList(); + QList< Qgis::SensorThingsEntity > expandedEntities; + for ( const QVariant &expansionVariant : expandTo ) { - const Qgis::SensorThingsEntity expandToEntityType = qgsEnumKeyToValue( expand, Qgis::SensorThingsEntity::Invalid ); - if ( expandToEntityType != Qgis::SensorThingsEntity::Invalid ) + const QgsSensorThingsExpansionDefinition expansion = expansionVariant.value< QgsSensorThingsExpansionDefinition >(); + if ( expansion.isValid() ) { - mExpandTo.append( expandToEntityType ); - // NOTE: from the specifications, it looks look SOMETIMES plural is used, sometimes singular?? - // We might need to be more flexible here to support all connections - expandQueryParts.append( QgsSensorThingsUtils::entityToSetString( expandToEntityType ) ); + mExpansions.append( expansion ); + expandedEntities.append( expansion.childEntity() ); } - } - mExpansionLimit = uriParts.value( QStringLiteral( "expansionLimit" ) ).toInt(); - if ( !expandQueryParts.empty() ) - { - mExpandQueryString = QStringLiteral( "$expand=" ) + expandQueryParts.join( '/' ); - if ( mExpansionLimit > 0 ) - mExpandQueryString += QStringLiteral( "($top=%1)" ).arg( mExpansionLimit ); + + mExpandQueryString = QgsSensorThingsUtils::asQueryString( mExpansions ); } - mFields = QgsSensorThingsUtils::fieldsForExpandedEntityType( mEntityType, mExpandTo ); + mFields = QgsSensorThingsUtils::fieldsForExpandedEntityType( mEntityType, expandedEntities ); mGeometryField = QgsSensorThingsUtils::geometryFieldForEntityType( mEntityType ); // use initial value of maximum page size as default @@ -190,7 +183,7 @@ long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const // MISSING PART -- how to handle feature count when we are expanding features? // This situation is not handled by the SensorThings standard at all, so we'll just have // to return an unknown count whenever expansion is used - if ( !mExpandTo.isEmpty() ) + if ( !mExpansions.isEmpty() ) { return static_cast< long long >( Qgis::FeatureCountState::UnknownCount ); } @@ -414,7 +407,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee const QString authcfg = mAuthCfg; const QgsHttpHeaders headers = mHeaders; const QgsFields fields = mFields; - const QList< Qgis::SensorThingsEntity > expandTo = mExpandTo; + const QList< QgsSensorThingsExpansionDefinition > expansions = mExpansions; while ( continueFetchingCallback() ) { @@ -587,7 +580,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee }; const QString iotId = getString( featureData, "@iot.id" ).toString(); - if ( expandTo.isEmpty() ) + if ( expansions.isEmpty() ) { auto existingFeatureIdIt = mIotIdToFeatureId.constFind( iotId ); if ( existingFeatureIdIt != mIotIdToFeatureId.constEnd() ) @@ -757,17 +750,17 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee }; const QString baseFeatureId = getString( featureData, "@iot.id" ).toString(); - if ( !expandTo.empty() ) + if ( !expansions.empty() ) { mRetrievedBaseFeatureCount++; - std::function< void( const nlohmann::json &, const QList &, const QString &, const QgsAttributes & ) > traverseExpansion; - traverseExpansion = [this, &feature, &getString, &traverseExpansion, &fetchedFeatureCallback, &extendAttributes, &processFeature]( const nlohmann::json & currentLevelData, const QList &expansionTargets, const QString & lowerLevelId, const QgsAttributes & lowerLevelAttributes ) + std::function< void( const nlohmann::json &, const QList &, const QString &, const QgsAttributes & ) > traverseExpansion; + traverseExpansion = [this, &feature, &getString, &traverseExpansion, &fetchedFeatureCallback, &extendAttributes, &processFeature]( const nlohmann::json & currentLevelData, const QList &expansionTargets, const QString & lowerLevelId, const QgsAttributes & lowerLevelAttributes ) { - const Qgis::SensorThingsEntity currentExpansionTarget = expansionTargets.at( 0 ); - const QList< Qgis::SensorThingsEntity > remainingExpansionTargets = expansionTargets.mid( 1 ); + const QgsSensorThingsExpansionDefinition currentExpansionTarget = expansionTargets.at( 0 ); + const QList< QgsSensorThingsExpansionDefinition > remainingExpansionTargets = expansionTargets.mid( 1 ); - const QString currentExpansionPropertyString = QgsSensorThingsUtils::entityToSetString( currentExpansionTarget ); + const QString currentExpansionPropertyString = QgsSensorThingsUtils::entityToSetString( currentExpansionTarget.childEntity() ); if ( currentLevelData.contains( currentExpansionPropertyString.toLocal8Bit().constData() ) ) { const auto &expandedEntity = currentLevelData[currentExpansionPropertyString.toLocal8Bit().constData()]; @@ -790,8 +783,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee } } - - extendAttributes( currentExpansionTarget, expandedEntityElement, expandedAttributes ); + extendAttributes( currentExpansionTarget.childEntity(), expandedEntityElement, expandedAttributes ); if ( !remainingExpansionTargets.empty() ) { // traverse deeper @@ -817,7 +809,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee } }; - traverseExpansion( featureData, expandTo, baseFeatureId, attributes ); + traverseExpansion( featureData, expansions, baseFeatureId, attributes ); if ( mFeatureLimit > 0 && mFeatureLimit <= mRetrievedBaseFeatureCount ) break; diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.h b/src/core/providers/sensorthings/qgssensorthingsshareddata.h index 84feeacf316..a0ddc5ea933 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.h +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.h @@ -82,10 +82,9 @@ class QgsSensorThingsSharedData QString mExpandQueryString; Qgis::SensorThingsEntity mEntityType = Qgis::SensorThingsEntity::Invalid; - QList< Qgis::SensorThingsEntity > mExpandTo; + QList< QgsSensorThingsExpansionDefinition > mExpansions; int mFeatureLimit = 0; - int mExpansionLimit = 0; Qgis::WkbType mGeometryType = Qgis::WkbType::Unknown; QString mGeometryField; QgsFields mFields; diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.cpp b/src/core/providers/sensorthings/qgssensorthingsutils.cpp index e39fd264444..977d72fe21c 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsutils.cpp @@ -24,8 +24,158 @@ #include "qgsrectangle.h" #include #include +#include +#include #include + +// +// QgsSensorThingsExpansionDefinition +// +QgsSensorThingsExpansionDefinition::QgsSensorThingsExpansionDefinition( Qgis::SensorThingsEntity childEntity, const QString &orderBy, Qt::SortOrder sortOrder, int limit ) + : mChildEntity( childEntity ) + , mOrderBy( orderBy ) + , mSortOrder( sortOrder ) + , mLimit( limit ) +{ + +} + +bool QgsSensorThingsExpansionDefinition::isValid() const +{ + return mChildEntity != Qgis::SensorThingsEntity::Invalid; +} + +Qgis::SensorThingsEntity QgsSensorThingsExpansionDefinition::childEntity() const +{ + return mChildEntity; +} + +void QgsSensorThingsExpansionDefinition::setChildEntity( Qgis::SensorThingsEntity entity ) +{ + mChildEntity = entity; +} + +Qt::SortOrder QgsSensorThingsExpansionDefinition::sortOrder() const +{ + return mSortOrder; +} + +void QgsSensorThingsExpansionDefinition::setSortOrder( Qt::SortOrder order ) +{ + mSortOrder = order; +} + +int QgsSensorThingsExpansionDefinition::limit() const +{ + return mLimit; +} + +void QgsSensorThingsExpansionDefinition::setLimit( int limit ) +{ + mLimit = limit; +} + +QString QgsSensorThingsExpansionDefinition::toString() const +{ + if ( !isValid() ) + return QString(); + + QStringList parts; + parts.append( qgsEnumValueToKey( mChildEntity ) ); + if ( !mOrderBy.isEmpty() ) + parts.append( QStringLiteral( "orderby=%1,%2" ).arg( mOrderBy, mSortOrder == Qt::SortOrder::AscendingOrder ? QStringLiteral( "asc" ) : QStringLiteral( "desc" ) ) ); + if ( mLimit >= 0 ) + parts.append( QStringLiteral( "limit=%1" ).arg( mLimit ) ); + return parts.join( ':' ); +} + +QgsSensorThingsExpansionDefinition QgsSensorThingsExpansionDefinition::fromString( const QString &string ) +{ + const QStringList parts = string.split( ':', Qt::SkipEmptyParts ); + if ( parts.empty() ) + return QgsSensorThingsExpansionDefinition(); + + QgsSensorThingsExpansionDefinition definition( qgsEnumKeyToValue( parts.at( 0 ), Qgis::SensorThingsEntity::Invalid ) ); + definition.setLimit( -1 ); + for ( int i = 1; i < parts.count(); ++i ) + { + const QString &part = parts.at( i ); + const thread_local QRegularExpression orderByRegEx( QStringLiteral( "^orderby=(.*),(.*?)$" ) ); + const thread_local QRegularExpression orderLimitRegEx( QStringLiteral( "^limit=(\\d+)$" ) ); + + const QRegularExpressionMatch orderByMatch = orderByRegEx.match( part ); + if ( orderByMatch.hasMatch() ) + { + definition.setOrderBy( orderByMatch.captured( 1 ) ); + definition.setSortOrder( orderByMatch.captured( 2 ) == QLatin1String( "asc" ) ? Qt::SortOrder::AscendingOrder : Qt::SortOrder::DescendingOrder ); + continue; + } + + const QRegularExpressionMatch limitMatch = orderLimitRegEx.match( part ); + if ( limitMatch.hasMatch() ) + { + definition.setLimit( limitMatch.captured( 1 ).toInt() ); + continue; + } + } + return definition; +} + +QString QgsSensorThingsExpansionDefinition::asQueryString( const QStringList &additionalOptions ) const +{ + if ( !isValid() ) + return QString(); + + // NOTE: from the specifications, it looks look SOMETIMES plural is used, sometimes singular?? + // We might need to be more flexible here to support all connections + QString res = QStringLiteral( "$expand=%1" ).arg( QgsSensorThingsUtils::entityToSetString( mChildEntity ) ); + + QStringList queryOptions; + if ( !mOrderBy.isEmpty() ) + queryOptions.append( QStringLiteral( "$orderby=%1%2" ).arg( mOrderBy, mSortOrder == Qt::SortOrder::AscendingOrder ? QString() : QStringLiteral( " desc" ) ) ); + + if ( mLimit > -1 ) + queryOptions.append( QStringLiteral( "$top=%1" ).arg( mLimit ) ); + + queryOptions.append( additionalOptions ); + + if ( !queryOptions.isEmpty() ) + res.append( QStringLiteral( "(%1)" ).arg( queryOptions.join( ';' ) ) ); + + return res; +} + +bool QgsSensorThingsExpansionDefinition::operator==( const QgsSensorThingsExpansionDefinition &other ) const +{ + if ( mChildEntity == Qgis::SensorThingsEntity::Invalid ) + return other.mChildEntity == Qgis::SensorThingsEntity::Invalid; + + return mChildEntity == other.mChildEntity + && mSortOrder == other.mSortOrder + && mLimit == other.mLimit + && mOrderBy == other.mOrderBy; +} + +bool QgsSensorThingsExpansionDefinition::operator!=( const QgsSensorThingsExpansionDefinition &other ) const +{ + return !( *this == other ); +} + +QString QgsSensorThingsExpansionDefinition::orderBy() const +{ + return mOrderBy; +} + +void QgsSensorThingsExpansionDefinition::setOrderBy( const QString &field ) +{ + mOrderBy = field; +} + +// +// QgsSensorThingsUtils +// + Qgis::SensorThingsEntity QgsSensorThingsUtils::stringToEntity( const QString &type ) { const QString trimmed = type.trimmed(); @@ -132,6 +282,110 @@ QString QgsSensorThingsUtils::entityToSetString( Qgis::SensorThingsEntity type ) BUILTIN_UNREACHABLE } +QStringList QgsSensorThingsUtils::propertiesForEntityType( Qgis::SensorThingsEntity type ) +{ + switch ( type ) + { + case Qgis::SensorThingsEntity::Invalid: + return {}; + + case Qgis::SensorThingsEntity::Thing: + // https://docs.ogc.org/is/18-088/18-088.html#thing + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "description" ), + QStringLiteral( "properties" ), + }; + + case Qgis::SensorThingsEntity::Location: + // https://docs.ogc.org/is/18-088/18-088.html#location + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "description" ), + QStringLiteral( "properties" ), + }; + + case Qgis::SensorThingsEntity::HistoricalLocation: + // https://docs.ogc.org/is/18-088/18-088.html#historicallocation + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "time" ), + }; + + case Qgis::SensorThingsEntity::Datastream: + // https://docs.ogc.org/is/18-088/18-088.html#datastream + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "description" ), + QStringLiteral( "unitOfMeasurement" ), + QStringLiteral( "observationType" ), + QStringLiteral( "properties" ), + QStringLiteral( "phenomenonTime" ), + QStringLiteral( "resultTime" ), + }; + + case Qgis::SensorThingsEntity::Sensor: + // https://docs.ogc.org/is/18-088/18-088.html#sensor + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "description" ), + QStringLiteral( "metadata" ), + QStringLiteral( "properties" ), + }; + + case Qgis::SensorThingsEntity::ObservedProperty: + // https://docs.ogc.org/is/18-088/18-088.html#observedproperty + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "definition" ), + QStringLiteral( "description" ), + QStringLiteral( "properties" ), + }; + + case Qgis::SensorThingsEntity::Observation: + // https://docs.ogc.org/is/18-088/18-088.html#observation + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "phenomenonTime" ), + QStringLiteral( "result" ), + QStringLiteral( "resultTime" ), + QStringLiteral( "resultQuality" ), + QStringLiteral( "validTime" ), + QStringLiteral( "parameters" ), + }; + + case Qgis::SensorThingsEntity::FeatureOfInterest: + // https://docs.ogc.org/is/18-088/18-088.html#featureofinterest + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "description" ), + QStringLiteral( "properties" ), + }; + + case Qgis::SensorThingsEntity::MultiDatastream: + // https://docs.ogc.org/is/18-088/18-088.html#multidatastream-extension + return { QStringLiteral( "id" ), + QStringLiteral( "selfLink" ), + QStringLiteral( "name" ), + QStringLiteral( "description" ), + QStringLiteral( "unitOfMeasurements" ), + QStringLiteral( "observationType" ), + QStringLiteral( "multiObservationDataTypes" ), + QStringLiteral( "properties" ), + QStringLiteral( "phenomenonTime" ), + QStringLiteral( "resultTime" ), + }; + } + + return {}; +} + QgsFields QgsSensorThingsUtils::fieldsForEntityType( Qgis::SensorThingsEntity type ) { QgsFields fields; @@ -503,7 +757,7 @@ QList QgsSensorThingsUtils::availableGeometryTypes( const QS return types; } -QList > QgsSensorThingsUtils::expandableTargets( Qgis::SensorThingsEntity type ) +QList QgsSensorThingsUtils::expandableTargets( Qgis::SensorThingsEntity type ) { // note that we are restricting these choices so that the geometry enabled entity type MUST be the base type switch ( type ) @@ -514,95 +768,79 @@ QList > QgsSensorThingsUtils::expandableTargets( case Qgis::SensorThingsEntity::Thing: return { - { Qgis::SensorThingsEntity::HistoricalLocation }, - { Qgis::SensorThingsEntity::Datastream }, - { Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Sensor }, - { Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::ObservedProperty }, - { Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Observation }, + Qgis::SensorThingsEntity::HistoricalLocation, + Qgis::SensorThingsEntity::Datastream }; case Qgis::SensorThingsEntity::Location: return { - { Qgis::SensorThingsEntity::Thing }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Sensor }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::ObservedProperty }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Observation }, - { Qgis::SensorThingsEntity::HistoricalLocation }, + Qgis::SensorThingsEntity::Thing, + Qgis::SensorThingsEntity::HistoricalLocation, }; case Qgis::SensorThingsEntity::HistoricalLocation: return { - {Qgis::SensorThingsEntity::Thing }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Sensor }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::ObservedProperty }, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Observation }, + Qgis::SensorThingsEntity::Thing }; case Qgis::SensorThingsEntity::Datastream: return { - {Qgis::SensorThingsEntity::Thing}, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::HistoricalLocation }, - {Qgis::SensorThingsEntity::Sensor}, - {Qgis::SensorThingsEntity::ObservedProperty}, - {Qgis::SensorThingsEntity::Observation} + Qgis::SensorThingsEntity::Thing, + Qgis::SensorThingsEntity::Sensor, + Qgis::SensorThingsEntity::ObservedProperty, + Qgis::SensorThingsEntity::Observation }; case Qgis::SensorThingsEntity::Sensor: return { - {Qgis::SensorThingsEntity::Datastream}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::HistoricalLocation }, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::ObservedProperty}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Observation} + Qgis::SensorThingsEntity::Datastream }; case Qgis::SensorThingsEntity::ObservedProperty: return { - {Qgis::SensorThingsEntity::Datastream}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Sensor}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::HistoricalLocation }, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Observation} + Qgis::SensorThingsEntity::Datastream }; case Qgis::SensorThingsEntity::Observation: return { - {Qgis::SensorThingsEntity::Datastream}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Sensor}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing}, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::HistoricalLocation }, - {Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::ObservedProperty} + Qgis::SensorThingsEntity::Datastream }; case Qgis::SensorThingsEntity::FeatureOfInterest: return { - {Qgis::SensorThingsEntity::Observation}, - {Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::Datastream}, - {Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Sensor}, - {Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing}, - {Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::HistoricalLocation }, - {Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::Datastream, Qgis::SensorThingsEntity::ObservedProperty} + Qgis::SensorThingsEntity::Observation }; - case Qgis::SensorThingsEntity::MultiDatastream: return { - {Qgis::SensorThingsEntity::Thing}, - { Qgis::SensorThingsEntity::Thing, Qgis::SensorThingsEntity::HistoricalLocation }, - {Qgis::SensorThingsEntity::Sensor}, - {Qgis::SensorThingsEntity::ObservedProperty}, - {Qgis::SensorThingsEntity::Observation} + Qgis::SensorThingsEntity::Thing, + Qgis::SensorThingsEntity::Sensor, + Qgis::SensorThingsEntity::ObservedProperty, + Qgis::SensorThingsEntity::Observation }; } BUILTIN_UNREACHABLE } + +QString QgsSensorThingsUtils::asQueryString( const QList &expansions ) +{ + QString res; + for ( int i = expansions.size() - 1; i >= 0 ; i-- ) + { + const QgsSensorThingsExpansionDefinition &expansion = expansions.at( i ); + if ( !expansion.isValid() ) + continue; + + res = expansion.asQueryString( res.isEmpty() ? QStringList() : QStringList{ res } ); + } + + return res; +} diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.h b/src/core/providers/sensorthings/qgssensorthingsutils.h index 6c73939aa82..e94b93f475e 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.h +++ b/src/core/providers/sensorthings/qgssensorthingsutils.h @@ -22,6 +22,7 @@ class QgsFields; class QgsFeedback; class QgsRectangle; +class QgsSensorThingsExpansionDefinition; /** * \ingroup core @@ -71,6 +72,13 @@ class CORE_EXPORT QgsSensorThingsUtils */ static QString entityToSetString( Qgis::SensorThingsEntity type ); + /** + * Returns the SensorThings properties which correspond to a specified entity \a type. + * + * \since QGIS 3.38 + */ + static QStringList propertiesForEntityType( Qgis::SensorThingsEntity type ); + /** * Returns the fields which correspond to a specified entity \a type. */ @@ -141,8 +149,163 @@ class CORE_EXPORT QgsSensorThingsUtils * * \since QGIS 3.38 */ - static QList< QList< Qgis::SensorThingsEntity > > expandableTargets( Qgis::SensorThingsEntity type ); + static QList< Qgis::SensorThingsEntity > expandableTargets( Qgis::SensorThingsEntity type ); + + /** + * Returns a list of \a expansions as a valid SensorThings API query string, eg "$expand=Locations($orderby=id desc;$top=3;$expand=Datastreams($top=101))". + * + * \since QGIS 3.38 + */ + static QString asQueryString( const QList< QgsSensorThingsExpansionDefinition > &expansions ); }; + +/** + * \ingroup core + * \brief Encapsulates information about how relationships in a SensorThings API service should be expanded. + * + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsSensorThingsExpansionDefinition +{ + public: + + /** + * Constructor for QgsSensorThingsExpansionDefinition, targeting the specified child entity type. + */ + QgsSensorThingsExpansionDefinition( Qgis::SensorThingsEntity childEntity = Qgis::SensorThingsEntity::Invalid, + const QString &orderBy = QString(), + Qt::SortOrder sortOrder = Qt::SortOrder::AscendingOrder, + int limit = QgsSensorThingsUtils::DEFAULT_EXPANSION_LIMIT ); + + /** + * Returns TRUE if the definition is valid. + */ + bool isValid() const; + + /** + * Returns the target child entity which should be expanded. + * + * \see setChildEntity() + */ + Qgis::SensorThingsEntity childEntity() const; + + /** + * Sets the target child \a entity which should be expanded. + * + * \see childEntity() + */ + void setChildEntity( Qgis::SensorThingsEntity entity ); + + /** + * Returns the field name to order the expanded child entities by. + * + * \see sortOrder() + * \see setOrderBy() + */ + QString orderBy() const; + + /** + * Sets the \a field name to order the expanded child entities by. + * + * \see orderBy() + * \see setSortOrder() + */ + void setOrderBy( const QString &field ); + + /** + * Returns the sort order for the expanded child entities. + * + * \see orderBy() + * \see setSortOrder() + */ + Qt::SortOrder sortOrder() const; + + /** + * Sets the sort order for the expanded child entities. + * + * \see setOrderBy() + * \see sortOrder() + */ + void setSortOrder( Qt::SortOrder order ); + + /** + * Returns the limit on the number of child features to fetch. + * + * Returns -1 if no limit is defined. + * + * \see setLimit() + */ + int limit() const; + + /** + * Sets the \a limit on the number of child features to fetch. + * + * Set to -1 if no limit is desired. + * + * \see limit() + */ + void setLimit( int limit ); + + /** + * Returns a string encapsulation of the expansion definition. + * + * \see fromString() + */ + QString toString() const; + + /** + * Returns a QgsSensorThingsExpansionDefinition from a string representation. + * + * \see toString() + */ + static QgsSensorThingsExpansionDefinition fromString( const QString &string ); + + /** + * Returns the expansion as a valid SensorThings API query string, eg "$expand=Observations($orderby=phenomenonTime desc;$top=10)". + * + * Optionally a list of additional query options can be specified for the expansion. + */ + QString asQueryString( const QStringList &additionalOptions = QStringList() ) const; + + bool operator==( const QgsSensorThingsExpansionDefinition &other ) const; + bool operator!=( const QgsSensorThingsExpansionDefinition &other ) const; + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + if ( !sipCpp->isValid() ) + { + sipRes = PyUnicode_FromString( "" ); + return; + } + + QString innerDefinition; + if ( !sipCpp->orderBy().isEmpty() ) + { + innerDefinition = QStringLiteral( "by %1 (%2)" ).arg( sipCpp->orderBy(), sipCpp->sortOrder() == Qt::SortOrder::AscendingOrder ? QStringLiteral( "asc" ) : QStringLiteral( "desc" ) ); + } + if ( sipCpp->limit() >= 0 ) + { + if ( !innerDefinition.isEmpty() ) + innerDefinition = QStringLiteral( "%1, limit %2" ).arg( innerDefinition ).arg( sipCpp->limit() ); + else + innerDefinition = QStringLiteral( "limit %1" ).arg( sipCpp->limit() ); + } + + QString str = QStringLiteral( "" ).arg( qgsEnumValueToKey( sipCpp->childEntity() ), innerDefinition.isEmpty() ? QString() : ( QStringLiteral( " " ) + innerDefinition ) ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End +#endif + + private: + + Qgis::SensorThingsEntity mChildEntity = Qgis::SensorThingsEntity::Invalid; + QString mOrderBy; + Qt::SortOrder mSortOrder = Qt::SortOrder::AscendingOrder; + int mLimit = QgsSensorThingsUtils::DEFAULT_EXPANSION_LIMIT; +}; +Q_DECLARE_METATYPE( QgsSensorThingsExpansionDefinition ) + #endif // QGSSENSORTHINGSUTILS_H diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index 9bb2f12161d..af22cc84914 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -86,6 +86,7 @@ #include "qgsinterval.h" #include "qgsgpsconnection.h" #include "qgssensorregistry.h" +#include "qgssensorthingsutils.h" #include "gps/qgsgpsconnectionregistry.h" #include "processing/qgsprocessingregistry.h" @@ -313,6 +314,7 @@ void QgsApplication::init( QString profileFolder ) qRegisterMetaType>( "QList" ); qRegisterMetaType< QAuthenticator * >( "QAuthenticator*" ); qRegisterMetaType< QgsGpsInformation >( "QgsGpsInformation" ); + qRegisterMetaType< QgsSensorThingsExpansionDefinition >( "QgsSensorThingsExpansionDefinition" ); } ); ( void ) resolvePkgPath(); diff --git a/tests/src/python/test_provider_sensorthings.py b/tests/src/python/test_provider_sensorthings.py index 0fa7196bc82..c8e70e032a3 100644 --- a/tests/src/python/test_provider_sensorthings.py +++ b/tests/src/python/test_provider_sensorthings.py @@ -22,7 +22,8 @@ from qgis.core import ( QgsSettings, QgsSensorThingsUtils, QgsFeatureRequest, - QgsRectangle + QgsRectangle, + QgsSensorThingsExpansionDefinition ) from qgis.testing import start_app, QgisTestCase @@ -230,6 +231,196 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): "MultiDatastreams", ) + def test_expansion_definition(self): + """ + Test QgsSensorThingsExpansionDefinition + """ + expansion = QgsSensorThingsExpansionDefinition() + self.assertFalse(expansion.isValid()) + self.assertFalse(expansion.asQueryString()) + + # test getters/setters + expansion = QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.ObservedProperty) + self.assertTrue(expansion.isValid()) + self.assertEqual(expansion.childEntity(), Qgis.SensorThingsEntity.ObservedProperty) + self.assertEqual(expansion.limit(), 100) + self.assertEqual(repr(expansion), '') + self.assertEqual(expansion.asQueryString(), '$expand=ObservedProperties($top=100)') + self.assertEqual(expansion.asQueryString(['$expand=Locations($top=101)']), + '$expand=ObservedProperties($top=100;$expand=Locations($top=101))') + + expansion.setChildEntity(Qgis.SensorThingsEntity.Location) + self.assertEqual(expansion.childEntity(), + Qgis.SensorThingsEntity.Location) + self.assertEqual(repr(expansion), + '') + self.assertEqual(expansion.asQueryString(), + '$expand=Locations($top=100)') + self.assertEqual(expansion.asQueryString(['$expand=Datastreams($top=101)']), + '$expand=Locations($top=100;$expand=Datastreams($top=101))') + + expansion.setLimit(-1) + self.assertEqual(expansion.limit(), -1) + self.assertEqual(repr(expansion), + '') + self.assertEqual(expansion.asQueryString(), + '$expand=Locations') + self.assertEqual(expansion.asQueryString(['$expand=Datastreams($top=101)']), + '$expand=Locations($expand=Datastreams($top=101))') + + expansion.setOrderBy('id') + self.assertEqual(expansion.orderBy(), 'id') + self.assertEqual(repr(expansion), + '') + self.assertEqual(expansion.asQueryString(), + '$expand=Locations($orderby=id)') + self.assertEqual(expansion.asQueryString(['$expand=Datastreams($top=101)']), + '$expand=Locations($orderby=id;$expand=Datastreams($top=101))') + expansion.setSortOrder(Qt.SortOrder.DescendingOrder) + self.assertEqual(expansion.sortOrder(), Qt.SortOrder.DescendingOrder) + self.assertEqual(repr(expansion), + '') + self.assertEqual(expansion.asQueryString(), + '$expand=Locations($orderby=id desc)') + self.assertEqual(expansion.asQueryString(['$expand=Datastreams($top=101)']), + '$expand=Locations($orderby=id desc;$expand=Datastreams($top=101))') + + expansion.setLimit(3) + self.assertEqual(repr(expansion), + '') + self.assertEqual(expansion.asQueryString(), + '$expand=Locations($orderby=id desc;$top=3)') + self.assertEqual(expansion.asQueryString(['$expand=Datastreams($top=101)']), + '$expand=Locations($orderby=id desc;$top=3;$expand=Datastreams($top=101))') + + # test equality + expansion1 = QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.ObservedProperty) + expansion2 = QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.ObservedProperty) + self.assertEqual(expansion1, expansion2) + self.assertNotEqual(expansion1, QgsSensorThingsExpansionDefinition()) + self.assertNotEqual(QgsSensorThingsExpansionDefinition(), expansion2) + self.assertEqual(QgsSensorThingsExpansionDefinition(), + QgsSensorThingsExpansionDefinition()) + + expansion2.setChildEntity(Qgis.SensorThingsEntity.Sensor) + self.assertNotEqual(expansion1, expansion2) + expansion2.setChildEntity(Qgis.SensorThingsEntity.ObservedProperty) + self.assertEqual(expansion1, expansion2) + + expansion2.setOrderBy('x') + self.assertNotEqual(expansion1, expansion2) + expansion2.setOrderBy('') + self.assertEqual(expansion1, expansion2) + + expansion2.setSortOrder(Qt.SortOrder.DescendingOrder) + self.assertNotEqual(expansion1, expansion2) + expansion2.setSortOrder(Qt.SortOrder.AscendingOrder) + self.assertEqual(expansion1, expansion2) + + expansion2.setLimit(33) + self.assertNotEqual(expansion1, expansion2) + expansion2.setLimit(100) + self.assertEqual(expansion1, expansion2) + + # test to/from string + expansion = QgsSensorThingsExpansionDefinition() + string = expansion.toString() + self.assertFalse(string) + res = QgsSensorThingsExpansionDefinition.fromString(string) + self.assertFalse(res.isValid()) + + expansion.setChildEntity(Qgis.SensorThingsEntity.Sensor) + expansion.setLimit(-1) + string = expansion.toString() + res = QgsSensorThingsExpansionDefinition.fromString(string) + self.assertTrue(res.isValid()) + self.assertEqual(res.childEntity(), Qgis.SensorThingsEntity.Sensor) + self.assertFalse(res.orderBy()) + self.assertEqual(res.limit(), -1) + + expansion.setOrderBy('test') + string = expansion.toString() + res = QgsSensorThingsExpansionDefinition.fromString(string) + self.assertTrue(res.isValid()) + self.assertEqual(res.childEntity(), Qgis.SensorThingsEntity.Sensor) + self.assertEqual(res.orderBy(), 'test') + self.assertEqual(res.sortOrder(), Qt.SortOrder.AscendingOrder) + self.assertEqual(res.limit(), -1) + + expansion.setSortOrder(Qt.SortOrder.DescendingOrder) + string = expansion.toString() + res = QgsSensorThingsExpansionDefinition.fromString(string) + self.assertTrue(res.isValid()) + self.assertEqual(res.childEntity(), Qgis.SensorThingsEntity.Sensor) + self.assertEqual(res.orderBy(), 'test') + self.assertEqual(res.sortOrder(), Qt.SortOrder.DescendingOrder) + self.assertEqual(res.limit(), -1) + + expansion.setLimit(5) + string = expansion.toString() + res = QgsSensorThingsExpansionDefinition.fromString(string) + self.assertTrue(res.isValid()) + self.assertEqual(res.childEntity(), Qgis.SensorThingsEntity.Sensor) + self.assertEqual(res.orderBy(), 'test') + self.assertEqual(res.sortOrder(), Qt.SortOrder.DescendingOrder) + self.assertEqual(res.limit(), 5) + + expansion.setOrderBy('') + string = expansion.toString() + res = QgsSensorThingsExpansionDefinition.fromString(string) + self.assertTrue(res.isValid()) + self.assertEqual(res.childEntity(), Qgis.SensorThingsEntity.Sensor) + self.assertFalse(res.orderBy()) + self.assertEqual(res.limit(), 5) + + def test_expansions_as_query_string(self): + """ + Test constructing query strings from a list of expansions + """ + self.assertFalse( + QgsSensorThingsUtils.asQueryString([]) + ) + self.assertEqual( + QgsSensorThingsUtils.asQueryString([ + QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.Location, + orderBy='id', limit=3) + ]), + '$expand=Locations($orderby=id;$top=3)' + ) + self.assertEqual( + QgsSensorThingsUtils.asQueryString([ + QgsSensorThingsExpansionDefinition(), + QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.Location, + orderBy='id', limit=3) + ]), + '$expand=Locations($orderby=id;$top=3)' + ) + self.assertEqual( + QgsSensorThingsUtils.asQueryString([ + QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.Location, + orderBy='id', limit=3), + QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.Sensor, + orderBy='description', limit=30) + ]), + '$expand=Locations($orderby=id;$top=3;$expand=Sensors($orderby=description;$top=30))' + ) + self.assertEqual( + QgsSensorThingsUtils.asQueryString([ + QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.Location, + orderBy='id', limit=3), + QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.Sensor, + orderBy='description', limit=30), + QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.Datastream, + orderBy='name', limit=-1) + ]), + '$expand=Locations($orderby=id;$top=3;$expand=Sensors($orderby=description;$top=30;$expand=Datastreams($orderby=name)))' + ) + def test_fields_for_expanded_entity(self): """ Test calculating fields for an expanded entity @@ -273,14 +464,9 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): """ self.assertEqual(QgsSensorThingsUtils.expandableTargets( Qgis.SensorThingsEntity.Thing), - [[Qgis.SensorThingsEntity.HistoricalLocation], - [Qgis.SensorThingsEntity.Datastream], - [Qgis.SensorThingsEntity.Datastream, - Qgis.SensorThingsEntity.Sensor], - [Qgis.SensorThingsEntity.Datastream, - Qgis.SensorThingsEntity.ObservedProperty], - [Qgis.SensorThingsEntity.Datastream, - Qgis.SensorThingsEntity.Observation]] + [Qgis.SensorThingsEntity.HistoricalLocation, + Qgis.SensorThingsEntity.Datastream + ] ) def test_filter_for_extent(self): @@ -3707,7 +3893,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): with open( sanitize(endpoint, - "/Locations?$top=2&$count=false&$expand=Things/Datastreams&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), + "/Locations?$top=2&$count=false&$expand=Things($expand=Datastreams)&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), "wt", encoding="utf8", ) as f: @@ -3877,7 +4063,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): } ], - "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$expand=Things/Datastreams&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'" + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$expand=Things($expand=Datastreams)&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'" } """.replace( "endpoint", "http://" + endpoint @@ -3886,7 +4072,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): with open( sanitize(endpoint, - "/Locations?$top=2&$skip=2&$expand=Things/Datastreams&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), + "/Locations?$top=2&$skip=2&$expand=Things($expand=Datastreams)&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), "wt", encoding="utf8", ) as f: @@ -3975,7 +4161,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' pageSize=2 type=MultiPolygonZ entity='Location' expandTo='Thing,Datastream'", + f"url='http://{endpoint}' pageSize=2 type=MultiPolygonZ entity='Location' expandTo='Thing;Datastream'", "test", "sensorthings", ) @@ -4162,7 +4348,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): with open( sanitize(endpoint, - "/Locations?$top=2&$count=false&$expand=Things/Datastreams($top=1)&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), + "/Locations?$top=2&$count=false&$expand=Things($expand=Datastreams($top=1))&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), "wt", encoding="utf8", ) as f: @@ -4289,7 +4475,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): } ], - "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$expand=Things/Datastreams($top=1)&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'" + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$expand=Things($expand=Datastreams($top=1))&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'" } """.replace( "endpoint", "http://" + endpoint @@ -4298,7 +4484,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): with open( sanitize(endpoint, - "/Locations?$top=2&$skip=2&$expand=Things/Datastreams($top=1)&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), + "/Locations?$top=2&$skip=2&$expand=Things($expand=Datastreams($top=1))&$filter=location/type eq 'Polygon' or location/geometry/type eq 'Polygon'"), "wt", encoding="utf8", ) as f: @@ -4371,7 +4557,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' pageSize=2 type=MultiPolygonZ entity='Location' expansionLimit=1 expandTo='Thing,Datastream'", + f"url='http://{endpoint}' pageSize=2 type=MultiPolygonZ entity='Location' expandTo='Thing;Datastream:limit=1'", "test", "sensorthings", ) @@ -4603,7 +4789,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): }, ) - uri = "url='https://sometest.com/api' type=MultiPolygonZ authcfg='abc' expandTo='Thing,Datastream' expansionLimit=30 entity='Location'" + uri = "url='https://sometest.com/api' type=MultiPolygonZ authcfg='abc' expandTo='Thing:orderby=description,asc:limit=5;Datastream:orderby=time,asc:limit=3' entity='Location'" parts = QgsProviderRegistry.instance().decodeUri("sensorthings", uri) self.assertEqual( parts, @@ -4612,8 +4798,12 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): "entity": "Location", "geometryType": "polygon", "authcfg": "abc", - 'expansionLimit': 30, - "expandTo": ['Thing', 'Datastream'] + "expandTo": [QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.Thing, orderBy='description', + limit=5), + QgsSensorThingsExpansionDefinition( + Qgis.SensorThingsEntity.Datastream, + orderBy='time', limit=3)], }, ) @@ -4714,13 +4904,13 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): "authcfg": "aaaaa", "entity": "location", "geometryType": "polygon", - "expandTo": ["Thing", "Datastream"], - 'expansionLimit': 30 + "expandTo": [QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.Thing, orderBy='description', limit=5), + QgsSensorThingsExpansionDefinition(Qgis.SensorThingsEntity.Datastream, orderBy='time', limit=3)] } uri = QgsProviderRegistry.instance().encodeUri("sensorthings", parts) self.assertEqual( uri, - "authcfg=aaaaa type=MultiPolygonZ entity='Location' expandTo='Thing,Datastream' expansionLimit='30' url='http://blah.com'", + "authcfg=aaaaa type=MultiPolygonZ entity='Location' expandTo='Thing:orderby=description,asc:limit=5;Datastream:orderby=time,asc:limit=3' url='http://blah.com'", )