mirror of
https://github.com/qgis/QGIS.git
synced 2025-12-15 00:07:25 -05:00
Add extent based filtering for SensorThings layers
Allows users to set an extent limit for the layer, so that features are only ever loaded within this extent The extent can be set from the data source manager before adding the layer initially, or modified from the layer properties, source tab.
This commit is contained in:
parent
43ad16022e
commit
24edefb684
@ -61,6 +61,25 @@ Returns ``True`` if the specified entity ``type`` can have geometry attached.
|
||||
%Docstring
|
||||
Returns a filter string which restricts results to those matching the specified
|
||||
``entityType`` and ``wkbType``.
|
||||
%End
|
||||
|
||||
static QString filterForExtent( const QString &geometryField, const QgsRectangle &extent );
|
||||
%Docstring
|
||||
Returns a filter string which restricts results to those within the specified
|
||||
``extent``.
|
||||
|
||||
The ``extent`` should always be specified in EPSG:4326.
|
||||
|
||||
.. versionadded:: 3.38
|
||||
%End
|
||||
|
||||
static QString combineFilters( const QStringList &filters );
|
||||
%Docstring
|
||||
Combines a set of SensorThings API filter operators.
|
||||
|
||||
See https://docs.ogc.org/is/18-088/18-088.html#requirement-request-data-filter
|
||||
|
||||
.. versionadded:: 3.38
|
||||
%End
|
||||
|
||||
static QList< Qgis::GeometryType > availableGeometryTypes( const QString &uri, Qgis::SensorThingsEntity type, QgsFeedback *feedback = 0, const QString &authCfg = QString() );
|
||||
|
||||
@ -61,6 +61,25 @@ Returns ``True`` if the specified entity ``type`` can have geometry attached.
|
||||
%Docstring
|
||||
Returns a filter string which restricts results to those matching the specified
|
||||
``entityType`` and ``wkbType``.
|
||||
%End
|
||||
|
||||
static QString filterForExtent( const QString &geometryField, const QgsRectangle &extent );
|
||||
%Docstring
|
||||
Returns a filter string which restricts results to those within the specified
|
||||
``extent``.
|
||||
|
||||
The ``extent`` should always be specified in EPSG:4326.
|
||||
|
||||
.. versionadded:: 3.38
|
||||
%End
|
||||
|
||||
static QString combineFilters( const QStringList &filters );
|
||||
%Docstring
|
||||
Combines a set of SensorThings API filter operators.
|
||||
|
||||
See https://docs.ogc.org/is/18-088/18-088.html#requirement-request-data-filter
|
||||
|
||||
.. versionadded:: 3.38
|
||||
%End
|
||||
|
||||
static QList< Qgis::GeometryType > availableGeometryTypes( const QString &uri, Qgis::SensorThingsEntity type, QgsFeedback *feedback = 0, const QString &authCfg = QString() );
|
||||
|
||||
@ -346,6 +346,22 @@ QVariantMap QgsSensorThingsProviderMetadata::decodeUri( const QString &uri ) con
|
||||
break;
|
||||
}
|
||||
|
||||
const QStringList bbox = dsUri.param( QStringLiteral( "bbox" ) ).split( ',' );
|
||||
if ( bbox.size() == 4 )
|
||||
{
|
||||
QgsRectangle r;
|
||||
bool xminOk = false;
|
||||
bool yminOk = false;
|
||||
bool xmaxOk = false;
|
||||
bool ymaxOk = false;
|
||||
r.setXMinimum( bbox[0].toDouble( &xminOk ) );
|
||||
r.setYMinimum( bbox[1].toDouble( &yminOk ) );
|
||||
r.setXMaximum( bbox[2].toDouble( &xmaxOk ) );
|
||||
r.setYMaximum( bbox[3].toDouble( &ymaxOk ) );
|
||||
if ( xminOk && yminOk && xmaxOk && ymaxOk )
|
||||
components.insert( QStringLiteral( "bounds" ), r );
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
@ -407,6 +423,12 @@ QString QgsSensorThingsProviderMetadata::encodeUri( const QVariantMap &parts ) c
|
||||
dsUri.setWkbType( Qgis::WkbType::MultiPolygonZ );
|
||||
}
|
||||
|
||||
if ( parts.contains( QStringLiteral( "bounds" ) ) && parts.value( QStringLiteral( "bounds" ) ).userType() == QMetaType::type( "QgsRectangle" ) )
|
||||
{
|
||||
const QgsRectangle bBox = parts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >();
|
||||
dsUri.setParam( QStringLiteral( "bbox" ), QStringLiteral( "%1,%2,%3,%4" ).arg( bBox.xMinimum() ).arg( bBox.yMinimum() ).arg( bBox.xMaximum() ).arg( bBox.yMaximum() ) );
|
||||
}
|
||||
|
||||
return dsUri.uri( false );
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri )
|
||||
mGeometryField = QgsSensorThingsUtils::geometryFieldForEntityType( mEntityType );
|
||||
// use initial value of maximum page size as default
|
||||
mMaximumPageSize = uriParts.value( QStringLiteral( "pageSize" ), mMaximumPageSize ).toInt();
|
||||
mFilterExtent = uriParts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >();
|
||||
|
||||
if ( QgsSensorThingsUtils::entityTypeHasGeometry( mEntityType ) )
|
||||
{
|
||||
@ -133,9 +134,11 @@ QUrl QgsSensorThingsSharedData::parseUrl( const QUrl &url, bool *isTestEndpoint
|
||||
QgsRectangle QgsSensorThingsSharedData::extent() const
|
||||
{
|
||||
QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
|
||||
// Since we can't retrieve the actual layer extent via SensorThings API, we use a pessimistic
|
||||
// global extent until we've retrieved all the features from the layer
|
||||
return hasCachedAllFeatures() ? mFetchedFeatureExtent : QgsRectangle( -180, -90, 180, 90 );
|
||||
return hasCachedAllFeatures() ? mFetchedFeatureExtent
|
||||
: ( !mFilterExtent.isNull() ? mFilterExtent : QgsRectangle( -180, -90, 180, 90 ) );
|
||||
}
|
||||
|
||||
long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const
|
||||
@ -150,8 +153,12 @@ long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const
|
||||
// return no features, just the total count
|
||||
QString countUri = QStringLiteral( "%1?$top=0&$count=true" ).arg( mEntityBaseUri );
|
||||
const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
|
||||
if ( !typeFilter.isEmpty() )
|
||||
countUri += QStringLiteral( "&$filter=" ) + typeFilter;
|
||||
const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
|
||||
QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter } );
|
||||
if ( !filterString.isEmpty() )
|
||||
filterString = QStringLiteral( "&$filter=" ) + filterString;
|
||||
if ( !filterString.isEmpty() )
|
||||
countUri += filterString;
|
||||
|
||||
const QUrl url = parseUrl( QUrl( countUri ) );
|
||||
|
||||
@ -223,8 +230,10 @@ bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsF
|
||||
locker.changeMode( QgsReadWriteLocker::Write );
|
||||
mNextPage = QStringLiteral( "%1?$top=%2&$count=false" ).arg( mEntityBaseUri ).arg( mMaximumPageSize );
|
||||
const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
|
||||
if ( !typeFilter.isEmpty() )
|
||||
mNextPage += QStringLiteral( "&$filter=" ) + typeFilter;
|
||||
const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent );
|
||||
const QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter } );
|
||||
if ( !filterString.isEmpty() )
|
||||
mNextPage += QStringLiteral( "&$filter=" ) + filterString;
|
||||
}
|
||||
|
||||
locker.unlock();
|
||||
@ -251,18 +260,22 @@ bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsF
|
||||
|
||||
QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectangle &extent, QgsFeedback *feedback, const QString &thisPage, QString &nextPage, const QgsFeatureIds &alreadyFetchedIds )
|
||||
{
|
||||
const QgsGeometry extentGeom = QgsGeometry::fromRect( extent );
|
||||
const QgsRectangle requestExtent = mFilterExtent.isNull() ? extent : extent.intersect( mFilterExtent );
|
||||
const QgsGeometry extentGeom = QgsGeometry::fromRect( requestExtent );
|
||||
QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
|
||||
if ( hasCachedAllFeatures() || mCachedExtent.contains( extentGeom ) )
|
||||
{
|
||||
// all features cached locally, rely on local spatial index
|
||||
return qgis::listToSet( mSpatialIndex.intersects( extent ) );
|
||||
return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
|
||||
}
|
||||
|
||||
// TODO -- is using 'geography' always correct here?
|
||||
const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType );
|
||||
QString queryUrl = !thisPage.isEmpty() ? thisPage : QStringLiteral( "%1?$top=%2&$count=false&$filter=geo.intersects(%3, geography'%4')%5" ).arg( mEntityBaseUri ).arg( mMaximumPageSize ).arg( mGeometryField, extent.asWktPolygon(), typeFilter.isEmpty() ? QString() : ( QStringLiteral( " and " ) + typeFilter ) );
|
||||
const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, requestExtent );
|
||||
QString filterString = QgsSensorThingsUtils::combineFilters( { extentFilter, typeFilter } );
|
||||
if ( !filterString.isEmpty() )
|
||||
filterString = QStringLiteral( "&$filter=" ) + filterString;
|
||||
QString queryUrl = !thisPage.isEmpty() ? thisPage : QStringLiteral( "%1?$top=%2&$count=false%3" ).arg( mEntityBaseUri ).arg( mMaximumPageSize ).arg( filterString );
|
||||
|
||||
if ( thisPage.isEmpty() && mCachedExtent.intersects( extentGeom ) )
|
||||
{
|
||||
@ -270,7 +283,7 @@ QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectang
|
||||
// This is slightly nicer from a rendering point of view, because panning the map won't see features
|
||||
// previously visible disappear temporarily while we wait for them to be included in the service's result set...
|
||||
nextPage = queryUrl;
|
||||
return qgis::listToSet( mSpatialIndex.intersects( extent ) );
|
||||
return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) );
|
||||
}
|
||||
|
||||
locker.unlock();
|
||||
|
||||
@ -84,6 +84,8 @@ class QgsSensorThingsSharedData
|
||||
QString mGeometryField;
|
||||
QgsFields mFields;
|
||||
|
||||
QgsRectangle mFilterExtent;
|
||||
|
||||
//! Extent calculated from features actually fetched so far
|
||||
QgsRectangle mFetchedFeatureExtent;
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include "qgsnetworkaccessmanager.h"
|
||||
#include "qgsblockingnetworkrequest.h"
|
||||
#include "qgslogger.h"
|
||||
#include "qgsrectangle.h"
|
||||
#include <QUrl>
|
||||
#include <QNetworkRequest>
|
||||
#include <nlohmann/json.hpp>
|
||||
@ -264,6 +265,30 @@ QString QgsSensorThingsUtils::filterForWkbType( Qgis::SensorThingsEntity entityT
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString QgsSensorThingsUtils::filterForExtent( const QString &geometryField, const QgsRectangle &extent )
|
||||
{
|
||||
// TODO -- confirm using 'geography' is always correct here
|
||||
return ( extent.isNull() || geometryField.isEmpty() )
|
||||
? QString()
|
||||
: QStringLiteral( "geo.intersects(%1, geography'%2')" ).arg( geometryField, extent.asWktPolygon() );
|
||||
}
|
||||
|
||||
QString QgsSensorThingsUtils::combineFilters( const QStringList &filters )
|
||||
{
|
||||
QStringList nonEmptyFilters;
|
||||
for ( const QString &filter : filters )
|
||||
{
|
||||
if ( !filter.isEmpty() )
|
||||
nonEmptyFilters.append( filter );
|
||||
}
|
||||
if ( nonEmptyFilters.empty() )
|
||||
return QString();
|
||||
if ( nonEmptyFilters.size() == 1 )
|
||||
return nonEmptyFilters.at( 0 );
|
||||
|
||||
return QStringLiteral( "(" ) + nonEmptyFilters.join( QStringLiteral( ") and (" ) ) + QStringLiteral( ")" );
|
||||
}
|
||||
|
||||
QList<Qgis::GeometryType> QgsSensorThingsUtils::availableGeometryTypes( const QString &uri, Qgis::SensorThingsEntity type, QgsFeedback *feedback, const QString &authCfg )
|
||||
{
|
||||
QNetworkRequest request = QNetworkRequest( QUrl( uri ) );
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
class QgsFields;
|
||||
class QgsFeedback;
|
||||
class QgsRectangle;
|
||||
|
||||
/**
|
||||
* \ingroup core
|
||||
@ -78,6 +79,25 @@ class CORE_EXPORT QgsSensorThingsUtils
|
||||
*/
|
||||
static QString filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType );
|
||||
|
||||
/**
|
||||
* Returns a filter string which restricts results to those within the specified
|
||||
* \a extent.
|
||||
*
|
||||
* The \a extent should always be specified in EPSG:4326.
|
||||
*
|
||||
* \since QGIS 3.38
|
||||
*/
|
||||
static QString filterForExtent( const QString &geometryField, const QgsRectangle &extent );
|
||||
|
||||
/**
|
||||
* Combines a set of SensorThings API filter operators.
|
||||
*
|
||||
* See https://docs.ogc.org/is/18-088/18-088.html#requirement-request-data-filter
|
||||
*
|
||||
* \since QGIS 3.38
|
||||
*/
|
||||
static QString combineFilters( const QStringList &filters );
|
||||
|
||||
/**
|
||||
* Returns a list of available geometry types for the server at the specified \a uri
|
||||
* and entity \a type.
|
||||
|
||||
@ -212,6 +212,12 @@ void QgsSensorThingsSourceSelect::addButtonClicked()
|
||||
emit addLayer( Qgis::LayerType::Vector, layerUri, baseName, QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY );
|
||||
}
|
||||
|
||||
void QgsSensorThingsSourceSelect::setMapCanvas( QgsMapCanvas *mapCanvas )
|
||||
{
|
||||
QgsAbstractDataSourceWidget::setMapCanvas( mapCanvas );
|
||||
mSourceWidget->setMapCanvas( mapCanvas );
|
||||
}
|
||||
|
||||
void QgsSensorThingsSourceSelect::populateConnectionList()
|
||||
{
|
||||
cmbConnections->blockSignals( true );
|
||||
|
||||
@ -33,9 +33,8 @@ class QgsSensorThingsSourceSelect : public QgsAbstractDataSourceWidget, private
|
||||
|
||||
public:
|
||||
QgsSensorThingsSourceSelect( QWidget *parent = nullptr, Qt::WindowFlags fl = QgsGuiUtils::ModalDialogFlags, QgsProviderRegistry::WidgetMode widgetMode = QgsProviderRegistry::WidgetMode::None );
|
||||
|
||||
//! Determines the layers the user selected
|
||||
void addButtonClicked() override;
|
||||
void setMapCanvas( QgsMapCanvas *mapCanvas ) override;
|
||||
|
||||
private slots:
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
#include "qgsiconutils.h"
|
||||
#include "qgssensorthingsconnectionpropertiestask.h"
|
||||
#include "qgsapplication.h"
|
||||
#include "qgsextentwidget.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QStandardItemModel>
|
||||
@ -33,6 +34,14 @@ QgsSensorThingsSourceWidget::QgsSensorThingsSourceWidget( QWidget *parent )
|
||||
{
|
||||
setupUi( this );
|
||||
|
||||
QVBoxLayout *vl = new QVBoxLayout();
|
||||
vl->setContentsMargins( 0, 0, 0, 0 );
|
||||
mExtentWidget = new QgsExtentWidget( nullptr, QgsExtentWidget::CondensedStyle );
|
||||
mExtentWidget->setNullValueAllowed( true, tr( "Not set" ) );
|
||||
mExtentWidget->setOutputCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
|
||||
vl->addWidget( mExtentWidget );
|
||||
mExtentLimitFrame->setLayout( vl );
|
||||
|
||||
mSpinPageSize->setClearValue( 0, tr( "Default (%1)" ).arg( QgsSensorThingsUtils::DEFAULT_PAGE_SIZE ) );
|
||||
|
||||
for ( Qgis::SensorThingsEntity type :
|
||||
@ -58,6 +67,7 @@ QgsSensorThingsSourceWidget::QgsSensorThingsSourceWidget( QWidget *parent )
|
||||
connect( mSpinPageSize, qOverload< int >( &QSpinBox::valueChanged ), this, &QgsSensorThingsSourceWidget::validate );
|
||||
connect( mRetrieveTypesButton, &QToolButton::clicked, this, &QgsSensorThingsSourceWidget::retrieveTypes );
|
||||
mRetrieveTypesButton->setEnabled( false );
|
||||
connect( mExtentWidget, &QgsExtentWidget::extentChanged, this, &QgsSensorThingsSourceWidget::validate );
|
||||
validate();
|
||||
}
|
||||
|
||||
@ -92,6 +102,17 @@ void QgsSensorThingsSourceWidget::setSourceUri( const QString &uri )
|
||||
mSpinPageSize->setValue( maxPageSizeParam );
|
||||
}
|
||||
|
||||
const QgsRectangle bounds = mSourceParts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >();
|
||||
if ( !bounds.isNull() )
|
||||
{
|
||||
mExtentWidget->setCurrentExtent( bounds, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
|
||||
mExtentWidget->setOutputExtentFromUser( bounds, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
mExtentWidget->clear();
|
||||
}
|
||||
|
||||
mIsValid = true;
|
||||
}
|
||||
|
||||
@ -108,6 +129,12 @@ QString QgsSensorThingsSourceWidget::groupTitle() const
|
||||
return QObject::tr( "SensorThings Configuration" );
|
||||
}
|
||||
|
||||
void QgsSensorThingsSourceWidget::setMapCanvas( QgsMapCanvas *mapCanvas )
|
||||
{
|
||||
QgsProviderSourceWidget::setMapCanvas( mapCanvas );
|
||||
mExtentWidget->setMapCanvas( mapCanvas, false );
|
||||
}
|
||||
|
||||
QString QgsSensorThingsSourceWidget::updateUriFromGui( const QString &connectionUri ) const
|
||||
{
|
||||
QVariantMap parts = QgsProviderRegistry::instance()->decodeUri(
|
||||
@ -152,6 +179,11 @@ QString QgsSensorThingsSourceWidget::updateUriFromGui( const QString &connection
|
||||
parts.remove( QStringLiteral( "pageSize" ) );
|
||||
}
|
||||
|
||||
if ( mExtentWidget->outputExtent().isNull() )
|
||||
parts.remove( QStringLiteral( "bounds" ) );
|
||||
else
|
||||
parts.insert( QStringLiteral( "bounds" ), QVariant::fromValue( mExtentWidget->outputExtent() ) );
|
||||
|
||||
return QgsProviderRegistry::instance()->encodeUri(
|
||||
QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY,
|
||||
parts
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
#include <QVariantMap>
|
||||
#include <QPointer>
|
||||
|
||||
class QgsFileWidget;
|
||||
class QgsExtentWidget;
|
||||
class QgsSensorThingsConnectionPropertiesTask;
|
||||
|
||||
///@cond PRIVATE
|
||||
@ -40,6 +40,7 @@ class QgsSensorThingsSourceWidget : public QgsProviderSourceWidget, protected Ui
|
||||
void setSourceUri( const QString &uri ) override;
|
||||
QString sourceUri() const override;
|
||||
QString groupTitle() const override;
|
||||
void setMapCanvas( QgsMapCanvas *mapCanvas ) override;
|
||||
|
||||
/**
|
||||
* Updates a connection uri with the layer specific URI settings defined in the widget.
|
||||
@ -59,6 +60,7 @@ class QgsSensorThingsSourceWidget : public QgsProviderSourceWidget, protected Ui
|
||||
void rebuildGeometryTypes( Qgis::SensorThingsEntity type );
|
||||
void setCurrentGeometryTypeFromString( const QString &geometryType );
|
||||
|
||||
QgsExtentWidget *mExtentWidget = nullptr;
|
||||
QVariantMap mSourceParts;
|
||||
bool mIsValid = false;
|
||||
QPointer< QgsSensorThingsConnectionPropertiesTask > mPropertiesTask;
|
||||
|
||||
@ -7,13 +7,13 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>537</width>
|
||||
<height>91</height>
|
||||
<height>134</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>QgsSensorThingsSourceWidgetBase</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<layout class="QGridLayout" name="gridLayout" columnstretch="1,2,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@ -40,6 +40,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="mComboGeometryType"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Extent limit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
@ -47,19 +57,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="mComboGeometryType"/>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="mComboEntityType"/>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QgsSpinBox" name="mSpinPageSize">
|
||||
<property name="maximum">
|
||||
<number>9999999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QToolButton" name="mRetrieveTypesButton">
|
||||
<property name="minimumSize">
|
||||
@ -87,6 +84,23 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="mComboEntityType"/>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QgsSpinBox" name="mSpinPageSize">
|
||||
<property name="maximum">
|
||||
<number>9999999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<widget class="QWidget" name="mExtentLimitFrame" native="true">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::StrongFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
@ -96,6 +110,13 @@
|
||||
<header>qgsspinbox.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>mComboEntityType</tabstop>
|
||||
<tabstop>mComboGeometryType</tabstop>
|
||||
<tabstop>mRetrieveTypesButton</tabstop>
|
||||
<tabstop>mSpinPageSize</tabstop>
|
||||
<tabstop>mExtentLimitFrame</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@ -170,6 +170,19 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase):
|
||||
Qgis.SensorThingsEntity.FeatureOfInterest,
|
||||
)
|
||||
|
||||
def test_filter_for_extent(self):
|
||||
self.assertFalse(QgsSensorThingsUtils.filterForExtent('', QgsRectangle()))
|
||||
self.assertFalse(QgsSensorThingsUtils.filterForExtent('test', QgsRectangle()))
|
||||
self.assertFalse(QgsSensorThingsUtils.filterForExtent('', QgsRectangle(1,2 ,3 ,4 )))
|
||||
self.assertEqual(QgsSensorThingsUtils.filterForExtent('test', QgsRectangle(1,2 ,3 ,4 )), "geo.intersects(test, geography'POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))')")
|
||||
|
||||
def test_combine_filters(self):
|
||||
self.assertFalse(QgsSensorThingsUtils.combineFilters([]))
|
||||
self.assertFalse(QgsSensorThingsUtils.combineFilters(['']))
|
||||
self.assertEqual(QgsSensorThingsUtils.combineFilters(['', 'a eq 1']), 'a eq 1')
|
||||
self.assertEqual(QgsSensorThingsUtils.combineFilters(['a eq 1', 'b eq 2']), '(a eq 1) and (b eq 2)')
|
||||
self.assertEqual(QgsSensorThingsUtils.combineFilters(['a eq 1', '', 'b eq 2', 'c eq 3']), '(a eq 1) and (b eq 2) and (c eq 3)')
|
||||
|
||||
def test_invalid_layer(self):
|
||||
vl = QgsVectorLayer(
|
||||
"url='http://fake.com/fake_qgis_http_endpoint'", "test", "sensorthings"
|
||||
@ -753,7 +766,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase):
|
||||
)
|
||||
|
||||
with open(
|
||||
sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))') and location/type eq 'Point'"),
|
||||
sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=(geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))')) and (location/type eq 'Point')"),
|
||||
"wt",
|
||||
encoding="utf8",
|
||||
) as f:
|
||||
@ -806,7 +819,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase):
|
||||
)
|
||||
|
||||
with open(
|
||||
sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=geo.intersects(location, geography'POLYGON((10 0, 20 0, 20 80, 10 80, 10 0))') and location/type eq 'Point'"),
|
||||
sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=(geo.intersects(location, geography'POLYGON((10 0, 20 0, 20 80, 10 80, 10 0))')) and (location/type eq 'Point')"),
|
||||
"wt",
|
||||
encoding="utf8",
|
||||
) as f:
|
||||
@ -927,6 +940,253 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase):
|
||||
["/Locations(1)", "/Locations(3)", "/Locations(2)"],
|
||||
)
|
||||
|
||||
def test_extent_limit(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_path = temp_dir.replace("\\", "/")
|
||||
endpoint = base_path + "/fake_qgis_http_endpoint"
|
||||
with open(sanitize(endpoint, ""), "wt", encoding="utf8") as f:
|
||||
f.write(
|
||||
"""
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"name": "Locations",
|
||||
"url": "endpoint/Locations"
|
||||
}
|
||||
],
|
||||
"serverSettings": {
|
||||
}
|
||||
}""".replace(
|
||||
"endpoint", "http://" + endpoint
|
||||
)
|
||||
)
|
||||
|
||||
with open(
|
||||
sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=(location/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))'))"),
|
||||
"wt",
|
||||
encoding="utf8",
|
||||
) as f:
|
||||
f.write("""{"@iot.count":2,"value":[]}""")
|
||||
|
||||
with open(
|
||||
sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))'))"),
|
||||
"wt",
|
||||
encoding="utf8",
|
||||
) as f:
|
||||
f.write(
|
||||
"""
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"@iot.selfLink": "endpoint/Locations(1)",
|
||||
"@iot.id": 1,
|
||||
"name": "Location 1",
|
||||
"description": "Desc 1",
|
||||
"encodingType": "application/geo+json",
|
||||
"location": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
1.623373,
|
||||
52.132017
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"owner": "owner 1"
|
||||
},
|
||||
"Things@iot.navigationLink": "endpoint/Locations(1)/Things",
|
||||
"HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations"
|
||||
},
|
||||
{
|
||||
"@iot.selfLink": "endpoint/Locations(3)",
|
||||
"@iot.id": 3,
|
||||
"name": "Location 3",
|
||||
"description": "Desc 3",
|
||||
"encodingType": "application/geo+json",
|
||||
"location": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
3.623373,
|
||||
55.132017
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"owner": "owner 3"
|
||||
},
|
||||
"Things@iot.navigationLink": "endpoint/Locations(3)/Things",
|
||||
"HistoricalLocations@iot.navigationLink": "endpoint/Locations(3)/HistoricalLocations"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".replace(
|
||||
"endpoint", "http://" + endpoint
|
||||
)
|
||||
)
|
||||
|
||||
with open(
|
||||
sanitize(endpoint,
|
||||
"/Locations?$top=2&$count=false&$filter=(geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))')) and (location/type eq 'Point')"),
|
||||
"wt",
|
||||
encoding="utf8",
|
||||
) as f:
|
||||
f.write(
|
||||
"""
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"@iot.selfLink": "endpoint/Locations(1)",
|
||||
"@iot.id": 1,
|
||||
"name": "Location 1",
|
||||
"description": "Desc 1",
|
||||
"encodingType": "application/geo+json",
|
||||
"location": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
1.623373,
|
||||
52.132017
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"owner": "owner 1"
|
||||
},
|
||||
"Things@iot.navigationLink": "endpoint/Locations(1)/Things",
|
||||
"HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations"
|
||||
},
|
||||
{
|
||||
"@iot.selfLink": "endpoint/Locations(3)",
|
||||
"@iot.id": 3,
|
||||
"name": "Location 3",
|
||||
"description": "Desc 3",
|
||||
"encodingType": "application/geo+json",
|
||||
"location": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
3.623373,
|
||||
55.132017
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"owner": "owner 3"
|
||||
},
|
||||
"Things@iot.navigationLink": "endpoint/Locations(3)/Things",
|
||||
"HistoricalLocations@iot.navigationLink": "endpoint/Locations(3)/HistoricalLocations"
|
||||
}
|
||||
]
|
||||
}""".replace(
|
||||
"endpoint", "http://" + endpoint
|
||||
)
|
||||
)
|
||||
|
||||
with open(
|
||||
sanitize(endpoint,
|
||||
"/Locations?$top=2&$count=false&$filter=(geo.intersects(location, geography'POLYGON((1 0, 3 0, 3 50, 1 50, 1 0))')) and (location/type eq 'Point')"),
|
||||
"wt",
|
||||
encoding="utf8",
|
||||
) as f:
|
||||
f.write(
|
||||
"""
|
||||
{
|
||||
"value": [
|
||||
{
|
||||
"@iot.selfLink": "endpoint/Locations(1)",
|
||||
"@iot.id": 1,
|
||||
"name": "Location 1",
|
||||
"description": "Desc 1",
|
||||
"encodingType": "application/geo+json",
|
||||
"location": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
1.623373,
|
||||
52.132017
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"owner": "owner 1"
|
||||
},
|
||||
"Things@iot.navigationLink": "endpoint/Locations(1)/Things",
|
||||
"HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations"
|
||||
}
|
||||
]
|
||||
}""".replace(
|
||||
"endpoint", "http://" + endpoint
|
||||
)
|
||||
)
|
||||
|
||||
vl = QgsVectorLayer(
|
||||
f"url='http://{endpoint}' bbox='1,0,10,80' type=PointZ pageSize=2 entity='Location'",
|
||||
"test",
|
||||
"sensorthings",
|
||||
)
|
||||
self.assertTrue(vl.isValid())
|
||||
self.assertEqual(vl.storageType(), "OGC SensorThings API")
|
||||
self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ)
|
||||
self.assertEqual(vl.featureCount(), 2)
|
||||
# should use the hardcoded extent limit as the initial guess, not global extents
|
||||
self.assertEqual(vl.extent(), QgsRectangle(1, 0, 10, 80))
|
||||
self.assertEqual(vl.crs().authid(), "EPSG:4326")
|
||||
self.assertIn("Entity Type</td><td>Location</td>",
|
||||
vl.htmlMetadata())
|
||||
self.assertIn(f'href="http://{endpoint}/Locations"',
|
||||
vl.htmlMetadata())
|
||||
|
||||
request = QgsFeatureRequest()
|
||||
request.setFilterRect(
|
||||
QgsRectangle(1, 0, 3, 50)
|
||||
)
|
||||
|
||||
features = list(vl.getFeatures(request))
|
||||
self.assertEqual([f["id"] for f in features], ["1"])
|
||||
self.assertEqual(
|
||||
[f["selfLink"][-13:] for f in features],
|
||||
["/Locations(1)"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[f["name"] for f in features],
|
||||
["Location 1"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[f["description"] for f in features],
|
||||
["Desc 1"]
|
||||
)
|
||||
self.assertEqual(
|
||||
[f["properties"] for f in features],
|
||||
[{"owner": "owner 1"}],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[f.geometry().asWkt(1) for f in features],
|
||||
["Point (1.6 52.1)"],
|
||||
)
|
||||
|
||||
request = QgsFeatureRequest()
|
||||
features = list(vl.getFeatures(request))
|
||||
self.assertEqual([f["id"] for f in features], ["1", "3"])
|
||||
self.assertEqual(
|
||||
[f["selfLink"][-13:] for f in features],
|
||||
["/Locations(1)", "/Locations(3)"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[f["name"] for f in features],
|
||||
["Location 1", "Location 3"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[f["description"] for f in features],
|
||||
["Desc 1", "Desc 3"]
|
||||
)
|
||||
self.assertEqual(
|
||||
[f["properties"] for f in features],
|
||||
[{"owner": "owner 1"},
|
||||
{"owner": "owner 3"}],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[f.geometry().asWkt(1) for f in features],
|
||||
["Point (1.6 52.1)",
|
||||
"Point (3.6 55.1)"],
|
||||
)
|
||||
|
||||
# should have accurate layer extent now
|
||||
self.assertEqual(vl.extent(), QgsRectangle(1.62337299999999995, 52.13201699999999761, 3.62337299999999995, 55.13201699999999761))
|
||||
|
||||
def test_historical_location(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_path = temp_dir.replace("\\", "/")
|
||||
@ -2093,6 +2353,19 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
uri = "url='https://sometest.com/api' bbox='1,2,3,4' type=MultiPolygonZ authcfg='abc' entity='Location'"
|
||||
parts = QgsProviderRegistry.instance().decodeUri("sensorthings", uri)
|
||||
self.assertEqual(
|
||||
parts,
|
||||
{
|
||||
"url": "https://sometest.com/api",
|
||||
"entity": "Location",
|
||||
"geometryType": "polygon",
|
||||
"authcfg": "abc",
|
||||
"bounds": QgsRectangle(1, 2, 3, 4)
|
||||
},
|
||||
)
|
||||
|
||||
def testEncodeUri(self):
|
||||
"""
|
||||
Test encoding a SensorThings uri
|
||||
@ -2146,6 +2419,19 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase):
|
||||
"authcfg=aaaaa type=MultiPolygonZ entity='Location' url='http://blah.com'",
|
||||
)
|
||||
|
||||
parts = {
|
||||
"url": "http://blah.com",
|
||||
"authcfg": "aaaaa",
|
||||
"entity": "location",
|
||||
"geometryType": "polygon",
|
||||
"bounds": QgsRectangle(1,2 ,3 ,4 )
|
||||
}
|
||||
uri = QgsProviderRegistry.instance().encodeUri("sensorthings", parts)
|
||||
self.assertEqual(
|
||||
uri,
|
||||
"authcfg=aaaaa type=MultiPolygonZ bbox='1,2,3,4' entity='Location' url='http://blah.com'",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user