mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-13 00:03:09 -04:00
[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:
parent
7705179a6c
commit
bcb3e5f425
@ -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 @@ 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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user