Skip to content

Commit

Permalink
Merge pull request #5185 from nyalldawson/materialize
Browse files Browse the repository at this point in the history
QgsFeatureSource::materialize experiment
  • Loading branch information
nyalldawson authored Oct 12, 2017
2 parents 5f40181 + 9d8854f commit 0028486
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 17 deletions.
27 changes: 27 additions & 0 deletions python/core/qgsfeaturesource.sip
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
class QgsFeatureIterator;
class QgsCoordinateReferenceSystem;
class QgsFields;
class QgsFeedback;

/**
* \class QgsFeatureSource
Expand Down Expand Up @@ -124,6 +125,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
118 changes: 101 additions & 17 deletions tests/src/python/test_qgsfeaturesource.py
Original file line number Diff line number Diff line change
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,8 +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__':
Expand Down

0 comments on commit 0028486

Please sign in to comment.