Port makeFeaturesCompatible to C++

as: QgsVectorLayerUtils::makeFeaturesCompatible

With tests.
This commit is contained in:
Alessandro Pasotti 2018-09-24 10:52:13 +02:00
parent 5173744818
commit 930c3f8e45
5 changed files with 153 additions and 94 deletions

View File

@ -133,7 +133,7 @@ Returns true if the attribute value is valid for the field. Any constraint failu
If the strength or origin parameter is set then only constraints with a matching strength/origin will be checked.
%End
static QgsFeature createFeature( QgsVectorLayer *layer,
static QgsFeature createFeature( const QgsVectorLayer *layer,
const QgsGeometry &geometry = QgsGeometry(),
const QgsAttributeMap &attributes = QgsAttributeMap(),
QgsExpressionContext *context = 0 );
@ -176,6 +176,28 @@ are padded with NULL values to match the required length).
.. versionadded:: 3.4
%End
static QgsFeatureList makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer );
%Docstring
Converts input ``features`` to be compatible with the given ``layer``.
This function returns a new list of transformed features compatible with the input
layer, note that the number of features returned might be greater than the number
of input featurers.
The following operations will be performed to convert the input features:
- convert single geometries to multi part
- drop additional attributes
- drop geometry if layer is geometry-less
- add missing attribute fields
- add back M/Z values (initialized to 0)
- drop Z/M
- convert multi part geometries to single part
.. versionadded:: 3.4
%End
};

View File

@ -70,85 +70,6 @@ def execute(alg, parameters, context=None, feedback=None):
return False, {}
def make_features_compatible(new_features, input_layer):
"""Try to make the new features compatible with old features by:
- converting single to multi part
- dropping additional attributes
- adding back M/Z values
- drop Z/M
- convert multi part to single part
:param new_features: new features
:type new_features: list of QgsFeatures
:param input_layer: input layer
:type input_layer: QgsVectorLayer
:return: modified features
:rtype: list of QgsFeatures
"""
input_wkb_type = input_layer.wkbType()
result_features = []
for new_f in new_features:
# Fix attributes
QgsVectorLayerUtils.matchAttributesToFields(new_f, input_layer.fields())
# Check if we need geometry manipulation
new_f_geom_type = QgsWkbTypes.geometryType(new_f.geometry().wkbType())
new_f_has_geom = new_f_geom_type not in (QgsWkbTypes.UnknownGeometry, QgsWkbTypes.NullGeometry)
input_layer_has_geom = input_wkb_type not in (QgsWkbTypes.NoGeometry, QgsWkbTypes.Unknown)
# Drop geometry if layer is geometry-less
if not input_layer_has_geom and new_f_has_geom:
f = QgsFeature(input_layer.fields())
f.setAttributes(new_f.attributes())
new_f = f
result_features.append(new_f)
continue # skip the rest
if input_layer_has_geom and new_f_has_geom and \
new_f.geometry().wkbType() != input_wkb_type: # Fix geometry
# Single -> Multi
if (QgsWkbTypes.isMultiType(input_wkb_type) and not
new_f.geometry().isMultipart()):
new_geom = new_f.geometry()
new_geom.convertToMultiType()
new_f.setGeometry(new_geom)
# Drop Z/M
if (new_f.geometry().constGet().is3D() and not QgsWkbTypes.hasZ(input_wkb_type)):
new_geom = new_f.geometry()
new_geom.get().dropZValue()
new_f.setGeometry(new_geom)
if (new_f.geometry().constGet().isMeasure() and not QgsWkbTypes.hasM(input_wkb_type)):
new_geom = new_f.geometry()
new_geom.get().dropMValue()
new_f.setGeometry(new_geom)
# Add Z/M back (set it to 0)
if (not new_f.geometry().constGet().is3D() and QgsWkbTypes.hasZ(input_wkb_type)):
new_geom = new_f.geometry()
new_geom.get().addZValue(0.0)
new_f.setGeometry(new_geom)
if (not new_f.geometry().constGet().isMeasure() and QgsWkbTypes.hasM(input_wkb_type)):
new_geom = new_f.geometry()
new_geom.get().addMValue(0.0)
new_f.setGeometry(new_geom)
# Multi -> Single
if (not QgsWkbTypes.isMultiType(input_wkb_type) and
new_f.geometry().isMultipart()):
g = new_f.geometry()
g2 = g.constGet()
for i in range(g2.partCount()):
# Clone or crash!
g4 = QgsGeometry(g2.geometryN(i).clone())
f = QgsVectorLayerUtils.createFeature(input_layer, g4, {i: new_f.attribute(i) for i in range(new_f.fields().count())})
result_features.append(f)
else:
result_features.append(new_f)
else:
result_features.append(new_f)
return result_features
def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=None, raise_exceptions=False):
"""Executes an algorithm modifying features in-place in the input layer.
@ -208,7 +129,7 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
# a shallow copy from processFeature
input_feature = QgsFeature(f)
new_features = alg.processFeature(input_feature, context, feedback)
new_features = make_features_compatible(new_features, active_layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible(new_features, active_layer)
if len(new_features) == 0:
active_layer.deleteFeature(f.id())
elif len(new_features) == 1:
@ -238,7 +159,8 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
new_features = []
for f in result_layer.getFeatures():
new_features.extend(make_features_compatible([f], active_layer))
new_features.extend(QgsVectorLayerUtils.
makeFeaturesCompatible([f], active_layer))
# Get the new ids
old_ids = set([f.id() for f in active_layer.getFeatures(req)])

View File

@ -25,6 +25,7 @@
#include "qgsfeedback.h"
#include "qgsvectorlayer.h"
#include "qgsthreadingutils.h"
#include "qgsgeometrycollection.h"
QgsFeatureIterator QgsVectorLayerUtils::getValuesIterator( const QgsVectorLayer *layer, const QString &fieldOrExpression, bool &ok, bool selectedOnly )
{
@ -348,7 +349,7 @@ bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const
return valid;
}
QgsFeature QgsVectorLayerUtils::createFeature( QgsVectorLayer *layer, const QgsGeometry &geometry,
QgsFeature QgsVectorLayerUtils::createFeature( const QgsVectorLayer *layer, const QgsGeometry &geometry,
const QgsAttributeMap &attributes, QgsExpressionContext *context )
{
if ( !layer )
@ -560,6 +561,97 @@ void QgsVectorLayerUtils::matchAttributesToFields( QgsFeature &feature, const Qg
}
}
QgsFeatureList QgsVectorLayerUtils::makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer )
{
QgsWkbTypes::Type inputWkbType( layer.wkbType( ) );
QgsFeatureList resultFeatures;
for ( const QgsFeature &f : features )
{
QgsFeature newF( f );
// Fix attributes
QgsVectorLayerUtils::matchAttributesToFields( newF, layer.fields( ) );
// Does geometry need tranformations?
QgsWkbTypes::GeometryType newFGeomType( QgsWkbTypes::geometryType( newF.geometry().wkbType() ) );
bool newFHasGeom = newFGeomType !=
QgsWkbTypes::GeometryType::UnknownGeometry &&
newFGeomType != QgsWkbTypes::GeometryType::NullGeometry;
bool layerHasGeom = inputWkbType !=
QgsWkbTypes::Type::NoGeometry &&
inputWkbType != QgsWkbTypes::Type::Unknown;
// Drop geometry if layer is geometry-less
if ( newFHasGeom && ! layerHasGeom )
{
QgsFeature _f = QgsFeature( layer.fields() );
_f.setAttributes( newF.attributes() );
resultFeatures.append( _f );
continue; // Skip the rest
}
// Geometry need fixing
if ( newFHasGeom && layerHasGeom && newF.geometry().wkbType() != inputWkbType )
{
// Single -> multi
if ( QgsWkbTypes::isMultiType( inputWkbType ) && ! newF.geometry().isMultipart( ) )
{
QgsGeometry newGeom( newF.geometry( ) );
newGeom.convertToMultiType();
newF.setGeometry( newGeom );
}
// Drop Z/M
if ( newF.geometry().constGet()->is3D() && ! QgsWkbTypes::hasZ( inputWkbType ) )
{
QgsGeometry newGeom( newF.geometry( ) );
newGeom.get()->dropZValue();
newF.setGeometry( newGeom );
}
if ( newF.geometry().constGet()->isMeasure() && ! QgsWkbTypes::hasM( inputWkbType ) )
{
QgsGeometry newGeom( newF.geometry( ) );
newGeom.get()->dropMValue();
newF.setGeometry( newGeom );
}
// Add Z/M back, set to 0
if ( ! newF.geometry().constGet()->is3D() && QgsWkbTypes::hasZ( inputWkbType ) )
{
QgsGeometry newGeom( newF.geometry( ) );
newGeom.get()->addZValue( 0.0 );
newF.setGeometry( newGeom );
}
if ( ! newF.geometry().constGet()->isMeasure() && QgsWkbTypes::hasM( inputWkbType ) )
{
QgsGeometry newGeom( newF.geometry( ) );
newGeom.get()->addMValue( 0.0 );
newF.setGeometry( newGeom );
}
// Multi -> single
if ( ! QgsWkbTypes::isMultiType( inputWkbType ) && newF.geometry().isMultipart( ) )
{
QgsGeometry newGeom( newF.geometry( ) );
const QgsGeometryCollection *parts( static_cast< const QgsGeometryCollection * >( newGeom.constGet() ) );
for ( int i = 0; i < parts->partCount( ); i++ )
{
QgsGeometry g( parts->geometryN( i )->clone() );
QgsAttributeMap attrMap;
for ( int j = 0; j < newF.fields().count(); j++ )
{
attrMap[j] = newF.attribute( j );
}
QgsFeature _f( QgsVectorLayerUtils::createFeature( &layer, g, attrMap ) );
resultFeatures.append( _f );
}
}
else
{
resultFeatures.append( newF );
}
}
else
{
resultFeatures.append( newF );
}
}
return resultFeatures;
}
QList<QgsVectorLayer *> QgsVectorLayerUtils::QgsDuplicateFeatureContext::layers() const
{
QList<QgsVectorLayer *> layers;

View File

@ -140,7 +140,7 @@ class CORE_EXPORT QgsVectorLayerUtils
* assuming that they respect the layer's constraints. Note that the created feature is not
* automatically inserted into the layer.
*/
static QgsFeature createFeature( QgsVectorLayer *layer,
static QgsFeature createFeature( const QgsVectorLayer *layer,
const QgsGeometry &geometry = QgsGeometry(),
const QgsAttributeMap &attributes = QgsAttributeMap(),
QgsExpressionContext *context = nullptr );
@ -187,6 +187,28 @@ class CORE_EXPORT QgsVectorLayerUtils
* \since QGIS 3.4
*/
static void matchAttributesToFields( QgsFeature &feature, const QgsFields &fields );
/**
* Converts input \a features to be compatible with the given \a layer.
*
* This function returns a new list of transformed features compatible with the input
* layer, note that the number of features returned might be greater than the number
* of input featurers.
*
* The following operations will be performed to convert the input features:
* - convert single geometries to multi part
* - drop additional attributes
* - drop geometry if layer is geometry-less
* - add missing attribute fields
* - add back M/Z values (initialized to 0)
* - drop Z/M
* - convert multi part geometries to single part
*
* \since QGIS 3.4
*/
static QgsFeatureList makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer );
};

View File

@ -17,10 +17,11 @@ from qgis.core import (
QgsFeature, QgsGeometry, QgsSettings, QgsApplication, QgsMemoryProviderUtils, QgsWkbTypes, QgsField, QgsFields, QgsProcessingFeatureSourceDefinition, QgsProcessingContext, QgsProcessingFeedback, QgsCoordinateReferenceSystem, QgsProject, QgsProcessingException
)
from processing.core.Processing import Processing
from processing.gui.AlgorithmExecutor import execute_in_place_run, make_features_compatible
from processing.gui.AlgorithmExecutor import execute_in_place_run
from qgis.testing import start_app, unittest
from qgis.PyQt.QtTest import QSignalSpy
from qgis.analysis import QgsNativeAlgorithms
from qgis.core import QgsVectorLayerUtils
start_app()
@ -185,7 +186,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
context.setProject(QgsProject.instance())
# Fix it!
new_features = make_features_compatible([f], layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f], layer)
for new_f in new_features:
self.assertEqual(new_f.geometry().wkbType(), layer.wkbType())
@ -193,7 +194,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
self.assertTrue(layer.addFeatures(new_features), "Fail: %s - %s - %s" % (feature_wkt, attrs, layer_wkb_name))
return layer, new_features
def test_make_features_compatible(self):
def test_QgsVectorLayerUtilsmakeFeaturesCompatible(self):
"""Test fixer function"""
# Test failure
with self.assertRaises(AssertionError):
@ -283,13 +284,13 @@ class TestQgsProcessingInPlace(unittest.TestCase):
f1['int_f'] = 1
f1['str_f'] = 'str'
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
new_features = make_features_compatible([f1], layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
self.assertEqual(new_features[0].attributes(), f1.attributes())
self.assertTrue(new_features[0].geometry().asWkt(), f1.geometry().asWkt())
# Test pad with 0 with fields
f1.setAttributes([])
new_features = make_features_compatible([f1], layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
self.assertEqual(len(new_features[0].attributes()), 2)
self.assertEqual(new_features[0].attributes()[0], QVariant())
self.assertEqual(new_features[0].attributes()[1], QVariant())
@ -297,7 +298,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
# Test pad with 0 without fields
f1 = QgsFeature()
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
new_features = make_features_compatible([f1], layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
self.assertEqual(len(new_features[0].attributes()), 2)
self.assertEqual(new_features[0].attributes()[0], QVariant())
self.assertEqual(new_features[0].attributes()[1], QVariant())
@ -306,7 +307,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
f1 = QgsFeature(layer.fields())
f1.setAttributes([1, 'foo', 'extra'])
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
new_features = make_features_compatible([f1], layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
self.assertEqual(len(new_features[0].attributes()), 2)
self.assertEqual(new_features[0].attributes()[0], 1)
self.assertEqual(new_features[0].attributes()[1], 'foo')
@ -322,7 +323,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
f1.setAttributes([1])
# Check that it is accepted on a Point layer
new_features = make_features_compatible([f1], layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
self.assertEqual(len(new_features), 1)
self.assertEqual(new_features[0].geometry().asWkt(), '')
@ -330,7 +331,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
nogeom_layer = QgsMemoryProviderUtils.createMemoryLayer(
'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326))
# Check that a geometry-less feature is accepted
new_features = make_features_compatible([f1], nogeom_layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer)
self.assertEqual(len(new_features), 1)
self.assertEqual(new_features[0].geometry().asWkt(), '')
@ -339,7 +340,7 @@ class TestQgsProcessingInPlace(unittest.TestCase):
'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326))
# Check that a Point feature is accepted but geometry was dropped
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
new_features = make_features_compatible([f1], nogeom_layer)
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer)
self.assertEqual(len(new_features), 1)
self.assertEqual(new_features[0].geometry().asWkt(), '')