Skip to content

Commit 41b0477

Browse files
domi4484m-kuhn
authored andcommittedMar 30, 2023
Move merge selected features logic to QgsVectorLayer
1 parent d115db2 commit 41b0477

File tree

4 files changed

+162
-91
lines changed

4 files changed

+162
-91
lines changed
 

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,19 @@ Configuration and logic to apply automatically on any edit happening on this lay
26522652
Returns the manager of the stored expressions for this layer.
26532653

26542654
.. versionadded:: 3.10
2655+
%End
2656+
2657+
bool mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage );
2658+
%Docstring
2659+
Merge selected features into a single one.
2660+
2661+
:param mergeAttributes: are the resulting attributes in the merged feature
2662+
:param unionGeometry: is the resulting geometry of the merged feature
2663+
:param errorMessage: will be set to a descriptive error message if any occurs
2664+
2665+
:return: ``True`` if the merge was successful, or ``False`` if the operation failed.
2666+
2667+
.. versionadded:: 3.30
26552668
%End
26562669

26572670
public slots:

‎src/app/qgisapp.cpp

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -9517,9 +9517,8 @@ void QgisApp::mergeSelectedFeatures()
95179517
return;
95189518
}
95199519

9520-
//get selected feature ids (as a QSet<int> )
9521-
const QgsFeatureIds &featureIdSet = vl->selectedFeatureIds();
9522-
if ( featureIdSet.size() < 2 )
9520+
// Check at least two features are selected
9521+
if ( vl->selectedFeatureIds().size() < 2 )
95239522
{
95249523
visibleMessageBar()->pushMessage(
95259524
tr( "Not enough features selected" ),
@@ -9593,104 +9592,37 @@ void QgisApp::mergeSelectedFeatures()
95939592
}
95949593
return;
95959594
}
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-
{
9614-
QgsFeatureRequest request;
9615-
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
9616-
// Handle multi pks
9617-
if ( vl->dataProvider()->pkAttributeIndexes().count() > 1 && vl->dataProvider()->pkAttributeIndexes().count() <= attrs.count() )
9618-
{
9619-
const auto pkIdxList { vl->dataProvider()->pkAttributeIndexes() };
9620-
QStringList conditions;
9621-
QStringList fieldNames;
9622-
for ( const int &pkIdx : std::as_const( pkIdxList ) )
9623-
{
9624-
const QgsField pkField { vl->fields().field( pkIdx ) };
9625-
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), attrs.at( pkIdx ), pkField.type( ) ) );
9626-
fieldNames.push_back( pkField.name() );
9627-
}
9628-
request.setSubsetOfAttributes( fieldNames, vl->fields( ) );
9629-
request.setFilterExpression( conditions.join( QStringLiteral( " AND " ) ) );
9630-
}
9631-
else // single pk
9632-
{
9633-
const QgsField pkField { vl->fields().field( i ) };
9634-
request.setSubsetOfAttributes( QStringList() << pkField.name(), vl->fields( ) );
9635-
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
9636-
}
9637-
9638-
QgsFeature f;
9639-
QgsFeatureIterator featureIterator = vl->getFeatures( request );
9640-
if ( featureIterator.nextFeature( f ) )
9595+
else if ( !QgsWkbTypes::isMultiType( vl->wkbType() ) )
9596+
{
9597+
const QgsGeometryCollection *c = qgsgeometry_cast<const QgsGeometryCollection *>( unionGeom.constGet() );
9598+
if ( ( c && c->partCount() > 1 ) || !unionGeom.convertToSingleType() )
96419599
{
9642-
mergeFeatureId = f.id( );
9600+
visibleMessageBar()->pushMessage(
9601+
tr( "Merge failed" ),
9602+
tr( "Resulting geometry type (multipart) is incompatible with layer type (singlepart)." ),
9603+
Qgis::MessageLevel::Critical );
9604+
return;
96439605
}
96449606
}
9645-
9646-
// convert to destination data type
9647-
if ( !isDefaultValue && !vl->fields().at( i ).convertCompatible( val, &errorMessage ) )
9648-
{
9649-
visibleMessageBar()->pushMessage(
9650-
tr( "Invalid result" ),
9651-
tr( "Could not store value '%1' in field of type %2: %3" ).arg( attrs.at( i ).toString(), vl->fields().at( i ).typeName(), errorMessage ),
9652-
Qgis::MessageLevel::Warning );
9653-
}
9654-
newAttributes[ i ] = val;
9655-
}
9656-
9657-
vl->beginEditCommand( tr( "Merged features" ) );
9658-
9659-
QgsFeature mergeFeature;
9660-
if ( mergeFeatureId == FID_NULL )
9661-
{
9662-
// Create new feature
9663-
mergeFeature = QgsVectorLayerUtils::createFeature( vl, unionGeom, newAttributes );
9664-
}
9665-
else
9666-
{
9667-
// Merge into existing feature
9668-
featureIdsAfter.remove( mergeFeatureId );
9669-
}
9670-
9671-
// Delete other features
9672-
QgsFeatureIds::const_iterator feature_it = featureIdsAfter.constBegin();
9673-
for ( ; feature_it != featureIdsAfter.constEnd(); ++feature_it )
9674-
{
9675-
vl->deleteFeature( *feature_it );
96769607
}
96779608

9609+
QString errorMessage;
9610+
bool success = vl->mergeSelectedFeatures( d.mergedAttributes(), unionGeom, errorMessage );
96789611

9679-
if ( mergeFeatureId == FID_NULL )
9612+
if ( !success )
96809613
{
9681-
// Add the new feature
9682-
vl->addFeature( mergeFeature );
9614+
visibleMessageBar()->pushMessage(
9615+
tr( "Merge failed" ),
9616+
errorMessage,
9617+
Qgis::MessageLevel::Critical );
96839618
}
9684-
else
9619+
else if ( success && !errorMessage.isEmpty() )
96859620
{
9686-
// Modify merge feature
9687-
vl->changeGeometry( mergeFeatureId, unionGeom );
9688-
vl->changeAttributeValues( mergeFeatureId, newAttributes );
9621+
visibleMessageBar()->pushMessage(
9622+
tr( "Invalid result" ),
9623+
errorMessage,
9624+
Qgis::MessageLevel::Warning );
96899625
}
9690-
9691-
vl->endEditCommand();
9692-
9693-
vl->triggerRepaint();
96949626
}
96959627

96969628
void QgisApp::vertexTool()

‎src/core/vector/qgsvectorlayer.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5863,6 +5863,120 @@ void QgsVectorLayer::setAllowCommit( bool allowCommit )
58635863
emit allowCommitChanged();
58645864
}
58655865

5866+
bool QgsVectorLayer::mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage )
5867+
{
5868+
errorMessage.clear();
5869+
5870+
if ( selectedFeatureIds().size() < 2 )
5871+
{
5872+
errorMessage = tr( "Not enough features selected, the merge tool requires at least two selected features" );
5873+
return false;
5874+
}
5875+
5876+
QgsAttributeMap newAttributes;
5877+
QgsFeatureId mergeFeatureId = FID_NULL;
5878+
for ( int i = 0; i < mergeAttributes.count(); ++i )
5879+
{
5880+
QVariant val = mergeAttributes.at( i );
5881+
5882+
bool isDefaultValue = fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
5883+
dataProvider() &&
5884+
dataProvider()->defaultValueClause( fields().fieldOriginIndex( i ) ) == val;
5885+
5886+
// Check if features can merged into an existing one
5887+
if ( mergeFeatureId == FID_NULL )
5888+
{
5889+
bool isPrimaryKey = fields().fieldOrigin( i ) == QgsFields::OriginProvider &&
5890+
dataProvider() &&
5891+
dataProvider()->pkAttributeIndexes().contains( fields().fieldOriginIndex( i ) );
5892+
5893+
if ( isPrimaryKey && !isDefaultValue )
5894+
{
5895+
QgsFeatureRequest request;
5896+
request.setFlags( QgsFeatureRequest::Flag::NoGeometry );
5897+
// Handle multi pks
5898+
if ( dataProvider()->pkAttributeIndexes().count() > 1 && dataProvider()->pkAttributeIndexes().count() <= mergeAttributes.count() )
5899+
{
5900+
const auto pkIdxList { dataProvider()->pkAttributeIndexes() };
5901+
QStringList conditions;
5902+
QStringList fieldNames;
5903+
for ( const int &pkIdx : std::as_const( pkIdxList ) )
5904+
{
5905+
const QgsField pkField { fields().field( pkIdx ) };
5906+
conditions.push_back( QgsExpression::createFieldEqualityExpression( pkField.name(), mergeAttributes.at( pkIdx ), pkField.type( ) ) );
5907+
fieldNames.push_back( pkField.name() );
5908+
}
5909+
request.setSubsetOfAttributes( fieldNames, fields( ) );
5910+
request.setFilterExpression( conditions.join( QStringLiteral( " AND " ) ) );
5911+
}
5912+
else // single pk
5913+
{
5914+
const QgsField pkField { fields().field( i ) };
5915+
request.setSubsetOfAttributes( QStringList() << pkField.name(), fields( ) );
5916+
request.setFilterExpression( QgsExpression::createFieldEqualityExpression( pkField.name(), val, pkField.type( ) ) );
5917+
}
5918+
5919+
QgsFeature f;
5920+
QgsFeatureIterator featureIterator = getFeatures( request );
5921+
if ( featureIterator.nextFeature( f ) )
5922+
{
5923+
mergeFeatureId = f.id( );
5924+
}
5925+
}
5926+
}
5927+
5928+
// convert to destination data type
5929+
QString errorMessageConvertCompatible;
5930+
if ( !isDefaultValue && !fields().at( i ).convertCompatible( val, &errorMessageConvertCompatible ) )
5931+
{
5932+
if ( errorMessage.isEmpty() )
5933+
errorMessage = tr( "Could not store value '%1' in field of type %2: %3" ).arg( mergeAttributes.at( i ).toString(), fields().at( i ).typeName(), errorMessageConvertCompatible );
5934+
}
5935+
newAttributes[ i ] = val;
5936+
}
5937+
5938+
beginEditCommand( tr( "Merged features" ) );
5939+
5940+
QgsFeatureIds featureIdsAfter = selectedFeatureIds();
5941+
5942+
QgsFeature mergeFeature;
5943+
if ( mergeFeatureId == FID_NULL )
5944+
{
5945+
// Create new feature
5946+
mergeFeature = QgsVectorLayerUtils::createFeature( this, unionGeometry, newAttributes );
5947+
}
5948+
else
5949+
{
5950+
// Merge into existing feature
5951+
featureIdsAfter.remove( mergeFeatureId );
5952+
}
5953+
5954+
// Delete other features
5955+
QgsFeatureIds::const_iterator feature_it = featureIdsAfter.constBegin();
5956+
for ( ; feature_it != featureIdsAfter.constEnd(); ++feature_it )
5957+
{
5958+
deleteFeature( *feature_it );
5959+
}
5960+
5961+
if ( mergeFeatureId == FID_NULL )
5962+
{
5963+
// Add the new feature
5964+
addFeature( mergeFeature );
5965+
}
5966+
else
5967+
{
5968+
// Modify merge feature
5969+
changeGeometry( mergeFeatureId, unionGeometry );
5970+
changeAttributeValues( mergeFeatureId, newAttributes );
5971+
}
5972+
5973+
endEditCommand();
5974+
5975+
triggerRepaint();
5976+
5977+
return true;
5978+
}
5979+
58665980
QgsGeometryOptions *QgsVectorLayer::geometryOptions() const
58675981
{
58685982
return mGeometryOptions.get();

‎src/core/vector/qgsvectorlayer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,6 +2480,18 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
24802480
*/
24812481
QgsStoredExpressionManager *storedExpressionManager() { return mStoredExpressionManager; }
24822482

2483+
/**
2484+
* Merge selected features into a single one.
2485+
* \param mergeAttributes are the resulting attributes in the merged feature
2486+
* \param unionGeometry is the resulting geometry of the merged feature
2487+
* \param errorMessage will be set to a descriptive error message if any occurs
2488+
*
2489+
* \returns TRUE if the merge was successful, or FALSE if the operation failed.
2490+
*
2491+
* \since QGIS 3.30
2492+
*/
2493+
bool mergeSelectedFeatures( const QgsAttributes &mergeAttributes, QgsGeometry unionGeometry, QString &errorMessage );
2494+
24832495
public slots:
24842496

24852497
/**

0 commit comments

Comments
 (0)
Please sign in to comment.