[FEATURE] Add 'materialize' method to QgsFeatureSource

When called, materialize takes a QgsFeatureRequest argument
and runs it over the source. The resultant features
are saved into a new memory provider based QgsVectorLayer, which
is returned by the function (along with ownership of the layer)

This makes it easy to create a new layer from a subset of an
existing one.

Materialize also considers subsets of attributes, so that the
returned layer only contains fetched fields (and not blank
fields filled with NULL values).
This commit is contained in:
Nyall Dawson 2017-09-13 14:48:54 +10:00
parent 7705179a6c
commit bcb3e5f425
4 changed files with 216 additions and 17 deletions

View File

@ -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
};

View File

@ -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<QVariant> 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();
}

View File

@ -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 * )

View File

@ -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()