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'", )