Skip to content

Commit 930c3f8

Browse files
committed
Port makeFeaturesCompatible to C++
as: QgsVectorLayerUtils::makeFeaturesCompatible With tests.
1 parent 5173744 commit 930c3f8

File tree

5 files changed

+153
-94
lines changed

5 files changed

+153
-94
lines changed

python/core/auto_generated/qgsvectorlayerutils.sip.in

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Returns true if the attribute value is valid for the field. Any constraint failu
133133
If the strength or origin parameter is set then only constraints with a matching strength/origin will be checked.
134134
%End
135135

136-
static QgsFeature createFeature( QgsVectorLayer *layer,
136+
static QgsFeature createFeature( const QgsVectorLayer *layer,
137137
const QgsGeometry &geometry = QgsGeometry(),
138138
const QgsAttributeMap &attributes = QgsAttributeMap(),
139139
QgsExpressionContext *context = 0 );
@@ -176,6 +176,28 @@ are padded with NULL values to match the required length).
176176

177177
.. versionadded:: 3.4
178178
%End
179+
180+
181+
static QgsFeatureList makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer );
182+
%Docstring
183+
Converts input ``features`` to be compatible with the given ``layer``.
184+
185+
This function returns a new list of transformed features compatible with the input
186+
layer, note that the number of features returned might be greater than the number
187+
of input featurers.
188+
189+
The following operations will be performed to convert the input features:
190+
- convert single geometries to multi part
191+
- drop additional attributes
192+
- drop geometry if layer is geometry-less
193+
- add missing attribute fields
194+
- add back M/Z values (initialized to 0)
195+
- drop Z/M
196+
- convert multi part geometries to single part
197+
198+
.. versionadded:: 3.4
199+
%End
200+
179201
};
180202

181203

python/plugins/processing/gui/AlgorithmExecutor.py

Lines changed: 3 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -70,85 +70,6 @@ def execute(alg, parameters, context=None, feedback=None):
7070
return False, {}
7171

7272

73-
def make_features_compatible(new_features, input_layer):
74-
"""Try to make the new features compatible with old features by:
75-
76-
- converting single to multi part
77-
- dropping additional attributes
78-
- adding back M/Z values
79-
- drop Z/M
80-
- convert multi part to single part
81-
82-
:param new_features: new features
83-
:type new_features: list of QgsFeatures
84-
:param input_layer: input layer
85-
:type input_layer: QgsVectorLayer
86-
:return: modified features
87-
:rtype: list of QgsFeatures
88-
"""
89-
90-
input_wkb_type = input_layer.wkbType()
91-
result_features = []
92-
for new_f in new_features:
93-
# Fix attributes
94-
QgsVectorLayerUtils.matchAttributesToFields(new_f, input_layer.fields())
95-
96-
# Check if we need geometry manipulation
97-
new_f_geom_type = QgsWkbTypes.geometryType(new_f.geometry().wkbType())
98-
new_f_has_geom = new_f_geom_type not in (QgsWkbTypes.UnknownGeometry, QgsWkbTypes.NullGeometry)
99-
input_layer_has_geom = input_wkb_type not in (QgsWkbTypes.NoGeometry, QgsWkbTypes.Unknown)
100-
101-
# Drop geometry if layer is geometry-less
102-
if not input_layer_has_geom and new_f_has_geom:
103-
f = QgsFeature(input_layer.fields())
104-
f.setAttributes(new_f.attributes())
105-
new_f = f
106-
result_features.append(new_f)
107-
continue # skip the rest
108-
109-
if input_layer_has_geom and new_f_has_geom and \
110-
new_f.geometry().wkbType() != input_wkb_type: # Fix geometry
111-
# Single -> Multi
112-
if (QgsWkbTypes.isMultiType(input_wkb_type) and not
113-
new_f.geometry().isMultipart()):
114-
new_geom = new_f.geometry()
115-
new_geom.convertToMultiType()
116-
new_f.setGeometry(new_geom)
117-
# Drop Z/M
118-
if (new_f.geometry().constGet().is3D() and not QgsWkbTypes.hasZ(input_wkb_type)):
119-
new_geom = new_f.geometry()
120-
new_geom.get().dropZValue()
121-
new_f.setGeometry(new_geom)
122-
if (new_f.geometry().constGet().isMeasure() and not QgsWkbTypes.hasM(input_wkb_type)):
123-
new_geom = new_f.geometry()
124-
new_geom.get().dropMValue()
125-
new_f.setGeometry(new_geom)
126-
# Add Z/M back (set it to 0)
127-
if (not new_f.geometry().constGet().is3D() and QgsWkbTypes.hasZ(input_wkb_type)):
128-
new_geom = new_f.geometry()
129-
new_geom.get().addZValue(0.0)
130-
new_f.setGeometry(new_geom)
131-
if (not new_f.geometry().constGet().isMeasure() and QgsWkbTypes.hasM(input_wkb_type)):
132-
new_geom = new_f.geometry()
133-
new_geom.get().addMValue(0.0)
134-
new_f.setGeometry(new_geom)
135-
# Multi -> Single
136-
if (not QgsWkbTypes.isMultiType(input_wkb_type) and
137-
new_f.geometry().isMultipart()):
138-
g = new_f.geometry()
139-
g2 = g.constGet()
140-
for i in range(g2.partCount()):
141-
# Clone or crash!
142-
g4 = QgsGeometry(g2.geometryN(i).clone())
143-
f = QgsVectorLayerUtils.createFeature(input_layer, g4, {i: new_f.attribute(i) for i in range(new_f.fields().count())})
144-
result_features.append(f)
145-
else:
146-
result_features.append(new_f)
147-
else:
148-
result_features.append(new_f)
149-
return result_features
150-
151-
15273
def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=None, raise_exceptions=False):
15374
"""Executes an algorithm modifying features in-place in the input layer.
15475
@@ -208,7 +129,7 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
208129
# a shallow copy from processFeature
209130
input_feature = QgsFeature(f)
210131
new_features = alg.processFeature(input_feature, context, feedback)
211-
new_features = make_features_compatible(new_features, active_layer)
132+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible(new_features, active_layer)
212133
if len(new_features) == 0:
213134
active_layer.deleteFeature(f.id())
214135
elif len(new_features) == 1:
@@ -238,7 +159,8 @@ def execute_in_place_run(alg, active_layer, parameters, context=None, feedback=N
238159
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
239160
new_features = []
240161
for f in result_layer.getFeatures():
241-
new_features.extend(make_features_compatible([f], active_layer))
162+
new_features.extend(QgsVectorLayerUtils.
163+
makeFeaturesCompatible([f], active_layer))
242164

243165
# Get the new ids
244166
old_ids = set([f.id() for f in active_layer.getFeatures(req)])

src/core/qgsvectorlayerutils.cpp

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "qgsfeedback.h"
2626
#include "qgsvectorlayer.h"
2727
#include "qgsthreadingutils.h"
28+
#include "qgsgeometrycollection.h"
2829

2930
QgsFeatureIterator QgsVectorLayerUtils::getValuesIterator( const QgsVectorLayer *layer, const QString &fieldOrExpression, bool &ok, bool selectedOnly )
3031
{
@@ -348,7 +349,7 @@ bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const
348349
return valid;
349350
}
350351

351-
QgsFeature QgsVectorLayerUtils::createFeature( QgsVectorLayer *layer, const QgsGeometry &geometry,
352+
QgsFeature QgsVectorLayerUtils::createFeature( const QgsVectorLayer *layer, const QgsGeometry &geometry,
352353
const QgsAttributeMap &attributes, QgsExpressionContext *context )
353354
{
354355
if ( !layer )
@@ -560,6 +561,97 @@ void QgsVectorLayerUtils::matchAttributesToFields( QgsFeature &feature, const Qg
560561
}
561562
}
562563

564+
QgsFeatureList QgsVectorLayerUtils::makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer )
565+
{
566+
QgsWkbTypes::Type inputWkbType( layer.wkbType( ) );
567+
QgsFeatureList resultFeatures;
568+
for ( const QgsFeature &f : features )
569+
{
570+
QgsFeature newF( f );
571+
// Fix attributes
572+
QgsVectorLayerUtils::matchAttributesToFields( newF, layer.fields( ) );
573+
// Does geometry need tranformations?
574+
QgsWkbTypes::GeometryType newFGeomType( QgsWkbTypes::geometryType( newF.geometry().wkbType() ) );
575+
bool newFHasGeom = newFGeomType !=
576+
QgsWkbTypes::GeometryType::UnknownGeometry &&
577+
newFGeomType != QgsWkbTypes::GeometryType::NullGeometry;
578+
bool layerHasGeom = inputWkbType !=
579+
QgsWkbTypes::Type::NoGeometry &&
580+
inputWkbType != QgsWkbTypes::Type::Unknown;
581+
// Drop geometry if layer is geometry-less
582+
if ( newFHasGeom && ! layerHasGeom )
583+
{
584+
QgsFeature _f = QgsFeature( layer.fields() );
585+
_f.setAttributes( newF.attributes() );
586+
resultFeatures.append( _f );
587+
continue; // Skip the rest
588+
}
589+
// Geometry need fixing
590+
if ( newFHasGeom && layerHasGeom && newF.geometry().wkbType() != inputWkbType )
591+
{
592+
// Single -> multi
593+
if ( QgsWkbTypes::isMultiType( inputWkbType ) && ! newF.geometry().isMultipart( ) )
594+
{
595+
QgsGeometry newGeom( newF.geometry( ) );
596+
newGeom.convertToMultiType();
597+
newF.setGeometry( newGeom );
598+
}
599+
// Drop Z/M
600+
if ( newF.geometry().constGet()->is3D() && ! QgsWkbTypes::hasZ( inputWkbType ) )
601+
{
602+
QgsGeometry newGeom( newF.geometry( ) );
603+
newGeom.get()->dropZValue();
604+
newF.setGeometry( newGeom );
605+
}
606+
if ( newF.geometry().constGet()->isMeasure() && ! QgsWkbTypes::hasM( inputWkbType ) )
607+
{
608+
QgsGeometry newGeom( newF.geometry( ) );
609+
newGeom.get()->dropMValue();
610+
newF.setGeometry( newGeom );
611+
}
612+
// Add Z/M back, set to 0
613+
if ( ! newF.geometry().constGet()->is3D() && QgsWkbTypes::hasZ( inputWkbType ) )
614+
{
615+
QgsGeometry newGeom( newF.geometry( ) );
616+
newGeom.get()->addZValue( 0.0 );
617+
newF.setGeometry( newGeom );
618+
}
619+
if ( ! newF.geometry().constGet()->isMeasure() && QgsWkbTypes::hasM( inputWkbType ) )
620+
{
621+
QgsGeometry newGeom( newF.geometry( ) );
622+
newGeom.get()->addMValue( 0.0 );
623+
newF.setGeometry( newGeom );
624+
}
625+
// Multi -> single
626+
if ( ! QgsWkbTypes::isMultiType( inputWkbType ) && newF.geometry().isMultipart( ) )
627+
{
628+
QgsGeometry newGeom( newF.geometry( ) );
629+
const QgsGeometryCollection *parts( static_cast< const QgsGeometryCollection * >( newGeom.constGet() ) );
630+
for ( int i = 0; i < parts->partCount( ); i++ )
631+
{
632+
QgsGeometry g( parts->geometryN( i )->clone() );
633+
QgsAttributeMap attrMap;
634+
for ( int j = 0; j < newF.fields().count(); j++ )
635+
{
636+
attrMap[j] = newF.attribute( j );
637+
}
638+
QgsFeature _f( QgsVectorLayerUtils::createFeature( &layer, g, attrMap ) );
639+
resultFeatures.append( _f );
640+
}
641+
}
642+
else
643+
{
644+
resultFeatures.append( newF );
645+
}
646+
}
647+
else
648+
{
649+
resultFeatures.append( newF );
650+
}
651+
}
652+
return resultFeatures;
653+
}
654+
563655
QList<QgsVectorLayer *> QgsVectorLayerUtils::QgsDuplicateFeatureContext::layers() const
564656
{
565657
QList<QgsVectorLayer *> layers;

src/core/qgsvectorlayerutils.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class CORE_EXPORT QgsVectorLayerUtils
140140
* assuming that they respect the layer's constraints. Note that the created feature is not
141141
* automatically inserted into the layer.
142142
*/
143-
static QgsFeature createFeature( QgsVectorLayer *layer,
143+
static QgsFeature createFeature( const QgsVectorLayer *layer,
144144
const QgsGeometry &geometry = QgsGeometry(),
145145
const QgsAttributeMap &attributes = QgsAttributeMap(),
146146
QgsExpressionContext *context = nullptr );
@@ -187,6 +187,28 @@ class CORE_EXPORT QgsVectorLayerUtils
187187
* \since QGIS 3.4
188188
*/
189189
static void matchAttributesToFields( QgsFeature &feature, const QgsFields &fields );
190+
191+
192+
/**
193+
* Converts input \a features to be compatible with the given \a layer.
194+
*
195+
* This function returns a new list of transformed features compatible with the input
196+
* layer, note that the number of features returned might be greater than the number
197+
* of input featurers.
198+
*
199+
* The following operations will be performed to convert the input features:
200+
* - convert single geometries to multi part
201+
* - drop additional attributes
202+
* - drop geometry if layer is geometry-less
203+
* - add missing attribute fields
204+
* - add back M/Z values (initialized to 0)
205+
* - drop Z/M
206+
* - convert multi part geometries to single part
207+
*
208+
* \since QGIS 3.4
209+
*/
210+
static QgsFeatureList makeFeaturesCompatible( const QgsFeatureList &features, QgsVectorLayer &layer );
211+
190212
};
191213

192214

tests/src/python/test_qgsprocessinginplace.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
QgsFeature, QgsGeometry, QgsSettings, QgsApplication, QgsMemoryProviderUtils, QgsWkbTypes, QgsField, QgsFields, QgsProcessingFeatureSourceDefinition, QgsProcessingContext, QgsProcessingFeedback, QgsCoordinateReferenceSystem, QgsProject, QgsProcessingException
1818
)
1919
from processing.core.Processing import Processing
20-
from processing.gui.AlgorithmExecutor import execute_in_place_run, make_features_compatible
20+
from processing.gui.AlgorithmExecutor import execute_in_place_run
2121
from qgis.testing import start_app, unittest
2222
from qgis.PyQt.QtTest import QSignalSpy
2323
from qgis.analysis import QgsNativeAlgorithms
24+
from qgis.core import QgsVectorLayerUtils
2425

2526
start_app()
2627

@@ -185,15 +186,15 @@ def _make_compatible_tester(self, feature_wkt, layer_wkb_name, attrs=[1]):
185186
context.setProject(QgsProject.instance())
186187

187188
# Fix it!
188-
new_features = make_features_compatible([f], layer)
189+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f], layer)
189190

190191
for new_f in new_features:
191192
self.assertEqual(new_f.geometry().wkbType(), layer.wkbType())
192193

193194
self.assertTrue(layer.addFeatures(new_features), "Fail: %s - %s - %s" % (feature_wkt, attrs, layer_wkb_name))
194195
return layer, new_features
195196

196-
def test_make_features_compatible(self):
197+
def test_QgsVectorLayerUtilsmakeFeaturesCompatible(self):
197198
"""Test fixer function"""
198199
# Test failure
199200
with self.assertRaises(AssertionError):
@@ -283,21 +284,21 @@ def test_make_features_compatible_attributes(self):
283284
f1['int_f'] = 1
284285
f1['str_f'] = 'str'
285286
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
286-
new_features = make_features_compatible([f1], layer)
287+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
287288
self.assertEqual(new_features[0].attributes(), f1.attributes())
288289
self.assertTrue(new_features[0].geometry().asWkt(), f1.geometry().asWkt())
289290

290291
# Test pad with 0 with fields
291292
f1.setAttributes([])
292-
new_features = make_features_compatible([f1], layer)
293+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
293294
self.assertEqual(len(new_features[0].attributes()), 2)
294295
self.assertEqual(new_features[0].attributes()[0], QVariant())
295296
self.assertEqual(new_features[0].attributes()[1], QVariant())
296297

297298
# Test pad with 0 without fields
298299
f1 = QgsFeature()
299300
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
300-
new_features = make_features_compatible([f1], layer)
301+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
301302
self.assertEqual(len(new_features[0].attributes()), 2)
302303
self.assertEqual(new_features[0].attributes()[0], QVariant())
303304
self.assertEqual(new_features[0].attributes()[1], QVariant())
@@ -306,7 +307,7 @@ def test_make_features_compatible_attributes(self):
306307
f1 = QgsFeature(layer.fields())
307308
f1.setAttributes([1, 'foo', 'extra'])
308309
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
309-
new_features = make_features_compatible([f1], layer)
310+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
310311
self.assertEqual(len(new_features[0].attributes()), 2)
311312
self.assertEqual(new_features[0].attributes()[0], 1)
312313
self.assertEqual(new_features[0].attributes()[1], 'foo')
@@ -322,15 +323,15 @@ def test_make_features_compatible_geometry(self):
322323
f1.setAttributes([1])
323324

324325
# Check that it is accepted on a Point layer
325-
new_features = make_features_compatible([f1], layer)
326+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], layer)
326327
self.assertEqual(len(new_features), 1)
327328
self.assertEqual(new_features[0].geometry().asWkt(), '')
328329

329330
# Make a geometry-less layer
330331
nogeom_layer = QgsMemoryProviderUtils.createMemoryLayer(
331332
'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326))
332333
# Check that a geometry-less feature is accepted
333-
new_features = make_features_compatible([f1], nogeom_layer)
334+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer)
334335
self.assertEqual(len(new_features), 1)
335336
self.assertEqual(new_features[0].geometry().asWkt(), '')
336337

@@ -339,7 +340,7 @@ def test_make_features_compatible_geometry(self):
339340
'nogeom_layer', layer.fields(), QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem(4326))
340341
# Check that a Point feature is accepted but geometry was dropped
341342
f1.setGeometry(QgsGeometry.fromWkt('Point(9 45)'))
342-
new_features = make_features_compatible([f1], nogeom_layer)
343+
new_features = QgsVectorLayerUtils.makeFeaturesCompatible([f1], nogeom_layer)
343344
self.assertEqual(len(new_features), 1)
344345
self.assertEqual(new_features[0].geometry().asWkt(), '')
345346

0 commit comments

Comments
 (0)