Skip to content

Commit 689a55a

Browse files
authoredMar 31, 2023
Merge pull request #52450 from m-kuhn/bp51456
Manual backport of 51456 merge feature relocation
2 parents 4b37c6e + 26d7fb2 commit 689a55a

File tree

9 files changed

+341
-62
lines changed

9 files changed

+341
-62
lines changed
 

‎python/core/auto_generated/vector/qgsvectorlayereditutils.sip.in

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,21 @@ editing.
310310
:return: 2 in case vertex already exists or point does not intersect segment
311311

312312
.. versionadded:: 3.16
313+
%End
314+
315+
bool mergeFeatures( const QgsFeatureId &targetFeatureId, const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, const QgsGeometry &unionGeometry, QString &errorMessage /Out/ );
316+
%Docstring
317+
Merge features into a single one.
318+
319+
:param targetFeatureId: id of the target feature (will be updated)
320+
:param mergeFeatureIds: id list of features to merge (will be deleted)
321+
:param mergeAttributes: are the resulting attributes in the merged feature
322+
:param unionGeometry: is the resulting geometry of the merged feature
323+
324+
:return: - ``True`` if the merge was successful, or ``False`` if the operation failed.
325+
- errorMessage: will be set to a descriptive error message if any occurs
326+
327+
.. versionadded:: 3.30
313328
%End
314329

315330
};

‎src/app/qgisapp.cpp

Lines changed: 26 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
#include "qgsdockablewidgethelper.h"
103103
#include "vertextool/qgsvertexeditor.h"
104104
#include "qgsvectorlayerutils.h"
105+
#include "qgsvectorlayereditutils.h"
105106
#include "qgsadvanceddigitizingdockwidget.h"
106107
#include "qgsabstractdatasourcewidget.h"
107108
#include "qgsmeshlayer.h"
@@ -9517,9 +9518,8 @@ void QgisApp::mergeSelectedFeatures()
95179518
return;
95189519
}
95199520

9520-
//get selected feature ids (as a QSet<int> )
9521-
const QgsFeatureIds &featureIdSet = vl->selectedFeatureIds();
9522-
if ( featureIdSet.size() < 2 )
9521+
// Check at least two features are selected
9522+
if ( vl->selectedFeatureIds().size() < 2 )
95239523
{
95249524
visibleMessageBar()->pushMessage(
95259525
tr( "Not enough features selected" ),
@@ -9593,73 +9593,38 @@ void QgisApp::mergeSelectedFeatures()
95939593
}
95949594
return;
95959595
}
9596-
}
9597-
9598-
QgsAttributes attrs = d.mergedAttributes();
9599-
QgsAttributeMap newAttributes;
9600-
QString errorMessage;
9601-
QgsFeatureId mergeFeatureId = FID_NULL;
9602-
for ( int i = 0; i < attrs.count(); ++i )
9603-
{
9604-
QVariant val = attrs.at( i );
9605-
bool isDefaultValue = vl->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
9606-
vl->dataProvider() &&
9607-
vl->dataProvider()->defaultValueClause( vl->fields().fieldOriginIndex( i ) ) == val;
9608-
bool isPrimaryKey = vl->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
9609-
vl->dataProvider() &&
9610-
vl->dataProvider()->pkAttributeIndexes().contains( vl->fields().fieldOriginIndex( i ) );
9611-
9612-
if ( isPrimaryKey && !isDefaultValue )
9613-
mergeFeatureId = val.toLongLong();
9614-
9615-
// convert to destination data type
9616-
if ( !isDefaultValue && !vl->fields().at( i ).convertCompatible( val, &errorMessage ) )
9596+
else if ( !QgsWkbTypes::isMultiType( vl->wkbType() ) )
96179597
{
9618-
visibleMessageBar()->pushMessage(
9619-
tr( "Invalid result" ),
9620-
tr( "Could not store value '%1' in field of type %2: %3" ).arg( attrs.at( i ).toString(), vl->fields().at( i ).typeName(), errorMessage ),
9621-
Qgis::MessageLevel::Warning );
9598+
const QgsGeometryCollection *c = qgsgeometry_cast<const QgsGeometryCollection *>( unionGeom.constGet() );
9599+
if ( ( c && c->partCount() > 1 ) || !unionGeom.convertToSingleType() )
9600+
{
9601+
visibleMessageBar()->pushMessage(
9602+
tr( "Merge failed" ),
9603+
tr( "Resulting geometry type (multipart) is incompatible with layer type (singlepart)." ),
9604+
Qgis::MessageLevel::Critical );
9605+
return;
9606+
}
96229607
}
9623-
newAttributes[ i ] = val;
9624-
}
9625-
9626-
vl->beginEditCommand( tr( "Merged features" ) );
9627-
9628-
QgsFeature mergeFeature;
9629-
if ( mergeFeatureId == FID_NULL )
9630-
{
9631-
// Create new feature
9632-
mergeFeature = QgsVectorLayerUtils::createFeature( vl, unionGeom, newAttributes );
9633-
}
9634-
else
9635-
{
9636-
// Merge into existing feature
9637-
featureIdsAfter.remove( mergeFeatureId );
9638-
}
9639-
9640-
// Delete other features
9641-
QgsFeatureIds::const_iterator feature_it = featureIdsAfter.constBegin();
9642-
for ( ; feature_it != featureIdsAfter.constEnd(); ++feature_it )
9643-
{
9644-
vl->deleteFeature( *feature_it );
96459608
}
96469609

9610+
QString errorMessage;
9611+
QgsVectorLayerEditUtils vectorLayerEditUtils( vl );
9612+
bool success = vectorLayerEditUtils.mergeFeatures( d.targetFeatureId(), vl->selectedFeatureIds(), d.mergedAttributes(), unionGeom, errorMessage );
96479613

9648-
if ( mergeFeatureId == FID_NULL )
9614+
if ( !success )
96499615
{
9650-
// Add the new feature
9651-
vl->addFeature( mergeFeature );
9616+
visibleMessageBar()->pushMessage(
9617+
tr( "Merge failed" ),
9618+
errorMessage,
9619+
Qgis::MessageLevel::Critical );
96529620
}
9653-
else
9621+
else if ( success && !errorMessage.isEmpty() )
96549622
{
9655-
// Modify merge feature
9656-
vl->changeGeometry( mergeFeatureId, unionGeom );
9657-
vl->changeAttributeValues( mergeFeatureId, newAttributes );
9623+
visibleMessageBar()->pushMessage(
9624+
tr( "Invalid result" ),
9625+
errorMessage,
9626+
Qgis::MessageLevel::Warning );
96589627
}
9659-
9660-
vl->endEditCommand();
9661-
9662-
vl->triggerRepaint();
96639628
}
96649629

96659630
void QgisApp::vertexTool()

‎src/app/qgsmergeattributesdialog.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ QgsMergeAttributesDialog::QgsMergeAttributesDialog( const QgsFeatureList &featur
108108
break;
109109
}
110110

111+
if ( ! mFeatureList.isEmpty() )
112+
mTargetFeatureId = mFeatureList.first().id();
113+
111114
connect( mSkipAllButton, &QAbstractButton::clicked, this, &QgsMergeAttributesDialog::setAllToSkip );
112115
connect( mTableWidget, &QTableWidget::cellChanged, this, &QgsMergeAttributesDialog::tableWidgetCellChanged );
113116

@@ -465,6 +468,8 @@ void QgsMergeAttributesDialog::setAllAttributesFromFeature( QgsFeatureId feature
465468
currentComboBox->setCurrentIndex( currentComboBox->findData( QStringLiteral( "f%1" ).arg( FID_TO_STRING( featureId ) ) ) );
466469
}
467470
}
471+
472+
mTargetFeatureId = featureId;
468473
}
469474

470475
QVariant QgsMergeAttributesDialog::calcStatistic( int col, QgsStatisticalSummary::Statistic stat )
@@ -666,12 +671,18 @@ void QgsMergeAttributesDialog::mRemoveFeatureFromSelectionButton_clicked()
666671
f_it != mFeatureList.end();
667672
++f_it )
668673
{
674+
if ( f_it->id() == mTargetFeatureId )
675+
mTargetFeatureId = FID_NULL;
676+
669677
if ( f_it->id() == featureId )
670678
{
671679
mFeatureList.erase( f_it );
672680
break;
673681
}
674682
}
683+
684+
if ( mTargetFeatureId == FID_NULL && !mFeatureList.isEmpty() )
685+
mTargetFeatureId = mFeatureList.first().id();
675686
}
676687

677688
void QgsMergeAttributesDialog::tableWidgetCellChanged( int row, int column )
@@ -745,6 +756,11 @@ QgsAttributes QgsMergeAttributesDialog::mergedAttributes() const
745756
return results;
746757
}
747758

759+
QgsFeatureId QgsMergeAttributesDialog::targetFeatureId() const
760+
{
761+
return mTargetFeatureId;
762+
}
763+
748764
QSet<int> QgsMergeAttributesDialog::skippedAttributeIndexes() const
749765
{
750766
QSet<int> skipped;

‎src/app/qgsmergeattributesdialog.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ class APP_EXPORT QgsMergeAttributesDialog: public QDialog, private Ui::QgsMergeA
4949

5050
QgsAttributes mergedAttributes() const;
5151

52+
/**
53+
* Returns the id of the target feature.
54+
* By default it is the first feature of the list. Otherwise the feature explicitly selected
55+
* with buttons "Take attributes from selected feature" or "Take attributes from feature with
56+
* the largest area".
57+
*
58+
* \returns The id of the target feature.
59+
*
60+
* \since QGIS 3.30
61+
*/
62+
QgsFeatureId targetFeatureId() const;
63+
5264
/**
5365
* Returns a list of attribute indexes which should be skipped when merging (e.g., attributes
5466
* which have been set to "skip"
@@ -102,6 +114,7 @@ class APP_EXPORT QgsMergeAttributesDialog: public QDialog, private Ui::QgsMergeA
102114
void createRubberBandForFeature( QgsFeatureId featureId );
103115

104116
QgsFeatureList mFeatureList;
117+
QgsFeatureId mTargetFeatureId = FID_NULL;
105118
QgsVectorLayer *mVectorLayer = nullptr;
106119
QgsMapCanvas *mMapCanvas = nullptr;
107120
//! Item that highlights the selected feature in the merge table

‎src/core/vector/qgsvectorlayereditutils.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,56 @@ int QgsVectorLayerEditUtils::addTopologicalPoints( const QgsPointXY &p )
721721
return addTopologicalPoints( QgsPoint( p ) );
722722
}
723723

724+
bool QgsVectorLayerEditUtils::mergeFeatures( const QgsFeatureId &targetFeatureId, const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, const QgsGeometry &unionGeometry, QString &errorMessage )
725+
{
726+
errorMessage.clear();
727+
728+
if ( mergeFeatureIds.isEmpty() )
729+
{
730+
errorMessage = QObject::tr( "List of features to merge is empty" );
731+
return false;
732+
}
733+
734+
QgsAttributeMap newAttributes;
735+
for ( int i = 0; i < mergeAttributes.count(); ++i )
736+
{
737+
QVariant val = mergeAttributes.at( i );
738+
739+
bool isDefaultValue = mLayer->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
740+
mLayer->dataProvider() &&
741+
mLayer->dataProvider()->defaultValueClause( mLayer->fields().fieldOriginIndex( i ) ) == val;
742+
743+
// convert to destination data type
744+
QString errorMessageConvertCompatible;
745+
if ( !isDefaultValue && !mLayer->fields().at( i ).convertCompatible( val, &errorMessageConvertCompatible ) )
746+
{
747+
if ( errorMessage.isEmpty() )
748+
errorMessage = QObject::tr( "Could not store value '%1' in field of type %2: %3" ).arg( mergeAttributes.at( i ).toString(), mLayer->fields().at( i ).typeName(), errorMessageConvertCompatible );
749+
}
750+
newAttributes[ i ] = val;
751+
}
752+
753+
mLayer->beginEditCommand( QObject::tr( "Merged features" ) );
754+
755+
// Delete other features but the target feature
756+
QgsFeatureIds::const_iterator feature_it = mergeFeatureIds.constBegin();
757+
for ( ; feature_it != mergeFeatureIds.constEnd(); ++feature_it )
758+
{
759+
if ( *feature_it != targetFeatureId )
760+
mLayer->deleteFeature( *feature_it );
761+
}
762+
763+
// Modify merge feature
764+
QgsGeometry mergeGeometry = unionGeometry;
765+
mLayer->changeGeometry( targetFeatureId, mergeGeometry );
766+
mLayer->changeAttributeValues( targetFeatureId, newAttributes );
767+
768+
mLayer->endEditCommand();
769+
770+
mLayer->triggerRepaint();
771+
772+
return true;
773+
}
724774

725775
bool QgsVectorLayerEditUtils::boundingBoxFromPointList( const QgsPointSequence &list, double &xmin, double &ymin, double &xmax, double &ymax ) const
726776
{

‎src/core/vector/qgsvectorlayereditutils.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,20 @@ class CORE_EXPORT QgsVectorLayerEditUtils
268268
*/
269269
int addTopologicalPoints( const QgsPointSequence &ps );
270270

271+
/**
272+
* Merge features into a single one.
273+
* \param targetFeatureId id of the target feature (will be updated)
274+
* \param mergeFeatureIds id list of features to merge (will be deleted)
275+
* \param mergeAttributes are the resulting attributes in the merged feature
276+
* \param unionGeometry is the resulting geometry of the merged feature
277+
* \param errorMessage will be set to a descriptive error message if any occurs
278+
*
279+
* \returns TRUE if the merge was successful, or FALSE if the operation failed.
280+
*
281+
* \since QGIS 3.30
282+
*/
283+
bool mergeFeatures( const QgsFeatureId &targetFeatureId, const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, const QgsGeometry &unionGeometry, QString &errorMessage SIP_OUT );
284+
271285
private:
272286

273287
/**

‎tests/src/app/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ set(TESTS
4646
testqgsmeshcalculatordialog.cpp
4747
testqgsgpsinformationwidget.cpp
4848
testqgslabelpropertydialog.cpp
49+
testqgsmergeattributesdialog.cpp
4950
)
5051

5152
if (HAVE_GEOREFERENCER)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/***************************************************************************
2+
testqgsmergeattributesdialog.cpp
3+
--------------------------------
4+
Date : Feb 2023
5+
Copyright : (C) 2023 by Damiano Lombardi
6+
Email : damiano at opengis dot ch
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgstest.h"
17+
#include <QObject>
18+
19+
#include "qgisapp.h"
20+
#include "qgsapplication.h"
21+
#include "qgsvectorlayer.h"
22+
#include "qgsmergeattributesdialog.h"
23+
24+
class TestQgsMergeattributesDialog : public QObject
25+
{
26+
Q_OBJECT
27+
28+
public:
29+
TestQgsMergeattributesDialog() = default;
30+
31+
private:
32+
QgisApp *mQgisApp = nullptr;
33+
34+
private slots:
35+
36+
void initTestCase()
37+
{
38+
QgsApplication::init();
39+
QgsApplication::initQgis();
40+
mQgisApp = new QgisApp();
41+
}
42+
43+
void cleanupTestCase()
44+
{
45+
QgsApplication::exitQgis();
46+
}
47+
48+
void test()
49+
{
50+
// Create test layer
51+
QgsVectorFileWriter::SaveVectorOptions options;
52+
QgsVectorLayer ml( "Polygon", "test", "memory" );
53+
QVERIFY( ml.isValid() );
54+
QTemporaryFile tmpFile( QDir::tempPath() + "/TestQgsMergeattributesDialog" );
55+
tmpFile.open();
56+
const QString fileName( tmpFile.fileName( ) );
57+
options.driverName = "GPKG";
58+
options.layerName = "test";
59+
QString newFilename;
60+
const QgsVectorFileWriter::WriterError error( QgsVectorFileWriter::writeAsVectorFormatV3(
61+
&ml,
62+
fileName,
63+
ml.transformContext(),
64+
options, nullptr,
65+
&newFilename ) );
66+
67+
QCOMPARE( error, QgsVectorFileWriter::WriterError::NoError );
68+
QgsVectorLayer layer( QStringLiteral( "%1|layername=test" ).arg( newFilename ), "src_test", "ogr" );
69+
QVERIFY( layer.startEditing() );
70+
QgsVectorDataProvider *pr = layer.dataProvider();
71+
72+
// Create a feature
73+
QgsFeature f1( layer.fields(), 1 );
74+
f1.setGeometry( QgsGeometry::fromWkt( "POLYGON((0 0, 5 0, 5 5, 0 5, 0 0))" ) );
75+
QVERIFY( pr->addFeature( f1 ) );
76+
QCOMPARE( layer.featureCount(), 1 );
77+
78+
// And a bigger feature
79+
QgsFeature f2( layer.fields(), 2 );
80+
f2.setGeometry( QgsGeometry::fromWkt( "POLYGON((3 3, 10 3, 10 10, 3 10, 3 3))" ) );
81+
QVERIFY( pr->addFeature( f2 ) );
82+
QCOMPARE( layer.featureCount(), 2 );
83+
84+
// Merge the attributes together
85+
QgsMergeAttributesDialog dialog( QgsFeatureList() << f1 << f2, &layer, mQgisApp->mapCanvas() );
86+
87+
// At beginnning the first feature of the list is the target
88+
QCOMPARE( dialog.targetFeatureId(), f1.id() );
89+
90+
// Check after taking feature with largest geometry
91+
QVERIFY( QMetaObject::invokeMethod( &dialog, "mFromLargestPushButton_clicked" ) );
92+
QCOMPARE( dialog.targetFeatureId(), f2.id() );
93+
}
94+
};
95+
96+
QGSTEST_MAIN( TestQgsMergeattributesDialog )
97+
#include "testqgsmergeattributesdialog.moc"

‎tests/src/python/test_qgsvectorlayereditutils.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,31 @@
1414

1515
import qgis # NOQA
1616

17+
import tempfile
18+
import os
19+
20+
from qgis.PyQt.QtCore import (
21+
QVariant,
22+
Qt,
23+
QTime,
24+
QTimer,
25+
QTemporaryDir,
26+
)
1727

1828
from qgis.core import (Qgis,
1929
QgsFeature,
30+
QgsField,
31+
QgsFields,
2032
QgsGeometry,
2133
QgsLineString,
2234
QgsPoint,
2335
QgsPointXY,
36+
QgsCoordinateReferenceSystem,
37+
QgsCoordinateTransformContext,
2438
QgsVectorLayer,
25-
QgsVectorLayerEditUtils)
39+
QgsVectorLayerEditUtils,
40+
QgsVectorFileWriter,
41+
QgsWkbTypes)
2642

2743

2844
from qgis.testing import start_app, unittest
@@ -317,6 +333,98 @@ def testSplitParts(self):
317333
'MultiPolygon (((2 12, 2 16, 4 16, 4 12, 2 12)),((2 16, 2 12, 0 12, 0 16, 2 16)),((6 12, 10 12, 10 16, 6 16, 6 12)))'
318334
)
319335

336+
def testMergeFeatures(self):
337+
layer = createEmptyPolygonLayer()
338+
self.assertTrue(layer.startEditing())
339+
layer.addAttribute(QgsField('name', QVariant.String))
340+
pr = layer.dataProvider()
341+
342+
f1 = QgsFeature(layer.fields(), 1)
343+
f1.setGeometry(QgsGeometry.fromWkt('POLYGON((0 0, 5 0, 5 5, 0 5, 0 0))'))
344+
f1.setAttribute('name', 'uno')
345+
assert pr.addFeatures([f1])
346+
self.assertEqual(layer.featureCount(), 1)
347+
348+
f2 = QgsFeature(layer.fields(), 2)
349+
f2.setGeometry(QgsGeometry.fromWkt('POLYGON((3 3, 8 3, 8 8, 3 8, 3 3))'))
350+
f2.setAttribute('name', 'due')
351+
assert pr.addFeatures([f2])
352+
self.assertEqual(layer.featureCount(), 2)
353+
354+
featureIds = [f1.id(), f2.id()]
355+
356+
unionGeom = f1.geometry()
357+
unionGeom = unionGeom.combine(f2.geometry())
358+
self.assertFalse(unionGeom.isNull())
359+
360+
vle = QgsVectorLayerEditUtils(layer)
361+
success, errorMessage = vle.mergeFeatures(f1.id(), featureIds, ['tre'], unionGeom)
362+
self.assertFalse(errorMessage)
363+
self.assertTrue(success)
364+
365+
layer.commitChanges()
366+
367+
self.assertEqual(layer.featureCount(), 1)
368+
mergedFeature = next(layer.getFeatures())
369+
self.assertEqual(
370+
mergedFeature.geometry().asWkt(),
371+
"Polygon ((5 0, 0 0, 0 5, 3 5, 3 8, 8 8, 8 3, 5 3, 5 0))"
372+
)
373+
self.assertEqual(mergedFeature.attribute('name'), 'tre')
374+
375+
def testMergeFeaturesIntoExisting(self):
376+
tempgpkg = os.path.join(tempfile.mkdtemp(), 'testMergeFeaturesIntoExisting.gpkg')
377+
fields = QgsFields()
378+
fields.append(QgsField('name', QVariant.String))
379+
380+
crs = QgsCoordinateReferenceSystem('epsg:4326')
381+
options = QgsVectorFileWriter.SaveVectorOptions()
382+
options.driverName = "GPKG"
383+
QgsVectorFileWriter.create(
384+
fileName=tempgpkg,
385+
fields=fields,
386+
geometryType=QgsWkbTypes.Polygon,
387+
srs=crs,
388+
transformContext=QgsCoordinateTransformContext(),
389+
options=options)
390+
391+
layer = QgsVectorLayer(tempgpkg, 'my_layer', 'ogr')
392+
self.assertTrue(layer.startEditing())
393+
pr = layer.dataProvider()
394+
395+
f1 = QgsFeature(layer.fields(), 1)
396+
f1.setGeometry(QgsGeometry.fromWkt('POLYGON((0 0, 5 0, 5 5, 0 5, 0 0))'))
397+
f1.setAttribute('name', 'uno')
398+
assert pr.addFeatures([f1])
399+
self.assertEqual(layer.featureCount(), 1)
400+
401+
f2 = QgsFeature(layer.fields(), 2)
402+
f2.setGeometry(QgsGeometry.fromWkt('POLYGON((3 3, 8 3, 8 8, 3 8, 3 3))'))
403+
f2.setAttribute('name', 'due')
404+
assert pr.addFeatures([f2])
405+
self.assertEqual(layer.featureCount(), 2)
406+
407+
featureIds = [f1.id(), f2.id()]
408+
409+
unionGeom = f1.geometry()
410+
unionGeom = unionGeom.combine(f2.geometry())
411+
self.assertFalse(unionGeom.isNull())
412+
413+
vle = QgsVectorLayerEditUtils(layer)
414+
success, errorMessage = vle.mergeFeatures(f2.id(), featureIds, [2, 'tre'], unionGeom)
415+
self.assertFalse(errorMessage)
416+
self.assertTrue(success)
417+
418+
layer.commitChanges()
419+
420+
self.assertEqual(layer.featureCount(), 1)
421+
mergedFeature = layer.getFeature(2)
422+
self.assertEqual(
423+
mergedFeature.geometry().asWkt(),
424+
"Polygon ((5 0, 0 0, 0 5, 3 5, 3 8, 8 8, 8 3, 5 3, 5 0))"
425+
)
426+
self.assertEqual(mergedFeature.attribute('name'), 'tre')
427+
320428

321429
if __name__ == '__main__':
322430
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.