Skip to content
Permalink
Browse files

[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).
  • Loading branch information
nyalldawson committed Sep 26, 2017
1 parent 7705179 commit bcb3e5f42594612177d07097f29e7c5e70c16584
@@ -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

};


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

@@ -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 * )
@@ -18,30 +18,33 @@
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 @@ def testUniqueValues(self):
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 @@ def testMinValues(self):
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 @@ def testMaxValues(self):
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()

0 comments on commit bcb3e5f

Please sign in to comment.
You can’t perform that action at this time.