Skip to content

Commit

Permalink
Explicitly set the target feature for merge operation
Browse files Browse the repository at this point in the history
  • Loading branch information
domi4484 authored and m-kuhn committed Mar 30, 2023
1 parent ae3573b commit 3ea1674
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 83 deletions.
Expand Up @@ -312,10 +312,11 @@ editing.
.. versionadded:: 3.16
%End

bool mergeFeatures( const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage /Out/ );
bool mergeFeatures( const QgsFeatureId &targetFeatureId, const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, const QgsGeometry &unionGeometry, QString &errorMessage /Out/ );
%Docstring
Merge features into a single one.

:param targetFeatureId: id of the target feature
:param mergeFeatureIds: id list of features to merge
:param mergeAttributes: are the resulting attributes in the merged feature
:param unionGeometry: is the resulting geometry of the merged feature
Expand Down
2 changes: 1 addition & 1 deletion src/app/qgisapp.cpp
Expand Up @@ -9609,7 +9609,7 @@ void QgisApp::mergeSelectedFeatures()

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

if ( !success )
{
Expand Down
16 changes: 16 additions & 0 deletions src/app/qgsmergeattributesdialog.cpp
Expand Up @@ -108,6 +108,9 @@ QgsMergeAttributesDialog::QgsMergeAttributesDialog( const QgsFeatureList &featur
break;
}

if ( ! mFeatureList.isEmpty() )
mTargetFeatureId = mFeatureList.first().id();

connect( mSkipAllButton, &QAbstractButton::clicked, this, &QgsMergeAttributesDialog::setAllToSkip );
connect( mTableWidget, &QTableWidget::cellChanged, this, &QgsMergeAttributesDialog::tableWidgetCellChanged );

Expand Down Expand Up @@ -465,6 +468,8 @@ void QgsMergeAttributesDialog::setAllAttributesFromFeature( QgsFeatureId feature
currentComboBox->setCurrentIndex( currentComboBox->findData( QStringLiteral( "f%1" ).arg( FID_TO_STRING( featureId ) ) ) );
}
}

mTargetFeatureId = featureId;
}

QVariant QgsMergeAttributesDialog::calcStatistic( int col, QgsStatisticalSummary::Statistic stat )
Expand Down Expand Up @@ -666,12 +671,18 @@ void QgsMergeAttributesDialog::mRemoveFeatureFromSelectionButton_clicked()
f_it != mFeatureList.end();
++f_it )
{
if ( f_it->id() == mTargetFeatureId )
mTargetFeatureId = FID_NULL;

if ( f_it->id() == featureId )
{
mFeatureList.erase( f_it );
break;
}
}

if ( mTargetFeatureId == FID_NULL && !mFeatureList.isEmpty() )
mTargetFeatureId = mFeatureList.first().id();
}

void QgsMergeAttributesDialog::tableWidgetCellChanged( int row, int column )
Expand Down Expand Up @@ -745,6 +756,11 @@ QgsAttributes QgsMergeAttributesDialog::mergedAttributes() const
return results;
}

QgsFeatureId QgsMergeAttributesDialog::targetFeatureId() const
{
return mTargetFeatureId;
}

QSet<int> QgsMergeAttributesDialog::skippedAttributeIndexes() const
{
QSet<int> skipped;
Expand Down
13 changes: 13 additions & 0 deletions src/app/qgsmergeattributesdialog.h
Expand Up @@ -49,6 +49,18 @@ class APP_EXPORT QgsMergeAttributesDialog: public QDialog, private Ui::QgsMergeA

QgsAttributes mergedAttributes() const;

/**
* Returns the id of the target feature.
* By default it is the first feature of the list. Otherwise the feature explicitly selected
* with buttons "Take attributes from selected feature" or "Take attributes from feature with
* the largest area".
*
* \returns The id of the target feature.
*
* \since QGIS 3.30
*/
QgsFeatureId targetFeatureId() const;

/**
* Returns a list of attribute indexes which should be skipped when merging (e.g., attributes
* which have been set to "skip"
Expand Down Expand Up @@ -102,6 +114,7 @@ class APP_EXPORT QgsMergeAttributesDialog: public QDialog, private Ui::QgsMergeA
void createRubberBandForFeature( QgsFeatureId featureId );

QgsFeatureList mFeatureList;
QgsFeatureId mTargetFeatureId = FID_NULL;
QgsVectorLayer *mVectorLayer = nullptr;
QgsMapCanvas *mMapCanvas = nullptr;
//! Item that highlights the selected feature in the merge table
Expand Down
87 changes: 12 additions & 75 deletions src/core/vector/qgsvectorlayereditutils.cpp
Expand Up @@ -721,18 +721,17 @@ int QgsVectorLayerEditUtils::addTopologicalPoints( const QgsPointXY &p )
return addTopologicalPoints( QgsPoint( p ) );
}

bool QgsVectorLayerEditUtils::mergeFeatures( const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage )
bool QgsVectorLayerEditUtils::mergeFeatures( const QgsFeatureId &targetFeatureId, const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, const QgsGeometry &unionGeometry, QString &errorMessage )
{
errorMessage.clear();

if ( mergeFeatureIds.size() < 2 )
if ( mergeFeatureIds.isEmpty() )
{
errorMessage = QObject::tr( "Not enough features to merge, the merge tool requires at least two features" );
errorMessage = QObject::tr( "List of features to merge is empty" );
return false;
}

QgsAttributeMap newAttributes;
QgsFeatureId mergeFeatureId = FID_NULL;
for ( int i = 0; i < mergeAttributes.count(); ++i )
{
QVariant val = mergeAttributes.at( i );
Expand All @@ -741,48 +740,6 @@ bool QgsVectorLayerEditUtils::mergeFeatures( const QgsFeatureIds &mergeFeatureId
mLayer->dataProvider() &&
mLayer->dataProvider()->defaultValueClause( mLayer->fields().fieldOriginIndex( i ) ) == val;

// Check if features can merged into an existing one
if ( mergeFeatureId == FID_NULL )
{
bool isPrimaryKey = mLayer->fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
mLayer->dataProvider() &&
mLayer->dataProvider()->pkAttributeIndexes().contains( mLayer->fields().fieldOriginIndex( i ) );

if ( isPrimaryKey && !isDefaultValue )
{
QgsFeatureRequest request;
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
// Handle multi pks
if ( mLayer->dataProvider()->pkAttributeIndexes().count() > 1 && mLayer->dataProvider()->pkAttributeIndexes().count() <= mergeAttributes.count() )
{
const auto pkIdxList { mLayer->dataProvider()->pkAttributeIndexes() };
QStringList conditions;
QStringList fieldNames;
for ( const int &pkIdx : std::as_const( pkIdxList ) )
{
const QgsField pkField { mLayer->fields().field( pkIdx ) };
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), mergeAttributes.at( pkIdx ), pkField.type( ) ) );
fieldNames.push_back( pkField.name() );
}
request.setSubsetOfAttributes( fieldNames, mLayer->fields( ) );
request.setFilterExpression( conditions.join( QLatin1String( " AND " ) ) );
}
else // single pk
{
const QgsField pkField { mLayer->fields().field( i ) };
request.setSubsetOfAttributes( QStringList() << pkField.name(), mLayer->fields( ) );
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
}

QgsFeature f;
QgsFeatureIterator featureIterator = mLayer->getFeatures( request );
if ( featureIterator.nextFeature( f ) )
{
mergeFeatureId = f.id( );
}
}
}

// convert to destination data type
QString errorMessageConvertCompatible;
if ( !isDefaultValue && !mLayer->fields().at( i ).convertCompatible( val, &errorMessageConvertCompatible ) )
Expand All @@ -795,38 +752,18 @@ bool QgsVectorLayerEditUtils::mergeFeatures( const QgsFeatureIds &mergeFeatureId

mLayer->beginEditCommand( QObject::tr( "Merged features" ) );

QgsFeatureIds featureIdsToDelete = mergeFeatureIds;

QgsFeature mergeFeature;
if ( mergeFeatureId == FID_NULL )
{
// Create new feature
mergeFeature = QgsVectorLayerUtils::createFeature( mLayer, unionGeometry, newAttributes );
}
else
{
// Merge into existing feature
featureIdsToDelete.remove( mergeFeatureId );
}

// Delete other features
QgsFeatureIds::const_iterator feature_it = featureIdsToDelete.constBegin();
for ( ; feature_it != featureIdsToDelete.constEnd(); ++feature_it )
// Delete other features but the target feature
QgsFeatureIds::const_iterator feature_it = mergeFeatureIds.constBegin();
for ( ; feature_it != mergeFeatureIds.constEnd(); ++feature_it )
{
mLayer->deleteFeature( *feature_it );
if ( *feature_it != targetFeatureId )
mLayer->deleteFeature( *feature_it );
}

if ( mergeFeatureId == FID_NULL )
{
// Add the new feature
mLayer->addFeature( mergeFeature );
}
else
{
// Modify merge feature
mLayer->changeGeometry( mergeFeatureId, unionGeometry );
mLayer->changeAttributeValues( mergeFeatureId, newAttributes );
}
// Modify merge feature
QgsGeometry mergeGeometry = unionGeometry;
mLayer->changeGeometry( targetFeatureId, mergeGeometry );
mLayer->changeAttributeValues( targetFeatureId, newAttributes );

mLayer->endEditCommand();

Expand Down
3 changes: 2 additions & 1 deletion src/core/vector/qgsvectorlayereditutils.h
Expand Up @@ -270,6 +270,7 @@ class CORE_EXPORT QgsVectorLayerEditUtils

/**
* Merge features into a single one.
* \param targetFeatureId id of the target feature
* \param mergeFeatureIds id list of features to merge
* \param mergeAttributes are the resulting attributes in the merged feature
* \param unionGeometry is the resulting geometry of the merged feature
Expand All @@ -279,7 +280,7 @@ class CORE_EXPORT QgsVectorLayerEditUtils
*
* \since QGIS 3.30
*/
bool mergeFeatures( const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage SIP_OUT );
bool mergeFeatures( const QgsFeatureId &targetFeatureId, const QgsFeatureIds &mergeFeatureIds, const QgsAttributes &mergeAttributes, const QgsGeometry &unionGeometry, QString &errorMessage SIP_OUT );

private:

Expand Down
1 change: 1 addition & 0 deletions tests/src/app/CMakeLists.txt
Expand Up @@ -46,6 +46,7 @@ set(TESTS
testqgsmeshcalculatordialog.cpp
testqgsgpsinformationwidget.cpp
testqgslabelpropertydialog.cpp
testqgsmergeattributesdialog.cpp
)

if (HAVE_GEOREFERENCER)
Expand Down
97 changes: 97 additions & 0 deletions tests/src/app/testqgsmergeattributesdialog.cpp
@@ -0,0 +1,97 @@
/***************************************************************************
testqgsmergeattributesdialog.cpp
--------------------------------
Date : Feb 2023
Copyright : (C) 2023 by Damiano Lombardi
Email : damiano at opengis dot ch
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgstest.h"
#include <QObject>

#include "qgisapp.h"
#include "qgsapplication.h"
#include "qgsvectorlayer.h"
#include "qgsmergeattributesdialog.h"

class TestQgsMergeattributesDialog : public QObject
{
Q_OBJECT

public:
TestQgsMergeattributesDialog() = default;

private:
QgisApp *mQgisApp = nullptr;

private slots:

void initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
mQgisApp = new QgisApp();
}

void cleanupTestCase()
{
QgsApplication::exitQgis();
}

void test()
{
// Create test layer
QgsVectorFileWriter::SaveVectorOptions options;
QgsVectorLayer ml( "Polygon", "test", "memory" );
QVERIFY( ml.isValid() );
QTemporaryFile tmpFile( QDir::tempPath() + "/TestQgsMergeattributesDialog" );
tmpFile.open();
const QString fileName( tmpFile.fileName( ) );
options.driverName = "GPKG";
options.layerName = "test";
QString newFilename;
const QgsVectorFileWriter::WriterError error( QgsVectorFileWriter::writeAsVectorFormatV3(
&ml,
fileName,
ml.transformContext(),
options, nullptr,
&newFilename ) );

QCOMPARE( error, QgsVectorFileWriter::WriterError::NoError );
QgsVectorLayer layer( QStringLiteral( "%1|layername=test" ).arg( newFilename ), "src_test", "ogr" );
QVERIFY( layer.startEditing() );
QgsVectorDataProvider *pr = layer.dataProvider();

// Create a feature
QgsFeature f1( layer.fields(), 1 );
f1.setGeometry( QgsGeometry::fromWkt( "POLYGON((0 0, 5 0, 5 5, 0 5, 0 0))" ) );
QVERIFY( pr->addFeature( f1 ) );
QCOMPARE( layer.featureCount(), 1 );

// And a bigger feature
QgsFeature f2( layer.fields(), 2 );
f2.setGeometry( QgsGeometry::fromWkt( "POLYGON((3 3, 10 3, 10 10, 3 10, 3 3))" ) );
QVERIFY( pr->addFeature( f2 ) );
QCOMPARE( layer.featureCount(), 2 );

// Merge the attributes together
QgsMergeAttributesDialog dialog( QgsFeatureList() << f1 << f2, &layer, mQgisApp->mapCanvas() );

// At beginnning the first feature of the list is the target
QCOMPARE( dialog.targetFeatureId(), f1.id() );

// Check after taking feature with largest geometry
QVERIFY( QMetaObject::invokeMethod( &dialog, "mFromLargestPushButton_clicked" ) );
QCOMPARE( dialog.targetFeatureId(), f2.id() );
}
};

QGSTEST_MAIN( TestQgsMergeattributesDialog )
#include "testqgsmergeattributesdialog.moc"
9 changes: 4 additions & 5 deletions tests/src/python/test_qgsvectorlayereditutils.py
Expand Up @@ -494,13 +494,13 @@ def testMergeFeatures(self):
self.assertTrue(layer.startEditing())
layer.addAttribute(QgsField('name', QVariant.String))
pr = layer.dataProvider()

f1 = QgsFeature(layer.fields(), 1)
f1.setGeometry(QgsGeometry.fromWkt('POLYGON((0 0, 5 0, 5 5, 0 5, 0 0))'))
f1.setAttribute('name', 'uno')
assert pr.addFeatures([f1])
self.assertEqual(layer.featureCount(), 1)

pr = layer.dataProvider()
f2 = QgsFeature(layer.fields(), 2)
f2.setGeometry(QgsGeometry.fromWkt('POLYGON((3 3, 8 3, 8 8, 3 8, 3 3))'))
f2.setAttribute('name', 'due')
Expand All @@ -514,7 +514,7 @@ def testMergeFeatures(self):
self.assertFalse(unionGeom.isNull())

vle = QgsVectorLayerEditUtils(layer)
success, errorMessage = vle.mergeFeatures(featureIds, ['tre'], unionGeom)
success, errorMessage = vle.mergeFeatures(f1.id(), featureIds, ['tre'], unionGeom)
self.assertFalse(errorMessage)
self.assertTrue(success)

Expand Down Expand Up @@ -545,16 +545,15 @@ def testMergeFeaturesIntoExisting(self):
options=options)

layer = QgsVectorLayer(tempgpkg, 'my_layer', 'ogr')

self.assertTrue(layer.startEditing())
pr = layer.dataProvider()

f1 = QgsFeature(layer.fields(), 1)
f1.setGeometry(QgsGeometry.fromWkt('POLYGON((0 0, 5 0, 5 5, 0 5, 0 0))'))
f1.setAttribute('name', 'uno')
assert pr.addFeatures([f1])
self.assertEqual(layer.featureCount(), 1)

pr = layer.dataProvider()
f2 = QgsFeature(layer.fields(), 2)
f2.setGeometry(QgsGeometry.fromWkt('POLYGON((3 3, 8 3, 8 8, 3 8, 3 3))'))
f2.setAttribute('name', 'due')
Expand All @@ -568,7 +567,7 @@ def testMergeFeaturesIntoExisting(self):
self.assertFalse(unionGeom.isNull())

vle = QgsVectorLayerEditUtils(layer)
success, errorMessage = vle.mergeFeatures(featureIds, [2, 'tre'], unionGeom)
success, errorMessage = vle.mergeFeatures(f2.id(), featureIds, [2, 'tre'], unionGeom)
self.assertFalse(errorMessage)
self.assertTrue(success)

Expand Down

0 comments on commit 3ea1674

Please sign in to comment.