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 07eb0d51b88..ae407673ac1 100644 --- a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,6 +55,12 @@ Returns the geometry field for a specified entity ``type``. static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); %Docstring Returns ``True`` if the specified entity ``type`` can have geometry attached. +%End + + static QString filterForWkbType( Qgis::WkbType type ); +%Docstring +Returns a filter string which restricts results to those matching the specified +WKB ``type``. %End }; diff --git a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index 07eb0d51b88..ae407673ac1 100644 --- a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,6 +55,12 @@ Returns the geometry field for a specified entity ``type``. static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); %Docstring Returns ``True`` if the specified entity ``type`` can have geometry attached. +%End + + static QString filterForWkbType( Qgis::WkbType type ); +%Docstring +Returns a filter string which restricts results to those matching the specified +WKB ``type``. %End }; diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index 74dbf6c1adc..0eda14b0e28 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -140,7 +140,12 @@ long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const mError.clear(); // return no features, just the total count - const QUrl url = parseUrl( QUrl( QStringLiteral( "%1?$top=0&$count=true" ).arg( mEntityBaseUri ) ) ); + QString countUri = QStringLiteral( "%1?$top=0&$count=true" ).arg( mEntityBaseUri ); + const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mGeometryType ); + if ( !typeFilter.isEmpty() ) + countUri += '&' + typeFilter; + + const QUrl url = parseUrl( QUrl( countUri ) ); QNetworkRequest request( url ); QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsSensorThingsSharedData" ) ); @@ -209,6 +214,9 @@ 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( mGeometryType ); + if ( !typeFilter.isEmpty() ) + mNextPage += '&' + typeFilter; } locker.unlock(); @@ -245,7 +253,8 @@ QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectang } // TODO -- is using 'geography' always correct here? - QString queryUrl = !thisPage.isEmpty() ? thisPage : QStringLiteral( "%1?$filter=geo.intersects(%2, geography'%3')&$top=%4&$count=false" ).arg( mEntityBaseUri, mGeometryField, extent.asWktPolygon() ).arg( mMaximumPageSize ); + const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mGeometryType ); + QString queryUrl = !thisPage.isEmpty() ? thisPage : QStringLiteral( "%1?$filter=geo.intersects(%2, geography'%3')&$top=%4&$count=false%5" ).arg( mEntityBaseUri, mGeometryField, extent.asWktPolygon() ).arg( mMaximumPageSize ).arg( typeFilter.isEmpty() ? QString() : ( QStringLiteral( "&" ) + typeFilter ) ); if ( thisPage.isEmpty() && mCachedExtent.intersects( extentGeom ) ) { diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.cpp b/src/core/providers/sensorthings/qgssensorthingsutils.cpp index 83a8fa38d65..9df6ea65d7b 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsutils.cpp @@ -16,6 +16,7 @@ #include "qgssensorthingsutils.h" #include "qgsfield.h" #include "qgsfields.h" +#include "qgswkbtypes.h" Qgis::SensorThingsEntity QgsSensorThingsUtils::stringToEntity( const QString &type ) { @@ -217,3 +218,21 @@ bool QgsSensorThingsUtils::entityTypeHasGeometry( Qgis::SensorThingsEntity type } BUILTIN_UNREACHABLE } + +QString QgsSensorThingsUtils::filterForWkbType( Qgis::WkbType type ) +{ + switch ( QgsWkbTypes::geometryType( type ) ) + { + case Qgis::GeometryType::Point: + return QStringLiteral( "$filter=location/type eq 'Point'" ); + case Qgis::GeometryType::Polygon: + return QStringLiteral( "$filter=location/type eq 'Polygon'" ); + case Qgis::GeometryType::Line: + // TODO -- confirm + return QStringLiteral( "$filter=location/type eq 'LineString'" ); + case Qgis::GeometryType::Unknown: + case Qgis::GeometryType::Null: + break; + } + return QString(); +} diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.h b/src/core/providers/sensorthings/qgssensorthingsutils.h index 1d142b118a2..81e89a08f8c 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.h +++ b/src/core/providers/sensorthings/qgssensorthingsutils.h @@ -68,6 +68,12 @@ class CORE_EXPORT QgsSensorThingsUtils */ static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); + /** + * Returns a filter string which restricts results to those matching the specified + * WKB \a type. + */ + static QString filterForWkbType( Qgis::WkbType type ); + }; #endif // QGSSENSORTHINGSUTILS_H diff --git a/tests/src/python/test_provider_sensorthings.py b/tests/src/python/test_provider_sensorthings.py index fb5046c0481..05c0c5ecb87 100644 --- a/tests/src/python/test_provider_sensorthings.py +++ b/tests/src/python/test_provider_sensorthings.py @@ -28,9 +28,13 @@ from qgis.testing import start_app, QgisTestCase def sanitize(endpoint, x): - if x.startswith("/Locations"): - x = x[len("/Locations"):] - endpoint = endpoint + "_Locations" + for prefix in ('/Locations', + '/HistoricalLocations', + '/Things', + '/FeaturesOfInterest'): + if x.startswith(prefix): + x = x[len(prefix):] + endpoint = endpoint + "_" + prefix[1:] if len(endpoint + x) > 150: ret = endpoint + hashlib.md5(x.encode()).hexdigest() @@ -74,6 +78,23 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) super().tearDownClass() + def test_filter_for_wkb_type(self): + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.WkbType.Point), "$filter=location/type eq 'Point'" + ) + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.WkbType.PointZ), "$filter=location/type eq 'Point'" + ) + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.WkbType.Polygon), "$filter=location/type eq 'Polygon'" + ) + # TODO -- there is NO documentation on what the type must be for line filtering, + # and I can't find any public servers with line geometries to test with! + # Find some way to confirm if this is 'Line' or 'LineString' or ... + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.WkbType.LineString), "$filter=location/type eq 'LineString'" + ) + def test_utils_string_to_entity(self): self.assertEqual( QgsSensorThingsUtils.stringToEntity("x"), Qgis.SensorThingsEntity.Invalid @@ -230,7 +251,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$top=0&$count=true"), + sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -449,14 +470,14 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$top=0&$count=true"), + sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Locations?$top=2&$count=false"), + sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -504,7 +525,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): } ], - "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2" + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'" } """.replace( "endpoint", "http://" + endpoint @@ -512,7 +533,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$top=2&$skip=2"), + sanitize(endpoint, "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -626,14 +647,14 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$top=0&$count=true"), + sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Locations?$top=2&$count=false"), + sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -681,7 +702,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): } ], - "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2" + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'" } """.replace( "endpoint", "http://" + endpoint @@ -689,7 +710,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$top=2&$skip=2"), + sanitize(endpoint, "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -724,7 +745,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$filter=geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))')&$top=2&$count=false"), + sanitize(endpoint, "/Locations?$filter=geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))')&$top=2&$count=false&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -777,7 +798,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/Locations?$filter=geo.intersects(location, geography'POLYGON((10 0, 20 0, 20 80, 10 80, 10 0))')&$top=2&$count=false"), + sanitize(endpoint, "/Locations?$filter=geo.intersects(location, geography'POLYGON((10 0, 20 0, 20 80, 10 80, 10 0))')&$top=2&$count=false&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -1841,14 +1862,14 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/FeaturesOfInterest?$top=0&$count=true"), + sanitize(endpoint, "/FeaturesOfInterest?$top=0&$count=true&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/FeaturesOfInterest?$top=2&$count=false"), + sanitize(endpoint, "/FeaturesOfInterest?$top=2&$count=false&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -1901,7 +1922,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): } ], - "@iot.nextLink": "endpoint/FeaturesOfInterest?$top=2&$skip=2" + "@iot.nextLink": "endpoint/FeaturesOfInterest?$top=2&$skip=2&$filter=location/type eq 'Point'" } """.replace( "endpoint", "http://" + endpoint @@ -1909,7 +1930,7 @@ class TestPyQgsSensorThingsProvider(QgisTestCase): # , ProviderTestCase): ) with open( - sanitize(endpoint, "/FeaturesOfInterest?$top=2&$skip=2"), + sanitize(endpoint, "/FeaturesOfInterest?$top=2&$skip=2&$filter=location/type eq 'Point'"), "wt", encoding="utf8", ) as f: