Skip to content

Commit

Permalink
Port makeFeaturesCompatible to C++
Browse files Browse the repository at this point in the history
as: QgsVectorLayerUtils::makeFeaturesCompatible

With tests.
  • Loading branch information
elpaso committed Sep 25, 2018
1 parent 5173744 commit 930c3f8
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 94 deletions.
24 changes: 23 additions & 1 deletion python/core/auto_generated/qgsvectorlayerutils.sip.in
Expand Up @@ -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 );
Expand Down Expand Up @@ -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

};


Expand Down
84 changes: 3 additions & 81 deletions python/plugins/processing/gui/AlgorithmExecutor.py
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)])
Expand Down
94 changes: 93 additions & 1 deletion src/core/qgsvectorlayerutils.cpp
Expand Up @@ -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 )
{
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 23 additions & 1 deletion src/core/qgsvectorlayerutils.h
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );

};


Expand Down
21 changes: 11 additions & 10 deletions tests/src/python/test_qgsprocessinginplace.py
Expand Up @@ -17,10 +17,11 @@
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()

Expand Down Expand Up @@ -185,15 +186,15 @@ def _make_compatible_tester(self, feature_wkt, layer_wkb_name, attrs=[1]):
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())

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):
Expand Down Expand Up @@ -283,21 +284,21 @@ def test_make_features_compatible_attributes(self):
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())

# 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())
Expand All @@ -306,7 +307,7 @@ def test_make_features_compatible_attributes(self):
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')
Expand All @@ -322,15 +323,15 @@ def test_make_features_compatible_geometry(self):
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(), '')

# Make a geometry-less layer
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(), '')

Expand All @@ -339,7 +340,7 @@ def test_make_features_compatible_geometry(self):
'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(), '')

Expand Down

0 comments on commit 930c3f8

Please sign in to comment.