Skip to content

Commit

Permalink
[FEATURE] Add 'materialize' method to QgsFeatureSource
Browse files Browse the repository at this point in the history
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 bcb3e5f
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 17 deletions.
27 changes: 27 additions & 0 deletions python/core/qgsfeaturesource.sip
Expand Up @@ -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

};


Expand Down
62 changes: 62 additions & 0 deletions src/core/qgsfeaturesource.cpp
Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
}

27 changes: 27 additions & 0 deletions src/core/qgsfeaturesource.h
Expand Up @@ -25,6 +25,7 @@
class QgsFeatureIterator;
class QgsCoordinateReferenceSystem;
class QgsFields;
class QgsFeedback;

/**
* \class QgsFeatureSource
Expand Down Expand Up @@ -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 * )
Expand Down
117 changes: 100 additions & 17 deletions tests/src/python/test_qgsfeaturesource.py
Expand Up @@ -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
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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.