diff --git a/python/core/qgsfeaturesource.sip b/python/core/qgsfeaturesource.sip index 24f585ca987..57d8701c02d 100644 --- a/python/core/qgsfeaturesource.sip +++ b/python/core/qgsfeaturesource.sip @@ -120,6 +120,33 @@ class QgsFeatureSource :rtype: QgsFeatureIds %End + QgsVectorLayer *materialize( const QgsFeatureRequest &request, + QgsFeedback *feedback = 0 ) /Factory/; +%Docstring + Materializes a ``request`` (query) made against this feature source, by running + it over the source and returning a new memory based vector layer containing + the result. All settings from feature ``request`` will be honored. + + If a subset of attributes has been set for the request, then only + those selected fields will be present in the output layer. + + The CRS for the output layer will match the input layer, unless + QgsFeatureRequest.setDestinationCrs() has been called with a valid QgsCoordinateReferenceSystem. + In this case the output layer will match the QgsFeatureRequest.destinationCrs() CRS. + + The returned layer WKB type will match wkbType(), unless the QgsFeatureRequest.NoGeometry flag is set + on the ``request``. In that case the returned layer will not be a spatial layer. + + An optional ``feedback`` argument can be used to cancel the materialization + before it has fully completed. + + The returned value is a new instance and the caller takes responsibility + for its ownership. + +.. versionadded:: 3.0 + :rtype: QgsVectorLayer +%End + }; diff --git a/src/core/qgsfeaturesource.cpp b/src/core/qgsfeaturesource.cpp index 71df55850c4..86be5ed232b 100644 --- a/src/core/qgsfeaturesource.cpp +++ b/src/core/qgsfeaturesource.cpp @@ -18,6 +18,10 @@ #include "qgsfeaturesource.h" #include "qgsfeaturerequest.h" #include "qgsfeatureiterator.h" +#include "qgsmemoryproviderutils.h" +#include "qgsfeedback.h" +#include "qgsvectorlayer.h" +#include "qgsvectordataprovider.h" QSet QgsFeatureSource::uniqueValues( int fieldIndex, int limit ) const { @@ -120,3 +124,61 @@ QgsFeatureIds QgsFeatureSource::allFeatureIds() const return ids; } +QgsVectorLayer *QgsFeatureSource::materialize( const QgsFeatureRequest &request, QgsFeedback *feedback ) +{ + QgsWkbTypes::Type outWkbType = request.flags() & QgsFeatureRequest::NoGeometry ? QgsWkbTypes::NoGeometry : wkbType(); + QgsCoordinateReferenceSystem crs = request.destinationCrs().isValid() ? request.destinationCrs() : sourceCrs(); + + QgsAttributeList requestedAttrs = request.subsetOfAttributes(); + + QgsFields outFields; + if ( request.flags() & QgsFeatureRequest::SubsetOfAttributes ) + { + int i = 0; + const QgsFields sourceFields = fields(); + for ( const QgsField &field : sourceFields ) + { + if ( requestedAttrs.contains( i ) ) + outFields.append( field ); + i++; + } + } + else + { + outFields = fields(); + } + + std::unique_ptr< QgsVectorLayer > layer( QgsMemoryProviderUtils::createMemoryLayer( + sourceName(), + outFields, + outWkbType, + crs ) ); + QgsFeature f; + QgsFeatureIterator it = getFeatures( request ); + int fieldCount = fields().count(); + while ( it.nextFeature( f ) ) + { + if ( feedback && feedback->isCanceled() ) + break; + + if ( request.flags() & QgsFeatureRequest::SubsetOfAttributes ) + { + // remove unused attributes + QgsAttributes attrs; + for ( int i = 0; i < fieldCount; ++i ) + { + if ( requestedAttrs.contains( i ) ) + { + attrs.append( f.attributes().at( i ) ); + } + } + + f.setAttributes( attrs ); + } + + layer->dataProvider()->addFeature( f, QgsFeatureSink::FastInsert ); + } + + return layer.release(); +} + diff --git a/src/core/qgsfeaturesource.h b/src/core/qgsfeaturesource.h index da617830621..567a33c82c3 100644 --- a/src/core/qgsfeaturesource.h +++ b/src/core/qgsfeaturesource.h @@ -25,6 +25,7 @@ class QgsFeatureIterator; class QgsCoordinateReferenceSystem; class QgsFields; +class QgsFeedback; /** * \class QgsFeatureSource @@ -122,6 +123,32 @@ class CORE_EXPORT QgsFeatureSource */ virtual QgsFeatureIds allFeatureIds() const; + /** + * Materializes a \a request (query) made against this feature source, by running + * it over the source and returning a new memory based vector layer containing + * the result. All settings from feature \a request will be honored. + * + * If a subset of attributes has been set for the request, then only + * those selected fields will be present in the output layer. + * + * The CRS for the output layer will match the input layer, unless + * QgsFeatureRequest::setDestinationCrs() has been called with a valid QgsCoordinateReferenceSystem. + * In this case the output layer will match the QgsFeatureRequest::destinationCrs() CRS. + * + * The returned layer WKB type will match wkbType(), unless the QgsFeatureRequest::NoGeometry flag is set + * on the \a request. In that case the returned layer will not be a spatial layer. + * + * An optional \a feedback argument can be used to cancel the materialization + * before it has fully completed. + * + * The returned value is a new instance and the caller takes responsibility + * for its ownership. + * + * \since QGIS 3.0 + */ + QgsVectorLayer *materialize( const QgsFeatureRequest &request, + QgsFeedback *feedback = nullptr ) SIP_FACTORY; + }; Q_DECLARE_METATYPE( QgsFeatureSource * ) diff --git a/tests/src/python/test_qgsfeaturesource.py b/tests/src/python/test_qgsfeaturesource.py index 61399601b6a..2f1c53a81b9 100644 --- a/tests/src/python/test_qgsfeaturesource.py +++ b/tests/src/python/test_qgsfeaturesource.py @@ -18,30 +18,33 @@ import os from qgis.core import (QgsVectorLayer, QgsFeature, QgsGeometry, - QgsPointXY) + QgsPointXY, + QgsFeatureRequest, + QgsWkbTypes, + QgsCoordinateReferenceSystem) from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app, unittest start_app() def createLayerWithFivePoints(): - layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", + layer = QgsVectorLayer("Point?field=id:integer&field=fldtxt:string&field=fldint:integer", "addfeat", "memory") pr = layer.dataProvider() f = QgsFeature() - f.setAttributes(["test", 1]) - f.setGeometry(QgsGeometry.fromPoint(QgsPointXY(100, 200))) + f.setAttributes([1, "test", 1]) + f.setGeometry(QgsGeometry.fromPoint(QgsPointXY(1, 2))) f2 = QgsFeature() - f2.setAttributes(["test2", 3]) - f2.setGeometry(QgsGeometry.fromPoint(QgsPointXY(200, 200))) + f2.setAttributes([2, "test2", 3]) + f2.setGeometry(QgsGeometry.fromPoint(QgsPointXY(2, 2))) f3 = QgsFeature() - f3.setAttributes(["test2", 3]) - f3.setGeometry(QgsGeometry.fromPoint(QgsPointXY(300, 200))) + f3.setAttributes([3, "test2", 3]) + f3.setGeometry(QgsGeometry.fromPoint(QgsPointXY(3, 2))) f4 = QgsFeature() - f4.setAttributes(["test3", 3]) - f4.setGeometry(QgsGeometry.fromPoint(QgsPointXY(400, 300))) + f4.setAttributes([4, "test3", 3]) + f4.setGeometry(QgsGeometry.fromPoint(QgsPointXY(4, 3))) f5 = QgsFeature() - f5.setAttributes(["test4", 4]) + f5.setAttributes([5, "test4", 4]) f5.setGeometry(QgsGeometry.fromPoint(QgsPointXY(0, 0))) assert pr.addFeatures([f, f2, f3, f4, f5]) assert layer.featureCount() == 5 @@ -59,8 +62,8 @@ class TestQgsFeatureSource(unittest.TestCase): layer = createLayerWithFivePoints() self.assertFalse(layer.dataProvider().uniqueValues(-1)) self.assertFalse(layer.dataProvider().uniqueValues(100)) - self.assertEqual(layer.dataProvider().uniqueValues(0), {'test', 'test2', 'test3', 'test4'}) - self.assertEqual(layer.dataProvider().uniqueValues(1), {1, 3, 3, 4}) + self.assertEqual(layer.dataProvider().uniqueValues(1), {'test', 'test2', 'test3', 'test4'}) + self.assertEqual(layer.dataProvider().uniqueValues(2), {1, 3, 3, 4}) def testMinValues(self): """ @@ -71,8 +74,8 @@ class TestQgsFeatureSource(unittest.TestCase): layer = createLayerWithFivePoints() self.assertFalse(layer.dataProvider().minimumValue(-1)) self.assertFalse(layer.dataProvider().minimumValue(100)) - self.assertEqual(layer.dataProvider().minimumValue(0), 'test') - self.assertEqual(layer.dataProvider().minimumValue(1), 1) + self.assertEqual(layer.dataProvider().minimumValue(1), 'test') + self.assertEqual(layer.dataProvider().minimumValue(2), 1) def testMaxValues(self): """ @@ -83,9 +86,89 @@ class TestQgsFeatureSource(unittest.TestCase): layer = createLayerWithFivePoints() self.assertFalse(layer.dataProvider().maximumValue(-1)) self.assertFalse(layer.dataProvider().maximumValue(100)) - self.assertEqual(layer.dataProvider().maximumValue(0), 'test4') - self.assertEqual(layer.dataProvider().maximumValue(1), 4) + self.assertEqual(layer.dataProvider().maximumValue(1), 'test4') + self.assertEqual(layer.dataProvider().maximumValue(2), 4) + def testMaterialize(self): + """ + Test materializing layers + """ + + layer = createLayerWithFivePoints() + original_features = {f[0]: f for f in layer.getFeatures()} + + # materialize all features, unchanged + request = QgsFeatureRequest() + new_layer = layer.materialize(request) + self.assertEqual(new_layer.fields(), layer.fields()) + self.assertEqual(new_layer.crs(), layer.crs()) + self.assertEqual(new_layer.featureCount(), 5) + self.assertEqual(new_layer.wkbType(), QgsWkbTypes.Point) + new_features = {f[0]: f for f in new_layer.getFeatures()} + for id, f in original_features.items(): + self.assertEqual(new_features[id].attributes(), f.attributes()) + self.assertEqual(new_features[id].geometry().exportToWkt(), f.geometry().exportToWkt()) + + # materialize with no geometry + request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry) + new_layer = layer.materialize(request) + self.assertEqual(new_layer.fields(), layer.fields()) + self.assertEqual(new_layer.crs(), layer.crs()) + self.assertEqual(new_layer.featureCount(), 5) + self.assertEqual(new_layer.wkbType(), QgsWkbTypes.NoGeometry) + new_features = {f[0]: f for f in new_layer.getFeatures()} + for id, f in original_features.items(): + self.assertEqual(new_features[id].attributes(), f.attributes()) + + # materialize with reprojection + request = QgsFeatureRequest().setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3785')) + new_layer = layer.materialize(request) + self.assertEqual(new_layer.fields(), layer.fields()) + self.assertEqual(new_layer.crs().authid(), 'EPSG:3785') + self.assertEqual(new_layer.featureCount(), 5) + self.assertEqual(new_layer.wkbType(), QgsWkbTypes.Point) + new_features = {f[0]: f for f in new_layer.getFeatures()} + + expected_geometry = {1: 'Point (111319 222684)', + 2: 'Point (222639 222684)', + 3: 'Point (333958 222684)', + 4: 'Point (445278 334111)', + 5: 'Point (0 -0)'} + for id, f in original_features.items(): + self.assertEqual(new_features[id].attributes(), f.attributes()) + self.assertEqual(new_features[id].geometry().exportToWkt(0), expected_geometry[id]) + + # materialize with attribute subset + request = QgsFeatureRequest().setSubsetOfAttributes([0, 2]) + new_layer = layer.materialize(request) + self.assertEqual(new_layer.fields().count(), 2) + self.assertEqual(new_layer.fields().at(0), layer.fields().at(0)) + self.assertEqual(new_layer.fields().at(1), layer.fields().at(2)) + self.assertEqual(new_layer.crs(), layer.crs()) + self.assertEqual(new_layer.featureCount(), 5) + self.assertEqual(new_layer.wkbType(), QgsWkbTypes.Point) + new_features = {f.attributes()[0]: f for f in new_layer.getFeatures()} + for id, f in original_features.items(): + self.assertEqual(new_features[id].attributes()[0], f.attributes()[0]) + self.assertEqual(new_features[id].attributes()[1], f.attributes()[2]) + + request = QgsFeatureRequest().setSubsetOfAttributes([0, 1]) + new_layer = layer.materialize(request) + self.assertEqual(new_layer.fields().count(), 2) + self.assertEqual(new_layer.fields().at(0), layer.fields().at(0)) + self.assertEqual(new_layer.fields().at(1), layer.fields().at(1)) + new_features = {f.attributes()[0]: f for f in new_layer.getFeatures()} + for id, f in original_features.items(): + self.assertEqual(new_features[id].attributes()[0], f.attributes()[0]) + self.assertEqual(new_features[id].attributes()[1], f.attributes()[1]) + + request = QgsFeatureRequest().setSubsetOfAttributes([0]) + new_layer = layer.materialize(request) + self.assertEqual(new_layer.fields().count(), 1) + self.assertEqual(new_layer.fields().at(0), layer.fields().at(0)) + new_features = {f.attributes()[0]: f for f in new_layer.getFeatures()} + for id, f in original_features.items(): + self.assertEqual(new_features[id].attributes()[0], f.attributes()[0]) if __name__ == '__main__': unittest.main()