Skip to content

Commit bcb3e5f

Browse files
committed
[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).
1 parent 7705179 commit bcb3e5f

File tree

4 files changed

+216
-17
lines changed

4 files changed

+216
-17
lines changed

python/core/qgsfeaturesource.sip

+27
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,33 @@ class QgsFeatureSource
120120
:rtype: QgsFeatureIds
121121
%End
122122

123+
QgsVectorLayer *materialize( const QgsFeatureRequest &request,
124+
QgsFeedback *feedback = 0 ) /Factory/;
125+
%Docstring
126+
Materializes a ``request`` (query) made against this feature source, by running
127+
it over the source and returning a new memory based vector layer containing
128+
the result. All settings from feature ``request`` will be honored.
129+
130+
If a subset of attributes has been set for the request, then only
131+
those selected fields will be present in the output layer.
132+
133+
The CRS for the output layer will match the input layer, unless
134+
QgsFeatureRequest.setDestinationCrs() has been called with a valid QgsCoordinateReferenceSystem.
135+
In this case the output layer will match the QgsFeatureRequest.destinationCrs() CRS.
136+
137+
The returned layer WKB type will match wkbType(), unless the QgsFeatureRequest.NoGeometry flag is set
138+
on the ``request``. In that case the returned layer will not be a spatial layer.
139+
140+
An optional ``feedback`` argument can be used to cancel the materialization
141+
before it has fully completed.
142+
143+
The returned value is a new instance and the caller takes responsibility
144+
for its ownership.
145+
146+
.. versionadded:: 3.0
147+
:rtype: QgsVectorLayer
148+
%End
149+
123150
};
124151

125152

src/core/qgsfeaturesource.cpp

+62
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
#include "qgsfeaturesource.h"
1919
#include "qgsfeaturerequest.h"
2020
#include "qgsfeatureiterator.h"
21+
#include "qgsmemoryproviderutils.h"
22+
#include "qgsfeedback.h"
23+
#include "qgsvectorlayer.h"
24+
#include "qgsvectordataprovider.h"
2125

2226
QSet<QVariant> QgsFeatureSource::uniqueValues( int fieldIndex, int limit ) const
2327
{
@@ -120,3 +124,61 @@ QgsFeatureIds QgsFeatureSource::allFeatureIds() const
120124
return ids;
121125
}
122126

127+
QgsVectorLayer *QgsFeatureSource::materialize( const QgsFeatureRequest &request, QgsFeedback *feedback )
128+
{
129+
QgsWkbTypes::Type outWkbType = request.flags() & QgsFeatureRequest::NoGeometry ? QgsWkbTypes::NoGeometry : wkbType();
130+
QgsCoordinateReferenceSystem crs = request.destinationCrs().isValid() ? request.destinationCrs() : sourceCrs();
131+
132+
QgsAttributeList requestedAttrs = request.subsetOfAttributes();
133+
134+
QgsFields outFields;
135+
if ( request.flags() & QgsFeatureRequest::SubsetOfAttributes )
136+
{
137+
int i = 0;
138+
const QgsFields sourceFields = fields();
139+
for ( const QgsField &field : sourceFields )
140+
{
141+
if ( requestedAttrs.contains( i ) )
142+
outFields.append( field );
143+
i++;
144+
}
145+
}
146+
else
147+
{
148+
outFields = fields();
149+
}
150+
151+
std::unique_ptr< QgsVectorLayer > layer( QgsMemoryProviderUtils::createMemoryLayer(
152+
sourceName(),
153+
outFields,
154+
outWkbType,
155+
crs ) );
156+
QgsFeature f;
157+
QgsFeatureIterator it = getFeatures( request );
158+
int fieldCount = fields().count();
159+
while ( it.nextFeature( f ) )
160+
{
161+
if ( feedback && feedback->isCanceled() )
162+
break;
163+
164+
if ( request.flags() & QgsFeatureRequest::SubsetOfAttributes )
165+
{
166+
// remove unused attributes
167+
QgsAttributes attrs;
168+
for ( int i = 0; i < fieldCount; ++i )
169+
{
170+
if ( requestedAttrs.contains( i ) )
171+
{
172+
attrs.append( f.attributes().at( i ) );
173+
}
174+
}
175+
176+
f.setAttributes( attrs );
177+
}
178+
179+
layer->dataProvider()->addFeature( f, QgsFeatureSink::FastInsert );
180+
}
181+
182+
return layer.release();
183+
}
184+

src/core/qgsfeaturesource.h

+27
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
class QgsFeatureIterator;
2626
class QgsCoordinateReferenceSystem;
2727
class QgsFields;
28+
class QgsFeedback;
2829

2930
/**
3031
* \class QgsFeatureSource
@@ -122,6 +123,32 @@ class CORE_EXPORT QgsFeatureSource
122123
*/
123124
virtual QgsFeatureIds allFeatureIds() const;
124125

126+
/**
127+
* Materializes a \a request (query) made against this feature source, by running
128+
* it over the source and returning a new memory based vector layer containing
129+
* the result. All settings from feature \a request will be honored.
130+
*
131+
* If a subset of attributes has been set for the request, then only
132+
* those selected fields will be present in the output layer.
133+
*
134+
* The CRS for the output layer will match the input layer, unless
135+
* QgsFeatureRequest::setDestinationCrs() has been called with a valid QgsCoordinateReferenceSystem.
136+
* In this case the output layer will match the QgsFeatureRequest::destinationCrs() CRS.
137+
*
138+
* The returned layer WKB type will match wkbType(), unless the QgsFeatureRequest::NoGeometry flag is set
139+
* on the \a request. In that case the returned layer will not be a spatial layer.
140+
*
141+
* An optional \a feedback argument can be used to cancel the materialization
142+
* before it has fully completed.
143+
*
144+
* The returned value is a new instance and the caller takes responsibility
145+
* for its ownership.
146+
*
147+
* \since QGIS 3.0
148+
*/
149+
QgsVectorLayer *materialize( const QgsFeatureRequest &request,
150+
QgsFeedback *feedback = nullptr ) SIP_FACTORY;
151+
125152
};
126153

127154
Q_DECLARE_METATYPE( QgsFeatureSource * )

tests/src/python/test_qgsfeaturesource.py

+100-17
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,33 @@
1818
from qgis.core import (QgsVectorLayer,
1919
QgsFeature,
2020
QgsGeometry,
21-
QgsPointXY)
21+
QgsPointXY,
22+
QgsFeatureRequest,
23+
QgsWkbTypes,
24+
QgsCoordinateReferenceSystem)
2225
from qgis.PyQt.QtCore import QVariant
2326
from qgis.testing import start_app, unittest
2427
start_app()
2528

2629

2730
def createLayerWithFivePoints():
28-
layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
31+
layer = QgsVectorLayer("Point?field=id:integer&field=fldtxt:string&field=fldint:integer",
2932
"addfeat", "memory")
3033
pr = layer.dataProvider()
3134
f = QgsFeature()
32-
f.setAttributes(["test", 1])
33-
f.setGeometry(QgsGeometry.fromPoint(QgsPointXY(100, 200)))
35+
f.setAttributes([1, "test", 1])
36+
f.setGeometry(QgsGeometry.fromPoint(QgsPointXY(1, 2)))
3437
f2 = QgsFeature()
35-
f2.setAttributes(["test2", 3])
36-
f2.setGeometry(QgsGeometry.fromPoint(QgsPointXY(200, 200)))
38+
f2.setAttributes([2, "test2", 3])
39+
f2.setGeometry(QgsGeometry.fromPoint(QgsPointXY(2, 2)))
3740
f3 = QgsFeature()
38-
f3.setAttributes(["test2", 3])
39-
f3.setGeometry(QgsGeometry.fromPoint(QgsPointXY(300, 200)))
41+
f3.setAttributes([3, "test2", 3])
42+
f3.setGeometry(QgsGeometry.fromPoint(QgsPointXY(3, 2)))
4043
f4 = QgsFeature()
41-
f4.setAttributes(["test3", 3])
42-
f4.setGeometry(QgsGeometry.fromPoint(QgsPointXY(400, 300)))
44+
f4.setAttributes([4, "test3", 3])
45+
f4.setGeometry(QgsGeometry.fromPoint(QgsPointXY(4, 3)))
4346
f5 = QgsFeature()
44-
f5.setAttributes(["test4", 4])
47+
f5.setAttributes([5, "test4", 4])
4548
f5.setGeometry(QgsGeometry.fromPoint(QgsPointXY(0, 0)))
4649
assert pr.addFeatures([f, f2, f3, f4, f5])
4750
assert layer.featureCount() == 5
@@ -59,8 +62,8 @@ def testUniqueValues(self):
5962
layer = createLayerWithFivePoints()
6063
self.assertFalse(layer.dataProvider().uniqueValues(-1))
6164
self.assertFalse(layer.dataProvider().uniqueValues(100))
62-
self.assertEqual(layer.dataProvider().uniqueValues(0), {'test', 'test2', 'test3', 'test4'})
63-
self.assertEqual(layer.dataProvider().uniqueValues(1), {1, 3, 3, 4})
65+
self.assertEqual(layer.dataProvider().uniqueValues(1), {'test', 'test2', 'test3', 'test4'})
66+
self.assertEqual(layer.dataProvider().uniqueValues(2), {1, 3, 3, 4})
6467

6568
def testMinValues(self):
6669
"""
@@ -71,8 +74,8 @@ def testMinValues(self):
7174
layer = createLayerWithFivePoints()
7275
self.assertFalse(layer.dataProvider().minimumValue(-1))
7376
self.assertFalse(layer.dataProvider().minimumValue(100))
74-
self.assertEqual(layer.dataProvider().minimumValue(0), 'test')
75-
self.assertEqual(layer.dataProvider().minimumValue(1), 1)
77+
self.assertEqual(layer.dataProvider().minimumValue(1), 'test')
78+
self.assertEqual(layer.dataProvider().minimumValue(2), 1)
7679

7780
def testMaxValues(self):
7881
"""
@@ -83,9 +86,89 @@ def testMaxValues(self):
8386
layer = createLayerWithFivePoints()
8487
self.assertFalse(layer.dataProvider().maximumValue(-1))
8588
self.assertFalse(layer.dataProvider().maximumValue(100))
86-
self.assertEqual(layer.dataProvider().maximumValue(0), 'test4')
87-
self.assertEqual(layer.dataProvider().maximumValue(1), 4)
89+
self.assertEqual(layer.dataProvider().maximumValue(1), 'test4')
90+
self.assertEqual(layer.dataProvider().maximumValue(2), 4)
8891

92+
def testMaterialize(self):
93+
"""
94+
Test materializing layers
95+
"""
96+
97+
layer = createLayerWithFivePoints()
98+
original_features = {f[0]: f for f in layer.getFeatures()}
99+
100+
# materialize all features, unchanged
101+
request = QgsFeatureRequest()
102+
new_layer = layer.materialize(request)
103+
self.assertEqual(new_layer.fields(), layer.fields())
104+
self.assertEqual(new_layer.crs(), layer.crs())
105+
self.assertEqual(new_layer.featureCount(), 5)
106+
self.assertEqual(new_layer.wkbType(), QgsWkbTypes.Point)
107+
new_features = {f[0]: f for f in new_layer.getFeatures()}
108+
for id, f in original_features.items():
109+
self.assertEqual(new_features[id].attributes(), f.attributes())
110+
self.assertEqual(new_features[id].geometry().exportToWkt(), f.geometry().exportToWkt())
111+
112+
# materialize with no geometry
113+
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)
114+
new_layer = layer.materialize(request)
115+
self.assertEqual(new_layer.fields(), layer.fields())
116+
self.assertEqual(new_layer.crs(), layer.crs())
117+
self.assertEqual(new_layer.featureCount(), 5)
118+
self.assertEqual(new_layer.wkbType(), QgsWkbTypes.NoGeometry)
119+
new_features = {f[0]: f for f in new_layer.getFeatures()}
120+
for id, f in original_features.items():
121+
self.assertEqual(new_features[id].attributes(), f.attributes())
122+
123+
# materialize with reprojection
124+
request = QgsFeatureRequest().setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3785'))
125+
new_layer = layer.materialize(request)
126+
self.assertEqual(new_layer.fields(), layer.fields())
127+
self.assertEqual(new_layer.crs().authid(), 'EPSG:3785')
128+
self.assertEqual(new_layer.featureCount(), 5)
129+
self.assertEqual(new_layer.wkbType(), QgsWkbTypes.Point)
130+
new_features = {f[0]: f for f in new_layer.getFeatures()}
131+
132+
expected_geometry = {1: 'Point (111319 222684)',
133+
2: 'Point (222639 222684)',
134+
3: 'Point (333958 222684)',
135+
4: 'Point (445278 334111)',
136+
5: 'Point (0 -0)'}
137+
for id, f in original_features.items():
138+
self.assertEqual(new_features[id].attributes(), f.attributes())
139+
self.assertEqual(new_features[id].geometry().exportToWkt(0), expected_geometry[id])
140+
141+
# materialize with attribute subset
142+
request = QgsFeatureRequest().setSubsetOfAttributes([0, 2])
143+
new_layer = layer.materialize(request)
144+
self.assertEqual(new_layer.fields().count(), 2)
145+
self.assertEqual(new_layer.fields().at(0), layer.fields().at(0))
146+
self.assertEqual(new_layer.fields().at(1), layer.fields().at(2))
147+
self.assertEqual(new_layer.crs(), layer.crs())
148+
self.assertEqual(new_layer.featureCount(), 5)
149+
self.assertEqual(new_layer.wkbType(), QgsWkbTypes.Point)
150+
new_features = {f.attributes()[0]: f for f in new_layer.getFeatures()}
151+
for id, f in original_features.items():
152+
self.assertEqual(new_features[id].attributes()[0], f.attributes()[0])
153+
self.assertEqual(new_features[id].attributes()[1], f.attributes()[2])
154+
155+
request = QgsFeatureRequest().setSubsetOfAttributes([0, 1])
156+
new_layer = layer.materialize(request)
157+
self.assertEqual(new_layer.fields().count(), 2)
158+
self.assertEqual(new_layer.fields().at(0), layer.fields().at(0))
159+
self.assertEqual(new_layer.fields().at(1), layer.fields().at(1))
160+
new_features = {f.attributes()[0]: f for f in new_layer.getFeatures()}
161+
for id, f in original_features.items():
162+
self.assertEqual(new_features[id].attributes()[0], f.attributes()[0])
163+
self.assertEqual(new_features[id].attributes()[1], f.attributes()[1])
164+
165+
request = QgsFeatureRequest().setSubsetOfAttributes([0])
166+
new_layer = layer.materialize(request)
167+
self.assertEqual(new_layer.fields().count(), 1)
168+
self.assertEqual(new_layer.fields().at(0), layer.fields().at(0))
169+
new_features = {f.attributes()[0]: f for f in new_layer.getFeatures()}
170+
for id, f in original_features.items():
171+
self.assertEqual(new_features[id].attributes()[0], f.attributes()[0])
89172

90173
if __name__ == '__main__':
91174
unittest.main()

0 commit comments

Comments
 (0)