diff --git a/python/core/auto_generated/qgsvectorlayerutils.sip.in b/python/core/auto_generated/qgsvectorlayerutils.sip.in index 7b74e28d756..877bc291d26 100644 --- a/python/core/auto_generated/qgsvectorlayerutils.sip.in +++ b/python/core/auto_generated/qgsvectorlayerutils.sip.in @@ -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 + }; diff --git a/python/plugins/processing/gui/AlgorithmExecutor.py b/python/plugins/processing/gui/AlgorithmExecutor.py index 573b2163b5f..000f4efe9ca 100644 --- a/python/plugins/processing/gui/AlgorithmExecutor.py +++ b/python/plugins/processing/gui/AlgorithmExecutor.py @@ -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)]) diff --git a/src/core/qgsvectorlayerutils.cpp b/src/core/qgsvectorlayerutils.cpp index ae9f69bddfd..6de8e0f3252 100644 --- a/src/core/qgsvectorlayerutils.cpp +++ b/src/core/qgsvectorlayerutils.cpp @@ -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 QgsVectorLayerUtils::QgsDuplicateFeatureContext::layers() const { QList layers; diff --git a/src/core/qgsvectorlayerutils.h b/src/core/qgsvectorlayerutils.h index 2659a8a98a3..bada0dc31b5 100644 --- a/src/core/qgsvectorlayerutils.h +++ b/src/core/qgsvectorlayerutils.h @@ -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 ); + }; diff --git a/tests/src/python/test_qgsprocessinginplace.py b/tests/src/python/test_qgsprocessinginplace.py index 1d83ae7bae1..c83f20fa04d 100644 --- a/tests/src/python/test_qgsprocessinginplace.py +++ b/tests/src/python/test_qgsprocessinginplace.py @@ -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(), '')