From e0d9585c230e907c99595cca9e0322d8b816a8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 19 Mar 2024 14:54:11 +0100 Subject: [PATCH 01/46] [dxf] Allow users to edit layer name in DXF Export (app) dialog --- src/app/qgsdxfexportdialog.cpp | 210 ++++++++++++++++++++------------- src/app/qgsdxfexportdialog.h | 1 + 2 files changed, 131 insertions(+), 80 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 9db2a40c3c586..9f34cc5d751b9 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -49,7 +49,23 @@ QWidget *FieldSelectorDelegate::createEditor( QWidget *parent, const QStyleOptio { Q_UNUSED( option ) - if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) + QgsVectorLayer *vl = indexToLayer( index.model(), index ); + if ( !vl ) + return nullptr; + + if ( index.column() == LAYER_COL ) + { + QLineEdit *le = new QLineEdit( parent ); + return le; + } + else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + { + QgsFieldComboBox *w = new QgsFieldComboBox( parent ); + w->setLayer( vl ); + w->setAllowEmptyFieldName( true ); + return w; + } + else if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) { return nullptr; } @@ -60,44 +76,68 @@ QWidget *FieldSelectorDelegate::createEditor( QWidget *parent, const QStyleOptio return le; } - QgsVectorLayer *vl = indexToLayer( index.model(), index ); - if ( !vl ) - return nullptr; - - QgsFieldComboBox *w = new QgsFieldComboBox( parent ); - w->setLayer( vl ); - w->setAllowEmptyFieldName( true ); - return w; + return nullptr; } void FieldSelectorDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const { - if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + QgsVectorLayer *vl = indexToLayer( index.model(), index ); + if ( !vl ) + return; + + if ( index.column() == LAYER_COL ) + { + QLineEdit *le = qobject_cast< QLineEdit * >( editor ); + if ( le ) + { + le->setText( index.data().toString() ); + } + } + else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + { + QgsFieldComboBox *fcb = qobject_cast( editor ); + if ( !fcb ) + return; + + int idx = attributeIndex( index.model(), vl ); + if ( vl->fields().exists( idx ) ) + { + fcb->setField( vl->fields().at( idx ).name() ); + } + } + else if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) { QLineEdit *le = qobject_cast( editor ); if ( le ) { le->setText( index.data().toString() ); } - return; } +} +void FieldSelectorDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ QgsVectorLayer *vl = indexToLayer( index.model(), index ); if ( !vl ) return; - QgsFieldComboBox *fcb = qobject_cast( editor ); - if ( !fcb ) - return; - - int idx = attributeIndex( index.model(), vl ); - if ( vl->fields().exists( idx ) ) - fcb->setField( vl->fields().at( idx ).name() ); -} + if ( index.column() == LAYER_COL ) + { + QLineEdit *le = qobject_cast( editor ); + if ( le ) + { + model->setData( index, le->text() ); + } + } + else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + { + QgsFieldComboBox *fcb = qobject_cast( editor ); + if ( !fcb ) + return; -void FieldSelectorDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const -{ - if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + model->setData( index, vl->fields().lookupField( fcb->currentField() ) ); + } + else if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) { QLineEdit *le = qobject_cast( editor ); if ( le ) @@ -105,16 +145,6 @@ void FieldSelectorDelegate::setModelData( QWidget *editor, QAbstractItemModel *m model->setData( index, le->text().toInt() ); } } - - QgsVectorLayer *vl = indexToLayer( index.model(), index ); - if ( !vl ) - return; - - QgsFieldComboBox *fcb = qobject_cast( editor ); - if ( !fcb ) - return; - - model->setData( index, vl->fields().lookupField( fcb->currentField() ) ); } QgsVectorLayer *FieldSelectorDelegate::indexToLayer( const QAbstractItemModel *model, const QModelIndex &index ) const @@ -167,7 +197,7 @@ Qt::ItemFlags QgsVectorLayerAndAttributeModel::flags( const QModelIndex &index ) QgsVectorLayer *vl = vectorLayer( index ); if ( index.column() == LAYER_COL ) { - return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; + return vl ? Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable : Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; } else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) { @@ -229,6 +259,9 @@ QVariant QgsVectorLayerAndAttributeModel::headerData( int section, Qt::Orientati QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role ) const { QgsVectorLayer *vl = vectorLayer( idx ); + if ( !vl ) + return QVariant(); + if ( idx.column() == LAYER_COL ) { if ( role == Qt::CheckStateRole ) @@ -277,12 +310,37 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role Q_ASSERT( hasUnchecked ); return Qt::Unchecked; } + else if ( role == Qt::DisplayRole && mOverriddenName.contains( vl ) ) + { + return mOverriddenName[ vl ]; + } else + { return QgsLayerTreeModel::data( idx, role ); + } + } + else if ( idx.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + { + int idx = mAttributeIdx.value( vl, -1 ); + if ( role == Qt::EditRole ) + return idx; + + if ( role == Qt::DisplayRole ) + { + if ( vl->fields().exists( idx ) ) + return vl->fields().at( idx ).name(); + else + return mOverriddenName.contains( vl ) ? mOverriddenName[ vl ] : vl->name(); + } + + if ( role == Qt::ToolTipRole ) + { + return tr( "Attribute containing the name of the destination layer in the DXF output." ); + } } else if ( idx.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) { - if ( !vl || vl->geometryType() != Qgis::GeometryType::Point ) + if ( vl->geometryType() != Qgis::GeometryType::Point ) { return QVariant(); } @@ -299,7 +357,7 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role } else if ( idx.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) { - if ( !vl || vl->geometryType() != Qgis::GeometryType::Point ) + if ( vl->geometryType() != Qgis::GeometryType::Point ) { return QVariant(); } @@ -317,74 +375,66 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role } } - - if ( idx.column() == OUTPUT_LAYER_ATTRIBUTE_COL && vl ) - { - int idx = mAttributeIdx.value( vl, -1 ); - if ( role == Qt::EditRole ) - return idx; - - if ( role == Qt::DisplayRole ) - { - if ( vl->fields().exists( idx ) ) - return vl->fields().at( idx ).name(); - else - return vl->name(); - } - - if ( role == Qt::ToolTipRole ) - { - return tr( "Attribute containing the name of the destination layer in the DXF output." ); - } - } - return QVariant(); } bool QgsVectorLayerAndAttributeModel::setData( const QModelIndex &index, const QVariant &value, int role ) { - if ( index.column() == LAYER_COL && role == Qt::CheckStateRole ) + QgsVectorLayer *vl = vectorLayer( index ); + + if ( index.column() == LAYER_COL ) { - int i = 0; - for ( i = 0; ; i++ ) + if ( role == Qt::CheckStateRole ) { - QModelIndex child = QgsVectorLayerAndAttributeModel::index( i, 0, index ); - if ( !child.isValid() ) - break; + int i = 0; + for ( i = 0; ; i++ ) + { + QModelIndex child = QgsVectorLayerAndAttributeModel::index( i, 0, index ); + if ( !child.isValid() ) + break; - setData( child, value, role ); - } + setData( child, value, role ); + } + + if ( i == 0 ) + { + if ( value.toInt() == Qt::Checked ) + mCheckedLeafs.insert( index ); + else if ( value.toInt() == Qt::Unchecked ) + mCheckedLeafs.remove( index ); + else + Q_ASSERT( "expected checked or unchecked" ); - if ( i == 0 ) + emit dataChanged( QModelIndex(), index ); + } + + return true; + } + else if ( role == Qt::EditRole ) { - if ( value.toInt() == Qt::Checked ) - mCheckedLeafs.insert( index ); - else if ( value.toInt() == Qt::Unchecked ) - mCheckedLeafs.remove( index ); + if ( !value.toString().trimmed().isEmpty() && value.toString() != vl->name() ) + { + mOverriddenName[ vl ] = value.toString(); + } else - Q_ASSERT( "expected checked or unchecked" ); - - emit dataChanged( QModelIndex(), index ); + { + mOverriddenName.remove( vl ); + } + return true; } - - return true; } - - QgsVectorLayer *vl = vectorLayer( index ); - if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) { if ( role != Qt::EditRole ) return false; - if ( vl ) { mAttributeIdx[ vl ] = value.toInt(); return true; } } - - if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL && role == Qt::CheckStateRole ) + else if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL && role == Qt::CheckStateRole ) { if ( vl ) { diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index d58d0059b460d..0a696617f6539 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -81,6 +81,7 @@ class QgsVectorLayerAndAttributeModel : public QgsLayerTreeModel QHash mAttributeIdx; QHash mCreateDDBlockInfo; QHash mDDBlocksMaxNumberOfClasses; + QHash mOverriddenName; QSet mCheckedLeafs; void applyVisibility( QSet &visibleLayers, QgsLayerTreeNode *node ); From 4b2c7deedc7d6ea199beb027bcfa6ab94fcb62ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 19 Mar 2024 17:36:55 +0100 Subject: [PATCH 02/46] [core] Allow users to override a layer name when exporting to DXF; DxfLayerJob->layerTitle replaced by layerDerivedName, to avoid confusion with one of the final DXF layer name sources (layer metadata title, layer server properties title, layer name, layer's overridden name) --- .../auto_generated/dxf/qgsdxfexport.sip.in | 9 ++++++++- .../auto_generated/dxf/qgsdxfexport.sip.in | 9 ++++++++- src/app/qgsdxfexportdialog.cpp | 12 ++++++++++-- src/core/dxf/qgsdxfexport.cpp | 18 +++++++++++++----- src/core/dxf/qgsdxfexport.h | 15 ++++++++++++++- src/core/dxf/qgsdxfexport_p.h | 7 +++---- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in b/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in index 98c957ce874bb..9ca5450c67304 100644 --- a/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in +++ b/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in @@ -27,7 +27,7 @@ Exports QGIS layers to the DXF format. struct DxfLayer { - DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = true, int ddBlocksMaxNumberOfClasses = -1 ); + DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = true, int ddBlocksMaxNumberOfClasses = -1, QString overriddenName = QString() ); QgsVectorLayer *layer() const; %Docstring @@ -66,6 +66,13 @@ Returns the maximum number of data defined symbol classes for which blocks are c :return: +.. versionadded:: 3.38 +%End + + QString overriddenName() const; +%Docstring +Returns the overridden layer name to be used in the exported DXF. + .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/dxf/qgsdxfexport.sip.in b/python/core/auto_generated/dxf/qgsdxfexport.sip.in index 67ede91a94d6d..61e474a533bde 100644 --- a/python/core/auto_generated/dxf/qgsdxfexport.sip.in +++ b/python/core/auto_generated/dxf/qgsdxfexport.sip.in @@ -27,7 +27,7 @@ Exports QGIS layers to the DXF format. struct DxfLayer { - DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = true, int ddBlocksMaxNumberOfClasses = -1 ); + DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = true, int ddBlocksMaxNumberOfClasses = -1, QString overriddenName = QString() ); QgsVectorLayer *layer() const; %Docstring @@ -66,6 +66,13 @@ Returns the maximum number of data defined symbol classes for which blocks are c :return: +.. versionadded:: 3.38 +%End + + QString overriddenName() const; +%Docstring +Returns the overridden layer name to be used in the exported DXF. + .. versionadded:: 3.38 %End diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 9f34cc5d751b9..2f1f5ee855670 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -473,7 +473,11 @@ QList< QgsDxfExport::DxfLayer > QgsVectorLayerAndAttributeModel::layers() const if ( !layerIdx.contains( vl->id() ) ) { layerIdx.insert( vl->id(), layers.size() ); - layers << QgsDxfExport::DxfLayer( vl, mAttributeIdx.value( vl, -1 ), mCreateDDBlockInfo.value( vl, true ), mDDBlocksMaxNumberOfClasses.value( vl, -1 ) ); + layers << QgsDxfExport::DxfLayer( vl, + mAttributeIdx.value( vl, -1 ), + mCreateDDBlockInfo.value( vl, true ), + mDDBlocksMaxNumberOfClasses.value( vl, -1 ), + mOverriddenName.value( vl, QString() ) ); } } } @@ -484,7 +488,11 @@ QList< QgsDxfExport::DxfLayer > QgsVectorLayerAndAttributeModel::layers() const if ( !layerIdx.contains( vl->id() ) ) { layerIdx.insert( vl->id(), layers.size() ); - layers << QgsDxfExport::DxfLayer( vl, mAttributeIdx.value( vl, -1 ), mCreateDDBlockInfo.value( vl, true ), mDDBlocksMaxNumberOfClasses.value( vl, -1 ) ); + layers << QgsDxfExport::DxfLayer( vl, + mAttributeIdx.value( vl, -1 ), + mCreateDDBlockInfo.value( vl, true ), + mDDBlocksMaxNumberOfClasses.value( vl, -1 ), + mOverriddenName.value( vl, QString() ) ); } } } diff --git a/src/core/dxf/qgsdxfexport.cpp b/src/core/dxf/qgsdxfexport.cpp index c974fa15fed87..3485abbff7dd4 100644 --- a/src/core/dxf/qgsdxfexport.cpp +++ b/src/core/dxf/qgsdxfexport.cpp @@ -89,17 +89,24 @@ void QgsDxfExport::addLayers( const QList &layers ) { mLayerList.clear(); mLayerNameAttribute.clear(); + mLayerOverriddenName.clear(); mLayerList.reserve( layers.size() ); for ( const DxfLayer &dxfLayer : layers ) { mLayerList << dxfLayer.layer(); if ( dxfLayer.layerOutputAttributeIndex() >= 0 ) + { mLayerNameAttribute.insert( dxfLayer.layer()->id(), dxfLayer.layerOutputAttributeIndex() ); + } if ( dxfLayer.buildDataDefinedBlocks() ) { mLayerDDBlockMaxNumberOfClasses.insert( dxfLayer.layer()->id(), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ); } + if ( dxfLayer.overriddenName() != QString() ) + { + mLayerOverriddenName.insert( dxfLayer.layer()->id(), dxfLayer.overriddenName() ); + } } } @@ -776,7 +783,7 @@ void QgsDxfExport::writeEntities() while ( featureIt.nextFeature( fet ) ) { mRenderContext.expressionContext().setFeature( fet ); - QString lName( dxfLayerName( job->splitLayerAttribute.isNull() ? job->layerTitle : fet.attribute( job->splitLayerAttribute ).toString() ) ); + QString lName( dxfLayerName( job->splitLayerAttribute.isNull() ? job->layerDerivedName : fet.attribute( job->splitLayerAttribute ).toString() ) ); sctx.setFeature( &fet ); @@ -903,7 +910,7 @@ void QgsDxfExport::prepareRenderers() const QgsFields fields = vl->fields(); if ( splitLayerAttributeIndex >= 0 && splitLayerAttributeIndex < fields.size() ) splitLayerAttribute = fields.at( splitLayerAttributeIndex ).name(); - DxfLayerJob *job = new DxfLayerJob( vl, mMapSettings.layerStyleOverrides().value( vl->id() ), mRenderContext, this, splitLayerAttribute ); + DxfLayerJob *job = new DxfLayerJob( vl, mMapSettings.layerStyleOverrides().value( vl->id() ), mRenderContext, this, splitLayerAttribute, layerName( vl ) ); mJobs.append( job ); } } @@ -2384,9 +2391,10 @@ QStringList QgsDxfExport::encodings() QString QgsDxfExport::layerName( QgsVectorLayer *vl ) const { Q_ASSERT( vl ); - return mLayerTitleAsName && ( !vl->metadata().title().isEmpty() || !vl->serverProperties()->title().isEmpty() ) - ? ( !vl->metadata().title().isEmpty() ? vl->metadata().title() : vl->serverProperties()->title() ) - : vl->name(); + if ( mLayerTitleAsName && ( !vl->metadata().title().isEmpty() || !vl->serverProperties()->title().isEmpty() ) ) + return !vl->metadata().title().isEmpty() ? vl->metadata().title() : vl->serverProperties()->title(); + else + return mLayerOverriddenName.value( vl->id(), vl->name() ); } void QgsDxfExport::drawLabel( const QString &layerId, QgsRenderContext &context, pal::LabelPosition *label, const QgsPalLayerSettings &settings ) diff --git a/src/core/dxf/qgsdxfexport.h b/src/core/dxf/qgsdxfexport.h index 110ef2f7e1b94..1c99c1f0b8e6f 100644 --- a/src/core/dxf/qgsdxfexport.h +++ b/src/core/dxf/qgsdxfexport.h @@ -73,11 +73,12 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink */ struct CORE_EXPORT DxfLayer { - DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = true, int ddBlocksMaxNumberOfClasses = -1 ) + DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = true, int ddBlocksMaxNumberOfClasses = -1, QString overriddenName = QString() ) : mLayer( vl ) , mLayerOutputAttributeIndex( layerOutputAttributeIndex ) , mBuildDDBlocks( buildDDBlocks ) , mDDBlocksMaxNumberOfClasses( ddBlocksMaxNumberOfClasses ) + , mOverriddenName( overriddenName ) {} //! Returns the layer @@ -112,6 +113,12 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink */ int dataDefinedBlocksMaximumNumberOfClasses() const { return mDDBlocksMaxNumberOfClasses; } + /** + * \brief Returns the overridden layer name to be used in the exported DXF. + * \since QGIS 3.38 + */ + QString overriddenName() const { return mOverriddenName; } + private: QgsVectorLayer *mLayer = nullptr; int mLayerOutputAttributeIndex = -1; @@ -125,6 +132,11 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink * \brief Limit for the number of data defined symbol block classes (keep only the most used ones). -1 means no limit */ int mDDBlocksMaxNumberOfClasses = -1; + + /** + * \brief Overridden name of the layer to be exported to DXF + */ + QString mOverriddenName = QString(); }; //! Export flags @@ -667,6 +679,7 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink QList mLayerList; QHash mLayerNameAttribute; QHash mLayerDDBlockMaxNumberOfClasses; + QHash mLayerOverriddenName; double mFactor = 1.0; bool mForce2d = false; diff --git a/src/core/dxf/qgsdxfexport_p.h b/src/core/dxf/qgsdxfexport_p.h index 742323aa3cb40..f315a97f09e2f 100644 --- a/src/core/dxf/qgsdxfexport_p.h +++ b/src/core/dxf/qgsdxfexport_p.h @@ -33,7 +33,7 @@ */ struct DxfLayerJob { - DxfLayerJob( QgsVectorLayer *vl, const QString &layerStyleOverride, QgsRenderContext &renderContext, QgsDxfExport *dxfExport, const QString &splitLayerAttribute ) + DxfLayerJob( QgsVectorLayer *vl, const QString &layerStyleOverride, QgsRenderContext &renderContext, QgsDxfExport *dxfExport, const QString &splitLayerAttribute, const QString &layerDerivedName ) : renderContext( renderContext ) , styleOverride( vl ) , featureSource( vl ) @@ -41,8 +41,7 @@ struct DxfLayerJob , crs( vl->crs() ) , layerName( vl->name() ) , splitLayerAttribute( splitLayerAttribute ) - , layerTitle( !vl->metadata().title().isEmpty() ? vl->metadata().title() - : vl->serverProperties()->title().isEmpty() ? vl->name() : vl->serverProperties()->title() ) + , layerDerivedName( layerDerivedName ) { if ( !layerStyleOverride.isNull() ) { @@ -106,7 +105,7 @@ struct DxfLayerJob QgsLabelSinkProvider *labelProvider = nullptr; QgsRuleBasedLabelSinkProvider *ruleBasedLabelProvider = nullptr; QString splitLayerAttribute; - QString layerTitle; + QString layerDerivedName; // Obtained from layer title, name or overridden name QSet attributes; private: From d4e46009bbcfe5bac021888137df566ac1423203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 19 Mar 2024 18:15:07 +0100 Subject: [PATCH 03/46] Let group nodes show its name in DXF Export dialog --- src/app/qgsdxfexportdialog.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 2f1f5ee855670..577fb3901183b 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -259,8 +259,6 @@ QVariant QgsVectorLayerAndAttributeModel::headerData( int section, Qt::Orientati QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role ) const { QgsVectorLayer *vl = vectorLayer( idx ); - if ( !vl ) - return QVariant(); if ( idx.column() == LAYER_COL ) { @@ -310,7 +308,7 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role Q_ASSERT( hasUnchecked ); return Qt::Unchecked; } - else if ( role == Qt::DisplayRole && mOverriddenName.contains( vl ) ) + else if ( role == Qt::DisplayRole && vl && mOverriddenName.contains( vl ) ) { return mOverriddenName[ vl ]; } @@ -319,7 +317,7 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role return QgsLayerTreeModel::data( idx, role ); } } - else if ( idx.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + else if ( idx.column() == OUTPUT_LAYER_ATTRIBUTE_COL && vl ) { int idx = mAttributeIdx.value( vl, -1 ); if ( role == Qt::EditRole ) @@ -340,7 +338,7 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role } else if ( idx.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) { - if ( vl->geometryType() != Qgis::GeometryType::Point ) + if ( !vl || vl->geometryType() != Qgis::GeometryType::Point ) { return QVariant(); } @@ -357,7 +355,7 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role } else if ( idx.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) { - if ( vl->geometryType() != Qgis::GeometryType::Point ) + if ( !vl || vl->geometryType() != Qgis::GeometryType::Point ) { return QVariant(); } From 1fa0b1651ea43cf2f72cfa3d5c527fa83df345c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Fri, 22 Mar 2024 15:58:16 +0100 Subject: [PATCH 04/46] [tests] Add tests for output layer name precedence in DXF Export (1: attribute, 2: layer title, 3: overridden name, 4: layer name) --- tests/src/core/testqgsdxfexport.cpp | 159 ++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/tests/src/core/testqgsdxfexport.cpp b/tests/src/core/testqgsdxfexport.cpp index adfe17feb8943..38ec4fb4b35ae 100644 --- a/tests/src/core/testqgsdxfexport.cpp +++ b/tests/src/core/testqgsdxfexport.cpp @@ -55,6 +55,7 @@ class TestQgsDxfExport : public QObject void testPoints(); void testPointsDataDefinedSizeAngle(); void testPointsDataDefinedSizeSymbol(); + void testPointsOverriddenName(); void testLines(); void testPolygons(); void testMultiSurface(); @@ -81,6 +82,7 @@ class TestQgsDxfExport : public QObject void testSelectedPolygons(); void testMultipleLayersWithSelection(); void testExtentWithSelection(); + void testOutputLayerNamePrecedence(); private: QgsVectorLayer *mPointLayer = nullptr; @@ -283,6 +285,40 @@ void TestQgsDxfExport::testPointsDataDefinedSizeSymbol() QVERIFY( dxfString.contains( QStringLiteral( "50\n5.0" ) ) ); } +void TestQgsDxfExport::testPointsOverriddenName() +{ + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer, -1, false, -1, QStringLiteral( "My Point Layer" ) ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mPointLayer->extent() ); + mapSettings.setLayers( QList() << mPointLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPointLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + + const QString file = getTempFileName( "point_overridden_name_dxf" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + QVERIFY( !fileContainsText( file, QStringLiteral( "nan.0" ) ) ); + QVERIFY( !fileContainsText( file, mPointLayer->name() ) ); // "points" + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + QgsFeature feature; + result->getFeatures().nextFeature( feature ); + QCOMPARE( feature.attribute( "Layer" ), QStringLiteral( "My Point Layer" ) ); +} + void TestQgsDxfExport::testLines() { QgsDxfExport d; @@ -1773,6 +1809,129 @@ void TestQgsDxfExport::testExtentWithSelection() mPointLayer->removeSelection(); } +void TestQgsDxfExport::testOutputLayerNamePrecedence() +{ + // Test that output layer name precedence is: + // 1) Attribute (if any) + // 2) Layer title (if any) + // 3) Overridden name (if any) + // 4) Layer name + + const QString layerTitle = QStringLiteral( "Point Layer Title" ); + const QString layerOverriddenName = QStringLiteral( "My Point Layer" ); + + // A) All layer name options are set + QgsDxfExport d; + mPointLayer->setTitle( layerTitle ); + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer, + 0, // Class attribute, 3 unique values + false, + -1, + layerOverriddenName ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mPointLayer->extent() ); + mapSettings.setLayers( QList() << mPointLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPointLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + d.setLayerTitleAsName( true ); + + const QString file = getTempFileName( "name_precedence_a_all_set_dxf" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + QVERIFY( !fileContainsText( file, QStringLiteral( "nan.0" ) ) ); + QVERIFY( !fileContainsText( file, layerTitle ) ); + QVERIFY( !fileContainsText( file, layerOverriddenName ) ); + QVERIFY( !fileContainsText( file, mPointLayer->name() ) ); + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + QSet values = result->uniqueValues( 0 ); // "Layer" field + QCOMPARE( values.count(), 3 ); + QVERIFY( values.contains( QVariant( "B52" ) ) ); + QVERIFY( values.contains( QVariant( "Jet" ) ) ); + QVERIFY( values.contains( QVariant( "Biplane" ) ) ); + + // B) No attribute given + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer, -1, false, -1, layerOverriddenName ) ); // this replaces layers + + const QString file2 = getTempFileName( "name_precedence_b_no_attr_dxf" ); + QFile dxfFile2( file2 ); + QCOMPARE( d.writeToFile( &dxfFile2, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile2.close(); + + QVERIFY( !fileContainsText( file2, QStringLiteral( "nan.0" ) ) ); + QVERIFY( fileContainsText( file2, layerTitle ) ); + QVERIFY( !fileContainsText( file2, layerOverriddenName ) ); + QVERIFY( !fileContainsText( file2, mPointLayer->name() ) ); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file2, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + QgsFeature feature; + result->getFeatures().nextFeature( feature ); + QCOMPARE( feature.attribute( "Layer" ), layerTitle ); + QCOMPARE( result->uniqueValues( 0 ).count(), 1 ); // "Layer" field + + // C) No attribute given, choose no title + d.setLayerTitleAsName( false ); + + const QString file3 = getTempFileName( "name_precedence_c_no_attr_no_title_dxf" ); + QFile dxfFile3( file3 ); + QCOMPARE( d.writeToFile( &dxfFile3, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile3.close(); + + QVERIFY( !fileContainsText( file3, QStringLiteral( "nan.0" ) ) ); + QVERIFY( !fileContainsText( file3, layerTitle ) ); + QVERIFY( fileContainsText( file3, layerOverriddenName ) ); + QVERIFY( !fileContainsText( file3, mPointLayer->name() ) ); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file3, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + result->getFeatures().nextFeature( feature ); + QCOMPARE( feature.attribute( "Layer" ), layerOverriddenName ); + QCOMPARE( result->uniqueValues( 0 ).count(), 1 ); // "Layer" field + + // D) No name options given, use default layer name + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer ) ); // This replaces layers + + const QString file4 = getTempFileName( "name_precedence_d_no_anything_dxf" ); + QFile dxfFile4( file4 ); + QCOMPARE( d.writeToFile( &dxfFile4, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile4.close(); + + QVERIFY( !fileContainsText( file4, QStringLiteral( "nan.0" ) ) ); + QVERIFY( !fileContainsText( file4, layerTitle ) ); + QVERIFY( !fileContainsText( file4, layerOverriddenName ) ); + QVERIFY( fileContainsText( file4, mPointLayer->name() ) ); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file4, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + result->getFeatures().nextFeature( feature ); + QCOMPARE( feature.attribute( "Layer" ), mPointLayer->name() ); + QCOMPARE( result->uniqueValues( 0 ).count(), 1 ); // "Layer" field + + mPointLayer->setTitle( QString() ); // Leave the original empty title +} + bool TestQgsDxfExport::fileContainsText( const QString &path, const QString &text, QString *debugInfo ) const { QStringList debugLines; From f2137549a4736582779fa8d6a05f7ef881c2b21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Mon, 25 Mar 2024 10:39:29 +0100 Subject: [PATCH 05/46] [processing] Allow users to override output layer name when exporting to DXF layers --- .../qgsprocessingparameterdxflayers.cpp | 12 +++++-- .../qgsprocessingdxflayerswidgetwrapper.cpp | 16 ++++++++- .../qgsprocessingdxflayerdetailswidgetbase.ui | 34 ++++++++++++------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/core/processing/qgsprocessingparameterdxflayers.cpp b/src/core/processing/qgsprocessingparameterdxflayers.cpp index 816e989363fee..ff9eb832d2ffc 100644 --- a/src/core/processing/qgsprocessingparameterdxflayers.cpp +++ b/src/core/processing/qgsprocessingparameterdxflayers.cpp @@ -85,7 +85,7 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in { const QVariantMap layerMap = variantLayer.toMap(); - if ( !layerMap.contains( QStringLiteral( "layer" ) ) && !layerMap.contains( QStringLiteral( "attributeIndex" ) ) ) + if ( !layerMap.contains( QStringLiteral( "layer" ) ) && !layerMap.contains( QStringLiteral( "attributeIndex" ) ) && !layerMap.contains( QStringLiteral( "overriddenLayerName" ) ) ) return false; if ( !context ) @@ -144,9 +144,12 @@ QString QgsProcessingParameterDxfLayers::valueAsPythonString( const QVariant &va { QStringList layerDefParts; layerDefParts << QStringLiteral( "'layer': " ) + QgsProcessingUtils::stringToPythonLiteral( QgsProcessingUtils::normalizeLayerSource( layer.layer()->source() ) ); + if ( layer.layerOutputAttributeIndex() >= -1 ) layerDefParts << QStringLiteral( "'attributeIndex': " ) + QgsProcessingUtils::variantToPythonLiteral( layer.layerOutputAttributeIndex() ); + layerDefParts << QStringLiteral( "'overriddenLayerName': " ) + QgsProcessingUtils::stringToPythonLiteral( layer.overriddenName() ); + const QString layerDef = QStringLiteral( "{%1}" ).arg( layerDefParts.join( ',' ) ); parts << layerDef; } @@ -239,7 +242,11 @@ QgsDxfExport::DxfLayer QgsProcessingParameterDxfLayers::variantMapAsLayer( const // bad } - QgsDxfExport::DxfLayer dxfLayer( inputLayer, layerVariantMap[ QStringLiteral( "attributeIndex" ) ].toInt() ); + QgsDxfExport::DxfLayer dxfLayer( inputLayer, + layerVariantMap[ QStringLiteral( "attributeIndex" ) ].toInt(), + false, + -1, + layerVariantMap[ QStringLiteral( "overriddenLayerName" ) ].toString() ); return dxfLayer; } @@ -251,5 +258,6 @@ QVariantMap QgsProcessingParameterDxfLayers::layerAsVariantMap( const QgsDxfExpo vm[ QStringLiteral( "layer" )] = layer.layer()->id(); vm[ QStringLiteral( "attributeIndex" ) ] = layer.layerOutputAttributeIndex(); + vm[ QStringLiteral( "overriddenLayerName" ) ] = layer.overriddenName(); return vm; } diff --git a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp index a3b87d3597412..56b4220ffbb67 100644 --- a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp +++ b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp @@ -51,14 +51,16 @@ QgsProcessingDxfLayerDetailsWidget::QgsProcessingDxfLayerDetailsWidget( const QV if ( mLayer->fields().exists( layer.layerOutputAttributeIndex() ) ) mFieldsComboBox->setField( mLayer->fields().at( layer.layerOutputAttributeIndex() ).name() ); + mOverriddenName->setText( layer.overriddenName() ); connect( mFieldsComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsPanelWidget::widgetChanged ); + connect( mOverriddenName, &QLineEdit::textChanged, this, &QgsPanelWidget::widgetChanged ); } QVariant QgsProcessingDxfLayerDetailsWidget::value() const { const int index = mLayer->fields().lookupField( mFieldsComboBox->currentField() ); - const QgsDxfExport::DxfLayer layer( mLayer, index ); + const QgsDxfExport::DxfLayer layer( mLayer, index, false, -1, mOverriddenName->text().trimmed() ); return QgsProcessingParameterDxfLayers::layerAsVariantMap( layer ); } @@ -104,6 +106,7 @@ QgsProcessingDxfLayersPanelWidget::QgsProcessingDxfLayersPanelWidget( QVariantMap vm; vm["layer"] = layer->id(); vm["attributeIndex"] = -1; + vm["overriddenLayerName"] = QString(); const QString title = layer->name(); addOption( vm, title, false ); @@ -166,8 +169,19 @@ QString QgsProcessingDxfLayersPanelWidget::titleForLayer( const QgsDxfExport::Dx { QString title = layer.layer()->name(); + // if both options are set, the split attribute takes precedence, + // so hide overridden message to give users a hint on the result. if ( layer.layerOutputAttributeIndex() != -1 ) + { title += tr( " [split attribute: %1]" ).arg( layer.splitLayerAttribute() ); + } + else + { + if ( !layer.overriddenName().isEmpty() ) + { + title += tr( " [overridden name: %1]" ).arg( layer.overriddenName() ); + } + } return title; } diff --git a/src/ui/processing/qgsprocessingdxflayerdetailswidgetbase.ui b/src/ui/processing/qgsprocessingdxflayerdetailswidgetbase.ui index ef88ed6fcd66d..542faa8f74840 100644 --- a/src/ui/processing/qgsprocessingdxflayerdetailswidgetbase.ui +++ b/src/ui/processing/qgsprocessingdxflayerdetailswidgetbase.ui @@ -7,28 +7,21 @@ 0 0 393 - 71 + 144 - - - - Attribute - - + + - + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + Qt::Vertical @@ -41,6 +34,23 @@ + + + + Attribute + + + + + + + Output layer name + + + + + + From d8466a4d7b449aa47d95bf03f5531ba4e6a49db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Mon, 25 Mar 2024 10:41:03 +0100 Subject: [PATCH 06/46] [tests] DXF export processing algorithm allows users to override output layer name --- tests/src/analysis/testqgsprocessing.cpp | 17 +++++++++++++++-- tests/src/gui/testprocessinggui.cpp | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index 4dfbd740774d2..9753c6bdc31e8 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -11040,6 +11040,7 @@ void TestQgsProcessing::parameterDxfLayers() QVERIFY( !def->checkValueIsAcceptable( layerList ) ); layerMap["layer"] = "layerName"; layerMap["attributeIndex"] = -1; + layerMap["overriddenLayerName"] = QString(); layerList[0] = layerMap; QVERIFY( def->checkValueIsAcceptable( layerList ) ); QVERIFY( !def->checkValueIsAcceptable( layerList, &context ) ); //no corresponding layer in the context's project @@ -11052,6 +11053,10 @@ void TestQgsProcessing::parameterDxfLayers() layerList[0] = layerMap; QVERIFY( def->checkValueIsAcceptable( layerList, &context ) ); + layerMap["overriddenLayerName"] = QStringLiteral( "My Point Layer" ); + layerList[0] = layerMap; + QVERIFY( def->checkValueIsAcceptable( layerList, &context ) ); + // checkValueIsAcceptable on non-spatial layers QgsVectorLayer *nonSpatialLayer = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "NonSpatialLayer" ), @@ -11073,15 +11078,16 @@ void TestQgsProcessing::parameterDxfLayers() QVariantMap wrongLayerMap; wrongLayerMap["layer"] = "NonSpatialLayer"; wrongLayerMap["attributeIndex"] = -1; + wrongLayerMap["overriddenLayerName"] = QString(); QVariantList wrongLayerMapList; wrongLayerMapList.append( wrongLayerMap ); QVERIFY( !def->checkValueIsAcceptable( wrongLayerMapList, &context ) ); // Check values const QString valueAsPythonString = def->valueAsPythonString( layerList, context ); - QCOMPARE( valueAsPythonString, QStringLiteral( "[{'layer': '%1','attributeIndex': -1}]" ).arg( vectorLayer->source() ) ); + QCOMPARE( valueAsPythonString, QStringLiteral( "[{'layer': '%1','attributeIndex': -1,'overriddenLayerName': 'My Point Layer'}]" ).arg( vectorLayer->source() ) ); QCOMPARE( QString::fromStdString( QgsJsonUtils::jsonFromVariant( def->valueAsJsonObject( layerList, context ) ).dump() ), - QStringLiteral( "[{\"attributeIndex\":-1,\"layer\":\"memory://%1\"}]" ).arg( vectorLayer->source() ) ); + QStringLiteral( "[{\"attributeIndex\":-1,\"layer\":\"memory://%1\",\"overriddenLayerName\":\"My Point Layer\"}]" ).arg( vectorLayer->source() ) ); bool ok = false; QCOMPARE( def->valueAsString( layerList, context, ok ), QString() ); QVERIFY( !ok ); @@ -11092,16 +11098,23 @@ void TestQgsProcessing::parameterDxfLayers() const QString pythonCode = def->asPythonString(); QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDxfLayers('dxf input layer', '')" ) ); + // Default values for parameters other than the vector layer + layerMap["overriddenLayerName"] = QString(); + layerList[0] = layerMap; + const QgsDxfExport::DxfLayer dxfLayer( vectorLayer ); QList dxfList = def->parameterAsLayers( QVariant( vectorLayer->source() ), context ); QCOMPARE( dxfList.at( 0 ).layer()->source(), dxfLayer.layer()->source() ); QCOMPARE( dxfList.at( 0 ).layerOutputAttributeIndex(), dxfLayer.layerOutputAttributeIndex() ); + QCOMPARE( dxfList.at( 0 ).overriddenName(), dxfLayer.overriddenName() ); dxfList = def->parameterAsLayers( QVariant( QStringList() << vectorLayer->source() ), context ); QCOMPARE( dxfList.at( 0 ).layer()->source(), dxfLayer.layer()->source() ); QCOMPARE( dxfList.at( 0 ).layerOutputAttributeIndex(), dxfLayer.layerOutputAttributeIndex() ); + QCOMPARE( dxfList.at( 0 ).overriddenName(), dxfLayer.overriddenName() ); dxfList = def->parameterAsLayers( layerList, context ); QCOMPARE( dxfList.at( 0 ).layer()->source(), dxfLayer.layer()->source() ); QCOMPARE( dxfList.at( 0 ).layerOutputAttributeIndex(), dxfLayer.layerOutputAttributeIndex() ); + QCOMPARE( dxfList.at( 0 ).overriddenName(), dxfLayer.overriddenName() ); } void TestQgsProcessing::parameterAnnotationLayer() diff --git a/tests/src/gui/testprocessinggui.cpp b/tests/src/gui/testprocessinggui.cpp index a73b8ba6eb6d2..00eb9bf2931d7 100644 --- a/tests/src/gui/testprocessinggui.cpp +++ b/tests/src/gui/testprocessinggui.cpp @@ -9988,6 +9988,7 @@ void TestProcessingGui::testDxfLayersWrapper() QVariantMap layerMap; layerMap["layer"] = "PointLayer"; layerMap["attributeIndex"] = -1; + layerMap["overriddenLayerName"] = QString(); layerList.append( layerMap ); QVERIFY( definition.checkValueIsAcceptable( layerList, &context ) ); @@ -9998,7 +9999,7 @@ void TestProcessingGui::testDxfLayersWrapper() QVERIFY( definition.checkValueIsAcceptable( value, &context ) ); QString valueAsPythonString = definition.valueAsPythonString( value, context ); - QCOMPARE( valueAsPythonString, QStringLiteral( "[{'layer': '%1','attributeIndex': -1}]" ).arg( vectorLayer->source() ) ); + QCOMPARE( valueAsPythonString, QStringLiteral( "[{'layer': '%1','attributeIndex': -1,'overriddenLayerName': ''}]" ).arg( vectorLayer->source() ) ); } void TestProcessingGui::testAlignRasterLayersWrapper() From eaa5f3df256db2aaaf7073d4791a700032af6b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 27 Mar 2024 16:11:08 +0100 Subject: [PATCH 07/46] [dxf] Avoid unavailable layers in DXF Export dialog's layer tree --- src/app/qgsdxfexportdialog.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 577fb3901183b..736dfdd31499b 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -826,7 +826,8 @@ void QgsDxfExportDialog::cleanGroup( QgsLayerTreeNode *node ) { if ( QgsLayerTree::isLayer( child ) && ( QgsLayerTree::toLayer( child )->layer()->type() != Qgis::LayerType::Vector || - ! QgsLayerTree::toLayer( child )->layer()->isSpatial() ) ) + ! QgsLayerTree::toLayer( child )->layer()->isSpatial() || + ! QgsLayerTree::toLayer( child )->layer()->isValid() ) ) { toRemove << child; continue; From 7e4e09b86fdc48a319d35c8aedec1ea98f683da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 27 Mar 2024 16:53:27 +0100 Subject: [PATCH 08/46] [ux] Add hints for users on how the titleAsLayerName option works, since precedence is until now not clear for them --- src/analysis/processing/qgsalgorithmdxfexport.cpp | 3 ++- src/ui/qgsdxfexportdialogbase.ui | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/analysis/processing/qgsalgorithmdxfexport.cpp b/src/analysis/processing/qgsalgorithmdxfexport.cpp index 123781eb9ba2a..a9ce7c69921e8 100644 --- a/src/analysis/processing/qgsalgorithmdxfexport.cpp +++ b/src/analysis/processing/qgsalgorithmdxfexport.cpp @@ -47,7 +47,8 @@ QString QgsDxfExportAlgorithm::groupId() const QString QgsDxfExportAlgorithm::shortHelpString() const { - return QObject::tr( "Exports layers to DXF file. For each layer, you can choose a field whose values are used to split features in generated destination layers in the DXF output." ); + return QObject::tr( "Exports layers to DXF file. For each layer, you can choose a field whose values are used to split features in generated destination layers in the DXF output.\n\n" + "If no field is chosen, you can still override the output layer name by preferring layer title (set in layer properties) or by directly entering a new output layer name in the Configure Layer panel." ); } QgsDxfExportAlgorithm *QgsDxfExportAlgorithm::createInstance() const diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index 679c46bf1b2cd..d8596064824cf 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -171,6 +171,9 @@ + + If no attribute is chosen, prefer layer title (set in layer properties) to layer name. + Use layer title as name if set From b18c169ce5ace91363f648ef29032a1759d08882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Fri, 5 Apr 2024 14:24:51 +0200 Subject: [PATCH 09/46] [dxf] Apply review comments --- src/core/dxf/qgsdxfexport.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/dxf/qgsdxfexport.h b/src/core/dxf/qgsdxfexport.h index 1c99c1f0b8e6f..54a213bc4fa92 100644 --- a/src/core/dxf/qgsdxfexport.h +++ b/src/core/dxf/qgsdxfexport.h @@ -136,7 +136,7 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink /** * \brief Overridden name of the layer to be exported to DXF */ - QString mOverriddenName = QString(); + QString mOverriddenName; }; //! Export flags From ed56c4c76827cb345cde02065a28b18409bd8ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 9 Apr 2024 16:03:28 +0200 Subject: [PATCH 10/46] [ux] Add help for DXF Export processing parameter 'Use layer title as name' --- src/analysis/processing/qgsalgorithmdxfexport.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/analysis/processing/qgsalgorithmdxfexport.cpp b/src/analysis/processing/qgsalgorithmdxfexport.cpp index a9ce7c69921e8..8d2fa75f0af33 100644 --- a/src/analysis/processing/qgsalgorithmdxfexport.cpp +++ b/src/analysis/processing/qgsalgorithmdxfexport.cpp @@ -71,7 +71,9 @@ void QgsDxfExportAlgorithm::initAlgorithm( const QVariantMap & ) extentParam->setHelp( QObject::tr( "Limit exported features to those with geometries intersecting the provided extent" ) ); addParameter( extentParam.release() ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "SELECTED_FEATURES_ONLY" ), QObject::tr( "Use only selected features" ), false ) ); - addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "USE_LAYER_TITLE" ), QObject::tr( "Use layer title as name" ), false ) ); + std::unique_ptr useTitleParam = std::make_unique( QStringLiteral( "USE_LAYER_TITLE" ), QObject::tr( "Use layer title as name" ), false ); + useTitleParam->setHelp( QObject::tr( "If no attribute is chosen, prefer layer title (set in layer properties) to layer name" ) ); + addParameter( useTitleParam.release() ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "FORCE_2D" ), QObject::tr( "Force 2D output" ), false ) ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "MTEXT" ), QObject::tr( "Export labels as MTEXT elements" ), true ) ); addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "DXF" ), QObject::tr( "DXF Files" ) + " (*.dxf *.DXF)" ) ); From a76439d76798abe29989c824ed3f18a7cc7b8ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 9 Apr 2024 20:45:07 +0200 Subject: [PATCH 11/46] Make it clear that we want a reference to avoid a compile warning --- src/app/qgsdxfexportdialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 736dfdd31499b..25b1772e1ab6a 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -1108,7 +1108,7 @@ void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const QgsVectorLayerRef vlRef; const QgsReadWriteContext rwContext = QgsReadWriteContext(); - for ( const auto dxfLayer : layers() ) + for ( const auto &dxfLayer : layers() ) { QDomElement layerElement = domDocument.createElement( QStringLiteral( "layer" ) ); vlRef.setLayer( dxfLayer.layer() ); From d9d6ad0079df77ba58de200ee59862eb95a6cfb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Wed, 10 Apr 2024 17:25:10 +0200 Subject: [PATCH 12/46] [tests] Replace setTitle() by serverProperties()->setTitle() (conflict resolution with PR 57103) --- tests/src/core/testqgsdxfexport.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/core/testqgsdxfexport.cpp b/tests/src/core/testqgsdxfexport.cpp index 38ec4fb4b35ae..b8101b7b57631 100644 --- a/tests/src/core/testqgsdxfexport.cpp +++ b/tests/src/core/testqgsdxfexport.cpp @@ -1822,7 +1822,7 @@ void TestQgsDxfExport::testOutputLayerNamePrecedence() // A) All layer name options are set QgsDxfExport d; - mPointLayer->setTitle( layerTitle ); + mPointLayer->serverProperties()->setTitle( layerTitle ); d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer, 0, // Class attribute, 3 unique values false, @@ -1929,7 +1929,7 @@ void TestQgsDxfExport::testOutputLayerNamePrecedence() QCOMPARE( feature.attribute( "Layer" ), mPointLayer->name() ); QCOMPARE( result->uniqueValues( 0 ).count(), 1 ); // "Layer" field - mPointLayer->setTitle( QString() ); // Leave the original empty title + mPointLayer->serverProperties()->setTitle( QString() ); // Leave the original empty title } bool TestQgsDxfExport::fileContainsText( const QString &path, const QString &text, QString *debugInfo ) const From c2e248befce59b81f0e6e2ce2b6d7cd6e7cdccd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 16 Apr 2024 11:32:50 +0200 Subject: [PATCH 13/46] [dxf] Allow users to recover original layer name after overriding it (by using the QgsFilterLineEdit with default value) --- src/app/qgsdxfexportdialog.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 25b1772e1ab6a..4e58c1bc01184 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -55,7 +55,8 @@ QWidget *FieldSelectorDelegate::createEditor( QWidget *parent, const QStyleOptio if ( index.column() == LAYER_COL ) { - QLineEdit *le = new QLineEdit( parent ); + QgsFilterLineEdit *le = new QgsFilterLineEdit( parent, vl->name() ); + return le; } else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) @@ -87,7 +88,7 @@ void FieldSelectorDelegate::setEditorData( QWidget *editor, const QModelIndex &i if ( index.column() == LAYER_COL ) { - QLineEdit *le = qobject_cast< QLineEdit * >( editor ); + QgsFilterLineEdit *le = qobject_cast< QgsFilterLineEdit * >( editor ); if ( le ) { le->setText( index.data().toString() ); @@ -123,7 +124,7 @@ void FieldSelectorDelegate::setModelData( QWidget *editor, QAbstractItemModel *m if ( index.column() == LAYER_COL ) { - QLineEdit *le = qobject_cast( editor ); + QgsFilterLineEdit *le = qobject_cast( editor ); if ( le ) { model->setData( index, le->text() ); From 82fb6ccd24c9c36dbc2d7547850d3803e401c76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Carrillo?= Date: Tue, 16 Apr 2024 17:54:02 +0200 Subject: [PATCH 14/46] [dxf] Address review: prefer overridden layer name over layer title (the former can be changed in the export dialog, whereas the latter must be done in the layer properties, therefore, overridden layer name is closer to the export intention and should take the precedence) --- .../processing/qgsalgorithmdxfexport.cpp | 4 ++-- src/core/dxf/qgsdxfexport.cpp | 12 ++++++++-- src/core/dxf/qgsdxfexport_p.h | 2 +- src/ui/qgsdxfexportdialogbase.ui | 2 +- tests/src/core/testqgsdxfexport.cpp | 23 ++++++++++--------- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/analysis/processing/qgsalgorithmdxfexport.cpp b/src/analysis/processing/qgsalgorithmdxfexport.cpp index 8d2fa75f0af33..9e625715923d0 100644 --- a/src/analysis/processing/qgsalgorithmdxfexport.cpp +++ b/src/analysis/processing/qgsalgorithmdxfexport.cpp @@ -48,7 +48,7 @@ QString QgsDxfExportAlgorithm::groupId() const QString QgsDxfExportAlgorithm::shortHelpString() const { return QObject::tr( "Exports layers to DXF file. For each layer, you can choose a field whose values are used to split features in generated destination layers in the DXF output.\n\n" - "If no field is chosen, you can still override the output layer name by preferring layer title (set in layer properties) or by directly entering a new output layer name in the Configure Layer panel." ); + "If no field is chosen, you can still override the output layer name by directly entering a new output layer name in the Configure Layer panel or by preferring layer title (set in layer properties) to layer name." ); } QgsDxfExportAlgorithm *QgsDxfExportAlgorithm::createInstance() const @@ -72,7 +72,7 @@ void QgsDxfExportAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( extentParam.release() ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "SELECTED_FEATURES_ONLY" ), QObject::tr( "Use only selected features" ), false ) ); std::unique_ptr useTitleParam = std::make_unique( QStringLiteral( "USE_LAYER_TITLE" ), QObject::tr( "Use layer title as name" ), false ); - useTitleParam->setHelp( QObject::tr( "If no attribute is chosen, prefer layer title (set in layer properties) to layer name" ) ); + useTitleParam->setHelp( QObject::tr( "If no attribute is chosen and layer name is not being overridden, prefer layer title (set in layer properties) to layer name" ) ); addParameter( useTitleParam.release() ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "FORCE_2D" ), QObject::tr( "Force 2D output" ), false ) ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "MTEXT" ), QObject::tr( "Export labels as MTEXT elements" ), true ) ); diff --git a/src/core/dxf/qgsdxfexport.cpp b/src/core/dxf/qgsdxfexport.cpp index 3485abbff7dd4..b34c48c0b370a 100644 --- a/src/core/dxf/qgsdxfexport.cpp +++ b/src/core/dxf/qgsdxfexport.cpp @@ -2391,10 +2391,18 @@ QStringList QgsDxfExport::encodings() QString QgsDxfExport::layerName( QgsVectorLayer *vl ) const { Q_ASSERT( vl ); - if ( mLayerTitleAsName && ( !vl->metadata().title().isEmpty() || !vl->serverProperties()->title().isEmpty() ) ) + if ( !mLayerOverriddenName.value( vl->id(), QString() ).isEmpty() ) + { + return mLayerOverriddenName.value( vl->id() ); + } + else if ( mLayerTitleAsName && ( !vl->metadata().title().isEmpty() || !vl->serverProperties()->title().isEmpty() ) ) + { return !vl->metadata().title().isEmpty() ? vl->metadata().title() : vl->serverProperties()->title(); + } else - return mLayerOverriddenName.value( vl->id(), vl->name() ); + { + return vl->name(); + } } void QgsDxfExport::drawLabel( const QString &layerId, QgsRenderContext &context, pal::LabelPosition *label, const QgsPalLayerSettings &settings ) diff --git a/src/core/dxf/qgsdxfexport_p.h b/src/core/dxf/qgsdxfexport_p.h index f315a97f09e2f..cb00d13f0be87 100644 --- a/src/core/dxf/qgsdxfexport_p.h +++ b/src/core/dxf/qgsdxfexport_p.h @@ -105,7 +105,7 @@ struct DxfLayerJob QgsLabelSinkProvider *labelProvider = nullptr; QgsRuleBasedLabelSinkProvider *ruleBasedLabelProvider = nullptr; QString splitLayerAttribute; - QString layerDerivedName; // Obtained from layer title, name or overridden name + QString layerDerivedName; // Obtained from overridden name, title or layer name QSet attributes; private: diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index d8596064824cf..39f0bd71b9048 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -172,7 +172,7 @@ - If no attribute is chosen, prefer layer title (set in layer properties) to layer name. + If no attribute is chosen and layer name is not being overridden, prefer layer title (set in layer properties) to layer name. Use layer title as name if set diff --git a/tests/src/core/testqgsdxfexport.cpp b/tests/src/core/testqgsdxfexport.cpp index b8101b7b57631..f409bbc18e66c 100644 --- a/tests/src/core/testqgsdxfexport.cpp +++ b/tests/src/core/testqgsdxfexport.cpp @@ -1813,8 +1813,8 @@ void TestQgsDxfExport::testOutputLayerNamePrecedence() { // Test that output layer name precedence is: // 1) Attribute (if any) - // 2) Layer title (if any) - // 3) Overridden name (if any) + // 2) Overridden name (if any) + // 3) Layer title (if any) // 4) Layer name const QString layerTitle = QStringLiteral( "Point Layer Title" ); @@ -1871,8 +1871,8 @@ void TestQgsDxfExport::testOutputLayerNamePrecedence() dxfFile2.close(); QVERIFY( !fileContainsText( file2, QStringLiteral( "nan.0" ) ) ); - QVERIFY( fileContainsText( file2, layerTitle ) ); - QVERIFY( !fileContainsText( file2, layerOverriddenName ) ); + QVERIFY( !fileContainsText( file2, layerTitle ) ); + QVERIFY( fileContainsText( file2, layerOverriddenName ) ); QVERIFY( !fileContainsText( file2, mPointLayer->name() ) ); // reload and compare @@ -1882,20 +1882,20 @@ void TestQgsDxfExport::testOutputLayerNamePrecedence() QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); QgsFeature feature; result->getFeatures().nextFeature( feature ); - QCOMPARE( feature.attribute( "Layer" ), layerTitle ); + QCOMPARE( feature.attribute( "Layer" ), layerOverriddenName ); QCOMPARE( result->uniqueValues( 0 ).count(), 1 ); // "Layer" field - // C) No attribute given, choose no title - d.setLayerTitleAsName( false ); + // C) No attribute given, no override + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer, -1, false, -1 ) ); // this replaces layers - const QString file3 = getTempFileName( "name_precedence_c_no_attr_no_title_dxf" ); + const QString file3 = getTempFileName( "name_precedence_c_no_attr_no_override_dxf" ); QFile dxfFile3( file3 ); QCOMPARE( d.writeToFile( &dxfFile3, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); dxfFile3.close(); QVERIFY( !fileContainsText( file3, QStringLiteral( "nan.0" ) ) ); - QVERIFY( !fileContainsText( file3, layerTitle ) ); - QVERIFY( fileContainsText( file3, layerOverriddenName ) ); + QVERIFY( fileContainsText( file3, layerTitle ) ); + QVERIFY( !fileContainsText( file3, layerOverriddenName ) ); QVERIFY( !fileContainsText( file3, mPointLayer->name() ) ); // reload and compare @@ -1904,11 +1904,12 @@ void TestQgsDxfExport::testOutputLayerNamePrecedence() QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); result->getFeatures().nextFeature( feature ); - QCOMPARE( feature.attribute( "Layer" ), layerOverriddenName ); + QCOMPARE( feature.attribute( "Layer" ), layerTitle ); QCOMPARE( result->uniqueValues( 0 ).count(), 1 ); // "Layer" field // D) No name options given, use default layer name d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer ) ); // This replaces layers + d.setLayerTitleAsName( false ); const QString file4 = getTempFileName( "name_precedence_d_no_anything_dxf" ); QFile dxfFile4( file4 ); From 5d796e9dfaf966bdd72688d2b1698c8f0f4860ff Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 17 Apr 2024 10:05:17 +0700 Subject: [PATCH 15/46] [qml] Make QgsFields a QGadet and make some functions invokable --- .../PyQt6/core/auto_generated/qgsfields.sip.in | 5 ++++- python/core/auto_generated/qgsfields.sip.in | 5 ++++- src/core/qgsfields.h | 18 ++++++++++-------- src/core/vector/qgsvectorlayer.h | 2 +- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/python/PyQt6/core/auto_generated/qgsfields.sip.in b/python/PyQt6/core/auto_generated/qgsfields.sip.in index 2a3168fa3930a..2df5fa580d373 100644 --- a/python/PyQt6/core/auto_generated/qgsfields.sip.in +++ b/python/PyQt6/core/auto_generated/qgsfields.sip.in @@ -28,6 +28,9 @@ In addition to storing a list of :py:class:`QgsField` instances, it also: %TypeHeaderCode #include "qgsfields.h" %End + public: + static const QMetaObject staticMetaObject; + public: enum FieldOrigin /BaseType=IntEnum/ @@ -289,7 +292,7 @@ name of the field. .. seealso:: :py:func:`lookupField` %End - int lookupField( const QString &fieldName ) const; + int lookupField( const QString &fieldName ) const; %Docstring Looks up field's index from the field name. This method matches in the following order: diff --git a/python/core/auto_generated/qgsfields.sip.in b/python/core/auto_generated/qgsfields.sip.in index b898d911e6abe..f94f5c453b7eb 100644 --- a/python/core/auto_generated/qgsfields.sip.in +++ b/python/core/auto_generated/qgsfields.sip.in @@ -28,6 +28,9 @@ In addition to storing a list of :py:class:`QgsField` instances, it also: %TypeHeaderCode #include "qgsfields.h" %End + public: + static const QMetaObject staticMetaObject; + public: enum FieldOrigin @@ -289,7 +292,7 @@ name of the field. .. seealso:: :py:func:`lookupField` %End - int lookupField( const QString &fieldName ) const; + int lookupField( const QString &fieldName ) const; %Docstring Looks up field's index from the field name. This method matches in the following order: diff --git a/src/core/qgsfields.h b/src/core/qgsfields.h index 4384248ef4d90..63d16aa95ddc4 100644 --- a/src/core/qgsfields.h +++ b/src/core/qgsfields.h @@ -43,6 +43,8 @@ class QgsFieldsPrivate; */ class CORE_EXPORT QgsFields { + Q_GADGET + public: enum FieldOrigin @@ -142,10 +144,10 @@ class CORE_EXPORT QgsFields void extend( const QgsFields &other ); //! Checks whether the container is empty - bool isEmpty() const; + Q_INVOKABLE bool isEmpty() const; //! Returns number of items - int count() const; + Q_INVOKABLE int count() const; #ifdef SIP_RUN int __len__() const; @@ -161,19 +163,19 @@ class CORE_EXPORT QgsFields #endif //! Returns number of items - int size() const; + Q_INVOKABLE int size() const; /** * Returns a list with field names */ - QStringList names() const; + Q_INVOKABLE QStringList names() const; /** * Returns if a field index is valid * \param i Index of the field which needs to be checked * \returns TRUE if the field exists */ - bool exists( int i ) const; + Q_INVOKABLE bool exists( int i ) const; #ifndef SIP_RUN //! Gets field at particular index (must be in range 0..N-1) @@ -354,7 +356,7 @@ class CORE_EXPORT QgsFields * \returns The field index if found or -1 in case it cannot be found. * \see lookupField For a more tolerant alternative. */ - int indexFromName( const QString &fieldName ) const; + Q_INVOKABLE int indexFromName( const QString &fieldName ) const; /** * Gets the field index from the field name. @@ -367,7 +369,7 @@ class CORE_EXPORT QgsFields * \returns The field index if found or -1 in case it cannot be found. * \see lookupField For a more tolerant alternative. */ - int indexOf( const QString &fieldName ) const; + Q_INVOKABLE int indexOf( const QString &fieldName ) const; /** * Looks up field's index from the field name. @@ -382,7 +384,7 @@ class CORE_EXPORT QgsFields * \returns The field index if found or -1 in case it cannot be found. * \see indexFromName For a more performant and precise but less tolerant alternative. */ - int lookupField( const QString &fieldName ) const; + Q_INVOKABLE int lookupField( const QString &fieldName ) const; /** * Utility function to get list of attribute indexes diff --git a/src/core/vector/qgsvectorlayer.h b/src/core/vector/qgsvectorlayer.h index 7a9145141a2fc..06625d70549c9 100644 --- a/src/core/vector/qgsvectorlayer.h +++ b/src/core/vector/qgsvectorlayer.h @@ -1660,7 +1660,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * * \returns A list of fields */ - QgsFields fields() const FINAL; + Q_INVOKABLE QgsFields fields() const FINAL; /** * Returns list of attribute indexes. i.e. a list from 0 ... fieldCount() From 37af25e0b6fc6ec98278b21e84137fd273d8e475 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 17 Apr 2024 12:19:17 +0700 Subject: [PATCH 16/46] Move some invokables to properties --- src/core/qgsfields.h | 12 ++++++++---- src/core/vector/qgsvectorlayer.h | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/core/qgsfields.h b/src/core/qgsfields.h index 63d16aa95ddc4..975ebdcaf7589 100644 --- a/src/core/qgsfields.h +++ b/src/core/qgsfields.h @@ -45,6 +45,10 @@ class CORE_EXPORT QgsFields { Q_GADGET + Q_PROPERTY( bool isEmpty READ isEmpty ) + Q_PROPERTY( int count READ count ) + Q_PROPERTY( QStringList names READ names ) + public: enum FieldOrigin @@ -144,10 +148,10 @@ class CORE_EXPORT QgsFields void extend( const QgsFields &other ); //! Checks whether the container is empty - Q_INVOKABLE bool isEmpty() const; + bool isEmpty() const; //! Returns number of items - Q_INVOKABLE int count() const; + int count() const; #ifdef SIP_RUN int __len__() const; @@ -163,12 +167,12 @@ class CORE_EXPORT QgsFields #endif //! Returns number of items - Q_INVOKABLE int size() const; + int size() const; /** * Returns a list with field names */ - Q_INVOKABLE QStringList names() const; + QStringList names() const; /** * Returns if a field index is valid diff --git a/src/core/vector/qgsvectorlayer.h b/src/core/vector/qgsvectorlayer.h index 06625d70549c9..26b73a65a0c08 100644 --- a/src/core/vector/qgsvectorlayer.h +++ b/src/core/vector/qgsvectorlayer.h @@ -405,6 +405,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte Q_PROPERTY( QgsEditFormConfig editFormConfig READ editFormConfig WRITE setEditFormConfig NOTIFY editFormConfigChanged ) Q_PROPERTY( bool readOnly READ isReadOnly WRITE setReadOnly NOTIFY readOnlyChanged ) Q_PROPERTY( bool supportsEditing READ supportsEditing NOTIFY supportsEditingChanged ) + Q_PROPERTY( QgsFields fields READ fields NOTIFY updatedFields ) public: @@ -1660,7 +1661,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * * \returns A list of fields */ - Q_INVOKABLE QgsFields fields() const FINAL; + QgsFields fields() const FINAL; /** * Returns list of attribute indexes. i.e. a list from 0 ... fieldCount() From 531afc39359a3ae5758e77721667972c1ef831d3 Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Wed, 17 Apr 2024 19:04:09 +0200 Subject: [PATCH 17/46] [SpatiaLite] Fix getQueryGeometryDetails() for Z, M and ZM --- .../spatialite/qgsspatialiteprovider.cpp | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/providers/spatialite/qgsspatialiteprovider.cpp b/src/providers/spatialite/qgsspatialiteprovider.cpp index 7e732b3bfa074..8874de4be9aaf 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.cpp +++ b/src/providers/spatialite/qgsspatialiteprovider.cpp @@ -5735,30 +5735,7 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() sqlite3_free_table( results ); } - if ( fType == QLatin1String( "POINT" ) ) - { - mGeomType = Qgis::WkbType::Point; - } - else if ( fType == QLatin1String( "MULTIPOINT" ) ) - { - mGeomType = Qgis::WkbType::MultiPoint; - } - else if ( fType == QLatin1String( "LINESTRING" ) ) - { - mGeomType = Qgis::WkbType::LineString; - } - else if ( fType == QLatin1String( "MULTILINESTRING" ) ) - { - mGeomType = Qgis::WkbType::MultiLineString; - } - else if ( fType == QLatin1String( "POLYGON" ) ) - { - mGeomType = Qgis::WkbType::Polygon; - } - else if ( fType == QLatin1String( "MULTIPOLYGON" ) ) - { - mGeomType = Qgis::WkbType::MultiPolygon; - } + mGeomType = QgsWkbTypes::parseType( fType ); mSrid = xSrid.toInt(); } From a9fee347eb701e3529a86ec6cd686399dcee4f1b Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Fri, 19 Apr 2024 15:10:30 +0200 Subject: [PATCH 18/46] [spatialite] Test getQueryGeometryDetails for geom with Z, M or ZM --- tests/src/python/test_provider_spatialite.py | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/src/python/test_provider_spatialite.py b/tests/src/python/test_provider_spatialite.py index 6a41635fb82b5..d2bddafd43f4e 100644 --- a/tests/src/python/test_provider_spatialite.py +++ b/tests/src/python/test_provider_spatialite.py @@ -302,6 +302,13 @@ def setUpClass(cls): sql = "INSERT INTO \"test_transactions2\" VALUES (NULL)" cur.execute(sql) + # table to test getQueryGeometryDetails() for geometries with Z, M and ZM + sql = "CREATE TABLE test_querygeometry (id INTEGER PRIMARY KEY, x DECIMAL, y DECIMAL, z DECIMAL, m DECIMAL, srid INTEGER)" + cur.execute(sql) + sql = "INSERT INTO test_querygeometry (id, x, y, z, m, srid) " + sql += "VALUES (1, 16, 41, 100, 10, 4326)" + cur.execute(sql) + # Commit all test data cur.execute("COMMIT") con.close() @@ -1819,6 +1826,61 @@ def testTransactions(self): self.assertEqual(vl.dataProvider().defaultValueClause(0), '') self.assertEqual(vl.dataProvider().defaultValue(0), 1) + def testGetQueryGeometryDetails(self): + """Test getQueryGeometryDetails() for geometries with Z, M and ZM""" + + query = "SELECT id, srid, x, y, MakePoint(x,y,srid) as geom FROM test_querygeometry" + vl = QgsVectorLayer(f"dbname={self.dbname} table='({query})' (geom) key='id'", + "QueryGeometryXY", "spatialite") + self.assertTrue(vl.isValid()) + self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.Point) + self.assertEqual(vl.featureCount(), 1) + feature = vl.getFeature(1) + self.assertEqual(vl.crs().postgisSrid(), feature.attributes()[1]) + geom = feature.geometry().constGet() + self.assertEqual(geom.x(), feature.attributes()[2]) + self.assertEqual(geom.y(), feature.attributes()[3]) + + query = "SELECT id, srid, x, y, z, MakePointZ(x,y,z,srid) as geom FROM test_querygeometry" + vl = QgsVectorLayer(f"dbname={self.dbname} table='({query})' (geom) key='id'", + "QueryGeometryXYZ", "spatialite") + self.assertTrue(vl.isValid()) + self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.PointZ) + self.assertEqual(vl.featureCount(), 1) + feature = vl.getFeature(1) + self.assertEqual(vl.crs().postgisSrid(), feature.attributes()[1]) + geom = feature.geometry().constGet() + self.assertEqual(geom.x(), feature.attributes()[2]) + self.assertEqual(geom.y(), feature.attributes()[3]) + self.assertEqual(geom.z(), feature.attributes()[4]) + + query = "SELECT id, srid, x, y, m, MakePointM(x,y,m,srid) as geom FROM test_querygeometry" + vl = QgsVectorLayer(f"dbname={self.dbname} table='({query})' (geom) key='id'", + "QueryGeometryXYM", "spatialite") + self.assertTrue(vl.isValid()) + self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.PointM) + self.assertEqual(vl.featureCount(), 1) + feature = vl.getFeature(1) + self.assertEqual(vl.crs().postgisSrid(), feature.attributes()[1]) + geom = feature.geometry().constGet() + self.assertEqual(geom.x(), feature.attributes()[2]) + self.assertEqual(geom.y(), feature.attributes()[3]) + self.assertEqual(geom.m(), feature.attributes()[4]) + + query = "SELECT id, srid, x, y, z, m, MakePointZM(x,y,z,m,srid) as geom FROM test_querygeometry" + vl = QgsVectorLayer(f"dbname={self.dbname} table='({query})' (geom) key='id'", + "QueryGeometryXYZM", "spatialite") + self.assertTrue(vl.isValid()) + self.assertEqual(vl.wkbType(), QgsWkbTypes.Type.PointZM) + self.assertEqual(vl.featureCount(), 1) + feature = vl.getFeature(1) + self.assertEqual(vl.crs().postgisSrid(), feature.attributes()[1]) + geom = feature.geometry().constGet() + self.assertEqual(geom.x(), feature.attributes()[2]) + self.assertEqual(geom.y(), feature.attributes()[3]) + self.assertEqual(geom.z(), feature.attributes()[4]) + self.assertEqual(geom.m(), feature.attributes()[5]) + def testViewsExtentFilter(self): """Test extent filtering of a views-based spatialite layer""" From 51378f9827fad57b9fae30d1641d0a1f538c8b4c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Apr 2024 12:53:08 +1000 Subject: [PATCH 19/46] [processing] Set the active layer to one of the generated outputs Fixes #57003 --- python/plugins/processing/gui/Postprocessing.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/plugins/processing/gui/Postprocessing.py b/python/plugins/processing/gui/Postprocessing.py index f3ae2512d67cd..04ea978ddefa5 100644 --- a/python/plugins/processing/gui/Postprocessing.py +++ b/python/plugins/processing/gui/Postprocessing.py @@ -40,6 +40,7 @@ QgsLayerTreeLayer, QgsLayerTreeGroup ) +from qgis.utils import iface from processing.core.ProcessingConfig import ProcessingConfig from processing.gui.RenderingStyles import RenderingStyles @@ -264,10 +265,15 @@ def handleAlgorithmResults(alg: QgsProcessingAlgorithm, added_layers, key=lambda x: x[1].customProperty(SORT_ORDER_CUSTOM_PROPERTY, 0) ) + have_set_active_layer = False for group, layer_node in sorted_layer_tree_layers: layer_node.removeCustomProperty(SORT_ORDER_CUSTOM_PROPERTY) group.insertChildNode(0, layer_node) + if not have_set_active_layer and iface is not None: + iface.setActiveLayer(layer_node.layer()) + have_set_active_layer = True + # all layers have been added to the layer tree, so safe to call # postProcessors now for layer, details in layers_to_post_process: From fc1922ae4328006a6b6d1ef50918bd20b8b95bfd Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Apr 2024 13:12:23 +1000 Subject: [PATCH 20/46] [processing] When no specific layer group is required for an output, place it above the current selected layer Fixes #56129 --- .../plugins/processing/gui/Postprocessing.py | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/python/plugins/processing/gui/Postprocessing.py b/python/plugins/processing/gui/Postprocessing.py index 04ea978ddefa5..9dfd104af5062 100644 --- a/python/plugins/processing/gui/Postprocessing.py +++ b/python/plugins/processing/gui/Postprocessing.py @@ -38,7 +38,8 @@ QgsProcessingContext, QgsProcessingAlgorithm, QgsLayerTreeLayer, - QgsLayerTreeGroup + QgsLayerTreeGroup, + QgsLayerTreeNode ) from qgis.utils import iface @@ -144,15 +145,15 @@ def create_layer_tree_layer(layer: QgsMapLayer, def get_layer_tree_results_group(details: QgsProcessingContext.LayerDetails, context: QgsProcessingContext) \ - -> QgsLayerTreeGroup: + -> Optional[QgsLayerTreeGroup]: """ - Returns the destination layer tree group to store results in + Returns the destination layer tree group to store results in, or None + if there is no specific destination tree group associated with the layer """ destination_project = details.project or context.project() - # default to placing results in the top level of the layer tree - results_group = details.project.layerTreeRoot() + results_group: Optional[QgsLayerTreeGroup] = None # if a specific results group is specified in Processing settings, # respect it (and create if necessary) @@ -169,6 +170,9 @@ def get_layer_tree_results_group(details: QgsProcessingContext.LayerDetails, # if this particular output layer has a specific output group assigned, # find or create it now if details.groupName: + if results_group is None: + results_group = destination_project.layerTreeRoot() + group = results_group.findGroup(details.groupName) if not group: group = results_group.insertGroup( @@ -198,7 +202,7 @@ def handleAlgorithmResults(alg: QgsProcessingAlgorithm, ) i = 0 - added_layers: List[Tuple[QgsLayerTreeGroup, QgsLayerTreeLayer]] = [] + added_layers: List[Tuple[Optional[QgsLayerTreeGroup], QgsLayerTreeLayer]] = [] layers_to_post_process: List[Tuple[QgsMapLayer, QgsProcessingContext.LayerDetails]] = [] @@ -266,9 +270,30 @@ def handleAlgorithmResults(alg: QgsProcessingAlgorithm, key=lambda x: x[1].customProperty(SORT_ORDER_CUSTOM_PROPERTY, 0) ) have_set_active_layer = False + + current_selected_node: Optional[QgsLayerTreeNode] = None + if iface is not None: + current_selected_node = iface.layerTreeView().currentNode() + iface.layerTreeView().setUpdatesEnabled(False) + for group, layer_node in sorted_layer_tree_layers: layer_node.removeCustomProperty(SORT_ORDER_CUSTOM_PROPERTY) - group.insertChildNode(0, layer_node) + if group is not None: + group.insertChildNode(0, layer_node) + else: + # no destination group for this layer, so should be placed + # above the current layer + if isinstance(current_selected_node, QgsLayerTreeLayer): + current_node_group = current_selected_node.parent() + current_node_index = current_node_group.children().index( + current_selected_node) + current_node_group.insertChildNode( + current_node_index, + layer_node) + elif isinstance(current_selected_node, QgsLayerTreeGroup): + current_selected_node.insertChildNode(0, layer_node) + elif context.project(): + context.project().layerTreeRoot().insertChildNode(0, layer_node) if not have_set_active_layer and iface is not None: iface.setActiveLayer(layer_node.layer()) @@ -282,6 +307,9 @@ def handleAlgorithmResults(alg: QgsProcessingAlgorithm, context, feedback) + if iface is not None: + iface.layerTreeView().setUpdatesEnabled(True) + feedback.setProgress(100) if wrong_layers: From 7355c24db83dd4b369def4da40624ae10df7b5b1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Apr 2024 13:22:52 +1000 Subject: [PATCH 21/46] [processing] Endpoint distance threshold for network analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional end point distance threshold parameter to the network analysis tools. Previously (and still, by default) endpoints will ALWAYS be snapped to the nearest point in the network layer, regardless of how far away from the network they actually are. This can result in meaningless results, as the tools will happily snap points to a road hundreds of kilometers away :) Now, there's an optional end point distance threshold parameter for these tools. The behaviour of the threshold depends on the algorithm: - For the “Service area (from layer)” tool an optional new output was added for “non routable features”. This output will contain any features which were deemed too far from the network. All other features which are within tolerance distance to the network will be stored in the standard output from the tool. - For the “Service area (from point)” tool an error will be raised if the point is too far from the network - For the “Shortest path (point to point)” tool an error will be raised if either the source or destination points are too far from the network. - For the “Shortest path (layer to point)” and “Shortest path (point to layer)” tools: - An error will be raised if the **point** is too far from the network. - A new optional output was added for “non routable features”. This output will contain any features which were deemed too far from the network. All other features which are within tolerance distance to the network will be stored in the standard output from the tool. Sponsored by City of Canning --- ...ervice_area_from_layer_lines_tolerance.gml | 30 +++ ...ervice_area_from_layer_lines_tolerance.xsd | 72 +++++ ...area_from_layer_non_routable_tolerance.gml | 26 ++ ...area_from_layer_non_routable_tolerance.xsd | 60 +++++ ...ath_layer_to_point_max_point_tolerance.gml | 43 +++ ...ath_layer_to_point_max_point_tolerance.xsd | 78 ++++++ ...rtest_path_layer_to_point_non_routable.gml | 18 ++ ...rtest_path_layer_to_point_non_routable.xsd | 60 +++++ .../expected/shortest_path_point_to_layer.gml | 43 +++ .../expected/shortest_path_point_to_layer.xsd | 78 ++++++ ...rtest_path_point_to_layer_non_routable.gml | 18 ++ ...rtest_path_point_to_layer_non_routable.xsd | 60 +++++ .../tests/testdata/qgis_algorithm_tests2.yaml | 247 ++++++++++++++++++ .../qgsalgorithmserviceareafromlayer.cpp | 58 +++- .../qgsalgorithmserviceareafrompoint.cpp | 22 +- .../qgsalgorithmshortestpathlayertopoint.cpp | 65 ++++- .../qgsalgorithmshortestpathpointtolayer.cpp | 65 ++++- .../qgsalgorithmshortestpathpointtopoint.cpp | 35 ++- 18 files changed, 1054 insertions(+), 24 deletions(-) create mode 100644 python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.gml create mode 100644 python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.xsd create mode 100644 python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.gml create mode 100644 python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.xsd create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.gml create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.xsd create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.gml create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.xsd create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.gml create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.xsd create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.gml create mode 100644 python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.xsd diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.gml b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.gml new file mode 100644 index 0000000000000..90860688f4e66 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.gml @@ -0,0 +1,30 @@ + + + 1001182.63176944 6220409.289059481002975.74639523 6222884.39323789 + + + + 1001182.63176944 6220409.289059481001275.12302677 6220462.84851569 + 1001186.34740161 6220459.66433954 1001182.63176944 6220456.480004971001186.34740161 6220459.66433954 1001190.06317444 6220462.848515691001186.34740161 6220459.66433954 1001190.60339941 6220457.249295091001275.12302677 6220409.28905948 1001269.29926109 6220412.59372591001269.29926109 6220412.5937259 1001275.12302677 6220409.289059481001269.29926109 6220412.5937259 1001186.34740161 6220459.66433954 + route_points.0 + 1 + lines + 1001269.16642, 6220412.35961 + + + + + 1002943.55552255 6222773.499634481002975.74639523 6222884.39323789 + 1002943.55552255 6222884.39323789 1002947.79277674 6222869.796417531002947.79277674 6222869.79641753 1002943.55552255 6222884.393237891002947.79277674 6222869.79641753 1002975.74639523 6222773.49963448 + route_points.2 + 3 + lines + 1002948.69578, 6222870.05855 + + + diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.xsd b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.xsd new file mode 100644 index 0000000000000..1bca69746f47a --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_lines_tolerance.xsd @@ -0,0 +1,72 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.gml b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.gml new file mode 100644 index 0000000000000..c7938dfa11d05 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.gml @@ -0,0 +1,26 @@ + + + 1002173.35050008 6221272.562366751003863.44040775 6222082.50310517 + + + + 1002173.35050008 6221272.562366751002173.35050008 6221272.56236675 + 1002173.35050008 6221272.56236675 + route_points.1 + 2 + + + + + 1003863.44040775 6222082.503105171003863.44040775 6222082.50310517 + 1003863.44040775 6222082.50310517 + route_points.2 + 3 + + + diff --git a/python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.xsd b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.xsd new file mode 100644 index 0000000000000..d5903b2ce3148 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/service_area_from_layer_non_routable_tolerance.xsd @@ -0,0 +1,60 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.gml b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.gml new file mode 100644 index 0000000000000..8449306709b5b --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.gml @@ -0,0 +1,43 @@ + + + 1001186.34740161 6220412.59372591004163.32616296 6223196.20398291 + + + + 1001186.34740161 6220412.59372591004163.32616296 6223196.20398291 + 1001269.29926109 6220412.5937259 1001186.34740161 6220459.66433954 1001230.60014311 6220497.58606688 1001417.00785562 6220703.65963346 1001474.56242038 6220771.95914347 1001535.25328051 6220851.26299517 1001730.88604383 6221182.3451556 1001806.08518826 6221307.13224234 1001821.61736105 6221332.90322892 1001835.96992268 6221356.72100035 1001891.40736133 6221430.94581986 1001930.78353526 6221467.36521594 1001968.37489204 6221502.13083684 1002137.81278244 6221630.62018051 1002215.13556068 6221689.25446887 1002288.4678461 6221744.85964495 1002334.20638747 6221779.49098284 1002425.63766482 6221848.74446543 1002462.62346654 6221875.38780442 1002518.06990805 6221915.34875741 1002626.85249869 6221993.7562202 1002657.33259138 6222018.20620536 1002781.74783802 6222107.45193255 1002852.18578258 6222162.74032983 1002903.48249244 6222204.64725757 1002916.58033846 6222216.16749784 1002945.17950103 6222241.31856099 1002991.11072264 6222281.72914386 1003040.80096358 6222322.18412711 1003081.26058941 6222355.11171034 1003105.74257399 6222376.7841218 1003153.88971381 6222419.41498029 1003172.94058079 6222436.28679202 1003198.63616199 6222463.1404267 1003261.7338751 6222520.86764589 1003285.57002497 6222541.37125149 1003318.51056419 6222558.84948158 1003416.77290022 6222570.20188393 1003430.63468333 6222565.77529146 1003461.44609399 6222555.86616133 1003536.86501513 6222502.0742548 1003555.31281829 6222483.13439712 1003602.8338769 6222436.67874613 1003647.3751082 6222401.82100567 1003696.4437017 6222455.04866221 1003745.10253898 6222507.54062578 1003852.72754463 6222623.65770678 1004026.21371718 6222810.84939244 1004138.7211059 6222934.89025765 1004027.57619058 6223045.89797913 1004078.05459543 6223104.58501454 1004136.89521736 6223167.16905176 1004163.32616296 6223196.20398291 + route_points.0 + 1 + 1001269.16642, 6220412.35961 + 1004160.88439, 6223198.42676 + 4583.05615667267 + + + + + 1002947.79277674 6222376.78412181004163.32616296 6223196.20398291 + 1002947.79277674 6222869.79641753 1003002.42639578 6222681.59030886 1002950.16219022 6222560.53103646 1002981.05280059 6222513.31733464 1002988.8334088 6222500.42759959 1003006.76410522 6222480.99306342 1003027.13399838 6222458.25399505 1003105.74257399 6222376.7841218 1003153.88971381 6222419.41498029 1003172.94058079 6222436.28679202 1003198.63616199 6222463.1404267 1003261.7338751 6222520.86764589 1003285.57002497 6222541.37125149 1003318.51056419 6222558.84948158 1003416.77290022 6222570.20188393 1003430.63468333 6222565.77529146 1003461.44609399 6222555.86616133 1003536.86501513 6222502.0742548 1003555.31281829 6222483.13439712 1003602.8338769 6222436.67874613 1003647.3751082 6222401.82100567 1003696.4437017 6222455.04866221 1003745.10253898 6222507.54062578 1003852.72754463 6222623.65770678 1004026.21371718 6222810.84939244 1004138.7211059 6222934.89025765 1004027.57619058 6223045.89797913 1004078.05459543 6223104.58501454 1004136.89521736 6223167.16905176 1004163.32616296 6223196.20398291 + route_points.2 + 3 + 1002948.69578, 6222870.05855 + 1004160.88439, 6223198.42676 + 2316.91329139706 + + + + + 1003525.93018347 6221908.194926441004163.32616296 6223196.20398291 + 1003866.67429938 6222081.06454087 1003835.88711567 6222011.8549713 1003839.54015074 6221908.19492644 1003775.57324976 6221922.76175725 1003645.34240628 6221954.38918302 1003525.93018347 6222064.10735181 1003596.67006395 6222145.79506248 1003745.40906699 6222317.55884368 1003692.73332615 6222357.0841711 1003647.3751082 6222401.82100567 1003696.4437017 6222455.04866221 1003745.10253898 6222507.54062578 1003852.72754463 6222623.65770678 1004026.21371718 6222810.84939244 1004138.7211059 6222934.89025765 1004027.57619058 6223045.89797913 1004078.05459543 6223104.58501454 1004136.89521736 6223167.16905176 1004163.32616296 6223196.20398291 + route_points.2 + 3 + 1003863.44041, 6222082.50311 + 1004160.88439, 6223198.42676 + 2085.04113760362 + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.xsd b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.xsd new file mode 100644 index 0000000000000..f8d5d3a7717f3 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_max_point_tolerance.xsd @@ -0,0 +1,78 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.gml b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.gml new file mode 100644 index 0000000000000..dd3526da099d2 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.gml @@ -0,0 +1,18 @@ + + + 1002173.35050008 6221272.562366751002173.35050008 6221272.56236675 + + + + 1002173.35050008 6221272.562366751002173.35050008 6221272.56236675 + 1002173.35050008 6221272.56236675 + route_points.1 + 2 + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.xsd b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.xsd new file mode 100644 index 0000000000000..92de7074cc578 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_layer_to_point_non_routable.xsd @@ -0,0 +1,60 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.gml b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.gml new file mode 100644 index 0000000000000..106d422be3ea9 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.gml @@ -0,0 +1,43 @@ + + + 1001186.34740161 6220412.59372591003866.67429938 6222869.79641753 + + + + 1001186.34740161 6220412.59372591002009.9943186 6221533.69197579 + 1002009.9943186 6221533.69197579 1001968.37489204 6221502.13083684 1001930.78353526 6221467.36521594 1001891.40736133 6221430.94581986 1001835.96992268 6221356.72100035 1001821.61736105 6221332.90322892 1001806.08518826 6221307.13224234 1001730.88604383 6221182.3451556 1001535.25328051 6220851.26299517 1001474.56242038 6220771.95914347 1001417.00785562 6220703.65963346 1001230.60014311 6220497.58606688 1001186.34740161 6220459.66433954 1001269.29926109 6220412.5937259 + route_points.0 + 1 + 1002012.24091, 6221530.72941 + 1001269.16642, 6220412.35961 + 1454.64389677481 + + + + + 1002009.9943186 6221533.691975791003002.42639578 6222869.79641753 + 1002009.9943186 6221533.69197579 1002137.81278244 6221630.62018051 1002215.13556068 6221689.25446887 1002288.4678461 6221744.85964495 1002334.20638747 6221779.49098284 1002425.63766482 6221848.74446543 1002462.62346654 6221875.38780442 1002518.06990805 6221915.34875741 1002485.75145034 6221954.55074419 1002621.6858386 6222058.82241101 1002529.45107897 6222163.92536965 1002624.53430232 6222242.61769963 1002724.17727046 6222326.95840711 1002761.33635685 6222354.19324527 1002788.09666598 6222371.6460413 1002865.83898131 6222432.39284309 1002852.54413666 6222449.88692926 1002896.08523225 6222491.56157257 1002923.08229091 6222512.3986315 1002931.70121622 6222526.36309541 1002950.16219022 6222560.53103646 1003002.42639578 6222681.59030886 1002947.79277674 6222869.79641753 + route_points.2 + 3 + 1002012.24091, 6221530.72941 + 1002948.69578, 6222870.05855 + 1922.30195140417 + + + + + 1002009.9943186 6221533.691975791003866.67429938 6222436.28679202 + 1002009.9943186 6221533.69197579 1002137.81278244 6221630.62018051 1002215.13556068 6221689.25446887 1002288.4678461 6221744.85964495 1002334.20638747 6221779.49098284 1002425.63766482 6221848.74446543 1002462.62346654 6221875.38780442 1002518.06990805 6221915.34875741 1002626.85249869 6221993.7562202 1002657.33259138 6222018.20620536 1002781.74783802 6222107.45193255 1002852.18578258 6222162.74032983 1002903.48249244 6222204.64725757 1002916.58033846 6222216.16749784 1002945.17950103 6222241.31856099 1002991.11072264 6222281.72914386 1003040.80096358 6222322.18412711 1003081.26058941 6222355.11171034 1003105.74257399 6222376.7841218 1003153.88971381 6222419.41498029 1003172.94058079 6222436.28679202 1003285.47179071 6222319.99149044 1003330.95107314 6222272.98904849 1003406.90500878 6222189.64158806 1003474.59453034 6222127.67457486 1003525.93018347 6222064.10735181 1003645.34240628 6221954.38918302 1003775.57324976 6221922.76175725 1003839.54015074 6221908.19492644 1003835.88711567 6222011.8549713 1003866.67429938 6222081.06454087 + route_points.2 + 3 + 1002012.24091, 6221530.72941 + 1003863.44041, 6222082.50311 + 2520.82219133417 + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.xsd b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.xsd new file mode 100644 index 0000000000000..b25c8791d87d4 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer.xsd @@ -0,0 +1,78 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.gml b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.gml new file mode 100644 index 0000000000000..54b9fe14c190d --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.gml @@ -0,0 +1,18 @@ + + + 1002173.35050008 6221272.562366751002173.35050008 6221272.56236675 + + + + 1002173.35050008 6221272.562366751002173.35050008 6221272.56236675 + 1002173.35050008 6221272.56236675 + route_points.1 + 2 + + + diff --git a/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.xsd b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.xsd new file mode 100644 index 0000000000000..e6f8ca6aa6516 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/shortest_path_point_to_layer_non_routable.xsd @@ -0,0 +1,60 @@ + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml index 7005e7b50f2c9..61616aea72e0d 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml @@ -1884,6 +1884,52 @@ tests: end: skip fid: skip + - algorithm: native:shortestpathpointtopoint + name: Shortest path (point to point, start point too far from network) + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 5.0 + END_POINT: 1003712.4162500285,6222484.5571899945 [EPSG:32733] + INPUT: + name: roads.gml + type: vector + START_POINT: 1000997.5971978485,6220343.83965781 [EPSG:32733] + STRATEGY: 0 + TOLERANCE: 0.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + POINT_TOLERANCE: 4 + results: + OUTPUT: + name: none + type: vector + expectedException: true + + - algorithm: native:shortestpathpointtopoint + name: Shortest path (point to point, end point too far from network) + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 5.0 + END_POINT: 1003842.4162500285,6222484.5571899945 [EPSG:32733] + INPUT: + name: roads.gml + type: vector + START_POINT: 1000997.5971978485,6220343.83965781 [EPSG:32733] + STRATEGY: 0 + TOLERANCE: 0.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + POINT_TOLERANCE: 14 + results: + OUTPUT: + name: none + type: vector + expectedException: true + - algorithm: native:shortestpathlayertopoint name: Shortest path layer to point ellipsoid: WGS84 @@ -1946,6 +1992,132 @@ tests: start: skip end: skip + - algorithm: native:shortestpathpointtolayer + name: Shortest path point to layer, start point too far + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 5.0 + END_POINTS: + name: custom/route_points.gml + type: vector + INPUT: + name: roads.gml + type: vector + START_POINT: 1001348.035822,6220968.188367 [EPSG:32733] + STRATEGY: 0 + TOLERANCE: 0.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + POINT_TOLERANCE: 5 + results: + OUTPUT: + name: name + type: vector + expectedException: true + + - algorithm: native:shortestpathpointtolayer + name: Shortest path point to layer, point tolerance + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 50.0 + END_POINTS: + name: custom/route_points.gml|layername=route_points + type: vector + INPUT: + name: roads.gml|layername=roads + type: vector + POINT_TOLERANCE: 5.0 + START_POINT: 1002012.240911,6221530.729412 [EPSG:32733] + STRATEGY: 0 + TOLERANCE: 0.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + results: + OUTPUT: + name: expected/shortest_path_point_to_layer.gml + type: vector + compare: + geometry: + precision: 2 + fields: + cost: + precision: 2 + start: skip + end: skip + OUTPUT_NON_ROUTABLE: + name: expected/shortest_path_point_to_layer_non_routable.gml + type: vector + compare: + geometry: + precision: 2 + + - algorithm: native:shortestpathlayertopoint + name: Shortest path layer to point, end point too far + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 5.0 + END_POINT: 1004434.104368,6223344.867804 [EPSG:32733] + INPUT: + name: roads.gml + type: vector + START_POINTS: + name: custom/route_points.gml + type: vector + STRATEGY: 0 + TOLERANCE: 0.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + POINT_TOLERANCE: 10 + results: + OUTPUT: + name: name + type: vector + expectedException: true + + - algorithm: native:shortestpathlayertopoint + name: Shortest path layer to point, with point tolerance + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 3.0 + END_POINT: 1004160.8843928401,6223198.426763908 [EPSG:32733] + INPUT: + name: roads.gml|layername=roads + type: vector + POINT_TOLERANCE: 4.5 + START_POINTS: + name: custom/route_points.gml|layername=route_points + type: vector + STRATEGY: 0 + TOLERANCE: 0.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + results: + OUTPUT: + name: expected/shortest_path_layer_to_point_max_point_tolerance.gml + type: vector + compare: + geometry: + precision: 2 + fields: + cost: + precision: 2 + start: skip + end: skip + OUTPUT_NON_ROUTABLE: + name: expected/shortest_path_layer_to_point_non_routable.gml + type: vector + compare: + geometry: + precision: 2 + - algorithm: native:serviceareafrompoint name: Service area from point (shortest, nodes) ellipsoid: WGS84 @@ -2107,6 +2279,30 @@ tests: fields: fid: skip + - algorithm: native:serviceareafrompoint + name: Service area from point (point too far from network) + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 50.0 + INCLUDE_BOUNDS: false + INPUT: + name: roads.gml + type: vector + START_POINT: 1002865.0089601517,6221875.432489508 [EPSG:32733] + STRATEGY: 0 + TOLERANCE: 0.0 + TRAVEL_COST: 700.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + POINT_TOLERANCE: 5 + results: + OUTPUT: + name: name + type: vector + expectedException: true + - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, nodes) ellipsoid: WGS84 @@ -2207,6 +2403,57 @@ tests: - d - type + - algorithm: native:serviceareafromlayer + name: Service area from layer (shortest, nodes, max distance from network) + ellipsoid: WGS84 + params: + DEFAULT_DIRECTION: 2 + DEFAULT_SPEED: 50.0 + INCLUDE_BOUNDS: false + INPUT: + name: roads.gml|layername=roads + type: vector + POINT_TOLERANCE: 1.0 + START_POINTS: + name: custom/route_points.gml|layername=route_points + type: vector + STRATEGY: 0 + TOLERANCE: 0.0 + TRAVEL_COST2: 100.0 + VALUE_BACKWARD: '' + VALUE_BOTH: '' + VALUE_FORWARD: '' + results: + OUTPUT_LINES: + name: expected/service_area_from_layer_lines_tolerance.gml + type: vector + compare: + ignore_crs_check: true + geometry: + precision: 2 + fields: + cost: + precision: 2 + start: skip + end: skip + pk: + - d + - type + OUTPUT_NON_ROUTABLE: + name: expected/service_area_from_layer_non_routable_tolerance.gml + type: vector + compare: + ignore_crs_check: true + geometry: + precision: 2 + fields: + cost: + precision: 2 + start: skip + end: skip + pk: + - d + - algorithm: native:createattributeindex name: Create attribute index params: diff --git a/src/analysis/processing/qgsalgorithmserviceareafromlayer.cpp b/src/analysis/processing/qgsalgorithmserviceareafromlayer.cpp index 750616d7016a8..faaa5ead4b12f 100644 --- a/src/analysis/processing/qgsalgorithmserviceareafromlayer.cpp +++ b/src/analysis/processing/qgsalgorithmserviceareafromlayer.cpp @@ -67,6 +67,11 @@ void QgsServiceAreaFromLayerAlgorithm::initAlgorithm( const QVariantMap & ) includeBounds->setFlags( includeBounds->flags() | Qgis::ProcessingParameterFlag::Advanced ); addParameter( includeBounds.release() ); + std::unique_ptr< QgsProcessingParameterNumber > maxPointDistanceFromNetwork = std::make_unique < QgsProcessingParameterDistance >( QStringLiteral( "POINT_TOLERANCE" ), QObject::tr( "Maximum point distance from network" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ); + maxPointDistanceFromNetwork->setFlags( maxPointDistanceFromNetwork->flags() | Qgis::ProcessingParameterFlag::Advanced ); + maxPointDistanceFromNetwork->setHelp( QObject::tr( "Specifies an optional limit on the distance from the points to the network layer. If a point is further from the network than this distance it will be treated as non-routable." ) ); + addParameter( maxPointDistanceFromNetwork.release() ); + std::unique_ptr< QgsProcessingParameterFeatureSink > outputLines = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "OUTPUT_LINES" ), QObject::tr( "Service area (lines)" ), Qgis::ProcessingSourceType::VectorLine, QVariant(), true ); outputLines->setCreateByDefault( true ); @@ -76,6 +81,12 @@ void QgsServiceAreaFromLayerAlgorithm::initAlgorithm( const QVariantMap & ) Qgis::ProcessingSourceType::VectorPoint, QVariant(), true ); outputPoints->setCreateByDefault( false ); addParameter( outputPoints.release() ); + + std::unique_ptr< QgsProcessingParameterFeatureSink > outputNonRoutable = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "OUTPUT_NON_ROUTABLE" ), QObject::tr( "Non-routable features" ), + Qgis::ProcessingSourceType::VectorPoint, QVariant(), true ); + outputNonRoutable->setHelp( QObject::tr( "An optional output which will be used to store any input features which could not be routed (e.g. those which are too far from the network layer)." ) ); + outputNonRoutable->setCreateByDefault( false ); + addParameter( outputNonRoutable.release() ); } QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) @@ -123,8 +134,13 @@ QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMa std::unique_ptr< QgsFeatureSink > linesSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT_LINES" ), context, linesSinkId, fields, Qgis::WkbType::MultiLineString, mNetwork->sourceCrs() ) ); + QString nonRoutableSinkId; + std::unique_ptr< QgsFeatureSink > nonRoutableSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT_NON_ROUTABLE" ), context, nonRoutableSinkId, startPoints->fields(), + Qgis::WkbType::Point, mNetwork->sourceCrs() ) ); + + const double pointDistanceThreshold = parameters.value( QStringLiteral( "POINT_TOLERANCE" ) ).isValid() ? parameterAsDouble( parameters, QStringLiteral( "POINT_TOLERANCE" ), context ) : -1; + int idxStart; - QString origPoint; QVector< int > tree; QVector< double > costs; @@ -144,8 +160,32 @@ QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMa break; } - idxStart = graph->findVertex( snappedPoints.at( i ) ); - origPoint = points.at( i ).toString(); + const QgsPointXY snappedPoint = snappedPoints.at( i ); + const QgsPointXY originalPoint = points.at( i ); + + if ( pointDistanceThreshold >= 0 ) + { + const double distancePointToNetwork = mBuilder->distanceArea()->measureLine( originalPoint, snappedPoint ); + if ( distancePointToNetwork > pointDistanceThreshold ) + { + feedback->pushWarning( QObject::tr( "Point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distancePointToNetwork ).arg( pointDistanceThreshold ) ); + if ( nonRoutableSink ) + { + feat.setGeometry( QgsGeometry::fromPointXY( originalPoint ) ); + attributes = sourceAttributes.value( i + 1 ); + feat.setAttributes( attributes ); + if ( !nonRoutableSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( nonRoutableSink.get(), parameters, QStringLiteral( "OUTPUT_NON_ROUTABLE" ) ) ); + } + + feedback->setProgress( i * step ); + continue; + } + } + + const QString originalPointString = originalPoint.toString(); + + idxStart = graph->findVertex( snappedPoint ); QgsGraphAnalyzer::dijkstra( graph.get(), idxStart, 0, &tree, &costs ); @@ -212,7 +252,7 @@ QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMa QgsGeometry geomPoints = QgsGeometry::fromMultiPointXY( areaPoints ); feat.setGeometry( geomPoints ); attributes = sourceAttributes.value( i + 1 ); - attributes << QStringLiteral( "within" ) << origPoint; + attributes << QStringLiteral( "within" ) << originalPointString; feat.setAttributes( attributes ); if ( !pointsSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( pointsSink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); @@ -249,14 +289,14 @@ QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMa feat.setGeometry( geomUpper ); attributes = sourceAttributes.value( i + 1 ); - attributes << QStringLiteral( "upper" ) << origPoint; + attributes << QStringLiteral( "upper" ) << originalPointString; feat.setAttributes( attributes ); if ( !pointsSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( pointsSink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); feat.setGeometry( geomLower ); attributes = sourceAttributes.value( i + 1 ); - attributes << QStringLiteral( "lower" ) << origPoint; + attributes << QStringLiteral( "lower" ) << originalPointString; feat.setAttributes( attributes ); if ( !pointsSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( pointsSink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); @@ -268,7 +308,7 @@ QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMa QgsGeometry geomLines = QgsGeometry::fromMultiPolylineXY( lines ); feat.setGeometry( geomLines ); attributes = sourceAttributes.value( i + 1 ); - attributes << QStringLiteral( "lines" ) << origPoint; + attributes << QStringLiteral( "lines" ) << originalPointString; feat.setAttributes( attributes ); if ( !linesSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( linesSink.get(), parameters, QStringLiteral( "OUTPUT_LINES" ) ) ); @@ -286,6 +326,10 @@ QVariantMap QgsServiceAreaFromLayerAlgorithm::processAlgorithm( const QVariantMa { outputs.insert( QStringLiteral( "OUTPUT_LINES" ), linesSinkId ); } + if ( nonRoutableSink ) + { + outputs.insert( QStringLiteral( "OUTPUT_NON_ROUTABLE" ), nonRoutableSinkId ); + } return outputs; } diff --git a/src/analysis/processing/qgsalgorithmserviceareafrompoint.cpp b/src/analysis/processing/qgsalgorithmserviceareafrompoint.cpp index 439534aa7e05f..7bca638bccac0 100644 --- a/src/analysis/processing/qgsalgorithmserviceareafrompoint.cpp +++ b/src/analysis/processing/qgsalgorithmserviceareafrompoint.cpp @@ -63,6 +63,11 @@ void QgsServiceAreaFromPointAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( new QgsProcessingParameterNumber( QStringLiteral( "TRAVEL_COST2" ), QObject::tr( "Travel cost (distance for 'Shortest', time for 'Fastest')" ), Qgis::ProcessingNumberParameterType::Double, 0, false, 0 ) ); + std::unique_ptr< QgsProcessingParameterNumber > maxPointDistanceFromNetwork = std::make_unique < QgsProcessingParameterDistance >( QStringLiteral( "POINT_TOLERANCE" ), QObject::tr( "Maximum point distance from network" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ); + maxPointDistanceFromNetwork->setFlags( maxPointDistanceFromNetwork->flags() | Qgis::ProcessingParameterFlag::Advanced ); + maxPointDistanceFromNetwork->setHelp( QObject::tr( "Specifies an optional limit on the distance from the point to the network layer. If the point is further from the network than this distance an error will be raised." ) ); + addParameter( maxPointDistanceFromNetwork.release() ); + std::unique_ptr< QgsProcessingParameterBoolean > includeBounds = std::make_unique< QgsProcessingParameterBoolean >( QStringLiteral( "INCLUDE_BOUNDS" ), QObject::tr( "Include upper/lower bound points" ), false, true ); includeBounds->setFlags( includeBounds->flags() | Qgis::ProcessingParameterFlag::Advanced ); addParameter( includeBounds.release() ); @@ -100,11 +105,24 @@ QVariantMap QgsServiceAreaFromPointAlgorithm::processAlgorithm( const QVariantMa feedback->pushInfo( QObject::tr( "Building graph…" ) ); QVector< QgsPointXY > snappedPoints; - mDirector->makeGraph( mBuilder.get(), QVector< QgsPointXY >() << startPoint, snappedPoints, feedback ); + mDirector->makeGraph( mBuilder.get(), { startPoint }, snappedPoints, feedback ); + const QgsPointXY snappedStartPoint = snappedPoints[0]; + + // check distance for the snapped point + if ( parameters.value( QStringLiteral( "POINT_TOLERANCE" ) ).isValid() ) + { + const double pointDistanceThreshold = parameterAsDouble( parameters, QStringLiteral( "POINT_TOLERANCE" ), context ); + + const double distancePointToNetwork = mBuilder->distanceArea()->measureLine( startPoint, snappedStartPoint ); + if ( distancePointToNetwork > pointDistanceThreshold ) + { + throw QgsProcessingException( QObject::tr( "Point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distancePointToNetwork ).arg( pointDistanceThreshold ) ); + } + } feedback->pushInfo( QObject::tr( "Calculating service area…" ) ); std::unique_ptr< QgsGraph> graph( mBuilder->takeGraph() ); - const int idxStart = graph->findVertex( snappedPoints[0] ); + const int idxStart = graph->findVertex( snappedStartPoint ); QVector< int > tree; QVector< double > costs; diff --git a/src/analysis/processing/qgsalgorithmshortestpathlayertopoint.cpp b/src/analysis/processing/qgsalgorithmshortestpathlayertopoint.cpp index e364bd56325a5..44170593cf24f 100644 --- a/src/analysis/processing/qgsalgorithmshortestpathlayertopoint.cpp +++ b/src/analysis/processing/qgsalgorithmshortestpathlayertopoint.cpp @@ -54,7 +54,18 @@ void QgsShortestPathLayerToPointAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "START_POINTS" ), QObject::tr( "Vector layer with start points" ), QList< int >() << static_cast< int >( Qgis::ProcessingSourceType::VectorPoint ) ) ); addParameter( new QgsProcessingParameterPoint( QStringLiteral( "END_POINT" ), QObject::tr( "End point" ) ) ); + std::unique_ptr< QgsProcessingParameterNumber > maxEndPointDistanceFromNetwork = std::make_unique < QgsProcessingParameterDistance >( QStringLiteral( "POINT_TOLERANCE" ), QObject::tr( "Maximum point distance from network" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ); + maxEndPointDistanceFromNetwork->setFlags( maxEndPointDistanceFromNetwork->flags() | Qgis::ProcessingParameterFlag::Advanced ); + maxEndPointDistanceFromNetwork->setHelp( QObject::tr( "Specifies an optional limit on the distance from the start and end points to the network layer. If the start feature is further from the network than this distance it will be treated as non-routable. If the end point is further from the network than this distance an error will be raised." ) ); + addParameter( maxEndPointDistanceFromNetwork.release() ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Shortest path" ), Qgis::ProcessingSourceType::VectorLine ) ); + + std::unique_ptr< QgsProcessingParameterFeatureSink > outputNonRoutable = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "OUTPUT_NON_ROUTABLE" ), QObject::tr( "Non-routable features" ), + Qgis::ProcessingSourceType::VectorPoint, QVariant(), true ); + outputNonRoutable->setHelp( QObject::tr( "An optional output which will be used to store any input features which could not be routed (e.g. those which are too far from the network layer)." ) ); + outputNonRoutable->setCreateByDefault( false ); + addParameter( outputNonRoutable.release() ); } QVariantMap QgsShortestPathLayerToPointAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) @@ -77,6 +88,12 @@ QVariantMap QgsShortestPathLayerToPointAlgorithm::processAlgorithm( const QVaria if ( !sink ) throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + QString nonRoutableSinkId; + std::unique_ptr< QgsFeatureSink > nonRoutableSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT_NON_ROUTABLE" ), context, nonRoutableSinkId, startPoints->fields(), + Qgis::WkbType::Point, mNetwork->sourceCrs() ) ); + + const double pointDistanceThreshold = parameters.value( QStringLiteral( "POINT_TOLERANCE" ) ).isValid() ? parameterAsDouble( parameters, QStringLiteral( "POINT_TOLERANCE" ), context ) : -1; + QVector< QgsPointXY > points; points.push_front( endPoint ); QHash< int, QgsAttributes > sourceAttributes; @@ -86,9 +103,20 @@ QVariantMap QgsShortestPathLayerToPointAlgorithm::processAlgorithm( const QVaria QVector< QgsPointXY > snappedPoints; mDirector->makeGraph( mBuilder.get(), points, snappedPoints, feedback ); + const QgsPointXY snappedEndPoint = snappedPoints[0]; + + if ( pointDistanceThreshold >= 0 ) + { + const double distanceEndPointToNetwork = mBuilder->distanceArea()->measureLine( endPoint, snappedEndPoint ); + if ( distanceEndPointToNetwork > pointDistanceThreshold ) + { + throw QgsProcessingException( QObject::tr( "End point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distanceEndPointToNetwork ).arg( pointDistanceThreshold ) ); + } + } + feedback->pushInfo( QObject::tr( "Calculating shortest paths…" ) ); std::unique_ptr< QgsGraph > graph( mBuilder->takeGraph() ); - const int idxEnd = graph->findVertex( snappedPoints[0] ); + const int idxEnd = graph->findVertex( snappedEndPoint ); int idxStart; int currentIdx; @@ -110,17 +138,40 @@ QVariantMap QgsShortestPathLayerToPointAlgorithm::processAlgorithm( const QVaria break; } - idxStart = graph->findVertex( snappedPoints[i] ); + const QgsPointXY snappedPoint = snappedPoints.at( i ); + const QgsPointXY originalPoint = points.at( i ); + + if ( pointDistanceThreshold >= 0 ) + { + const double distancePointToNetwork = mBuilder->distanceArea()->measureLine( originalPoint, snappedPoint ); + if ( distancePointToNetwork > pointDistanceThreshold ) + { + feedback->pushWarning( QObject::tr( "Point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distancePointToNetwork ).arg( pointDistanceThreshold ) ); + if ( nonRoutableSink ) + { + feat.setGeometry( QgsGeometry::fromPointXY( originalPoint ) ); + attributes = sourceAttributes.value( i ); + feat.setAttributes( attributes ); + if ( !nonRoutableSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( nonRoutableSink.get(), parameters, QStringLiteral( "OUTPUT_NON_ROUTABLE" ) ) ); + } + + feedback->setProgress( i * step ); + continue; + } + } + + idxStart = graph->findVertex( snappedPoint ); QgsGraphAnalyzer::dijkstra( graph.get(), idxStart, 0, &tree, &costs ); if ( tree.at( idxEnd ) == -1 ) { feedback->reportError( QObject::tr( "There is no route from start point (%1) to end point (%2)." ) - .arg( points[i].toString(), + .arg( originalPoint.toString(), endPoint.toString() ) ); feat.clearGeometry(); attributes = sourceAttributes.value( i ); - attributes.append( points[i].toString() ); + attributes.append( originalPoint.toString() ); feat.setAttributes( attributes ); if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); @@ -141,7 +192,7 @@ QVariantMap QgsShortestPathLayerToPointAlgorithm::processAlgorithm( const QVaria QgsFeature feat; feat.setFields( fields ); attributes = sourceAttributes.value( i ); - attributes.append( points[i].toString() ); + attributes.append( originalPoint.toString() ); attributes.append( endPoint.toString() ); attributes.append( cost / mMultiplier ); feat.setAttributes( attributes ); @@ -154,6 +205,10 @@ QVariantMap QgsShortestPathLayerToPointAlgorithm::processAlgorithm( const QVaria QVariantMap outputs; outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + if ( nonRoutableSink ) + { + outputs.insert( QStringLiteral( "OUTPUT_NON_ROUTABLE" ), nonRoutableSinkId ); + } return outputs; } diff --git a/src/analysis/processing/qgsalgorithmshortestpathpointtolayer.cpp b/src/analysis/processing/qgsalgorithmshortestpathpointtolayer.cpp index f6cafdfe778ed..e6b2e902a067f 100644 --- a/src/analysis/processing/qgsalgorithmshortestpathpointtolayer.cpp +++ b/src/analysis/processing/qgsalgorithmshortestpathpointtolayer.cpp @@ -54,7 +54,18 @@ void QgsShortestPathPointToLayerAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( new QgsProcessingParameterPoint( QStringLiteral( "START_POINT" ), QObject::tr( "Start point" ) ) ); addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "END_POINTS" ), QObject::tr( "Vector layer with end points" ), QList< int >() << static_cast< int >( Qgis::ProcessingSourceType::VectorPoint ) ) ); + std::unique_ptr< QgsProcessingParameterNumber > maxEndPointDistanceFromNetwork = std::make_unique < QgsProcessingParameterDistance >( QStringLiteral( "POINT_TOLERANCE" ), QObject::tr( "Maximum point distance from network" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ); + maxEndPointDistanceFromNetwork->setFlags( maxEndPointDistanceFromNetwork->flags() | Qgis::ProcessingParameterFlag::Advanced ); + maxEndPointDistanceFromNetwork->setHelp( QObject::tr( "Specifies an optional limit on the distance from the start and end points to the network layer.If the start point is further from the network than this distance an error will be raised. If the end feature is further from the network than this distance it will be treated as non-routable." ) ); + addParameter( maxEndPointDistanceFromNetwork.release() ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Shortest path" ), Qgis::ProcessingSourceType::VectorLine ) ); + + std::unique_ptr< QgsProcessingParameterFeatureSink > outputNonRoutable = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "OUTPUT_NON_ROUTABLE" ), QObject::tr( "Non-routable features" ), + Qgis::ProcessingSourceType::VectorPoint, QVariant(), true ); + outputNonRoutable->setHelp( QObject::tr( "An optional output which will be used to store any input features which could not be routed (e.g. those which are too far from the network layer)." ) ); + outputNonRoutable->setCreateByDefault( false ); + addParameter( outputNonRoutable.release() ); } QVariantMap QgsShortestPathPointToLayerAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) @@ -77,6 +88,12 @@ QVariantMap QgsShortestPathPointToLayerAlgorithm::processAlgorithm( const QVaria if ( !sink ) throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + QString nonRoutableSinkId; + std::unique_ptr< QgsFeatureSink > nonRoutableSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT_NON_ROUTABLE" ), context, nonRoutableSinkId, endPoints->fields(), + Qgis::WkbType::Point, mNetwork->sourceCrs() ) ); + + const double pointDistanceThreshold = parameters.value( QStringLiteral( "POINT_TOLERANCE" ) ).isValid() ? parameterAsDouble( parameters, QStringLiteral( "POINT_TOLERANCE" ), context ) : -1; + QVector< QgsPointXY > points; points.push_front( startPoint ); QHash< int, QgsAttributes > sourceAttributes; @@ -86,9 +103,20 @@ QVariantMap QgsShortestPathPointToLayerAlgorithm::processAlgorithm( const QVaria QVector< QgsPointXY > snappedPoints; mDirector->makeGraph( mBuilder.get(), points, snappedPoints, feedback ); + const QgsPointXY snappedStartPoint = snappedPoints[0]; + + if ( pointDistanceThreshold >= 0 ) + { + const double distanceStartPointToNetwork = mBuilder->distanceArea()->measureLine( startPoint, snappedStartPoint ); + if ( distanceStartPointToNetwork > pointDistanceThreshold ) + { + throw QgsProcessingException( QObject::tr( "Start point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distanceStartPointToNetwork ).arg( pointDistanceThreshold ) ); + } + } + feedback->pushInfo( QObject::tr( "Calculating shortest paths…" ) ); std::unique_ptr< QgsGraph > graph( mBuilder->takeGraph() ); - const int idxStart = graph->findVertex( snappedPoints[0] ); + const int idxStart = graph->findVertex( snappedStartPoint ); int idxEnd; QVector< int > tree; @@ -110,16 +138,39 @@ QVariantMap QgsShortestPathPointToLayerAlgorithm::processAlgorithm( const QVaria break; } - idxEnd = graph->findVertex( snappedPoints[i] ); + const QgsPointXY snappedPoint = snappedPoints.at( i ); + const QgsPointXY originalPoint = points.at( i ); + + if ( pointDistanceThreshold >= 0 ) + { + const double distancePointToNetwork = mBuilder->distanceArea()->measureLine( originalPoint, snappedPoint ); + if ( distancePointToNetwork > pointDistanceThreshold ) + { + feedback->pushWarning( QObject::tr( "Point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distancePointToNetwork ).arg( pointDistanceThreshold ) ); + if ( nonRoutableSink ) + { + feat.setGeometry( QgsGeometry::fromPointXY( originalPoint ) ); + attributes = sourceAttributes.value( i ); + feat.setAttributes( attributes ); + if ( !nonRoutableSink->addFeature( feat, QgsFeatureSink::FastInsert ) ) + throw QgsProcessingException( writeFeatureError( nonRoutableSink.get(), parameters, QStringLiteral( "OUTPUT_NON_ROUTABLE" ) ) ); + } + + feedback->setProgress( i * step ); + continue; + } + } + + idxEnd = graph->findVertex( snappedPoint ); if ( tree.at( idxEnd ) == -1 ) { feedback->reportError( QObject::tr( "There is no route from start point (%1) to end point (%2)." ) .arg( startPoint.toString(), - points[i].toString() ) ); + originalPoint.toString() ) ); feat.clearGeometry(); attributes = sourceAttributes.value( i ); attributes.append( QVariant() ); - attributes.append( points[i].toString() ); + attributes.append( originalPoint.toString() ); feat.setAttributes( attributes ); if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) ) throw QgsProcessingException( writeFeatureError( sink.get(), parameters, QStringLiteral( "OUTPUT" ) ) ); @@ -140,7 +191,7 @@ QVariantMap QgsShortestPathPointToLayerAlgorithm::processAlgorithm( const QVaria feat.setFields( fields ); attributes = sourceAttributes.value( i ); attributes.append( startPoint.toString() ); - attributes.append( points[i].toString() ); + attributes.append( originalPoint.toString() ); attributes.append( cost / mMultiplier ); feat.setAttributes( attributes ); feat.setGeometry( geom ); @@ -152,6 +203,10 @@ QVariantMap QgsShortestPathPointToLayerAlgorithm::processAlgorithm( const QVaria QVariantMap outputs; outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + if ( nonRoutableSink ) + { + outputs.insert( QStringLiteral( "OUTPUT_NON_ROUTABLE" ), nonRoutableSinkId ); + } return outputs; } diff --git a/src/analysis/processing/qgsalgorithmshortestpathpointtopoint.cpp b/src/analysis/processing/qgsalgorithmshortestpathpointtopoint.cpp index 5c790ee1b2cda..11939b344051f 100644 --- a/src/analysis/processing/qgsalgorithmshortestpathpointtopoint.cpp +++ b/src/analysis/processing/qgsalgorithmshortestpathpointtopoint.cpp @@ -53,6 +53,12 @@ void QgsShortestPathPointToPointAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( new QgsProcessingParameterPoint( QStringLiteral( "END_POINT" ), QObject::tr( "End point" ) ) ); addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Shortest path" ), Qgis::ProcessingSourceType::VectorLine ) ); + + std::unique_ptr< QgsProcessingParameterNumber > maxEndPointDistanceFromNetwork = std::make_unique < QgsProcessingParameterDistance >( QStringLiteral( "POINT_TOLERANCE" ), QObject::tr( "Maximum point distance from network" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ); + maxEndPointDistanceFromNetwork->setFlags( maxEndPointDistanceFromNetwork->flags() | Qgis::ProcessingParameterFlag::Advanced ); + maxEndPointDistanceFromNetwork->setHelp( QObject::tr( "Specifies an optional limit on the distance from the start and end points to the network layer. If either point is further from the network than this distance an error will be raised." ) ); + addParameter( maxEndPointDistanceFromNetwork.release() ); + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "TRAVEL_COST" ), QObject::tr( "Travel cost" ) ) ); } @@ -74,15 +80,34 @@ QVariantMap QgsShortestPathPointToPointAlgorithm::processAlgorithm( const QVaria const QgsPointXY endPoint = parameterAsPoint( parameters, QStringLiteral( "END_POINT" ), context, mNetwork->sourceCrs() ); feedback->pushInfo( QObject::tr( "Building graph…" ) ); - QVector< QgsPointXY > points; - points << startPoint << endPoint; QVector< QgsPointXY > snappedPoints; - mDirector->makeGraph( mBuilder.get(), points, snappedPoints, feedback ); + mDirector->makeGraph( mBuilder.get(), { startPoint, endPoint }, snappedPoints, feedback ); + const QgsPointXY snappedStartPoint = snappedPoints[0]; + const QgsPointXY snappedEndPoint = snappedPoints[1]; + + // check distance for the snapped start/end points + if ( parameters.value( QStringLiteral( "POINT_TOLERANCE" ) ).isValid() ) + { + const double pointDistanceThreshold = parameterAsDouble( parameters, QStringLiteral( "POINT_TOLERANCE" ), context ); + + const double distanceStartPointToNetwork = mBuilder->distanceArea()->measureLine( startPoint, snappedStartPoint ); + if ( distanceStartPointToNetwork > pointDistanceThreshold ) + { + throw QgsProcessingException( QObject::tr( "Start point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distanceStartPointToNetwork ).arg( pointDistanceThreshold ) ); + } + + const double distanceEndPointToNetwork = mBuilder->distanceArea()->measureLine( endPoint, snappedEndPoint ); + if ( distanceEndPointToNetwork > pointDistanceThreshold ) + { + throw QgsProcessingException( QObject::tr( "End point is too far from the network layer (%1, maximum permitted is %2)" ).arg( distanceEndPointToNetwork ).arg( pointDistanceThreshold ) ); + } + } feedback->pushInfo( QObject::tr( "Calculating shortest path…" ) ); std::unique_ptr< QgsGraph > graph( mBuilder->takeGraph() ); - const int idxStart = graph->findVertex( snappedPoints[0] ); - int idxEnd = graph->findVertex( snappedPoints[1] ); + + const int idxStart = graph->findVertex( snappedStartPoint ); + int idxEnd = graph->findVertex( snappedEndPoint ); QVector< int > tree; QVector< double > costs; From 1f447c6e04bc66ceb88bcbd5761597ae15796d7f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 11 Apr 2024 12:50:36 +1000 Subject: [PATCH 22/46] [processing] Show marker on canvas at picked point location When an algorithm has a point parameter and is run from the toolbox, show a X rubber band marker at the picked point on the map canvas This makes it easier to see where the point parameter is set to. The marker is cleared as soon as the algorithm dialog is closed. --- .../qgsprocessingwidgetwrapperimpl.cpp | 84 +++++++++++++++++++ .../qgsprocessingwidgetwrapperimpl.h | 8 ++ 2 files changed, 92 insertions(+) diff --git a/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp b/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp index e861500ec7b06..bab371f1dc610 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp +++ b/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp @@ -3474,6 +3474,7 @@ QgsProcessingPointPanel::QgsProcessingPointPanel( QWidget *parent ) setLayout( l ); connect( mLineEdit, &QLineEdit::textChanged, this, &QgsProcessingPointPanel::changed ); + connect( mLineEdit, &QLineEdit::textChanged, this, &QgsProcessingPointPanel::textChanged ); connect( mButton, &QToolButton::clicked, this, &QgsProcessingPointPanel::selectOnCanvas ); mButton->setVisible( false ); } @@ -3494,6 +3495,22 @@ void QgsProcessingPointPanel::setAllowNull( bool allowNull ) mLineEdit->setShowClearButton( allowNull ); } +void QgsProcessingPointPanel::setShowPointOnCanvas( bool show ) +{ + if ( mShowPointOnCanvas == show ) + return; + + mShowPointOnCanvas = show; + if ( mShowPointOnCanvas ) + { + updateRubberBand(); + } + else + { + mMapPointRubberBand.reset(); + } +} + QVariant QgsProcessingPointPanel::value() const { return mLineEdit->showClearButton() && mLineEdit->text().trimmed().isEmpty() ? QVariant() : QVariant( mLineEdit->text() ); @@ -3506,6 +3523,7 @@ void QgsProcessingPointPanel::clear() void QgsProcessingPointPanel::setValue( const QgsPointXY &point, const QgsCoordinateReferenceSystem &crs ) { + mPoint = point; QString newText = QStringLiteral( "%1,%2" ) .arg( QString::number( point.x(), 'f' ), QString::number( point.y(), 'f' ) ); @@ -3516,6 +3534,7 @@ void QgsProcessingPointPanel::setValue( const QgsPointXY &point, const QgsCoordi newText += QStringLiteral( " [%1]" ).arg( mCrs.authid() ); } mLineEdit->setText( newText ); + updateRubberBand(); } void QgsProcessingPointPanel::selectOnCanvas() @@ -3544,6 +3563,68 @@ void QgsProcessingPointPanel::pointPicked() emit toggleDialogVisibility( true ); } +void QgsProcessingPointPanel::textChanged( const QString &text ) +{ + const thread_local QRegularExpression rx( QStringLiteral( "^\\s*\\(?\\s*(.*?)\\s*,\\s*(.*?)\\s*(?:\\[(.*)\\])?\\s*\\)?\\s*$" ) ); + + const QRegularExpressionMatch match = rx.match( text ); + if ( match.hasMatch() ) + { + bool xOk = false; + const double x = match.captured( 1 ).toDouble( &xOk ); + bool yOk = false; + const double y = match.captured( 2 ).toDouble( &yOk ); + + if ( xOk && yOk ) + { + mPoint = QgsPointXY( x, y ); + + const QgsCoordinateReferenceSystem pointCrs( match.captured( 3 ) ); + if ( pointCrs.isValid() ) + { + mCrs = pointCrs; + } + } + else + { + mPoint = QgsPointXY(); + } + } + else + { + mPoint = QgsPointXY(); + } + + updateRubberBand(); +} + +void QgsProcessingPointPanel::updateRubberBand() +{ + if ( !mShowPointOnCanvas || !mCanvas ) + return; + + if ( mPoint.isEmpty() ) + { + mMapPointRubberBand.reset(); + return; + } + + if ( !mMapPointRubberBand ) + { + mMapPointRubberBand.reset( new QgsRubberBand( mCanvas, Qgis::GeometryType::Point ) ); + mMapPointRubberBand->setZValue( 1000 ); + mMapPointRubberBand->setIcon( QgsRubberBand::ICON_X ); + + const double scaleFactor = mCanvas->fontMetrics().xHeight() * .4; + mMapPointRubberBand->setWidth( scaleFactor ); + mMapPointRubberBand->setIconSize( scaleFactor * 5 ); + + mMapPointRubberBand->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) ); + mMapPointRubberBand->setColor( QColor( 200, 0, 200 ) ); + } + + mMapPointRubberBand->setToGeometry( QgsGeometry::fromPointXY( mPoint ), mCrs ); +} // @@ -3599,6 +3680,9 @@ QWidget *QgsProcessingPointWidgetWrapper::createWidget() if ( pointParam->flags() & Qgis::ProcessingParameterFlag::Optional ) mPanel->setAllowNull( true ); + if ( type() == QgsProcessingGui::Standard ) + mPanel->setShowPointOnCanvas( true ); + mPanel->setToolTip( parameterDefinition()->toolTip() ); connect( mPanel, &QgsProcessingPointPanel::changed, this, [ = ] diff --git a/src/gui/processing/qgsprocessingwidgetwrapperimpl.h b/src/gui/processing/qgsprocessingwidgetwrapperimpl.h index 2b50c5aa8261c..882840ce9bfb2 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapperimpl.h +++ b/src/gui/processing/qgsprocessingwidgetwrapperimpl.h @@ -27,6 +27,7 @@ #include "qgspointcloudattribute.h" #include "qgspointcloudlayer.h" #include "qgsprocessingmodelchildparametersource.h" +#include "qobjectuniqueptr.h" #include @@ -72,6 +73,7 @@ class QgsCheckableComboBox; class QgsMapLayerComboBox; class QgsProcessingPointCloudExpressionLineEdit; class QgsProcessingRasterCalculatorExpressionLineEdit; +class QgsRubberBand; ///@cond PRIVATE @@ -1005,6 +1007,7 @@ class GUI_EXPORT QgsProcessingPointPanel : public QWidget QgsProcessingPointPanel( QWidget *parent ); void setMapCanvas( QgsMapCanvas *canvas ); void setAllowNull( bool allowNull ); + void setShowPointOnCanvas( bool show ); QVariant value() const; void clear(); @@ -1020,15 +1023,20 @@ class GUI_EXPORT QgsProcessingPointPanel : public QWidget void selectOnCanvas(); void updatePoint( const QgsPointXY &point ); void pointPicked(); + void textChanged( const QString &text ); private: + void updateRubberBand(); QgsFilterLineEdit *mLineEdit = nullptr; + bool mShowPointOnCanvas = false; QToolButton *mButton = nullptr; QgsMapCanvas *mCanvas = nullptr; QgsCoordinateReferenceSystem mCrs; QPointer< QgsMapTool > mPrevTool; std::unique_ptr< QgsProcessingPointMapTool > mTool; + QgsPointXY mPoint; + QObjectUniquePtr mMapPointRubberBand; friend class TestProcessingGui; }; From 19585bf1fdd993571ec43288ff8bb5ba9d5ed656 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 14:11:13 +1000 Subject: [PATCH 23/46] Add api to set QgsMapLayer IDs With appropriate warnings on when this should be used... --- .../core/auto_generated/qgsmaplayer.sip.in | 21 +++++++++++++++++++ python/core/auto_generated/qgsmaplayer.sip.in | 21 +++++++++++++++++++ src/core/qgsmaplayer.cpp | 7 +++++++ src/core/qgsmaplayer.h | 19 ++++++++++++++++- tests/src/core/testqgsmaplayer.cpp | 8 +++++++ 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in index 1049072bd0906..bb5714d2b2b77 100644 --- a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in +++ b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in @@ -187,6 +187,27 @@ Returns the extension of a Property. QString id() const; %Docstring Returns the layer's unique ID, which is used to access this layer from :py:class:`QgsProject`. + +.. seealso:: :py:func:`setId` +%End + + void setId( const QString &id ); +%Docstring +Sets the layer's ``id``. + +.. warning:: + + It is the caller's responsibility to ensure that the layer ID is unique in the desired context. + Generally this method should not be called, and the auto generated ID should be used instead. + +.. warning:: + + This method should not be called on layers which already belong to a :py:class:`QgsProject` or :py:class:`QgsMapLayerStore`. + + +.. seealso:: :py:func:`id` + +.. versionadded:: 3.38 %End void setName( const QString &name ); diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index 426f8b22ebe07..90156e2faf97c 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -187,6 +187,27 @@ Returns the extension of a Property. QString id() const; %Docstring Returns the layer's unique ID, which is used to access this layer from :py:class:`QgsProject`. + +.. seealso:: :py:func:`setId` +%End + + void setId( const QString &id ); +%Docstring +Sets the layer's ``id``. + +.. warning:: + + It is the caller's responsibility to ensure that the layer ID is unique in the desired context. + Generally this method should not be called, and the auto generated ID should be used instead. + +.. warning:: + + This method should not be called on layers which already belong to a :py:class:`QgsProject` or :py:class:`QgsMapLayerStore`. + + +.. seealso:: :py:func:`id` + +.. versionadded:: 3.38 %End void setName( const QString &name ); diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index c21c8ba43be07..1cbd0ab33ee34 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -202,6 +202,13 @@ QString QgsMapLayer::id() const return mID; } +void QgsMapLayer::setId( const QString &id ) +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + + mID = id; +} + void QgsMapLayer::setName( const QString &name ) { QGIS_PROTECT_QOBJECT_THREAD_ACCESS diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index f36ed5f429cca..0b89974be9156 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -256,9 +256,26 @@ class CORE_EXPORT QgsMapLayer : public QObject */ static QString extensionPropertyType( PropertyType type ); - //! Returns the layer's unique ID, which is used to access this layer from QgsProject. + /** + * Returns the layer's unique ID, which is used to access this layer from QgsProject. + * + * \see setId() + */ QString id() const; + /** + * Sets the layer's \a id. + * + * \warning It is the caller's responsibility to ensure that the layer ID is unique in the desired context. + * Generally this method should not be called, and the auto generated ID should be used instead. + * + * \warning This method should not be called on layers which already belong to a QgsProject or QgsMapLayerStore. + * + * \see id() + * \since QGIS 3.38 + */ + void setId( const QString &id ); + /** * Set the display \a name of the layer. * \see name() diff --git a/tests/src/core/testqgsmaplayer.cpp b/tests/src/core/testqgsmaplayer.cpp index 0d4eea05207fa..a9caa8d05ff33 100644 --- a/tests/src/core/testqgsmaplayer.cpp +++ b/tests/src/core/testqgsmaplayer.cpp @@ -49,6 +49,7 @@ class TestQgsMapLayer : public QObject void cleanup(); // will be called after every testfunction. void isValid(); + void testId(); void formatName(); void setBlendMode(); @@ -113,6 +114,13 @@ void TestQgsMapLayer::isValid() QVERIFY( mpLayer->isValid() ); } +void TestQgsMapLayer::testId() +{ + std::unique_ptr< QgsVectorLayer > layer = std::make_unique< QgsVectorLayer >( QStringLiteral( "Point" ), QStringLiteral( "a" ), QStringLiteral( "memory" ) ); + layer->setId( QStringLiteral( "my forced id" ) ); + QCOMPARE( layer->id(), QStringLiteral( "my forced id" ) ); +} + void TestQgsMapLayer::formatName() { QCOMPARE( QgsMapLayer::formatLayerName( QString() ), QString() ); From ed2d4f559b5b87406506580937390337ef704c08 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 15:02:38 +1000 Subject: [PATCH 24/46] Add idChanged signal, property --- .../PyQt6/core/auto_generated/qgsmaplayer.sip.in | 15 +++++++++++++++ python/core/auto_generated/qgsmaplayer.sip.in | 15 +++++++++++++++ src/core/qgsmaplayer.cpp | 3 +++ src/core/qgsmaplayer.h | 14 +++++++++++++- tests/src/core/testqgsmaplayer.cpp | 7 +++++++ 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in index bb5714d2b2b77..8a62d7e057e44 100644 --- a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in +++ b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in @@ -189,6 +189,8 @@ Returns the extension of a Property. Returns the layer's unique ID, which is used to access this layer from :py:class:`QgsProject`. .. seealso:: :py:func:`setId` + +.. seealso:: :py:func:`idChanged` %End void setId( const QString &id ); @@ -207,6 +209,8 @@ Sets the layer's ``id``. .. seealso:: :py:func:`id` +.. seealso:: :py:func:`idChanged` + .. versionadded:: 3.38 %End @@ -1796,6 +1800,17 @@ just before the references of this layer are resolved. void statusChanged( const QString &status ); %Docstring Emit a signal with status (e.g. to be caught by QgisApp and display a msg on status bar) +%End + + void idChanged( const QString &id ); +%Docstring +Emitted when the layer's ID has been changed. + +.. seealso:: :py:func:`id` + +.. seealso:: :py:func:`setId` + +.. versionadded:: 3.38 %End void nameChanged(); diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index 90156e2faf97c..f3dd9a7682bd2 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -189,6 +189,8 @@ Returns the extension of a Property. Returns the layer's unique ID, which is used to access this layer from :py:class:`QgsProject`. .. seealso:: :py:func:`setId` + +.. seealso:: :py:func:`idChanged` %End void setId( const QString &id ); @@ -207,6 +209,8 @@ Sets the layer's ``id``. .. seealso:: :py:func:`id` +.. seealso:: :py:func:`idChanged` + .. versionadded:: 3.38 %End @@ -1796,6 +1800,17 @@ just before the references of this layer are resolved. void statusChanged( const QString &status ); %Docstring Emit a signal with status (e.g. to be caught by QgisApp and display a msg on status bar) +%End + + void idChanged( const QString &id ); +%Docstring +Emitted when the layer's ID has been changed. + +.. seealso:: :py:func:`id` + +.. seealso:: :py:func:`setId` + +.. versionadded:: 3.38 %End void nameChanged(); diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 1cbd0ab33ee34..50efac954d120 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -205,8 +205,11 @@ QString QgsMapLayer::id() const void QgsMapLayer::setId( const QString &id ) { QGIS_PROTECT_QOBJECT_THREAD_ACCESS + if ( id == mID ) + return; mID = id; + emit idChanged( id ); } void QgsMapLayer::setName( const QString &name ) diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 0b89974be9156..40e637f8ed66d 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -75,6 +75,7 @@ class CORE_EXPORT QgsMapLayer : public QObject { Q_OBJECT + Q_PROPERTY( QString id READ id WRITE setId NOTIFY idChanged ) Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged ) Q_PROPERTY( int autoRefreshInterval READ autoRefreshInterval WRITE setAutoRefreshInterval NOTIFY autoRefreshIntervalChanged ) Q_PROPERTY( QgsLayerMetadata metadata READ metadata WRITE setMetadata NOTIFY metadataChanged ) @@ -260,6 +261,7 @@ class CORE_EXPORT QgsMapLayer : public QObject * Returns the layer's unique ID, which is used to access this layer from QgsProject. * * \see setId() + * \see idChanged() */ QString id() const; @@ -272,6 +274,7 @@ class CORE_EXPORT QgsMapLayer : public QObject * \warning This method should not be called on layers which already belong to a QgsProject or QgsMapLayerStore. * * \see id() + * \see idChanged() * \since QGIS 3.38 */ void setId( const QString &id ); @@ -1809,8 +1812,17 @@ class CORE_EXPORT QgsMapLayer : public QObject void statusChanged( const QString &status ); /** - * Emitted when the name has been changed + * Emitted when the layer's ID has been changed. * + * \see id() + * \see setId() + * + * \since QGIS 3.38 + */ + void idChanged( const QString &id ); + + /** + * Emitted when the name has been changed */ void nameChanged(); diff --git a/tests/src/core/testqgsmaplayer.cpp b/tests/src/core/testqgsmaplayer.cpp index a9caa8d05ff33..ba97efc591ad7 100644 --- a/tests/src/core/testqgsmaplayer.cpp +++ b/tests/src/core/testqgsmaplayer.cpp @@ -117,8 +117,15 @@ void TestQgsMapLayer::isValid() void TestQgsMapLayer::testId() { std::unique_ptr< QgsVectorLayer > layer = std::make_unique< QgsVectorLayer >( QStringLiteral( "Point" ), QStringLiteral( "a" ), QStringLiteral( "memory" ) ); + QSignalSpy spy( layer.get(), &QgsMapLayer::idChanged ); layer->setId( QStringLiteral( "my forced id" ) ); QCOMPARE( layer->id(), QStringLiteral( "my forced id" ) ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.at( 0 ).at( 0 ).toString(), QStringLiteral( "my forced id " ) ); + + // same id, should not emit signal + layer->setId( QStringLiteral( "my forced id" ) ); + QCOMPARE( spy.count(), 1 ); } void TestQgsMapLayer::formatName() From d750c9d89cedb43833496e79b12802d77cdc4f90 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 18:49:52 +1000 Subject: [PATCH 25/46] Fix test --- tests/src/core/testqgsmaplayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/core/testqgsmaplayer.cpp b/tests/src/core/testqgsmaplayer.cpp index ba97efc591ad7..29bf9306daf80 100644 --- a/tests/src/core/testqgsmaplayer.cpp +++ b/tests/src/core/testqgsmaplayer.cpp @@ -121,7 +121,7 @@ void TestQgsMapLayer::testId() layer->setId( QStringLiteral( "my forced id" ) ); QCOMPARE( layer->id(), QStringLiteral( "my forced id" ) ); QCOMPARE( spy.count(), 1 ); - QCOMPARE( spy.at( 0 ).at( 0 ).toString(), QStringLiteral( "my forced id " ) ); + QCOMPARE( spy.at( 0 ).at( 0 ).toString(), QStringLiteral( "my forced id" ) ); // same id, should not emit signal layer->setId( QStringLiteral( "my forced id" ) ); From 031b2c755d391e0951dcfbd38417563c00e853c3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 20 Apr 2024 10:18:54 +1000 Subject: [PATCH 26/46] Emit idChanged on readLayerXml if id is changed --- src/core/qgsmaplayer.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 50efac954d120..1c0ac6eaeca17 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -598,7 +598,12 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon mne = mnl.toElement(); if ( ! mne.isNull() && mne.text().length() > 10 ) // should be at least 17 (yyyyMMddhhmmsszzz) { - mID = mne.text(); + const QString newId = mne.text(); + if ( newId != mID ) + { + mID = mne.text(); + emit idChanged( mID ); + } } } From 9d6faf3d00c3bf39b9a97bbf4fea210a0d5e69bc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 20 Apr 2024 10:29:28 +1000 Subject: [PATCH 27/46] Prevent changing ID if layer is owned --- .../core/auto_generated/qgsmaplayer.sip.in | 5 +++- python/core/auto_generated/qgsmaplayer.sip.in | 5 +++- src/core/qgsmaplayer.cpp | 11 ++++++-- src/core/qgsmaplayer.h | 5 +++- tests/src/core/testqgsmaplayer.cpp | 26 ++++++++++++++++++- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in index 8a62d7e057e44..e7509e7753b4c 100644 --- a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in +++ b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in @@ -193,10 +193,13 @@ Returns the layer's unique ID, which is used to access this layer from :py:class .. seealso:: :py:func:`idChanged` %End - void setId( const QString &id ); + bool setId( const QString &id ); %Docstring Sets the layer's ``id``. +Returns ``True`` if the layer ID was successfully changed, or ``False`` if it could not be changed (e.g. because +the layer is owned by a :py:class:`QgsProject` or :py:class:`QgsMapLayerStore`). + .. warning:: It is the caller's responsibility to ensure that the layer ID is unique in the desired context. diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index f3dd9a7682bd2..de3e0845e3f1c 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -193,10 +193,13 @@ Returns the layer's unique ID, which is used to access this layer from :py:class .. seealso:: :py:func:`idChanged` %End - void setId( const QString &id ); + bool setId( const QString &id ); %Docstring Sets the layer's ``id``. +Returns ``True`` if the layer ID was successfully changed, or ``False`` if it could not be changed (e.g. because +the layer is owned by a :py:class:`QgsProject` or :py:class:`QgsMapLayerStore`). + .. warning:: It is the caller's responsibility to ensure that the layer ID is unique in the desired context. diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 1c0ac6eaeca17..cdfc40277ef86 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -202,14 +202,21 @@ QString QgsMapLayer::id() const return mID; } -void QgsMapLayer::setId( const QString &id ) +bool QgsMapLayer::setId( const QString &id ) { QGIS_PROTECT_QOBJECT_THREAD_ACCESS + if ( qobject_cast< QgsMapLayerStore * >( parent() ) ) + { + // layer is already registered, cannot change id + return false; + } + if ( id == mID ) - return; + return false; mID = id; emit idChanged( id ); + return true; } void QgsMapLayer::setName( const QString &name ) diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 40e637f8ed66d..9001af953261c 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -268,6 +268,9 @@ class CORE_EXPORT QgsMapLayer : public QObject /** * Sets the layer's \a id. * + * Returns TRUE if the layer ID was successfully changed, or FALSE if it could not be changed (e.g. because + * the layer is owned by a QgsProject or QgsMapLayerStore). + * * \warning It is the caller's responsibility to ensure that the layer ID is unique in the desired context. * Generally this method should not be called, and the auto generated ID should be used instead. * @@ -277,7 +280,7 @@ class CORE_EXPORT QgsMapLayer : public QObject * \see idChanged() * \since QGIS 3.38 */ - void setId( const QString &id ); + bool setId( const QString &id ); /** * Set the display \a name of the layer. diff --git a/tests/src/core/testqgsmaplayer.cpp b/tests/src/core/testqgsmaplayer.cpp index 29bf9306daf80..a448585ec00a6 100644 --- a/tests/src/core/testqgsmaplayer.cpp +++ b/tests/src/core/testqgsmaplayer.cpp @@ -29,6 +29,8 @@ #include "qgsvectorlayerref.h" #include "qgsmaplayerlistutils_p.h" #include "qgsmaplayerproxymodel.h" +#include "qgsmaplayerstore.h" +#include "qgsproject.h" #include "qgsxmlutils.h" /** @@ -118,7 +120,7 @@ void TestQgsMapLayer::testId() { std::unique_ptr< QgsVectorLayer > layer = std::make_unique< QgsVectorLayer >( QStringLiteral( "Point" ), QStringLiteral( "a" ), QStringLiteral( "memory" ) ); QSignalSpy spy( layer.get(), &QgsMapLayer::idChanged ); - layer->setId( QStringLiteral( "my forced id" ) ); + QVERIFY( layer->setId( QStringLiteral( "my forced id" ) ) ); QCOMPARE( layer->id(), QStringLiteral( "my forced id" ) ); QCOMPARE( spy.count(), 1 ); QCOMPARE( spy.at( 0 ).at( 0 ).toString(), QStringLiteral( "my forced id" ) ); @@ -126,6 +128,28 @@ void TestQgsMapLayer::testId() // same id, should not emit signal layer->setId( QStringLiteral( "my forced id" ) ); QCOMPARE( spy.count(), 1 ); + + // if layer is owned by QgsMapLayerStore, cannot change ID + QgsMapLayerStore store; + QgsVectorLayer *layer2 = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "a" ), QStringLiteral( "memory" ) ); + QSignalSpy spy2( layer2, &QgsMapLayer::idChanged ); + layer2->setId( QStringLiteral( "my forced id" ) ); + QCOMPARE( spy2.count(), 1 ); + store.addMapLayer( layer2 ); + QVERIFY( !layer2->setId( QStringLiteral( "aaa" ) ) ); + QCOMPARE( layer2->id(), QStringLiteral( "my forced id" ) ); + QCOMPARE( spy2.count(), 1 ); + + // if layer is owned by QgsProject, cannot change ID + QgsProject project; + QgsVectorLayer *layer3 = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "a" ), QStringLiteral( "memory" ) ); + QSignalSpy spy3( layer3, &QgsMapLayer::idChanged ); + layer3->setId( QStringLiteral( "my forced id" ) ); + QCOMPARE( spy3.count(), 1 ); + project.addMapLayer( layer3 ); + QVERIFY( !layer3->setId( QStringLiteral( "aaa" ) ) ); + QCOMPARE( layer3->id(), QStringLiteral( "my forced id" ) ); + QCOMPARE( spy3.count(), 1 ); } void TestQgsMapLayer::formatName() From 7f4fe65bee2f4eae98b3a5fe3bf4ed37cc82c16a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 18 Apr 2024 11:34:20 +1000 Subject: [PATCH 28/46] Ensure that QgsProcessingAlgorithm::postProcess is always called We now always call QgsProcessingAlgorithm::postProcess, even when the algorithm fails for some reason (eg it raises an exception). This ensures that the context cleanup logic in postProcess is always run. Note that we still ONLY call an algorithm's specific postProcessAlgorithm implementation for successful executions, so the public facing API and behavior remains unchanged. This is intented to provide a consistent handling of the cleanup logic in postProcess, specifically to make sure that that the context thread handling logic is triggered in all cases. --- .../processing/qgsprocessingalgorithm.sip.in | 6 +- .../processing/qgsprocessingalgorithm.sip.in | 6 +- .../models/qgsprocessingmodelalgorithm.cpp | 17 ++- .../processing/qgsprocessingalgorithm.cpp | 31 +++-- src/core/processing/qgsprocessingalgorithm.h | 7 +- .../processing/qgsprocessingalgrunnertask.cpp | 6 +- tests/src/analysis/testqgsprocessing.cpp | 104 ++++++++++++++- .../testqgsprocessingmodelalgorithm.cpp | 86 ++++++++++++- .../src/python/test_qgsprocessingalgrunner.py | 118 ++++++++++++++++++ 9 files changed, 354 insertions(+), 27 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingalgorithm.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingalgorithm.sip.in index ed5adf833d0c4..2862b6174d558 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingalgorithm.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingalgorithm.sip.in @@ -369,7 +369,7 @@ to allow the algorithm to perform any required cleanup tasks and return its fina of the algorithm should be created with :py:func:`~QgsProcessingAlgorithm.clone` and :py:func:`~QgsProcessingAlgorithm.prepare`/:py:func:`~QgsProcessingAlgorithm.runPrepared` called on the copy. %End - QVariantMap postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback ); + QVariantMap postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool runResult = true ); %Docstring Should be called in the main thread following the completion of :py:func:`~QgsProcessingAlgorithm.runPrepared`. This method allows the algorithm to perform any required cleanup tasks. The returned variant map @@ -380,6 +380,10 @@ includes the results evaluated by the algorithm. This method modifies the algorithm instance, so it is not safe to call on algorithms directly retrieved from :py:class:`QgsProcessingRegistry` and :py:class:`QgsProcessingProvider`. Instead, a copy of the algorithm should be created with :py:func:`~QgsProcessingAlgorithm.clone` and :py:func:`~QgsProcessingAlgorithm.prepare`/:py:func:`~QgsProcessingAlgorithm.runPrepared` called on the copy. + +Since QGIS 3.38, :py:func:`~QgsProcessingAlgorithm.postProcess` will always be called even for unsuccessful run executions, to allow +the algorithm to gracefully clean up. The ``runResult`` argument is used to indicate whether the run +was successful. The algorithm's :py:func:`~QgsProcessingAlgorithm.postProcessAlgorithm` method will only be called when ``runResult`` is ``True``. %End virtual QWidget *createCustomParametersWidget( QWidget *parent = 0 ) const /Factory/; diff --git a/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in b/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in index ed5adf833d0c4..2862b6174d558 100644 --- a/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in @@ -369,7 +369,7 @@ to allow the algorithm to perform any required cleanup tasks and return its fina of the algorithm should be created with :py:func:`~QgsProcessingAlgorithm.clone` and :py:func:`~QgsProcessingAlgorithm.prepare`/:py:func:`~QgsProcessingAlgorithm.runPrepared` called on the copy. %End - QVariantMap postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback ); + QVariantMap postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool runResult = true ); %Docstring Should be called in the main thread following the completion of :py:func:`~QgsProcessingAlgorithm.runPrepared`. This method allows the algorithm to perform any required cleanup tasks. The returned variant map @@ -380,6 +380,10 @@ includes the results evaluated by the algorithm. This method modifies the algorithm instance, so it is not safe to call on algorithms directly retrieved from :py:class:`QgsProcessingRegistry` and :py:class:`QgsProcessingProvider`. Instead, a copy of the algorithm should be created with :py:func:`~QgsProcessingAlgorithm.clone` and :py:func:`~QgsProcessingAlgorithm.prepare`/:py:func:`~QgsProcessingAlgorithm.runPrepared` called on the copy. + +Since QGIS 3.38, :py:func:`~QgsProcessingAlgorithm.postProcess` will always be called even for unsuccessful run executions, to allow +the algorithm to gracefully clean up. The ``runResult`` argument is used to indicate whether the run +was successful. The algorithm's :py:func:`~QgsProcessingAlgorithm.postProcessAlgorithm` method will only be called when ``runResult`` is ``True``. %End virtual QWidget *createCustomParametersWidget( QWidget *parent = 0 ) const /Factory/; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 521aab7edaa7c..36b1ee53d5ad3 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -437,6 +437,8 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa } QVariantMap results; + QString runError; + bool runResult = false; try { if ( ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::NoThreading ) && ( QThread::currentThread() != qApp->thread() ) ) @@ -460,26 +462,26 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa // safe to run on model thread results = childAlg->runPrepared( childParams, context, &modelFeedback ); } + runResult = true; } catch ( QgsProcessingException &e ) { - const QString error = ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::CustomException ) ? e.what() : QObject::tr( "Error encountered while running %1: %2" ).arg( child.description(), e.what() ); - throw QgsProcessingException( error ); + error = ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::CustomException ) ? e.what() : QObject::tr( "Error encountered while running %1: %2" ).arg( child.description(), e.what() ); } Q_ASSERT_X( QThread::currentThread() == context.thread(), "QgsProcessingModelAlgorithm::processAlgorithm", "context was not transferred back to model thread" ); QVariantMap ppRes; - auto postProcessOnMainThread = [modelThread, &ppRes, &childAlg, &context, &modelFeedback] + auto postProcessOnMainThread = [modelThread, &ppRes, &childAlg, &context, &modelFeedback, runResult] { Q_ASSERT_X( QThread::currentThread() == qApp->thread(), "QgsProcessingModelAlgorithm::processAlgorithm", "childAlg->postProcess() must be run on the main thread" ); - ppRes = childAlg->postProcess( context, &modelFeedback ); + ppRes = childAlg->postProcess( context, &modelFeedback, runResult ); context.pushToThread( modelThread ); }; // Make sure we only run postProcess steps on the main thread! if ( modelThread == qApp->thread() ) - ppRes = childAlg->postProcess( context, &modelFeedback ); + ppRes = childAlg->postProcess( context, &modelFeedback, runResult ); else { context.pushToThread( qApp->thread() ); @@ -491,6 +493,11 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa if ( !ppRes.isEmpty() ) results = ppRes; + if ( !runResult ) + { + throw QgsProcessingException( error ); + } + if ( feedback && !skipGenericLogging ) { const QVariantMap displayOutputs = QgsProcessingUtils::removePointerValuesFromMap( results ); diff --git a/src/core/processing/qgsprocessingalgorithm.cpp b/src/core/processing/qgsprocessingalgorithm.cpp index 863fbb631ac00..bd131754473ca 100644 --- a/src/core/processing/qgsprocessingalgorithm.cpp +++ b/src/core/processing/qgsprocessingalgorithm.cpp @@ -536,24 +536,28 @@ QVariantMap QgsProcessingAlgorithm::run( const QVariantMap ¶meters, QgsProce return QVariantMap(); QVariantMap runRes; + bool success = false; try { runRes = alg->runPrepared( parameters, context, feedback ); + success = true; } catch ( QgsProcessingException &e ) { if ( !catchExceptions ) + { + alg->postProcess( context, feedback, false ); throw e; + } QgsMessageLog::logMessage( e.what(), QObject::tr( "Processing" ), Qgis::MessageLevel::Critical ); feedback->reportError( e.what() ); - return QVariantMap(); } if ( ok ) - *ok = true; + *ok = success; - QVariantMap ppRes = alg->postProcess( context, feedback ); + QVariantMap ppRes = alg->postProcess( context, feedback, success ); if ( !ppRes.isEmpty() ) return ppRes; else @@ -608,11 +612,11 @@ QVariantMap QgsProcessingAlgorithm::runPrepared( const QVariantMap ¶meters, runContext = mLocalContext.get(); } + mHasExecuted = true; try { QVariantMap runResults = processAlgorithm( parameters, *runContext, feedback ); - mHasExecuted = true; if ( mLocalContext ) { // ok, time to clean things up. We need to push the temporary context back into @@ -634,7 +638,7 @@ QVariantMap QgsProcessingAlgorithm::runPrepared( const QVariantMap ¶meters, } } -QVariantMap QgsProcessingAlgorithm::postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +QVariantMap QgsProcessingAlgorithm::postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool runResult ) { // cppcheck-suppress assertWithSideEffect Q_ASSERT_X( QThread::currentThread() == context.temporaryLayerStore()->thread(), "QgsProcessingAlgorithm::postProcess", "postProcess() must be called from the same thread the context was created in" ); @@ -652,14 +656,21 @@ QVariantMap QgsProcessingAlgorithm::postProcess( QgsProcessingContext &context, } mHasPostProcessed = true; - try + if ( runResult ) { - return postProcessAlgorithm( context, feedback ); + try + { + return postProcessAlgorithm( context, feedback ); + } + catch ( QgsProcessingException &e ) + { + QgsMessageLog::logMessage( e.what(), QObject::tr( "Processing" ), Qgis::MessageLevel::Critical ); + feedback->reportError( e.what() ); + return QVariantMap(); + } } - catch ( QgsProcessingException &e ) + else { - QgsMessageLog::logMessage( e.what(), QObject::tr( "Processing" ), Qgis::MessageLevel::Critical ); - feedback->reportError( e.what() ); return QVariantMap(); } } diff --git a/src/core/processing/qgsprocessingalgorithm.h b/src/core/processing/qgsprocessingalgorithm.h index c6c2f4e323d74..7191f809540f1 100644 --- a/src/core/processing/qgsprocessingalgorithm.h +++ b/src/core/processing/qgsprocessingalgorithm.h @@ -391,8 +391,12 @@ class CORE_EXPORT QgsProcessingAlgorithm * \note This method modifies the algorithm instance, so it is not safe to call * on algorithms directly retrieved from QgsProcessingRegistry and QgsProcessingProvider. Instead, a copy * of the algorithm should be created with clone() and prepare()/runPrepared() called on the copy. + * + * Since QGIS 3.38, postProcess() will always be called even for unsuccessful run executions, to allow + * the algorithm to gracefully clean up. The \a runResult argument is used to indicate whether the run + * was successful. The algorithm's postProcessAlgorithm() method will only be called when \a runResult is TRUE. */ - QVariantMap postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback ); + QVariantMap postProcess( QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool runResult = true ); /** * If an algorithm subclass implements a custom parameters widget, a copy of this widget @@ -1104,6 +1108,7 @@ class CORE_EXPORT QgsProcessingAlgorithm friend class TestQgsProcessing; friend class QgsProcessingModelAlgorithm; friend class QgsProcessingToolboxProxyModel; + friend class DummyRaiseExceptionAlgorithm; #ifdef SIP_RUN QgsProcessingAlgorithm( const QgsProcessingAlgorithm &other ); diff --git a/src/core/processing/qgsprocessingalgrunnertask.cpp b/src/core/processing/qgsprocessingalgrunnertask.cpp index 970669e5c43b6..2d08243ae1929 100644 --- a/src/core/processing/qgsprocessingalgrunnertask.cpp +++ b/src/core/processing/qgsprocessingalgrunnertask.cpp @@ -80,10 +80,6 @@ bool QgsProcessingAlgRunnerTask::run() void QgsProcessingAlgRunnerTask::finished( bool result ) { Q_UNUSED( result ) - QVariantMap ppResults; - if ( result ) - { - ppResults = mAlgorithm->postProcess( mContext, mFeedback ); - } + const QVariantMap ppResults = mAlgorithm->postProcess( mContext, mFeedback, result ); emit executed( result, !ppResults.isEmpty() ? ppResults : mResults ); } diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index 5a640d857db8a..daff9a8d293e0 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -65,12 +65,44 @@ class DummyAlgorithm : public QgsProcessingAlgorithm { public: - DummyAlgorithm( const QString &name ) : mName( name ) { mFlags = Qgis::ProcessingAlgorithmFlags(); } + DummyAlgorithm( const QString &name ) + : mName( name ) + { + mFlags = Qgis::ProcessingAlgorithmFlags(); + } void initAlgorithm( const QVariantMap & = QVariantMap() ) override {} QString name() const override { return mName; } QString displayName() const override { return mName; } - QVariantMap processAlgorithm( const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) override { return QVariantMap(); } + + // we use static members here as the algorithm instance will be internally privately cloned before executing + static bool mRaiseProcessException; + static bool mPrepared; + + bool prepareAlgorithm( const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) final + { + mPrepared = true; + return true; + } + static bool mProcessed; + QVariantMap processAlgorithm( const QVariantMap &, QgsProcessingContext &context, QgsProcessingFeedback * ) final + { + mProcessed = true; + + QgsVectorLayer *layer3111 = new QgsVectorLayer( "Point?crs=epsg:3111", "v1", "memory" ); + context.temporaryLayerStore()->addMapLayer( layer3111 ); + + if ( mRaiseProcessException ) + throw QgsProcessingException( QString() ); + + return QVariantMap(); + } + static bool mPostProcessed; + QVariantMap postProcessAlgorithm( QgsProcessingContext &, QgsProcessingFeedback * ) final + { + mPostProcessed = true; + return QVariantMap(); + } Qgis::ProcessingAlgorithmFlags flags() const override { return mFlags; } DummyAlgorithm *createInstance() const override { return new DummyAlgorithm( name() ); } @@ -515,6 +547,10 @@ class DummyProvider : public QgsProcessingProvider // clazy:exclude=missing-qobj friend class TestQgsProcessing; }; +bool DummyAlgorithm::mPrepared = false; +bool DummyAlgorithm::mRaiseProcessException = false; +bool DummyAlgorithm::mProcessed = false; +bool DummyAlgorithm::mPostProcessed = false; class DummyProviderNoLoad : public DummyProvider // clazy:exclude=missing-qobject-macro { @@ -789,6 +825,7 @@ class TestQgsProcessing: public QgsTest void parameterAnnotationLayer(); void parameterVectorTileOut(); void checkParamValues(); + void runAlgorithm(); void combineLayerExtent(); void processingFeatureSource(); void processingFeatureSink(); @@ -11770,6 +11807,69 @@ void TestQgsProcessing::checkParamValues() a.checkParameterVals(); } +void TestQgsProcessing::runAlgorithm() +{ + std::unique_ptr< DummyAlgorithm > a = std::make_unique< DummyAlgorithm >( "asd" ); + a->mRaiseProcessException = false; + a->mPrepared = false; + a->mProcessed = false; + a->mPostProcessed = false; + QgsProcessingContext context; + QgsProcessingFeedback feedback; + bool ok = false; + QVariantMap res = a->run( QVariantMap(), context, &feedback, &ok ); + QVERIFY( ok ); + QVERIFY( a->mPrepared ); + QVERIFY( a->mProcessed ); + QVERIFY( a->mPostProcessed ); + // ensure layer added by the algorithm in processAlgorithm is present in context + QCOMPARE( context.temporaryLayerStore()->count(), 1 ); + context.temporaryLayerStore()->removeAllMapLayers(); + + // try with an algorithm which raises an exception, postProcessAlgorithm should not be called + a = std::make_unique< DummyAlgorithm >( "asd" ); + a->mRaiseProcessException = true; + a->mPrepared = false; + a->mProcessed = false; + a->mPostProcessed = false; + ok = false; + res = a->run( QVariantMap(), context, &feedback, &ok ); + QVERIFY( !ok ); + QVERIFY( a->mPrepared ); + QVERIFY( a->mProcessed ); + QVERIFY( !a->mPostProcessed ); + // ensure layer added by the algorithm in processAlgorithm is present in context, this should + // always be the case even if an exception occurs + QCOMPARE( context.temporaryLayerStore()->count(), 1 ); + context.temporaryLayerStore()->removeAllMapLayers(); + + // try without internally catching exceptions in run, postProcessAlgorithm should not be called + a = std::make_unique< DummyAlgorithm >( "asd" ); + a->mRaiseProcessException = true; + a->mPrepared = false; + a->mProcessed = false; + a->mPostProcessed = false; + ok = false; + bool caught = false; + try + { + res = a->run( QVariantMap(), context, &feedback, &ok, QVariantMap(), false ); + } + catch ( QgsProcessingException & ) + { + caught = true; + } + QVERIFY( caught ); + QVERIFY( !ok ); + QVERIFY( a->mPrepared ); + QVERIFY( a->mProcessed ); + QVERIFY( !a->mPostProcessed ); + // ensure layer added by the algorithm in processAlgorithm is present in context, this should + // always be the case even if an exception occurs + QCOMPARE( context.temporaryLayerStore()->count(), 1 ); + context.temporaryLayerStore()->removeAllMapLayers(); +} + void TestQgsProcessing::combineLayerExtent() { QgsProcessingContext context; diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index d72be4132c940..856cd9936544c 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -51,6 +51,44 @@ class DummyAlgorithm2 : public QgsProcessingAlgorithm }; +class DummyRaiseExceptionAlgorithm : public QgsProcessingAlgorithm +{ + public: + + DummyRaiseExceptionAlgorithm( const QString &name ) : mName( name ) { mFlags = QgsProcessingAlgorithm::flags(); hasPostProcessed = false; } + static bool hasPostProcessed; + ~DummyRaiseExceptionAlgorithm() + { + hasPostProcessed |= mHasPostProcessed; + } + + void initAlgorithm( const QVariantMap & = QVariantMap() ) override + { + } + QString name() const override { return mName; } + QString displayName() const override { return mName; } + QVariantMap processAlgorithm( const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) override + { + throw QgsProcessingException( QString() ); + } + static bool postProcessAlgorithmCalled; + QVariantMap postProcessAlgorithm( QgsProcessingContext &, QgsProcessingFeedback * ) final + { + postProcessAlgorithmCalled = true; + return QVariantMap(); + } + + Qgis::ProcessingAlgorithmFlags flags() const override { return mFlags; } + DummyRaiseExceptionAlgorithm *createInstance() const override { return new DummyRaiseExceptionAlgorithm( name() ); } + + QString mName; + + Qgis::ProcessingAlgorithmFlags mFlags; + +}; +bool DummyRaiseExceptionAlgorithm::hasPostProcessed = false; +bool DummyRaiseExceptionAlgorithm::postProcessAlgorithmCalled = false; + class DummyProvider4 : public QgsProcessingProvider // clazy:exclude=missing-qobject-macro { public: @@ -77,6 +115,7 @@ class DummyProvider4 : public QgsProcessingProvider // clazy:exclude=missing-qob void loadAlgorithms() override { QVERIFY( addAlgorithm( new DummyAlgorithm2( QStringLiteral( "alg1" ) ) ) ); + QVERIFY( addAlgorithm( new DummyRaiseExceptionAlgorithm( QStringLiteral( "raise" ) ) ) ); } }; @@ -108,6 +147,7 @@ class TestQgsProcessingModelAlgorithm: public QgsTest void modelValidate(); void modelInputs(); void modelOutputs(); + void modelWithChildException(); void modelDependencies(); void modelSource(); void modelNameMatchesFileName(); @@ -133,6 +173,7 @@ void TestQgsProcessingModelAlgorithm::initTestCase() settings.clear(); QgsApplication::processingRegistry()->addProvider( new QgsNativeAlgorithms( QgsApplication::processingRegistry() ) ); + QgsApplication::processingRegistry()->addProvider( new DummyProvider4() ); } void TestQgsProcessingModelAlgorithm::cleanupTestCase() @@ -1684,8 +1725,6 @@ void TestQgsProcessingModelAlgorithm::modelBranchPruningConditional() void TestQgsProcessingModelAlgorithm::modelWithProviderWithLimitedTypes() { - QgsApplication::processingRegistry()->addProvider( new DummyProvider4() ); - QgsProcessingModelAlgorithm alg( "test", "testGroup" ); QgsProcessingModelChildAlgorithm algc1; algc1.setChildId( QStringLiteral( "cx1" ) ); @@ -2274,6 +2313,49 @@ void TestQgsProcessingModelAlgorithm::modelOutputs() QCOMPARE( context.layerToLoadOnCompletionDetails( destC ).layerSortKey, 1 ); } + +void TestQgsProcessingModelAlgorithm::modelWithChildException() +{ + QgsProcessingModelAlgorithm m; + + const QgsProcessingModelParameter sourceParam( "INPUT" ); + m.addModelParameter( new QgsProcessingParameterFeatureSource( "INPUT" ), sourceParam ); + + QgsProcessingModelChildAlgorithm algWhichCreatesLayer; + algWhichCreatesLayer.setChildId( QStringLiteral( "buffer" ) ); + algWhichCreatesLayer.setAlgorithmId( "native:buffer" ); + algWhichCreatesLayer.addParameterSources( "INPUT", { QgsProcessingModelChildParameterSource::fromModelParameter( "INPUT" ) } ); + + m.addChildAlgorithm( algWhichCreatesLayer ); + + QgsProcessingModelChildAlgorithm algWhichRaisesException; + algWhichRaisesException.setChildId( QStringLiteral( "raise" ) ); + algWhichRaisesException.setAlgorithmId( "dummy4:raise" ); + algWhichRaisesException.setDependencies( {QgsProcessingModelChildDependency( QStringLiteral( "buffer" ) )} ); + m.addChildAlgorithm( algWhichRaisesException ); + + // run and check context details + QgsProcessingContext context; + QgsProcessingFeedback feedback; + QVariantMap params; + QgsVectorLayer *layer3111 = new QgsVectorLayer( "Point?crs=epsg:3111", "v1", "memory" ); + QgsProject p; + p.addMapLayer( layer3111 ); + context.setProject( &p ); + params.insert( QStringLiteral( "INPUT" ), QStringLiteral( "v1" ) ); + + bool ok = false; + QVariantMap results = m.run( params, context, &feedback, &ok ); + // model should fail, exception was raised + QVERIFY( !ok ); + // but result from successful buffer step should still be available in the context + QCOMPARE( context.temporaryLayerStore()->count(), 1 ); + // confirm that QgsProcessingAlgorithm::postProcess was called for failing DummyRaiseExceptionAlgorithm step + QVERIFY( DummyRaiseExceptionAlgorithm::hasPostProcessed ); + // but not DummyRaiseExceptionAlgorithm::postProcessAlgorithm + QVERIFY( !DummyRaiseExceptionAlgorithm::postProcessAlgorithmCalled ); +} + void TestQgsProcessingModelAlgorithm::modelDependencies() { const QgsProcessingModelChildDependency dep( QStringLiteral( "childId" ), QStringLiteral( "branch" ) ); diff --git a/tests/src/python/test_qgsprocessingalgrunner.py b/tests/src/python/test_qgsprocessingalgrunner.py index 3e744699c4355..5fcd6004d0dd9 100644 --- a/tests/src/python/test_qgsprocessingalgrunner.py +++ b/tests/src/python/test_qgsprocessingalgrunner.py @@ -21,6 +21,8 @@ QgsProject, QgsSettings, QgsTask, + QgsProcessingException, + QgsVectorLayer ) import unittest from qgis.testing import start_app, QgisTestCase @@ -74,6 +76,68 @@ def processAlgorithm(self, parameters, context, feedback): return {self.OUTPUT: 'an_id'} +class TestAlgorithm(QgsProcessingAlgorithm): + + INPUT = 'INPUT' + OUTPUT = 'OUTPUT' + + def createInstance(self): + return TestAlgorithm() + + def name(self): + return 'test' + + def displayName(self): + return 'test' + + def group(self): + return 'test' + + def groupId(self): + return 'test' + + def shortHelpString(self): + return 'test' + + def initAlgorithm(self, config=None): + pass + + def processAlgorithm(self, parameters, context, feedback): + context.temporaryLayerStore().addMapLayer(QgsVectorLayer("Point?crs=epsg:3111", "v1", "memory")) + return {self.OUTPUT: 'an_id'} + + +class ExceptionAlgorithm(QgsProcessingAlgorithm): + + INPUT = 'INPUT' + OUTPUT = 'OUTPUT' + + def createInstance(self): + return ExceptionAlgorithm() + + def name(self): + return 'test' + + def displayName(self): + return 'test' + + def group(self): + return 'test' + + def groupId(self): + return 'test' + + def shortHelpString(self): + return 'test' + + def initAlgorithm(self, config=None): + pass + + def processAlgorithm(self, parameters, context, feedback): + context.temporaryLayerStore().addMapLayer(QgsVectorLayer("Point?crs=epsg:3111", "v1", "memory")) + raise QgsProcessingException('error') + + class TestQgsProcessingAlgRunner(QgisTestCase): @classmethod @@ -141,6 +205,60 @@ def test_bad_script_dont_crash(self): # spellok self.assertTrue(task.isCanceled()) self.assertIn('name \'ExampleProcessingAlgorithm\' is not defined', feedback._error) + def test_good(self): + """ + Test a good algorithm + """ + + context = QgsProcessingContext() + context.setProject(QgsProject.instance()) + feedback = ConsoleFeedBack() + + task = QgsProcessingAlgRunnerTask(TestAlgorithm(), {}, context=context, feedback=feedback) + self.assertFalse(task.isCanceled()) + TestQgsProcessingAlgRunner.finished = False + TestQgsProcessingAlgRunner.success = None + + def on_executed(success, results): + TestQgsProcessingAlgRunner.finished = True + TestQgsProcessingAlgRunner.success = success + + task.executed.connect(on_executed) + QgsApplication.taskManager().addTask(task) + task.waitForFinished() + + self.assertTrue(TestQgsProcessingAlgRunner.finished) + self.assertTrue(TestQgsProcessingAlgRunner.success) + self.assertEqual(context.temporaryLayerStore().count(), 1) + + def test_raises_exception(self): + """ + Test an algorithm which raises an exception + """ + + context = QgsProcessingContext() + context.setProject(QgsProject.instance()) + feedback = ConsoleFeedBack() + + task = QgsProcessingAlgRunnerTask(ExceptionAlgorithm(), {}, context=context, feedback=feedback) + self.assertFalse(task.isCanceled()) + TestQgsProcessingAlgRunner.finished = False + TestQgsProcessingAlgRunner.success = None + + def on_executed(success, results): + TestQgsProcessingAlgRunner.finished = True + TestQgsProcessingAlgRunner.success = success + + task.executed.connect(on_executed) + QgsApplication.taskManager().addTask(task) + task.waitForFinished() + + self.assertTrue(TestQgsProcessingAlgRunner.finished) + self.assertFalse(TestQgsProcessingAlgRunner.success) + # layer added by algorithm should have been transferred to the context, even when an + # exception was raised + self.assertEqual(context.temporaryLayerStore().count(), 1) + if __name__ == '__main__': unittest.main() From 133bad76eea57e7722fc80f2aa32dc239b66ec24 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 16 Apr 2024 13:53:02 +1000 Subject: [PATCH 29/46] Move some model designer code to c++ --- .../models/qgsmodeldesignerdialog.sip.in | 2 + .../models/qgsmodeldesignerdialog.sip.in | 2 + .../processing/modeler/ModelerDialog.py | 33 +------------ .../models/qgsmodeldesignerdialog.cpp | 49 ++++++++++++++++++- .../models/qgsmodeldesignerdialog.h | 4 ++ 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 8646916822fc5..89d56d3ece85e 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -94,6 +94,8 @@ Raise, unminimize and activate this window. virtual bool saveModel( bool saveAs = false ) = 0; + virtual QgsProcessingAlgorithmDialogBase *createExecutionDialog() = 0 /TransferBack/; + QToolBar *toolbar(); QAction *actionOpen(); QAction *actionSaveInProject(); diff --git a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 8646916822fc5..89d56d3ece85e 100644 --- a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -94,6 +94,8 @@ Raise, unminimize and activate this window. virtual bool saveModel( bool saveAs = false ) = 0; + virtual QgsProcessingAlgorithmDialogBase *createExecutionDialog() = 0 /TransferBack/; + QToolBar *toolbar(); QAction *actionOpen(); QAction *actionSaveInProject(); diff --git a/python/plugins/processing/modeler/ModelerDialog.py b/python/plugins/processing/modeler/ModelerDialog.py index d7b9e2e8eeaf1..bbec5d6b0068e 100644 --- a/python/plugins/processing/modeler/ModelerDialog.py +++ b/python/plugins/processing/modeler/ModelerDialog.py @@ -100,7 +100,6 @@ def __init__(self, model=None, parent=None): self.actionOpen().triggered.connect(self.openModel) self.actionSaveInProject().triggered.connect(self.saveInProject) - self.actionRun().triggered.connect(self.runModel) if model is not None: _model = model.create() @@ -122,37 +121,9 @@ def processingContext(self): self.context_generator = ContextGenerator(self.processing_context) - def runModel(self): - valid, errors = self.model().validate() - if not valid: - message_box = QMessageBox() - message_box.setWindowTitle(self.tr('Model is Invalid')) - message_box.setIcon(QMessageBox.Icon.Warning) - message_box.setText(self.tr('This model is not valid and contains one or more issues. Are you sure you want to run it in this state?')) - message_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel) - message_box.setDefaultButton(QMessageBox.StandardButton.Cancel) - - error_string = '' - for e in errors: - e = re.sub(r'<[^>]*>', '', e) - error_string += f'• {e}\n' - - message_box.setDetailedText(error_string) - if message_box.exec() == QMessageBox.StandardButton.Cancel: - return - - def on_finished(successful, results): - self.setLastRunChildAlgorithmResults(dlg.results().get('CHILD_RESULTS', {})) - self.setLastRunChildAlgorithmInputs(dlg.results().get('CHILD_INPUTS', {})) - + def createExecutionDialog(self): dlg = AlgorithmDialog(self.model().create(), parent=self) - dlg.setLogLevel(QgsProcessingContext.LogLevel.ModelDebug) - dlg.setParameters(self.model().designerParameterValues()) - dlg.algorithmFinished.connect(on_finished) - dlg.exec() - - if dlg.wasExecuted(): - self.model().setDesignerParameterValues(dlg.createProcessingParameters(flags=QgsProcessingParametersGenerator.Flags(QgsProcessingParametersGenerator.Flag.SkipDefaultValueParameters))) + return dlg def saveInProject(self): if not self.validateSave(QgsModelDesignerDialog.SaveAction.SaveInProject): diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 38dad5759716c..374074c6bb386 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -38,7 +38,7 @@ #include "qgsprocessinghelpeditorwidget.h" #include "qgsscreenhelper.h" #include "qgsmessagelog.h" - +#include "qgsprocessingalgorithmdialogbase.h" #include #include #include @@ -157,6 +157,7 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags connect( mActionReorderOutputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderOutputs ); connect( mActionEditHelp, &QAction::triggered, this, &QgsModelDesignerDialog::editHelp ); connect( mReorderInputsButton, &QPushButton::clicked, this, &QgsModelDesignerDialog::reorderInputs ); + connect( mActionRun, &QAction::triggered, this, &QgsModelDesignerDialog::run ); mActionSnappingEnabled->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), false ).toBool() ); connect( mActionSnappingEnabled, &QAction::toggled, this, [ = ]( bool enabled ) @@ -994,6 +995,52 @@ void QgsModelDesignerDialog::editHelp() } } +void QgsModelDesignerDialog::run() +{ + QStringList errors; + const bool isValid = model()->validate( errors ); + if ( !isValid ) + { + QMessageBox messageBox; + messageBox.setWindowTitle( tr( "Model is Invalid" ) ); + messageBox.setIcon( QMessageBox::Icon::Warning ); + messageBox.setText( tr( "This model is not valid and contains one or more issues. Are you sure you want to run it in this state?" ) ); + messageBox.setStandardButtons( QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::Cancel ); + messageBox.setDefaultButton( QMessageBox::StandardButton::Cancel ); + + QString errorString; + for ( const QString &error : std::as_const( errors ) ) + { + QString cleanedError = error; + const thread_local QRegularExpression re( QStringLiteral( "<[^>]*>" ) ); + cleanedError.replace( re, QString() ); + errorString += QStringLiteral( "• %1\n" ).arg( cleanedError ); + } + + messageBox.setDetailedText( errorString ); + if ( messageBox.exec() == QMessageBox::StandardButton::Cancel ) + return; + } + + std::unique_ptr< QgsProcessingAlgorithmDialogBase > dialog( createExecutionDialog() ); + if ( !dialog ) + return; + + dialog->setLogLevel( Qgis::ProcessingLogLevel::ModelDebug ); + dialog->setParameters( mModel->designerParameterValues() ); + + connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmFinished, this, [this, &dialog]( bool, const QVariantMap & ) + { + const QVariantMap dialogResults = dialog->results(); + setLastRunChildAlgorithmResults( dialogResults.value( QStringLiteral( "CHILD_RESULTS" ), QVariantMap() ).toMap() ); + setLastRunChildAlgorithmInputs( dialogResults.value( QStringLiteral( "CHILD_INPUTS" ), QVariantMap() ).toMap() ); + + mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) ); + } ); + + dialog->exec(); +} + void QgsModelDesignerDialog::validate() { QStringList issues; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 7400093c5c798..f1fe65ec5db1b 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -30,6 +30,7 @@ class QUndoView; class QgsModelViewToolPan; class QgsModelViewToolSelect; class QgsScreenHelper; +class QgsProcessingAlgorithmDialogBase; ///@cond NOT_STABLE @@ -126,6 +127,8 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode virtual void exportAsScriptAlgorithm() = 0; // cppcheck-suppress pureVirtualCall virtual bool saveModel( bool saveAs = false ) = 0; + // cppcheck-suppress pureVirtualCall + virtual QgsProcessingAlgorithmDialogBase *createExecutionDialog() = 0 SIP_TRANSFERBACK; QToolBar *toolbar() { return mToolbar; } QAction *actionOpen() { return mActionOpen; } @@ -186,6 +189,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void reorderOutputs(); void setPanelVisibility( bool hidden ); void editHelp(); + void run(); private: From 3deec36065e65c61707c416a645ab3a1e9e83a8a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 16 Apr 2024 14:26:26 +1000 Subject: [PATCH 30/46] Transfer temporary results to a layer store in the designer dialog After running a model through the designer dialog, any layers generated during the execution of the model will now be transferred into a layer store attached to the dialog, instead of immediately discarded. This includes layers generated by child algorithms as intermediate steps and not just model outputs. By moving the lifetime of the generated layers to the designer dialog, we are able to do things with these layers after the run algorithm dialog is closed. (Eg allowing users to load them to a project) The designer's layer store is cleared on each run of the model. --- src/gui/processing/models/qgsmodeldesignerdialog.cpp | 7 +++++++ src/gui/processing/models/qgsmodeldesignerdialog.h | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 374074c6bb386..776c4c7244b1f 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -86,6 +86,8 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags { setupUi( this ); + mLayerStore.setProject( QgsProject::instance() ); + mScreenHelper = new QgsScreenHelper( this ); setAttribute( Qt::WA_DeleteOnClose ); @@ -1036,6 +1038,11 @@ void QgsModelDesignerDialog::run() setLastRunChildAlgorithmInputs( dialogResults.value( QStringLiteral( "CHILD_INPUTS" ), QVariantMap() ).toMap() ); mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) ); + + // take child output layers + mLayerStore.temporaryLayerStore()->removeAllMapLayers(); + QgsProcessingContext *context = dialog->processingContext(); + mLayerStore.takeResultsFrom( *context ); } ); dialog->exec(); diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index f1fe65ec5db1b..7097fdc28bc81 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -251,6 +251,8 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode bool isActive; }; QMap< QString, PanelStatus > mPanelStatus; + + QgsProcessingContext mLayerStore; }; From 5ebbb3da4689c68d59c99701892ad8041af75696 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 18 Apr 2024 13:15:52 +1000 Subject: [PATCH 31/46] Make child results and inputs always available for models Even when the model execution fails, store the child inputs and results for all steps which successfully completed in the context. This gives more debugging information to a user when a model fails, because these values will now be reflected in the model designer view for all steps which completed. --- .../processing/qgsprocessingcontext.sip.in | 22 ++++++++++ .../processing/qgsprocessingcontext.sip.in | 22 ++++++++++ .../models/qgsprocessingmodelalgorithm.cpp | 17 +++----- src/core/processing/qgsprocessingcontext.cpp | 2 + src/core/processing/qgsprocessingcontext.h | 43 +++++++++++++++++++ .../models/qgsmodeldesignerdialog.cpp | 8 ++-- tests/src/analysis/testqgsprocessing.cpp | 20 +++++++++ .../testqgsprocessingmodelalgorithm.cpp | 5 +++ 8 files changed, 125 insertions(+), 14 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index ef1d7fab1808b..382ddb0701809 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -658,6 +658,28 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End + QVariantMap modelChildResults() const; +%Docstring +Returns the map of child algorithm results, populated when the context is used +to run a model algorithm. + +.. seealso:: :py:func:`modelChildInputs` + +.. versionadded:: 3.38 +%End + + + QVariantMap modelChildInputs() const; +%Docstring +Returns the map of child algorithm inputs, populated when the context is used +to run a model algorithm. + +.. seealso:: :py:func:`modelChildResults` + +.. versionadded:: 3.38 +%End + + private: QgsProcessingContext( const QgsProcessingContext &other ); }; diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index dc7115e43f403..358f4d3cfdbfc 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -658,6 +658,28 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End + QVariantMap modelChildResults() const; +%Docstring +Returns the map of child algorithm results, populated when the context is used +to run a model algorithm. + +.. seealso:: :py:func:`modelChildInputs` + +.. versionadded:: 3.38 +%End + + + QVariantMap modelChildInputs() const; +%Docstring +Returns the map of child algorithm inputs, populated when the context is used +to run a model algorithm. + +.. seealso:: :py:func:`modelChildResults` + +.. versionadded:: 3.38 +%End + + private: QgsProcessingContext( const QgsProcessingContext &other ); }; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 36b1ee53d5ad3..deae41568dd3c 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -320,9 +320,6 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QgsProcessingMultiStepFeedback modelFeedback( toExecute.count(), feedback ); QgsExpressionContext baseContext = createExpressionContext( parameters, context ); - QVariantMap childResults; - QVariantMap childInputs; - QVariantMap finalResults; QSet< QString > executed; bool executedAlg = true; @@ -380,18 +377,18 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QgsExpressionContext expContext = baseContext; expContext << QgsExpressionContextUtils::processingAlgorithmScope( child.algorithm(), parameters, context ) - << createExpressionContextScopeForChildAlgorithm( childId, context, parameters, childResults ); + << createExpressionContextScopeForChildAlgorithm( childId, context, parameters, context.modelChildResults() ); context.setExpressionContext( expContext ); QString error; - QVariantMap childParams = parametersForChildAlgorithm( child, parameters, childResults, expContext, error, &context ); + QVariantMap childParams = parametersForChildAlgorithm( child, parameters, context.modelChildResults(), expContext, error, &context ); if ( !error.isEmpty() ) throw QgsProcessingException( error ); if ( feedback && !skipGenericLogging ) feedback->setProgressText( QObject::tr( "Running %1 [%2/%3]" ).arg( child.description() ).arg( executed.count() + 1 ).arg( toExecute.count() ) ); - childInputs.insert( childId, QgsProcessingUtils::removePointerValuesFromMap( childParams ) ); + context.modelChildInputs().insert( childId, QgsProcessingUtils::removePointerValuesFromMap( childParams ) ); QStringList params; for ( auto childParamIt = childParams.constBegin(); childParamIt != childParams.constEnd(); ++childParamIt ) { @@ -437,7 +434,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa } QVariantMap results; - QString runError; + bool runResult = false; try { @@ -511,7 +508,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); } - childResults.insert( childId, results ); + context.modelChildResults().insert( childId, results ); // look through child alg's outputs to determine whether any of these should be copied // to the final model outputs @@ -620,8 +617,8 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %n algorithm(s) total in %1 s.", nullptr, executed.count() ).arg( totalTime.elapsed() / 1000.0 ) ); mResults = finalResults; - mResults.insert( QStringLiteral( "CHILD_RESULTS" ), childResults ); - mResults.insert( QStringLiteral( "CHILD_INPUTS" ), childInputs ); + mResults.insert( QStringLiteral( "CHILD_RESULTS" ), context.modelChildResults() ); + mResults.insert( QStringLiteral( "CHILD_INPUTS" ), context.modelChildInputs() ); return mResults; } diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index 1cb75ab047bab..9a6e236504784 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -123,6 +123,8 @@ std::function QgsProcessingContext::defaultInvalidG void QgsProcessingContext::takeResultsFrom( QgsProcessingContext &context ) { setLayersToLoadOnCompletion( context.mLayersToLoadOnCompletion ); + mModelChildInputs = context.mModelChildInputs; + mModelChildResults = context.mModelChildResults; context.mLayersToLoadOnCompletion.clear(); tempLayerStore.transferLayersFromStore( context.temporaryLayerStore() ); } diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index 470fa6e77aeb1..f9a0d938c4a4e 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -89,6 +89,8 @@ class CORE_EXPORT QgsProcessingContext mLogLevel = other.mLogLevel; mTemporaryFolderOverride = other.mTemporaryFolderOverride; mMaximumThreads = other.mMaximumThreads; + mModelChildInputs = other.mModelChildInputs; + mModelChildResults = other.mModelChildResults; } /** @@ -734,6 +736,44 @@ class CORE_EXPORT QgsProcessingContext */ QStringList asQgisProcessArguments( QgsProcessingContext::ProcessArgumentFlags flags = QgsProcessingContext::ProcessArgumentFlags() ) const; + /** + * Returns the map of child algorithm results, populated when the context is used + * to run a model algorithm. + * + * \see modelChildInputs() + * \since QGIS 3.38 + */ + QVariantMap modelChildResults() const { return mModelChildResults; } + + /** + * Returns a reference to the map of child algorithm results, populated when the context is used + * to run a model algorithm. + * + * \note Not available in Python bindings + * \see modelChildInputs() + * \since QGIS 3.38 + */ + QVariantMap &modelChildResults() SIP_SKIP { return mModelChildResults; } + + /** + * Returns the map of child algorithm inputs, populated when the context is used + * to run a model algorithm. + * + * \see modelChildResults() + * \since QGIS 3.38 + */ + QVariantMap modelChildInputs() const { return mModelChildInputs; } + + /** + * Returns a reference to the map of child algorithm inputs, populated when the context is used + * to run a model algorithm. + * + * \note Not available in Python bindings + * \see modelChildResults() + * \since QGIS 3.38 + */ + QVariantMap &modelChildInputs() SIP_SKIP { return mModelChildInputs; } + private: QgsProcessingContext::Flags mFlags = QgsProcessingContext::Flags(); @@ -768,6 +808,9 @@ class CORE_EXPORT QgsProcessingContext QString mTemporaryFolderOverride; int mMaximumThreads = QThread::idealThreadCount(); + QVariantMap mModelChildResults; + QVariantMap mModelChildInputs; + #ifdef SIP_RUN QgsProcessingContext( const QgsProcessingContext &other ); #endif diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 776c4c7244b1f..dd3e22bcc6dcc 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -1033,15 +1033,15 @@ void QgsModelDesignerDialog::run() connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmFinished, this, [this, &dialog]( bool, const QVariantMap & ) { - const QVariantMap dialogResults = dialog->results(); - setLastRunChildAlgorithmResults( dialogResults.value( QStringLiteral( "CHILD_RESULTS" ), QVariantMap() ).toMap() ); - setLastRunChildAlgorithmInputs( dialogResults.value( QStringLiteral( "CHILD_INPUTS" ), QVariantMap() ).toMap() ); + QgsProcessingContext *context = dialog->processingContext(); + + setLastRunChildAlgorithmResults( context->modelChildResults() ); + setLastRunChildAlgorithmInputs( context->modelChildInputs() ); mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) ); // take child output layers mLayerStore.temporaryLayerStore()->removeAllMapLayers(); - QgsProcessingContext *context = dialog->processingContext(); mLayerStore.takeResultsFrom( *context ); } ); diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index daff9a8d293e0..ea5afb811266c 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -1309,12 +1309,23 @@ void TestQgsProcessing::context() context.temporaryLayerStore()->addMapLayer( vector ); QCOMPARE( context.temporaryLayerStore()->mapLayer( vector->id() ), vector ); + context.modelChildInputs().insert( QStringLiteral( "CHILD1" ), 1 ); + context.modelChildInputs().insert( QStringLiteral( "CHILD2" ), 2 ); + context.modelChildResults().insert( QStringLiteral( "RESULT1" ), 1 ); + context.modelChildResults().insert( QStringLiteral( "RESULT2" ), 2 ); + QgsProcessingContext context2; context2.copyThreadSafeSettings( context ); QCOMPARE( context2.defaultEncoding(), context.defaultEncoding() ); QCOMPARE( context2.invalidGeometryCheck(), context.invalidGeometryCheck() ); QCOMPARE( context2.flags(), context.flags() ); QCOMPARE( context2.project(), context.project() ); + QCOMPARE( context2.modelChildInputs().count(), 2 ); + QCOMPARE( context2.modelChildInputs().value( QStringLiteral( "CHILD1" ) ).toInt(), 1 ); + QCOMPARE( context2.modelChildInputs().value( QStringLiteral( "CHILD2" ) ).toInt(), 2 ); + QCOMPARE( context2.modelChildResults().count(), 2 ); + QCOMPARE( context2.modelChildResults().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); + QCOMPARE( context2.modelChildResults().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); QCOMPARE( static_cast< int >( context2.logLevel() ), static_cast< int >( Qgis::ProcessingLogLevel::Verbose ) ); // layers from temporaryLayerStore must not be copied by copyThreadSafeSettings QVERIFY( context2.temporaryLayerStore()->mapLayers().isEmpty() ); @@ -1392,6 +1403,15 @@ void TestQgsProcessing::context() QCOMPARE( context2.layersToLoadOnCompletion().keys().at( 0 ), v1->id() ); QCOMPARE( context2.layersToLoadOnCompletion().keys().at( 1 ), v2->id() ); + QgsProcessingContext context3; + context3.takeResultsFrom( context ); + QCOMPARE( context3.modelChildInputs().count(), 2 ); + QCOMPARE( context3.modelChildInputs().value( QStringLiteral( "CHILD1" ) ).toInt(), 1 ); + QCOMPARE( context3.modelChildInputs().value( QStringLiteral( "CHILD2" ) ).toInt(), 2 ); + QCOMPARE( context3.modelChildResults().count(), 2 ); + QCOMPARE( context3.modelChildResults().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); + QCOMPARE( context3.modelChildResults().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); + // make sure postprocessor is correctly deleted ppDeleted = false; pp = new TestPostProcessor( &ppDeleted ); diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 856cd9936544c..c47a7a2695c20 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2354,6 +2354,11 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QVERIFY( DummyRaiseExceptionAlgorithm::hasPostProcessed ); // but not DummyRaiseExceptionAlgorithm::postProcessAlgorithm QVERIFY( !DummyRaiseExceptionAlgorithm::postProcessAlgorithmCalled ); + + // results and inputs from buffer child should be available through the context + QCOMPARE( context.modelChildInputs().value( "buffer" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); + QCOMPARE( context.modelChildInputs().value( "buffer" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); + QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).toMap().value( "OUTPUT" ).toString() ) ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From 6c22cd03551d6eea43a4e53144c65f210567208b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 18 Apr 2024 13:24:49 +1000 Subject: [PATCH 32/46] [feature] Add "View Output Layers" option for model child algorithms When editing a model through the designer (and after having run that model), you can now right click any child step in the model and select "View Output Layers". This will add the output layers from that step as new layers in the current QGIS project. This action is available for ALL child algorithms in the model, even if the model is not configured to use the outputs from those children as model outputs. This is designed as a helpful debugging action. If a user's model fails (or gives unexpected results), they can then trace through the model and view the outputs for suspected problematic steps. It avoids the need to add temporary outputs to a model and re-run to test. Additionally, this action is always available after running the model, EVEN if the model itself failed (eg because of a misconfigured step later in the model). Sponsored by City of Canning --- .../qgsmodelcomponentgraphicitem.sip.in | 9 +++ .../models/qgsmodelgraphicsscene.sip.in | 7 ++ .../qgsmodelcomponentgraphicitem.sip.in | 9 +++ .../models/qgsmodelgraphicsscene.sip.in | 7 ++ .../models/qgsmodelcomponentgraphicitem.cpp | 15 ++++ .../models/qgsmodelcomponentgraphicitem.h | 9 +++ .../models/qgsmodeldesignerdialog.cpp | 79 +++++++++++++++++++ .../models/qgsmodeldesignerdialog.h | 1 + .../models/qgsmodelgraphicsscene.cpp | 12 ++- .../processing/models/qgsmodelgraphicsscene.h | 7 ++ 10 files changed, 152 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index ec898e937c042..e58f4674f619d 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -404,6 +404,15 @@ Sets the results obtained for this child algorithm for the last model execution void setInputs( const QVariantMap &inputs ); %Docstring Sets the inputs used for this child algorithm for the last model execution through the dialog. +%End + + signals: + + void showPreviousResults(); +%Docstring +Emitted when the user opts to view previous results from this child algorithm. + +.. versionadded:: 3.38 %End protected: diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index eca0686d03703..aa6648a85e111 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -177,6 +177,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void showPreviousResults( const QString &childId ); +%Docstring +Emitted when the user opts to view previous results from the child algorithm with matching ID. + +.. versionadded:: 3.38 %End protected: diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 9295a8196835a..ff591ffb348af 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -404,6 +404,15 @@ Sets the results obtained for this child algorithm for the last model execution void setInputs( const QVariantMap &inputs ); %Docstring Sets the inputs used for this child algorithm for the last model execution through the dialog. +%End + + signals: + + void showPreviousResults(); +%Docstring +Emitted when the user opts to view previous results from this child algorithm. + +.. versionadded:: 3.38 %End protected: diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index f625089a41f19..9e9373749dc16 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -177,6 +177,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void showPreviousResults( const QString &childId ); +%Docstring +Emitted when the user opts to view previous results from the child algorithm with matching ID. + +.. versionadded:: 3.38 %End protected: diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index 138e4aa313346..02cbb26bfdda9 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -890,6 +890,21 @@ void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextM QAction *deactivateAction = popupmenu->addAction( QObject::tr( "Deactivate" ) ); connect( deactivateAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::deactivateAlgorithm ); } + + // only show the "View Output Layers" action for algorithms which create layers + if ( const QgsProcessingAlgorithm *algorithm = child->algorithm() ) + { + const QList< const QgsProcessingParameterDefinition * > outputParams = algorithm->destinationParameterDefinitions(); + if ( !outputParams.isEmpty() ) + { + popupmenu->addSeparator(); + QAction *viewOutputLayersAction = popupmenu->addAction( QObject::tr( "View Output Layers" ) ); + viewOutputLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionShowSelectedLayers.svg" ) ) ); + connect( viewOutputLayersAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::showPreviousResults ); + if ( mResults.empty() ) + viewOutputLayersAction->setEnabled( false ); + } + } } popupmenu->exec( event->screenPos() ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index c3cb7c23f2862..2499526c2756b 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -470,6 +470,15 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra */ void setInputs( const QVariantMap &inputs ); + signals: + + /** + * Emitted when the user opts to view previous results from this child algorithm. + * + * \since QGIS 3.38 + */ + void showPreviousResults(); + protected: QColor fillColor( State state ) const override; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index dd3e22bcc6dcc..8cc5168cae045 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -39,6 +39,7 @@ #include "qgsscreenhelper.h" #include "qgsmessagelog.h" #include "qgsprocessingalgorithmdialogbase.h" +#include "qgsproject.h" #include #include #include @@ -511,6 +512,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) } ); connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } ); connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } ); + connect( mScene, &QgsModelGraphicsScene::showPreviousResults, this, &QgsModelDesignerDialog::showPreviousResults ); mView->centerOn( center ); @@ -1048,6 +1050,83 @@ void QgsModelDesignerDialog::run() dialog->exec(); } +void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) +{ + const QString childDescription = mModel->childAlgorithm( childId ).description(); + + const QVariantMap childAlgorithmResults = mChildResults.value( childId ).toMap(); + if ( childAlgorithmResults.isEmpty() ) + { + mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) ); + return; + } + + const QgsProcessingAlgorithm *algorithm = mModel->childAlgorithm( childId ).algorithm(); + if ( !algorithm ) + { + mMessageBar->pushCritical( QString(), tr( "Results cannot be shown for an invalid model component" ) ); + return; + } + + const QList< const QgsProcessingParameterDefinition * > outputParams = algorithm->destinationParameterDefinitions(); + if ( outputParams.isEmpty() ) + { + // this situation should not arise in normal use, we don't show the action in this case + QgsDebugError( "Cannot show results for algorithms with no outputs" ); + return; + } + + bool foundResults = false; + for ( const QgsProcessingParameterDefinition *outputParam : outputParams ) + { + const QVariant output = childAlgorithmResults.value( outputParam->name() ); + if ( !output.isValid() ) + continue; + + if ( output.type() == QVariant::String ) + { + if ( QgsMapLayer *resultLayer = QgsProcessingUtils::mapLayerFromString( output.toString(), mLayerStore ) ) + { + QgsDebugMsgLevel( QStringLiteral( "Loading previous result for %1: %2" ).arg( outputParam->name(), output.toString() ), 2 ); + + std::unique_ptr< QgsMapLayer > layer( resultLayer->clone() ); + + QString baseName; + if ( outputParams.size() > 1 ) + baseName = tr( "%1 — %2" ).arg( childDescription, outputParam->name() ); + else + baseName = childDescription; + + // make name unique, so that's it's easy to see which is the most recent result. + // (this helps when running the model multiple times.) + QString name = baseName; + int counter = 1; + while ( !QgsProject::instance()->mapLayersByName( name ).empty() ) + { + counter += 1; + name = tr( "%1 (%2)" ).arg( baseName ).arg( counter ); + } + + layer->setName( name ); + + QgsProject::instance()->addMapLayer( layer.release() ); + foundResults = true; + } + else + { + // should not happen in normal operation + QgsDebugError( QStringLiteral( "Could not load previous result for %1: %2" ).arg( outputParam->name(), output.toString() ) ); + } + } + } + + if ( !foundResults ) + { + mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) ); + return; + } +} + void QgsModelDesignerDialog::validate() { QStringList issues; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 7097fdc28bc81..256d8bc482b0e 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -190,6 +190,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void setPanelVisibility( bool hidden ); void editHelp(); void run(); + void showPreviousResults( const QString &childId ); private: diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index b9c712750fe72..359d82981017f 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -138,12 +138,18 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs QgsModelChildAlgorithmGraphicItem *item = createChildAlgGraphicItem( model, it.value().clone() ); addItem( item ); item->setPos( it.value().position().x(), it.value().position().y() ); - item->setResults( mChildResults.value( it.value().childId() ).toMap() ); - item->setInputs( mChildInputs.value( it.value().childId() ).toMap() ); - mChildAlgorithmItems.insert( it.value().childId(), item ); + + const QString childId = it.value().childId(); + item->setResults( mChildResults.value( childId ).toMap() ); + item->setInputs( mChildInputs.value( childId ).toMap() ); + mChildAlgorithmItems.insert( childId, item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); connect( item, &QgsModelComponentGraphicItem::aboutToChange, this, &QgsModelGraphicsScene::componentAboutToChange ); + connect( item, &QgsModelChildAlgorithmGraphicItem::showPreviousResults, this, [this, childId] + { + emit showPreviousResults( childId ); + } ); addCommentItemForComponent( model, it.value(), item ); } diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index b37f5fe0b2c7b..ed31e46469c58 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -192,6 +192,13 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ void selectedItemChanged( QgsModelComponentGraphicItem *selected ); + /** + * Emitted when the user opts to view previous results from the child algorithm with matching ID. + * + * \since QGIS 3.38 + */ + void showPreviousResults( const QString &childId ); + protected: /** From 833f02b09be28492ed38a03e7979afcae97e9a21 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 18 Apr 2024 14:46:35 +1000 Subject: [PATCH 33/46] Fix warning --- tests/src/analysis/testqgsprocessingmodelalgorithm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index c47a7a2695c20..bc60b78e113ae 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2345,7 +2345,7 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() params.insert( QStringLiteral( "INPUT" ), QStringLiteral( "v1" ) ); bool ok = false; - QVariantMap results = m.run( params, context, &feedback, &ok ); + m.run( params, context, &feedback, &ok ); // model should fail, exception was raised QVERIFY( !ok ); // but result from successful buffer step should still be available in the context From 8302259463e14ee0b5e32711b673281e23690025 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 09:59:54 +1000 Subject: [PATCH 34/46] Make child results handling API more flexible --- CMakeLists.txt | 2 +- .../processing/qgsprocessingcontext.sip.in | 64 ++++++++++--- .../qgsmodelcomponentgraphicitem.sip.in | 9 +- .../models/qgsmodeldesignerdialog.sip.in | 9 +- .../models/qgsmodelgraphicsscene.sip.in | 9 +- .../processing/qgsprocessingcontext.sip.in | 64 ++++++++++--- .../qgsmodelcomponentgraphicitem.sip.in | 9 +- .../models/qgsmodeldesignerdialog.sip.in | 9 +- .../models/qgsmodelgraphicsscene.sip.in | 9 +- .../models/qgsprocessingmodelalgorithm.cpp | 23 +++-- src/core/processing/qgsprocessingcontext.cpp | 14 ++- src/core/processing/qgsprocessingcontext.h | 89 ++++++++++++++----- .../models/qgsmodelcomponentgraphicitem.cpp | 24 ++--- .../models/qgsmodelcomponentgraphicitem.h | 13 +-- .../models/qgsmodeldesignerdialog.cpp | 21 ++--- .../models/qgsmodeldesignerdialog.h | 12 +-- .../models/qgsmodelgraphicsscene.cpp | 20 +---- .../processing/models/qgsmodelgraphicsscene.h | 12 +-- tests/src/analysis/testqgsprocessing.cpp | 32 ++++--- .../testqgsprocessingmodelalgorithm.cpp | 6 +- 20 files changed, 261 insertions(+), 189 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e69f5558320c6..e6f1278f7bf45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1102,7 +1102,7 @@ if (WITH_CORE AND WITH_BINDINGS) include(SIPMacros) set(SIP_INCLUDES ${PYQT_SIP_DIR} ${CMAKE_SOURCE_DIR}/python) - set(SIP_CONCAT_PARTS 25) + set(SIP_CONCAT_PARTS 26) if (NOT BINDINGS_GLOBAL_INSTALL) set(Python_SITEARCH ${QGIS_DATA_DIR}/python) diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index 382ddb0701809..340299fd85e88 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -12,6 +12,55 @@ +class QgsProcessingModelChildAlgorithmResult +{ +%Docstring(signature="appended") +Encapsulates the results of running a child algorithm within a model + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsprocessingcontext.h" +%End + public: + + QgsProcessingModelChildAlgorithmResult(); + + QVariantMap inputs() const; +%Docstring +Returns the inputs used for the child algorithm. + +.. seealso:: :py:func:`setInputs` +%End + + void setInputs( const QVariantMap &inputs ); +%Docstring +Sets the ``inputs`` used for the child algorithm. + +.. seealso:: :py:func:`inputs` +%End + + QVariantMap outputs() const; +%Docstring +Returns the outputs generated by the child algorithm. + +.. seealso:: :py:func:`setOutputs` +%End + + void setOutputs( const QVariantMap &outputs ); +%Docstring +Sets the ``outputs`` used for the child algorithm. + +.. seealso:: :py:func:`outputs` +%End + + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; + bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const; + +}; + + class QgsProcessingContext { @@ -658,23 +707,14 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End - QVariantMap modelChildResults() const; + QMap< QString, QgsProcessingModelChildAlgorithmResult > modelChildResults() const; %Docstring Returns the map of child algorithm results, populated when the context is used to run a model algorithm. -.. seealso:: :py:func:`modelChildInputs` - -.. versionadded:: 3.38 -%End - +Map keys refer to the child algorithm IDs. - QVariantMap modelChildInputs() const; -%Docstring -Returns the map of child algorithm inputs, populated when the context is used -to run a model algorithm. - -.. seealso:: :py:func:`modelChildResults` +.. seealso:: :py:func:`modelChildInputs` .. versionadded:: 3.38 %End diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index e58f4674f619d..e8265886976e0 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -396,14 +396,9 @@ Ownership of ``child`` is transferred to the item. virtual bool canDeleteComponent(); - void setResults( const QVariantMap &results ); + void setResults( const QgsProcessingModelChildAlgorithmResult &results ); %Docstring -Sets the results obtained for this child algorithm for the last model execution through the dialog. -%End - - void setInputs( const QVariantMap &inputs ); -%Docstring -Sets the inputs used for this child algorithm for the last model execution through the dialog. +Sets the ``results`` obtained for this child algorithm for the last model execution through the dialog. %End signals: diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 89d56d3ece85e..75523aabd4af8 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -117,14 +117,9 @@ Checks if there are unsaved changes in the model, and if so, prompts the user to Returns ``False`` if the cancel option was selected %End - void setLastRunChildAlgorithmResults( const QVariantMap &results ); + void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); %Docstring -Sets the results of child algorithms for the last run of the model through the designer window. -%End - - void setLastRunChildAlgorithmInputs( const QVariantMap &inputs ); -%Docstring -Sets the inputs for child algorithms for the last run of the model through the designer window. +Sets the ``results`` of child algorithms for the last run of the model through the designer window. %End void setModelName( const QString &name ); diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index aa6648a85e111..b97e646a511ca 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -124,14 +124,9 @@ not correctly emit signals to allow the scene's model to update. Clears any selected items and sets ``item`` as the current selection. %End - void setChildAlgorithmResults( const QVariantMap &results ); + void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); %Docstring -Sets the results for child algorithms for the last model execution. -%End - - void setChildAlgorithmInputs( const QVariantMap &inputs ); -%Docstring -Sets the inputs for child algorithms for the last model execution. +Sets the ``results`` of child algorithms for the last run of the model through the designer window. %End QgsMessageBar *messageBar() const; diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index 358f4d3cfdbfc..ff37951de8cf2 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -12,6 +12,55 @@ +class QgsProcessingModelChildAlgorithmResult +{ +%Docstring(signature="appended") +Encapsulates the results of running a child algorithm within a model + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsprocessingcontext.h" +%End + public: + + QgsProcessingModelChildAlgorithmResult(); + + QVariantMap inputs() const; +%Docstring +Returns the inputs used for the child algorithm. + +.. seealso:: :py:func:`setInputs` +%End + + void setInputs( const QVariantMap &inputs ); +%Docstring +Sets the ``inputs`` used for the child algorithm. + +.. seealso:: :py:func:`inputs` +%End + + QVariantMap outputs() const; +%Docstring +Returns the outputs generated by the child algorithm. + +.. seealso:: :py:func:`setOutputs` +%End + + void setOutputs( const QVariantMap &outputs ); +%Docstring +Sets the ``outputs`` used for the child algorithm. + +.. seealso:: :py:func:`outputs` +%End + + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; + bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const; + +}; + + class QgsProcessingContext { @@ -658,23 +707,14 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End - QVariantMap modelChildResults() const; + QMap< QString, QgsProcessingModelChildAlgorithmResult > modelChildResults() const; %Docstring Returns the map of child algorithm results, populated when the context is used to run a model algorithm. -.. seealso:: :py:func:`modelChildInputs` - -.. versionadded:: 3.38 -%End - +Map keys refer to the child algorithm IDs. - QVariantMap modelChildInputs() const; -%Docstring -Returns the map of child algorithm inputs, populated when the context is used -to run a model algorithm. - -.. seealso:: :py:func:`modelChildResults` +.. seealso:: :py:func:`modelChildInputs` .. versionadded:: 3.38 %End diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index ff591ffb348af..72a74d0a9aecf 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -396,14 +396,9 @@ Ownership of ``child`` is transferred to the item. virtual bool canDeleteComponent(); - void setResults( const QVariantMap &results ); + void setResults( const QgsProcessingModelChildAlgorithmResult &results ); %Docstring -Sets the results obtained for this child algorithm for the last model execution through the dialog. -%End - - void setInputs( const QVariantMap &inputs ); -%Docstring -Sets the inputs used for this child algorithm for the last model execution through the dialog. +Sets the ``results`` obtained for this child algorithm for the last model execution through the dialog. %End signals: diff --git a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 89d56d3ece85e..75523aabd4af8 100644 --- a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -117,14 +117,9 @@ Checks if there are unsaved changes in the model, and if so, prompts the user to Returns ``False`` if the cancel option was selected %End - void setLastRunChildAlgorithmResults( const QVariantMap &results ); + void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); %Docstring -Sets the results of child algorithms for the last run of the model through the designer window. -%End - - void setLastRunChildAlgorithmInputs( const QVariantMap &inputs ); -%Docstring -Sets the inputs for child algorithms for the last run of the model through the designer window. +Sets the ``results`` of child algorithms for the last run of the model through the designer window. %End void setModelName( const QString &name ); diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 9e9373749dc16..230cd0d7584d8 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -124,14 +124,9 @@ not correctly emit signals to allow the scene's model to update. Clears any selected items and sets ``item`` as the current selection. %End - void setChildAlgorithmResults( const QVariantMap &results ); + void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); %Docstring -Sets the results for child algorithms for the last model execution. -%End - - void setChildAlgorithmInputs( const QVariantMap &inputs ); -%Docstring -Sets the inputs for child algorithms for the last model execution. +Sets the ``results`` of child algorithms for the last run of the model through the designer window. %End QgsMessageBar *messageBar() const; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index deae41568dd3c..dc2dc9809bccf 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -320,6 +320,9 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QgsProcessingMultiStepFeedback modelFeedback( toExecute.count(), feedback ); QgsExpressionContext baseContext = createExpressionContext( parameters, context ); + QVariantMap childResults; + QVariantMap childInputs; + QVariantMap finalResults; QSet< QString > executed; bool executedAlg = true; @@ -377,18 +380,23 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QgsExpressionContext expContext = baseContext; expContext << QgsExpressionContextUtils::processingAlgorithmScope( child.algorithm(), parameters, context ) - << createExpressionContextScopeForChildAlgorithm( childId, context, parameters, context.modelChildResults() ); + << createExpressionContextScopeForChildAlgorithm( childId, context, parameters, childResults ); context.setExpressionContext( expContext ); QString error; - QVariantMap childParams = parametersForChildAlgorithm( child, parameters, context.modelChildResults(), expContext, error, &context ); + QVariantMap childParams = parametersForChildAlgorithm( child, parameters, childResults, expContext, error, &context ); if ( !error.isEmpty() ) throw QgsProcessingException( error ); if ( feedback && !skipGenericLogging ) feedback->setProgressText( QObject::tr( "Running %1 [%2/%3]" ).arg( child.description() ).arg( executed.count() + 1 ).arg( toExecute.count() ) ); - context.modelChildInputs().insert( childId, QgsProcessingUtils::removePointerValuesFromMap( childParams ) ); + QgsProcessingModelChildAlgorithmResult childResult; + + const QVariantMap thisChildParams = QgsProcessingUtils::removePointerValuesFromMap( childParams ); + childInputs.insert( childId, thisChildParams ); + childResult.setInputs( thisChildParams ); + QStringList params; for ( auto childParamIt = childParams.constBegin(); childParamIt != childParams.constEnd(); ++childParamIt ) { @@ -508,7 +516,10 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); } - context.modelChildResults().insert( childId, results ); + childResults.insert( childId, results ); + childResult.setOutputs( results ); + + context.modelChildResults().insert( childId, childResult ); // look through child alg's outputs to determine whether any of these should be copied // to the final model outputs @@ -617,8 +628,8 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %n algorithm(s) total in %1 s.", nullptr, executed.count() ).arg( totalTime.elapsed() / 1000.0 ) ); mResults = finalResults; - mResults.insert( QStringLiteral( "CHILD_RESULTS" ), context.modelChildResults() ); - mResults.insert( QStringLiteral( "CHILD_INPUTS" ), context.modelChildInputs() ); + mResults.insert( QStringLiteral( "CHILD_RESULTS" ), childResults ); + mResults.insert( QStringLiteral( "CHILD_INPUTS" ), childInputs ); return mResults; } diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index 9a6e236504784..c274f3b5c0a78 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -21,6 +21,17 @@ #include "qgsproviderregistry.h" #include "qgsprocessing.h" +// +// QgsProcessingModelChildResult +// + +QgsProcessingModelChildAlgorithmResult::QgsProcessingModelChildAlgorithmResult() = default; + + +// +// QgsProcessingContext +// + QgsProcessingContext::QgsProcessingContext() : mPreferredVectorFormat( QgsProcessingUtils::defaultVectorExtension() ) , mPreferredRasterFormat( QgsProcessingUtils::defaultRasterExtension() ) @@ -123,7 +134,6 @@ std::function QgsProcessingContext::defaultInvalidG void QgsProcessingContext::takeResultsFrom( QgsProcessingContext &context ) { setLayersToLoadOnCompletion( context.mLayersToLoadOnCompletion ); - mModelChildInputs = context.mModelChildInputs; mModelChildResults = context.mModelChildResults; context.mLayersToLoadOnCompletion.clear(); tempLayerStore.transferLayersFromStore( context.temporaryLayerStore() ); @@ -306,3 +316,5 @@ void QgsProcessingContext::LayerDetails::setOutputLayerName( QgsMapLayer *layer layer->setName( name ); } } + + diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index f9a0d938c4a4e..2dfacc70587eb 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -31,6 +31,64 @@ class QgsProcessingLayerPostProcessorInterface; +/** + * \class QgsProcessingModelChildResult + * \ingroup core + * \brief Encapsulates the results of running a child algorithm within a model + * + * \since QGIS 3.38 +*/ +class CORE_EXPORT QgsProcessingModelChildAlgorithmResult +{ + public: + + QgsProcessingModelChildAlgorithmResult(); + + /** + * Returns the inputs used for the child algorithm. + * + * \see setInputs() + */ + QVariantMap inputs() const { return mInputs; } + + /** + * Sets the \a inputs used for the child algorithm. + * + * \see inputs() + */ + void setInputs( const QVariantMap &inputs ) { mInputs = inputs; } + + /** + * Returns the outputs generated by the child algorithm. + * + * \see setOutputs() + */ + QVariantMap outputs() const { return mOutputs; } + + /** + * Sets the \a outputs used for the child algorithm. + * + * \see outputs() + */ + void setOutputs( const QVariantMap &outputs ) { mOutputs = outputs; } + + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const + { + return mInputs == other.mInputs && mOutputs == other.mOutputs; + } + bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const + { + return !( *this == other ); + } + + private: + + QVariantMap mInputs; + QVariantMap mOutputs; + +}; + + /** * \class QgsProcessingContext * \ingroup core @@ -89,7 +147,6 @@ class CORE_EXPORT QgsProcessingContext mLogLevel = other.mLogLevel; mTemporaryFolderOverride = other.mTemporaryFolderOverride; mMaximumThreads = other.mMaximumThreads; - mModelChildInputs = other.mModelChildInputs; mModelChildResults = other.mModelChildResults; } @@ -740,39 +797,24 @@ class CORE_EXPORT QgsProcessingContext * Returns the map of child algorithm results, populated when the context is used * to run a model algorithm. * - * \see modelChildInputs() - * \since QGIS 3.38 - */ - QVariantMap modelChildResults() const { return mModelChildResults; } - - /** - * Returns a reference to the map of child algorithm results, populated when the context is used - * to run a model algorithm. + * Map keys refer to the child algorithm IDs. * - * \note Not available in Python bindings * \see modelChildInputs() * \since QGIS 3.38 */ - QVariantMap &modelChildResults() SIP_SKIP { return mModelChildResults; } + QMap< QString, QgsProcessingModelChildAlgorithmResult > modelChildResults() const { return mModelChildResults; } /** - * Returns the map of child algorithm inputs, populated when the context is used + * Returns a reference to the map of child algorithm results, populated when the context is used * to run a model algorithm. * - * \see modelChildResults() - * \since QGIS 3.38 - */ - QVariantMap modelChildInputs() const { return mModelChildInputs; } - - /** - * Returns a reference to the map of child algorithm inputs, populated when the context is used - * to run a model algorithm. + * Map keys refer to the child algorithm IDs. * * \note Not available in Python bindings - * \see modelChildResults() + * \see modelChildInputs() * \since QGIS 3.38 */ - QVariantMap &modelChildInputs() SIP_SKIP { return mModelChildInputs; } + QMap< QString, QgsProcessingModelChildAlgorithmResult > &modelChildResults() SIP_SKIP { return mModelChildResults; } private: @@ -808,8 +850,7 @@ class CORE_EXPORT QgsProcessingContext QString mTemporaryFolderOverride; int mMaximumThreads = QThread::idealThreadCount(); - QVariantMap mModelChildResults; - QVariantMap mModelChildInputs; + QMap< QString, QgsProcessingModelChildAlgorithmResult > mModelChildResults; #ifdef SIP_RUN QgsProcessingContext( const QgsProcessingContext &other ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index 02cbb26bfdda9..251fa4c4dbb52 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -901,7 +901,7 @@ void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextM QAction *viewOutputLayersAction = popupmenu->addAction( QObject::tr( "View Output Layers" ) ); viewOutputLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionShowSelectedLayers.svg" ) ) ); connect( viewOutputLayersAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::showPreviousResults ); - if ( mResults.empty() ) + if ( mResults.outputs().empty() ) viewOutputLayersAction->setEnabled( false ); } } @@ -1001,6 +1001,8 @@ QString QgsModelChildAlgorithmGraphicItem::linkPointText( Qt::Edge edge, int ind if ( !child->algorithm() ) return QString(); + const QVariantMap inputs = mResults.inputs(); + const QVariantMap outputs = mResults.outputs(); switch ( edge ) { case Qt::BottomEdge: @@ -1016,9 +1018,9 @@ QString QgsModelChildAlgorithmGraphicItem::linkPointText( Qt::Edge edge, int ind const QgsProcessingOutputDefinition *output = child->algorithm()->outputDefinitions().at( index ); QString title = output->description(); - if ( mResults.contains( output->name() ) ) + if ( outputs.contains( output->name() ) ) { - title += QStringLiteral( ": %1" ).arg( mResults.value( output->name() ).toString() ); + title += QStringLiteral( ": %1" ).arg( outputs.value( output->name() ).toString() ); } return truncatedTextForItem( title ); } @@ -1041,8 +1043,8 @@ QString QgsModelChildAlgorithmGraphicItem::linkPointText( Qt::Edge edge, int ind } QString title = params.at( index )->description(); - if ( !mInputs.value( params.at( index )->name() ).toString().isEmpty() ) - title += QStringLiteral( ": %1" ).arg( mInputs.value( params.at( index )->name() ).toString() ); + if ( !inputs.value( params.at( index )->name() ).toString().isEmpty() ) + title += QStringLiteral( ": %1" ).arg( inputs.value( params.at( index )->name() ).toString() ); return truncatedTextForItem( title ); } @@ -1072,7 +1074,7 @@ bool QgsModelChildAlgorithmGraphicItem::canDeleteComponent() return false; } -void QgsModelChildAlgorithmGraphicItem::setResults( const QVariantMap &results ) +void QgsModelChildAlgorithmGraphicItem::setResults( const QgsProcessingModelChildAlgorithmResult &results ) { if ( mResults == results ) return; @@ -1082,16 +1084,6 @@ void QgsModelChildAlgorithmGraphicItem::setResults( const QVariantMap &results ) emit updateArrowPaths(); } -void QgsModelChildAlgorithmGraphicItem::setInputs( const QVariantMap &inputs ) -{ - if ( mInputs == inputs ) - return; - - mInputs = inputs; - update(); - emit updateArrowPaths(); -} - void QgsModelChildAlgorithmGraphicItem::deleteComponent() { if ( const QgsProcessingModelChildAlgorithm *child = dynamic_cast< const QgsProcessingModelChildAlgorithm * >( component() ) ) diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index 2499526c2756b..27849685eb9d0 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -18,6 +18,7 @@ #include "qgis.h" #include "qgis_gui.h" +#include "qgsprocessingcontext.h" #include #include #include @@ -461,14 +462,9 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra bool canDeleteComponent() override; /** - * Sets the results obtained for this child algorithm for the last model execution through the dialog. + * Sets the \a results obtained for this child algorithm for the last model execution through the dialog. */ - void setResults( const QVariantMap &results ); - - /** - * Sets the inputs used for this child algorithm for the last model execution through the dialog. - */ - void setInputs( const QVariantMap &inputs ); + void setResults( const QgsProcessingModelChildAlgorithmResult &results ); signals: @@ -502,8 +498,7 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra private: QPicture mPicture; QPixmap mPixmap; - QVariantMap mResults; - QVariantMap mInputs; + QgsProcessingModelChildAlgorithmResult mResults; bool mIsValid = true; }; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 8cc5168cae045..e23dd69819efd 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -493,7 +493,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) mScene = scene; mScene->setParent( this ); - mScene->setChildAlgorithmResults( mChildResults ); + mScene->setLastRunChildAlgorithmResults( mChildResults ); mScene->setModel( mModel.get() ); mScene->setMessageBar( mMessageBar ); @@ -595,18 +595,11 @@ bool QgsModelDesignerDialog::checkForUnsavedChanges() } } -void QgsModelDesignerDialog::setLastRunChildAlgorithmResults( const QVariantMap &results ) +void QgsModelDesignerDialog::setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ) { mChildResults = results; if ( mScene ) - mScene->setChildAlgorithmResults( mChildResults ); -} - -void QgsModelDesignerDialog::setLastRunChildAlgorithmInputs( const QVariantMap &inputs ) -{ - mChildInputs = inputs; - if ( mScene ) - mScene->setChildAlgorithmInputs( mChildInputs ); + mScene->setLastRunChildAlgorithmResults( mChildResults ); } void QgsModelDesignerDialog::setModelName( const QString &name ) @@ -1038,7 +1031,6 @@ void QgsModelDesignerDialog::run() QgsProcessingContext *context = dialog->processingContext(); setLastRunChildAlgorithmResults( context->modelChildResults() ); - setLastRunChildAlgorithmInputs( context->modelChildInputs() ); mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) ); @@ -1054,8 +1046,9 @@ void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) { const QString childDescription = mModel->childAlgorithm( childId ).description(); - const QVariantMap childAlgorithmResults = mChildResults.value( childId ).toMap(); - if ( childAlgorithmResults.isEmpty() ) + const QgsProcessingModelChildAlgorithmResult result = mChildResults.value( childId ); + const QVariantMap childAlgorithmOutputs = result.outputs(); + if ( childAlgorithmOutputs.isEmpty() ) { mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) ); return; @@ -1079,7 +1072,7 @@ void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) bool foundResults = false; for ( const QgsProcessingParameterDefinition *outputParam : outputParams ) { - const QVariant output = childAlgorithmResults.value( outputParam->name() ); + const QVariant output = childAlgorithmOutputs.value( outputParam->name() ); if ( !output.isValid() ) continue; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 256d8bc482b0e..74ba526497fa8 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -152,14 +152,9 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode bool checkForUnsavedChanges(); /** - * Sets the results of child algorithms for the last run of the model through the designer window. + * Sets the \a results of child algorithms for the last run of the model through the designer window. */ - void setLastRunChildAlgorithmResults( const QVariantMap &results ); - - /** - * Sets the inputs for child algorithms for the last run of the model through the designer window. - */ - void setLastRunChildAlgorithmInputs( const QVariantMap &inputs ); + void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); /** * Sets the model \a name. @@ -234,8 +229,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode int mBlockRepaints = 0; - QVariantMap mChildResults; - QVariantMap mChildInputs; + QMap< QString, QgsProcessingModelChildAlgorithmResult > mChildResults; bool isDirty() const; diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 359d82981017f..2ac1a0aeb521f 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -140,8 +140,7 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs item->setPos( it.value().position().x(), it.value().position().y() ); const QString childId = it.value().childId(); - item->setResults( mChildResults.value( childId ).toMap() ); - item->setInputs( mChildInputs.value( childId ).toMap() ); + item->setResults( mChildResults.value( childId ) ); mChildAlgorithmItems.insert( childId, item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); @@ -360,7 +359,7 @@ void QgsModelGraphicsScene::setSelectedItem( QgsModelComponentGraphicItem *item emit selectedItemChanged( item ); } -void QgsModelGraphicsScene::setChildAlgorithmResults( const QVariantMap &results ) +void QgsModelGraphicsScene::setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ) { mChildResults = results; @@ -368,20 +367,7 @@ void QgsModelGraphicsScene::setChildAlgorithmResults( const QVariantMap &results { if ( QgsModelChildAlgorithmGraphicItem *item = mChildAlgorithmItems.value( it.key() ) ) { - item->setResults( it.value().toMap() ); - } - } -} - -void QgsModelGraphicsScene::setChildAlgorithmInputs( const QVariantMap &inputs ) -{ - mChildInputs = inputs; - - for ( auto it = mChildInputs.constBegin(); it != mChildInputs.constEnd(); ++it ) - { - if ( QgsModelChildAlgorithmGraphicItem *item = mChildAlgorithmItems.value( it.key() ) ) - { - item->setInputs( it.value().toMap() ); + item->setResults( it.value() ); } } } diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index ed31e46469c58..84044323a688c 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -138,14 +138,9 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene void setSelectedItem( QgsModelComponentGraphicItem *item ); /** - * Sets the results for child algorithms for the last model execution. + * Sets the \a results of child algorithms for the last run of the model through the designer window. */ - void setChildAlgorithmResults( const QVariantMap &results ); - - /** - * Sets the inputs for child algorithms for the last model execution. - */ - void setChildAlgorithmInputs( const QVariantMap &inputs ); + void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); /** * Returns the message bar associated with the scene. @@ -247,8 +242,7 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene QMap< QString, QgsModelChildAlgorithmGraphicItem * > mChildAlgorithmItems; QMap< QString, QMap< QString, QgsModelComponentGraphicItem * > > mOutputItems; QMap< QString, QgsModelComponentGraphicItem * > mGroupBoxItems; - QVariantMap mChildResults; - QVariantMap mChildInputs; + QMap< QString, QgsProcessingModelChildAlgorithmResult > mChildResults; QgsMessageBar *mMessageBar = nullptr; diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index ea5afb811266c..8dddccceffc94 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -1309,10 +1309,16 @@ void TestQgsProcessing::context() context.temporaryLayerStore()->addMapLayer( vector ); QCOMPARE( context.temporaryLayerStore()->mapLayer( vector->id() ), vector ); - context.modelChildInputs().insert( QStringLiteral( "CHILD1" ), 1 ); - context.modelChildInputs().insert( QStringLiteral( "CHILD2" ), 2 ); - context.modelChildResults().insert( QStringLiteral( "RESULT1" ), 1 ); - context.modelChildResults().insert( QStringLiteral( "RESULT2" ), 2 ); + QgsProcessingModelChildAlgorithmResult res1; + res1.setInputs( {{ QStringLiteral( "INPUT1" ), 1 }} ); + res1.setOutputs( {{ QStringLiteral( "RESULT1" ), 1 }} ); + + QgsProcessingModelChildAlgorithmResult res2; + res2.setInputs( {{ QStringLiteral( "INPUT2" ), 2 }} ); + res2.setOutputs( {{ QStringLiteral( "RESULT2" ), 2 }} ); + + context.modelChildResults().insert( QStringLiteral( "CHILD1" ), res1 ); + context.modelChildResults().insert( QStringLiteral( "CHILD2" ), res2 ); QgsProcessingContext context2; context2.copyThreadSafeSettings( context ); @@ -1320,12 +1326,11 @@ void TestQgsProcessing::context() QCOMPARE( context2.invalidGeometryCheck(), context.invalidGeometryCheck() ); QCOMPARE( context2.flags(), context.flags() ); QCOMPARE( context2.project(), context.project() ); - QCOMPARE( context2.modelChildInputs().count(), 2 ); - QCOMPARE( context2.modelChildInputs().value( QStringLiteral( "CHILD1" ) ).toInt(), 1 ); - QCOMPARE( context2.modelChildInputs().value( QStringLiteral( "CHILD2" ) ).toInt(), 2 ); QCOMPARE( context2.modelChildResults().count(), 2 ); - QCOMPARE( context2.modelChildResults().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); - QCOMPARE( context2.modelChildResults().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); + QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD1" ) ).inputs().value( QStringLiteral( "INPUT1" ) ).toInt(), 1 ); + QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD2" ) ).inputs().value( QStringLiteral( "INPUT2" ) ).toInt(), 2 ); + QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD1" ) ).outputs().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); + QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD2" ) ).outputs().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); QCOMPARE( static_cast< int >( context2.logLevel() ), static_cast< int >( Qgis::ProcessingLogLevel::Verbose ) ); // layers from temporaryLayerStore must not be copied by copyThreadSafeSettings QVERIFY( context2.temporaryLayerStore()->mapLayers().isEmpty() ); @@ -1405,12 +1410,11 @@ void TestQgsProcessing::context() QgsProcessingContext context3; context3.takeResultsFrom( context ); - QCOMPARE( context3.modelChildInputs().count(), 2 ); - QCOMPARE( context3.modelChildInputs().value( QStringLiteral( "CHILD1" ) ).toInt(), 1 ); - QCOMPARE( context3.modelChildInputs().value( QStringLiteral( "CHILD2" ) ).toInt(), 2 ); QCOMPARE( context3.modelChildResults().count(), 2 ); - QCOMPARE( context3.modelChildResults().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); - QCOMPARE( context3.modelChildResults().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); + QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD1" ) ).inputs().value( QStringLiteral( "INPUT1" ) ).toInt(), 1 ); + QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD2" ) ).inputs().value( QStringLiteral( "INPUT2" ) ).toInt(), 2 ); + QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD1" ) ).outputs().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); + QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD2" ) ).outputs().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); // make sure postprocessor is correctly deleted ppDeleted = false; diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index bc60b78e113ae..755853092b03d 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2356,9 +2356,9 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QVERIFY( !DummyRaiseExceptionAlgorithm::postProcessAlgorithmCalled ); // results and inputs from buffer child should be available through the context - QCOMPARE( context.modelChildInputs().value( "buffer" ).toMap().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); - QCOMPARE( context.modelChildInputs().value( "buffer" ).toMap().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); - QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).toMap().value( "OUTPUT" ).toString() ) ); + QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); + QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); + QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From 172a1c2b036be75cee7e6a4906587b74891ac6b7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 10:24:53 +1000 Subject: [PATCH 35/46] Make execution status available for child algorithms --- python/PyQt6/core/auto_additions/qgis.py | 7 +++++++ .../processing/qgsprocessingcontext.sip.in | 14 ++++++++++++++ python/PyQt6/core/auto_generated/qgis.sip.in | 7 +++++++ python/core/auto_additions/qgis.py | 7 +++++++ .../processing/qgsprocessingcontext.sip.in | 14 ++++++++++++++ python/core/auto_generated/qgis.sip.in | 7 +++++++ .../models/qgsprocessingmodelalgorithm.cpp | 12 +++++++----- src/core/processing/qgsprocessingcontext.cpp | 1 - src/core/processing/qgsprocessingcontext.h | 19 ++++++++++++++++++- src/core/qgis.h | 13 +++++++++++++ .../testqgsprocessingmodelalgorithm.cpp | 3 +++ 11 files changed, 97 insertions(+), 7 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index f98d3e2e37ada..704615038b041 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -3165,6 +3165,13 @@ Qgis.ProcessingModelChildParameterSource.__doc__ = "Processing model child parameter sources.\n\n.. versionadded:: 3.34\n\n" + '* ``ModelParameter``: ' + Qgis.ProcessingModelChildParameterSource.ModelParameter.__doc__ + '\n' + '* ``ChildOutput``: ' + Qgis.ProcessingModelChildParameterSource.ChildOutput.__doc__ + '\n' + '* ``StaticValue``: ' + Qgis.ProcessingModelChildParameterSource.StaticValue.__doc__ + '\n' + '* ``Expression``: ' + Qgis.ProcessingModelChildParameterSource.Expression.__doc__ + '\n' + '* ``ExpressionText``: ' + Qgis.ProcessingModelChildParameterSource.ExpressionText.__doc__ + '\n' + '* ``ModelOutput``: ' + Qgis.ProcessingModelChildParameterSource.ModelOutput.__doc__ # -- Qgis.ProcessingModelChildParameterSource.baseClass = Qgis +# monkey patching scoped based enum +Qgis.ProcessingModelChildAlgorithmExecutionStatus.NotExecuted.__doc__ = "Child has not been executed" +Qgis.ProcessingModelChildAlgorithmExecutionStatus.Success.__doc__ = "Child was successfully executed" +Qgis.ProcessingModelChildAlgorithmExecutionStatus.Failed.__doc__ = "Child encountered an error while executing" +Qgis.ProcessingModelChildAlgorithmExecutionStatus.__doc__ = "Reflects the status of a child algorithm in a Processing model.\n\n.. versionadded:: 3.38\n\n" + '* ``NotExecuted``: ' + Qgis.ProcessingModelChildAlgorithmExecutionStatus.NotExecuted.__doc__ + '\n' + '* ``Success``: ' + Qgis.ProcessingModelChildAlgorithmExecutionStatus.Success.__doc__ + '\n' + '* ``Failed``: ' + Qgis.ProcessingModelChildAlgorithmExecutionStatus.Failed.__doc__ +# -- +Qgis.ProcessingModelChildAlgorithmExecutionStatus.baseClass = Qgis QgsProcessingParameterTinInputLayers.Type = Qgis.ProcessingTinInputLayerType # monkey patching scoped based enum QgsProcessingParameterTinInputLayers.Vertices = Qgis.ProcessingTinInputLayerType.Vertices diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index 340299fd85e88..c1bbb8a14b28d 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -27,6 +27,20 @@ Encapsulates the results of running a child algorithm within a model QgsProcessingModelChildAlgorithmResult(); + Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const; +%Docstring +Returns the status of executing the child algorithm. + +.. seealso:: :py:func:`setExecutionStatus` +%End + + void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ); +%Docstring +Sets the ``status`` of executing the child algorithm. + +.. seealso:: :py:func:`executionStatus` +%End + QVariantMap inputs() const; %Docstring Returns the inputs used for the child algorithm. diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index d94e39c35ce2a..e0e52feb4a716 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1791,6 +1791,13 @@ The development version ModelOutput, }; + enum class ProcessingModelChildAlgorithmExecutionStatus /BaseType=IntEnum/ + { + NotExecuted, + Success, + Failed, + }; + enum class ProcessingTinInputLayerType /BaseType=IntEnum/ { Vertices, diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 906e22c813ff7..44632c05bce2b 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -3110,6 +3110,13 @@ Qgis.ProcessingModelChildParameterSource.__doc__ = "Processing model child parameter sources.\n\n.. versionadded:: 3.34\n\n" + '* ``ModelParameter``: ' + Qgis.ProcessingModelChildParameterSource.ModelParameter.__doc__ + '\n' + '* ``ChildOutput``: ' + Qgis.ProcessingModelChildParameterSource.ChildOutput.__doc__ + '\n' + '* ``StaticValue``: ' + Qgis.ProcessingModelChildParameterSource.StaticValue.__doc__ + '\n' + '* ``Expression``: ' + Qgis.ProcessingModelChildParameterSource.Expression.__doc__ + '\n' + '* ``ExpressionText``: ' + Qgis.ProcessingModelChildParameterSource.ExpressionText.__doc__ + '\n' + '* ``ModelOutput``: ' + Qgis.ProcessingModelChildParameterSource.ModelOutput.__doc__ # -- Qgis.ProcessingModelChildParameterSource.baseClass = Qgis +# monkey patching scoped based enum +Qgis.ProcessingModelChildAlgorithmExecutionStatus.NotExecuted.__doc__ = "Child has not been executed" +Qgis.ProcessingModelChildAlgorithmExecutionStatus.Success.__doc__ = "Child was successfully executed" +Qgis.ProcessingModelChildAlgorithmExecutionStatus.Failed.__doc__ = "Child encountered an error while executing" +Qgis.ProcessingModelChildAlgorithmExecutionStatus.__doc__ = "Reflects the status of a child algorithm in a Processing model.\n\n.. versionadded:: 3.38\n\n" + '* ``NotExecuted``: ' + Qgis.ProcessingModelChildAlgorithmExecutionStatus.NotExecuted.__doc__ + '\n' + '* ``Success``: ' + Qgis.ProcessingModelChildAlgorithmExecutionStatus.Success.__doc__ + '\n' + '* ``Failed``: ' + Qgis.ProcessingModelChildAlgorithmExecutionStatus.Failed.__doc__ +# -- +Qgis.ProcessingModelChildAlgorithmExecutionStatus.baseClass = Qgis QgsProcessingParameterTinInputLayers.Type = Qgis.ProcessingTinInputLayerType # monkey patching scoped based enum QgsProcessingParameterTinInputLayers.Vertices = Qgis.ProcessingTinInputLayerType.Vertices diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index ff37951de8cf2..3d5c310e1eb86 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -27,6 +27,20 @@ Encapsulates the results of running a child algorithm within a model QgsProcessingModelChildAlgorithmResult(); + Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const; +%Docstring +Returns the status of executing the child algorithm. + +.. seealso:: :py:func:`setExecutionStatus` +%End + + void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ); +%Docstring +Sets the ``status`` of executing the child algorithm. + +.. seealso:: :py:func:`executionStatus` +%End + QVariantMap inputs() const; %Docstring Returns the inputs used for the child algorithm. diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index a4b4aa8c6320c..d9f0567f5ab78 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1791,6 +1791,13 @@ The development version ModelOutput, }; + enum class ProcessingModelChildAlgorithmExecutionStatus + { + NotExecuted, + Success, + Failed, + }; + enum class ProcessingTinInputLayerType { Vertices, diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index dc2dc9809bccf..41a7920e93170 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -468,10 +468,12 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa results = childAlg->runPrepared( childParams, context, &modelFeedback ); } runResult = true; + childResult.setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); } catch ( QgsProcessingException &e ) { error = ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::CustomException ) ? e.what() : QObject::tr( "Error encountered while running %1: %2" ).arg( child.description(), e.what() ); + childResult.setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); } Q_ASSERT_X( QThread::currentThread() == context.thread(), "QgsProcessingModelAlgorithm::processAlgorithm", "context was not transferred back to model thread" ); @@ -498,6 +500,11 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa if ( !ppRes.isEmpty() ) results = ppRes; + childResults.insert( childId, results ); + childResult.setOutputs( results ); + + context.modelChildResults().insert( childId, childResult ); + if ( !runResult ) { throw QgsProcessingException( error ); @@ -516,11 +523,6 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); } - childResults.insert( childId, results ); - childResult.setOutputs( results ); - - context.modelChildResults().insert( childId, childResult ); - // look through child alg's outputs to determine whether any of these should be copied // to the final model outputs const QMap outputs = child.modelOutputs(); diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index c274f3b5c0a78..de3a939bb04d9 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -317,4 +317,3 @@ void QgsProcessingContext::LayerDetails::setOutputLayerName( QgsMapLayer *layer } } - diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index 2dfacc70587eb..0b197d3f5bc1e 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -44,6 +44,20 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult QgsProcessingModelChildAlgorithmResult(); + /** + * Returns the status of executing the child algorithm. + * + * \see setExecutionStatus() + */ + Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const { return mExecutionStatus; } + + /** + * Sets the \a status of executing the child algorithm. + * + * \see executionStatus() + */ + void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ) { mExecutionStatus = status; } + /** * Returns the inputs used for the child algorithm. * @@ -74,7 +88,9 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const { - return mInputs == other.mInputs && mOutputs == other.mOutputs; + return mExecutionStatus == other.mExecutionStatus + && mInputs == other.mInputs + && mOutputs == other.mOutputs; } bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const { @@ -83,6 +99,7 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult private: + Qgis::ProcessingModelChildAlgorithmExecutionStatus mExecutionStatus = Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted; QVariantMap mInputs; QVariantMap mOutputs; diff --git a/src/core/qgis.h b/src/core/qgis.h index db2fb2512997a..f9b25ee044f19 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -3145,6 +3145,19 @@ class CORE_EXPORT Qgis }; Q_ENUM( ProcessingModelChildParameterSource ) + /** + * Reflects the status of a child algorithm in a Processing model. + * + * \since QGIS 3.38 + */ + enum class ProcessingModelChildAlgorithmExecutionStatus : int + { + NotExecuted, //!< Child has not been executed + Success, //!< Child was successfully executed + Failed, //!< Child encountered an error while executing + }; + Q_ENUM( ProcessingModelChildAlgorithmExecutionStatus ) + /** * Defines the type of input layer for a Processing TIN input. * diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 755853092b03d..6a465ac5f2758 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2356,9 +2356,12 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QVERIFY( !DummyRaiseExceptionAlgorithm::postProcessAlgorithmCalled ); // results and inputs from buffer child should be available through the context + QCOMPARE( context.modelChildResults().value( "buffer" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); + + QCOMPARE( context.modelChildResults().value( "raise" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From d4369f714d74c708ed40f2e85da5c44d8bac916d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 11:15:56 +1000 Subject: [PATCH 36/46] Make log of each separate child algorithm available for retrieval after running model --- .../processing/qgsprocessingcontext.sip.in | 14 ++ .../processing/qgsprocessingcontext.sip.in | 14 ++ .../models/qgsprocessingmodelalgorithm.cpp | 201 ++++++++++-------- src/core/processing/qgsprocessingcontext.h | 16 ++ .../testqgsprocessingmodelalgorithm.cpp | 8 +- 5 files changed, 161 insertions(+), 92 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index c1bbb8a14b28d..09d71dabc9651 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -67,6 +67,20 @@ Returns the outputs generated by the child algorithm. Sets the ``outputs`` used for the child algorithm. .. seealso:: :py:func:`outputs` +%End + + QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`setHtmlLog` +%End + + void setHtmlLog( const QString &log ); +%Docstring +Sets the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`htmlLog` %End bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index 3d5c310e1eb86..45847f9052c44 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -67,6 +67,20 @@ Returns the outputs generated by the child algorithm. Sets the ``outputs`` used for the child algorithm. .. seealso:: :py:func:`outputs` +%End + + QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`setHtmlLog` +%End + + void setHtmlLog( const QString &log ); +%Docstring +Sets the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`htmlLog` %End bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 41a7920e93170..a9efe94b4ca67 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -326,6 +326,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QVariantMap finalResults; QSet< QString > executed; bool executedAlg = true; + int previousHtmlLogLength = 0; while ( executedAlg && executed.count() < toExecute.count() ) { executedAlg = false; @@ -503,124 +504,142 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa childResults.insert( childId, results ); childResult.setOutputs( results ); - context.modelChildResults().insert( childId, childResult ); - - if ( !runResult ) - { - throw QgsProcessingException( error ); - } - - if ( feedback && !skipGenericLogging ) + if ( runResult ) { - const QVariantMap displayOutputs = QgsProcessingUtils::removePointerValuesFromMap( results ); - QStringList formattedOutputs; - for ( auto displayOutputIt = displayOutputs.constBegin(); displayOutputIt != displayOutputs.constEnd(); ++displayOutputIt ) + if ( feedback && !skipGenericLogging ) { - formattedOutputs << QStringLiteral( "%1: %2" ).arg( displayOutputIt.key(), - QgsProcessingUtils::variantToPythonLiteral( displayOutputIt.value() ) );; + const QVariantMap displayOutputs = QgsProcessingUtils::removePointerValuesFromMap( results ); + QStringList formattedOutputs; + for ( auto displayOutputIt = displayOutputs.constBegin(); displayOutputIt != displayOutputs.constEnd(); ++displayOutputIt ) + { + formattedOutputs << QStringLiteral( "%1: %2" ).arg( displayOutputIt.key(), + QgsProcessingUtils::variantToPythonLiteral( displayOutputIt.value() ) );; + } + feedback->pushInfo( QObject::tr( "Results:" ) ); + feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); } - feedback->pushInfo( QObject::tr( "Results:" ) ); - feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); - } - // look through child alg's outputs to determine whether any of these should be copied - // to the final model outputs - const QMap outputs = child.modelOutputs(); - for ( auto outputIt = outputs.constBegin(); outputIt != outputs.constEnd(); ++outputIt ) - { - const int outputSortKey = mOutputOrder.indexOf( QStringLiteral( "%1:%2" ).arg( childId, outputIt->childOutputName() ) ); - switch ( mInternalVersion ) + // look through child alg's outputs to determine whether any of these should be copied + // to the final model outputs + const QMap outputs = child.modelOutputs(); + for ( auto outputIt = outputs.constBegin(); outputIt != outputs.constEnd(); ++outputIt ) { - case QgsProcessingModelAlgorithm::InternalVersion::Version1: - finalResults.insert( childId + ':' + outputIt->name(), results.value( outputIt->childOutputName() ) ); - break; - case QgsProcessingModelAlgorithm::InternalVersion::Version2: - if ( const QgsProcessingParameterDefinition *modelParam = modelParameterFromChildIdAndOutputName( child.childId(), outputIt.key() ) ) - { - finalResults.insert( modelParam->name(), results.value( outputIt->childOutputName() ) ); - } - break; - } + const int outputSortKey = mOutputOrder.indexOf( QStringLiteral( "%1:%2" ).arg( childId, outputIt->childOutputName() ) ); + switch ( mInternalVersion ) + { + case QgsProcessingModelAlgorithm::InternalVersion::Version1: + finalResults.insert( childId + ':' + outputIt->name(), results.value( outputIt->childOutputName() ) ); + break; + case QgsProcessingModelAlgorithm::InternalVersion::Version2: + if ( const QgsProcessingParameterDefinition *modelParam = modelParameterFromChildIdAndOutputName( child.childId(), outputIt.key() ) ) + { + finalResults.insert( modelParam->name(), results.value( outputIt->childOutputName() ) ); + } + break; + } - if ( !results.value( outputIt->childOutputName() ).toString().isEmpty() ) - { - QgsProcessingContext::LayerDetails &details = context.layerToLoadOnCompletionDetails( results.value( outputIt->childOutputName() ).toString() ); - details.groupName = mOutputGroup; - if ( outputSortKey > 0 ) - details.layerSortKey = outputSortKey; + if ( !results.value( outputIt->childOutputName() ).toString().isEmpty() ) + { + QgsProcessingContext::LayerDetails &details = context.layerToLoadOnCompletionDetails( results.value( outputIt->childOutputName() ).toString() ); + details.groupName = mOutputGroup; + if ( outputSortKey > 0 ) + details.layerSortKey = outputSortKey; + } } - } - executed.insert( childId ); + executed.insert( childId ); - std::function< void( const QString &, const QString & )> pruneAlgorithmBranchRecursive; - pruneAlgorithmBranchRecursive = [&]( const QString & id, const QString &branch = QString() ) - { - const QSet toPrune = dependentChildAlgorithms( id, branch ); - for ( const QString &targetId : toPrune ) + std::function< void( const QString &, const QString & )> pruneAlgorithmBranchRecursive; + pruneAlgorithmBranchRecursive = [&]( const QString & id, const QString &branch = QString() ) { - if ( executed.contains( targetId ) ) - continue; + const QSet toPrune = dependentChildAlgorithms( id, branch ); + for ( const QString &targetId : toPrune ) + { + if ( executed.contains( targetId ) ) + continue; - executed.insert( targetId ); - pruneAlgorithmBranchRecursive( targetId, branch ); - } - }; + executed.insert( targetId ); + pruneAlgorithmBranchRecursive( targetId, branch ); + } + }; - // prune remaining algorithms if they are dependent on a branch from this child which didn't eventuate - const QgsProcessingOutputDefinitions outputDefs = childAlg->outputDefinitions(); - for ( const QgsProcessingOutputDefinition *outputDef : outputDefs ) - { - if ( outputDef->type() == QgsProcessingOutputConditionalBranch::typeName() && !results.value( outputDef->name() ).toBool() ) + // prune remaining algorithms if they are dependent on a branch from this child which didn't eventuate + const QgsProcessingOutputDefinitions outputDefs = childAlg->outputDefinitions(); + for ( const QgsProcessingOutputDefinition *outputDef : outputDefs ) { - pruneAlgorithmBranchRecursive( childId, outputDef->name() ); + if ( outputDef->type() == QgsProcessingOutputConditionalBranch::typeName() && !results.value( outputDef->name() ).toBool() ) + { + pruneAlgorithmBranchRecursive( childId, outputDef->name() ); + } } - } - if ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::PruneModelBranchesBasedOnAlgorithmResults ) - { - // check if any dependent algorithms should be canceled based on the outputs of this algorithm run - // first find all direct dependencies of this algorithm by looking through all remaining child algorithms - for ( const QString &candidateId : std::as_const( toExecute ) ) + if ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::PruneModelBranchesBasedOnAlgorithmResults ) { - if ( executed.contains( candidateId ) ) - continue; - - // a pending algorithm was found..., check it's parameter sources to see if it links to any of the current - // algorithm's outputs - const QgsProcessingModelChildAlgorithm &candidate = mChildAlgorithms[ candidateId ]; - const QMap candidateParams = candidate.parameterSources(); - QMap::const_iterator paramIt = candidateParams.constBegin(); - bool pruned = false; - for ( ; paramIt != candidateParams.constEnd(); ++paramIt ) + // check if any dependent algorithms should be canceled based on the outputs of this algorithm run + // first find all direct dependencies of this algorithm by looking through all remaining child algorithms + for ( const QString &candidateId : std::as_const( toExecute ) ) { - for ( const QgsProcessingModelChildParameterSource &source : paramIt.value() ) + if ( executed.contains( candidateId ) ) + continue; + + // a pending algorithm was found..., check it's parameter sources to see if it links to any of the current + // algorithm's outputs + const QgsProcessingModelChildAlgorithm &candidate = mChildAlgorithms[ candidateId ]; + const QMap candidateParams = candidate.parameterSources(); + QMap::const_iterator paramIt = candidateParams.constBegin(); + bool pruned = false; + for ( ; paramIt != candidateParams.constEnd(); ++paramIt ) { - if ( source.source() == Qgis::ProcessingModelChildParameterSource::ChildOutput && source.outputChildId() == childId ) + for ( const QgsProcessingModelChildParameterSource &source : paramIt.value() ) { - // ok, this one is dependent on the current alg. Did we get a value for it? - if ( !results.contains( source.outputName() ) ) + if ( source.source() == Qgis::ProcessingModelChildParameterSource::ChildOutput && source.outputChildId() == childId ) { - // oh no, nothing returned for this parameter. Gotta trim the branch back! - pruned = true; - // skip the dependent alg.. - executed.insert( candidateId ); - //... and everything which depends on it - pruneAlgorithmBranchRecursive( candidateId, QString() ); - break; + // ok, this one is dependent on the current alg. Did we get a value for it? + if ( !results.contains( source.outputName() ) ) + { + // oh no, nothing returned for this parameter. Gotta trim the branch back! + pruned = true; + // skip the dependent alg.. + executed.insert( candidateId ); + //... and everything which depends on it + pruneAlgorithmBranchRecursive( candidateId, QString() ); + break; + } } } + if ( pruned ) + break; } - if ( pruned ) - break; } } + + childAlg.reset( nullptr ); + modelFeedback.setCurrentStep( executed.count() ); + if ( feedback && !skipGenericLogging ) + { + feedback->pushInfo( QObject::tr( "OK. Execution took %1 s (%n output(s)).", nullptr, results.count() ).arg( childTime.elapsed() / 1000.0 ) ); + } } - childAlg.reset( nullptr ); - modelFeedback.setCurrentStep( executed.count() ); - if ( feedback && !skipGenericLogging ) - feedback->pushInfo( QObject::tr( "OK. Execution took %1 s (%n output(s)).", nullptr, results.count() ).arg( childTime.elapsed() / 1000.0 ) ); + // trim out just the portion of the overall log which relates to this child + const QString thisAlgorithmHtmlLog = feedback->htmlLog().mid( previousHtmlLogLength ); + previousHtmlLogLength = feedback->htmlLog().length(); + + if ( !runResult ) + { + const QString formattedException = QStringLiteral( "%1
" ).arg( error.toHtmlEscaped() ).replace( '\n', QLatin1String( "
" ) ); + const QString formattedRunTime = QStringLiteral( "%1
" ).arg( QObject::tr( "Failed after %1 s." ).arg( childTime.elapsed() / 1000.0 ).toHtmlEscaped() ).replace( '\n', QLatin1String( "
" ) ); + + childResult.setHtmlLog( thisAlgorithmHtmlLog + formattedException + formattedRunTime ); + context.modelChildResults().insert( childId, childResult ); + + throw QgsProcessingException( error ); + } + else + { + childResult.setHtmlLog( thisAlgorithmHtmlLog ); + context.modelChildResults().insert( childId, childResult ); + } } if ( feedback && feedback->isCanceled() ) diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index 0b197d3f5bc1e..378355d620a2e 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -86,9 +86,24 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult */ void setOutputs( const QVariantMap &outputs ) { mOutputs = outputs; } + /** + * Returns the HTML formatted contents of logged messages which occurred while running the child. + * + * \see setHtmlLog() + */ + QString htmlLog() const { return mHtmlLog; } + + /** + * Sets the HTML formatted contents of logged messages which occurred while running the child. + * + * \see htmlLog() + */ + void setHtmlLog( const QString &log ) { mHtmlLog = log; } + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const { return mExecutionStatus == other.mExecutionStatus + && mHtmlLog == other.mHtmlLog && mInputs == other.mInputs && mOutputs == other.mOutputs; } @@ -102,6 +117,7 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult Qgis::ProcessingModelChildAlgorithmExecutionStatus mExecutionStatus = Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted; QVariantMap mInputs; QVariantMap mOutputs; + QString mHtmlLog; }; diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 6a465ac5f2758..df094d16e7686 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -69,7 +69,7 @@ class DummyRaiseExceptionAlgorithm : public QgsProcessingAlgorithm QString displayName() const override { return mName; } QVariantMap processAlgorithm( const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) override { - throw QgsProcessingException( QString() ); + throw QgsProcessingException( QStringLiteral( "something bad happened" ) ); } static bool postProcessAlgorithmCalled; QVariantMap postProcessAlgorithm( QgsProcessingContext &, QgsProcessingFeedback * ) final @@ -2330,12 +2330,14 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QgsProcessingModelChildAlgorithm algWhichRaisesException; algWhichRaisesException.setChildId( QStringLiteral( "raise" ) ); + algWhichRaisesException.setDescription( QStringLiteral( "my second step" ) ); algWhichRaisesException.setAlgorithmId( "dummy4:raise" ); algWhichRaisesException.setDependencies( {QgsProcessingModelChildDependency( QStringLiteral( "buffer" ) )} ); m.addChildAlgorithm( algWhichRaisesException ); // run and check context details QgsProcessingContext context; + context.setLogLevel( Qgis::ProcessingLogLevel::ModelDebug ); QgsProcessingFeedback feedback; QVariantMap params; QgsVectorLayer *layer3111 = new QgsVectorLayer( "Point?crs=epsg:3111", "v1", "memory" ); @@ -2359,9 +2361,13 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QCOMPARE( context.modelChildResults().value( "buffer" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); + QCOMPARE( context.modelChildResults().value( "buffer" ).htmlLog().left( 50 ), QStringLiteral( "Prepare algorithm: buffer" ) ); + QCOMPARE( context.modelChildResults().value( "buffer" ).htmlLog().right( 21 ), QStringLiteral( "s (1 output(s)).
" ) ); QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); QCOMPARE( context.modelChildResults().value( "raise" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); + QCOMPARE( context.modelChildResults().value( "raise" ).htmlLog().left( 49 ), QStringLiteral( "Prepare algorithm: raise" ) ); + QVERIFY( context.modelChildResults().value( "raise" ).htmlLog().contains( QStringLiteral( "Error encountered while running my second step: something bad happened" ) ) ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From e2d6b54f357ef27743b747cbb54f108215bea443 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 11:16:24 +1000 Subject: [PATCH 37/46] Add "View Log" action for child algorithms This action shows the log of that child step, regardless of whether or not it failed. This is handy for debugging model errors after testing, when you've already closed the algorithm window...! --- .../qgsmodelcomponentgraphicitem.sip.in | 7 +++++ .../models/qgsmodelgraphicsscene.sip.in | 7 +++++ .../qgsmodelcomponentgraphicitem.sip.in | 7 +++++ .../models/qgsmodelgraphicsscene.sip.in | 7 +++++ src/core/processing/qgsprocessingfeedback.cpp | 4 ++- .../models/qgsmodelcomponentgraphicitem.cpp | 27 +++++++++++++++++-- .../models/qgsmodelcomponentgraphicitem.h | 7 +++++ .../models/qgsmodeldesignerdialog.cpp | 19 +++++++++++++ .../models/qgsmodeldesignerdialog.h | 1 + .../models/qgsmodelgraphicsscene.cpp | 4 +++ .../processing/models/qgsmodelgraphicsscene.h | 7 +++++ 11 files changed, 94 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index e8265886976e0..86f58913e8527 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -407,6 +407,13 @@ Sets the ``results`` obtained for this child algorithm for the last model execut %Docstring Emitted when the user opts to view previous results from this child algorithm. +.. versionadded:: 3.38 +%End + + void showLog(); +%Docstring +Emitted when the user opts to view the previous log from this child algorithm. + .. versionadded:: 3.38 %End diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index b97e646a511ca..5e0271b16f8ef 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -178,6 +178,13 @@ If ``None``, no item is selected. %Docstring Emitted when the user opts to view previous results from the child algorithm with matching ID. +.. versionadded:: 3.38 +%End + + void showLog( const QString &childId ); +%Docstring +Emitted when the user opts to view the previous log from the child algorithm with matching ID. + .. versionadded:: 3.38 %End diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 72a74d0a9aecf..105283cfb8a11 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -407,6 +407,13 @@ Sets the ``results`` obtained for this child algorithm for the last model execut %Docstring Emitted when the user opts to view previous results from this child algorithm. +.. versionadded:: 3.38 +%End + + void showLog(); +%Docstring +Emitted when the user opts to view the previous log from this child algorithm. + .. versionadded:: 3.38 %End diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 230cd0d7584d8..91a6a9e6e9063 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -178,6 +178,13 @@ If ``None``, no item is selected. %Docstring Emitted when the user opts to view previous results from the child algorithm with matching ID. +.. versionadded:: 3.38 +%End + + void showLog( const QString &childId ); +%Docstring +Emitted when the user opts to view the previous log from the child algorithm with matching ID. + .. versionadded:: 3.38 %End diff --git a/src/core/processing/qgsprocessingfeedback.cpp b/src/core/processing/qgsprocessingfeedback.cpp index af3bfeb7c0a4c..06c26d72f147c 100644 --- a/src/core/processing/qgsprocessingfeedback.cpp +++ b/src/core/processing/qgsprocessingfeedback.cpp @@ -33,8 +33,10 @@ QgsProcessingFeedback::QgsProcessingFeedback( bool logFeedback ) } -void QgsProcessingFeedback::setProgressText( const QString & ) +void QgsProcessingFeedback::setProgressText( const QString &text ) { + mHtmlLog.append( text.toHtmlEscaped().replace( '\n', QLatin1String( "
" ) ) + QStringLiteral( "
" ) ); + mTextLog.append( text + '\n' ); } void QgsProcessingFeedback::log( const QString &htmlMessage, const QString &textMessage ) diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index 251fa4c4dbb52..a9b0f6da59d60 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -901,10 +901,33 @@ void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextM QAction *viewOutputLayersAction = popupmenu->addAction( QObject::tr( "View Output Layers" ) ); viewOutputLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionShowSelectedLayers.svg" ) ) ); connect( viewOutputLayersAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::showPreviousResults ); - if ( mResults.outputs().empty() ) - viewOutputLayersAction->setEnabled( false ); + // enable this action only when the child succeeded + switch ( mResults.executionStatus() ) + { + case Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted: + case Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed: + viewOutputLayersAction->setEnabled( false ); + break; + + case Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success: + break; + } } } + + QAction *viewLogAction = popupmenu->addAction( QObject::tr( "View Log…" ) ); + connect( viewLogAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::showLog ); + // enable this action even when the child failed + switch ( mResults.executionStatus() ) + { + case Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted: + viewLogAction->setEnabled( false ); + break; + + case Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success: + case Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed: + break; + } } popupmenu->exec( event->screenPos() ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index 27849685eb9d0..296619661c718 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -475,6 +475,13 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra */ void showPreviousResults(); + /** + * Emitted when the user opts to view the previous log from this child algorithm. + * + * \since QGIS 3.38 + */ + void showLog(); + protected: QColor fillColor( State state ) const override; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index e23dd69819efd..d6271795ccddb 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -513,6 +513,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } ); connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } ); connect( mScene, &QgsModelGraphicsScene::showPreviousResults, this, &QgsModelDesignerDialog::showPreviousResults ); + connect( mScene, &QgsModelGraphicsScene::showLog, this, &QgsModelDesignerDialog::showLog ); mView->centerOn( center ); @@ -1120,6 +1121,24 @@ void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) } } +void QgsModelDesignerDialog::showLog( const QString &childId ) +{ + const QString childDescription = mModel->childAlgorithm( childId ).description(); + + const QgsProcessingModelChildAlgorithmResult result = mChildResults.value( childId ); + if ( result.htmlLog().isEmpty() ) + { + mMessageBar->pushWarning( QString(), tr( "No log is available for %1" ).arg( childDescription ) ); + return; + } + + QgsMessageViewer m( this, QgsGuiUtils::ModalDialogFlags, false ); + m.setWindowTitle( childDescription ); + m.setCheckBoxVisible( false ); + m.setMessageAsHtml( result.htmlLog() ); + m.exec(); +} + void QgsModelDesignerDialog::validate() { QStringList issues; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 74ba526497fa8..462e3aeb09205 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -186,6 +186,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void editHelp(); void run(); void showPreviousResults( const QString &childId ); + void showLog( const QString &childId ); private: diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 2ac1a0aeb521f..8cede3fe36599 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -149,6 +149,10 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs { emit showPreviousResults( childId ); } ); + connect( item, &QgsModelChildAlgorithmGraphicItem::showLog, this, [this, childId] + { + emit showLog( childId ); + } ); addCommentItemForComponent( model, it.value(), item ); } diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index 84044323a688c..e3fa979cf727a 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -194,6 +194,13 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ void showPreviousResults( const QString &childId ); + /** + * Emitted when the user opts to view the previous log from the child algorithm with matching ID. + * + * \since QGIS 3.38 + */ + void showLog( const QString &childId ); + protected: /** From 75455dffad0698356c57eeb46f554f0d3d638a97 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 11:38:09 +1000 Subject: [PATCH 38/46] Move class to own file --- .../models/qgsprocessingmodelresult.sip.in | 99 +++++++++++++++ .../processing/qgsprocessingcontext.sip.in | 78 ------------ python/PyQt6/core/core_auto.sip | 1 + .../models/qgsprocessingmodelresult.sip.in | 99 +++++++++++++++ .../processing/qgsprocessingcontext.sip.in | 78 ------------ python/core/core_auto.sip | 1 + src/core/CMakeLists.txt | 2 + .../models/qgsprocessingmodelresult.cpp | 25 ++++ .../models/qgsprocessingmodelresult.h | 119 ++++++++++++++++++ src/core/processing/qgsprocessingcontext.cpp | 7 -- src/core/processing/qgsprocessingcontext.h | 93 +------------- 11 files changed, 347 insertions(+), 255 deletions(-) create mode 100644 python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in create mode 100644 python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in create mode 100644 src/core/processing/models/qgsprocessingmodelresult.cpp create mode 100644 src/core/processing/models/qgsprocessingmodelresult.h diff --git a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in new file mode 100644 index 0000000000000..60c61b0cd0c48 --- /dev/null +++ b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -0,0 +1,99 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelresult.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsProcessingModelChildAlgorithmResult +{ +%Docstring(signature="appended") +Encapsulates the results of running a child algorithm within a model + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsprocessingmodelresult.h" +%End + public: + + QgsProcessingModelChildAlgorithmResult(); + + Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const; +%Docstring +Returns the status of executing the child algorithm. + +.. seealso:: :py:func:`setExecutionStatus` +%End + + void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ); +%Docstring +Sets the ``status`` of executing the child algorithm. + +.. seealso:: :py:func:`executionStatus` +%End + + QVariantMap inputs() const; +%Docstring +Returns the inputs used for the child algorithm. + +.. seealso:: :py:func:`setInputs` +%End + + void setInputs( const QVariantMap &inputs ); +%Docstring +Sets the ``inputs`` used for the child algorithm. + +.. seealso:: :py:func:`inputs` +%End + + QVariantMap outputs() const; +%Docstring +Returns the outputs generated by the child algorithm. + +.. seealso:: :py:func:`setOutputs` +%End + + void setOutputs( const QVariantMap &outputs ); +%Docstring +Sets the ``outputs`` used for the child algorithm. + +.. seealso:: :py:func:`outputs` +%End + + QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`setHtmlLog` +%End + + void setHtmlLog( const QString &log ); +%Docstring +Sets the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`htmlLog` +%End + + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; + bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const; + +}; + + + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelresult.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index 09d71dabc9651..b7cbc067c6e85 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -12,84 +12,6 @@ -class QgsProcessingModelChildAlgorithmResult -{ -%Docstring(signature="appended") -Encapsulates the results of running a child algorithm within a model - -.. versionadded:: 3.38 -%End - -%TypeHeaderCode -#include "qgsprocessingcontext.h" -%End - public: - - QgsProcessingModelChildAlgorithmResult(); - - Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const; -%Docstring -Returns the status of executing the child algorithm. - -.. seealso:: :py:func:`setExecutionStatus` -%End - - void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ); -%Docstring -Sets the ``status`` of executing the child algorithm. - -.. seealso:: :py:func:`executionStatus` -%End - - QVariantMap inputs() const; -%Docstring -Returns the inputs used for the child algorithm. - -.. seealso:: :py:func:`setInputs` -%End - - void setInputs( const QVariantMap &inputs ); -%Docstring -Sets the ``inputs`` used for the child algorithm. - -.. seealso:: :py:func:`inputs` -%End - - QVariantMap outputs() const; -%Docstring -Returns the outputs generated by the child algorithm. - -.. seealso:: :py:func:`setOutputs` -%End - - void setOutputs( const QVariantMap &outputs ); -%Docstring -Sets the ``outputs`` used for the child algorithm. - -.. seealso:: :py:func:`outputs` -%End - - QString htmlLog() const; -%Docstring -Returns the HTML formatted contents of logged messages which occurred while running the child. - -.. seealso:: :py:func:`setHtmlLog` -%End - - void setHtmlLog( const QString &log ); -%Docstring -Sets the HTML formatted contents of logged messages which occurred while running the child. - -.. seealso:: :py:func:`htmlLog` -%End - - bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; - bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const; - -}; - - - class QgsProcessingContext { %Docstring(signature="appended") diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 1127fa0d7f2ac..b70acd56a4684 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -551,6 +551,7 @@ %Include auto_generated/processing/models/qgsprocessingmodelgroupbox.sip %Include auto_generated/processing/models/qgsprocessingmodeloutput.sip %Include auto_generated/processing/models/qgsprocessingmodelparameter.sip +%Include auto_generated/processing/models/qgsprocessingmodelresult.sip %Include auto_generated/processing/qgsprocessing.sip %Include auto_generated/processing/qgsprocessingalgorithm.sip %Include auto_generated/processing/qgsprocessingalgrunnertask.sip diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in new file mode 100644 index 0000000000000..60c61b0cd0c48 --- /dev/null +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -0,0 +1,99 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelresult.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsProcessingModelChildAlgorithmResult +{ +%Docstring(signature="appended") +Encapsulates the results of running a child algorithm within a model + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsprocessingmodelresult.h" +%End + public: + + QgsProcessingModelChildAlgorithmResult(); + + Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const; +%Docstring +Returns the status of executing the child algorithm. + +.. seealso:: :py:func:`setExecutionStatus` +%End + + void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ); +%Docstring +Sets the ``status`` of executing the child algorithm. + +.. seealso:: :py:func:`executionStatus` +%End + + QVariantMap inputs() const; +%Docstring +Returns the inputs used for the child algorithm. + +.. seealso:: :py:func:`setInputs` +%End + + void setInputs( const QVariantMap &inputs ); +%Docstring +Sets the ``inputs`` used for the child algorithm. + +.. seealso:: :py:func:`inputs` +%End + + QVariantMap outputs() const; +%Docstring +Returns the outputs generated by the child algorithm. + +.. seealso:: :py:func:`setOutputs` +%End + + void setOutputs( const QVariantMap &outputs ); +%Docstring +Sets the ``outputs`` used for the child algorithm. + +.. seealso:: :py:func:`outputs` +%End + + QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`setHtmlLog` +%End + + void setHtmlLog( const QString &log ); +%Docstring +Sets the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`htmlLog` +%End + + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; + bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const; + +}; + + + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelresult.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index 45847f9052c44..e8127223c7ed7 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -12,84 +12,6 @@ -class QgsProcessingModelChildAlgorithmResult -{ -%Docstring(signature="appended") -Encapsulates the results of running a child algorithm within a model - -.. versionadded:: 3.38 -%End - -%TypeHeaderCode -#include "qgsprocessingcontext.h" -%End - public: - - QgsProcessingModelChildAlgorithmResult(); - - Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const; -%Docstring -Returns the status of executing the child algorithm. - -.. seealso:: :py:func:`setExecutionStatus` -%End - - void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ); -%Docstring -Sets the ``status`` of executing the child algorithm. - -.. seealso:: :py:func:`executionStatus` -%End - - QVariantMap inputs() const; -%Docstring -Returns the inputs used for the child algorithm. - -.. seealso:: :py:func:`setInputs` -%End - - void setInputs( const QVariantMap &inputs ); -%Docstring -Sets the ``inputs`` used for the child algorithm. - -.. seealso:: :py:func:`inputs` -%End - - QVariantMap outputs() const; -%Docstring -Returns the outputs generated by the child algorithm. - -.. seealso:: :py:func:`setOutputs` -%End - - void setOutputs( const QVariantMap &outputs ); -%Docstring -Sets the ``outputs`` used for the child algorithm. - -.. seealso:: :py:func:`outputs` -%End - - QString htmlLog() const; -%Docstring -Returns the HTML formatted contents of logged messages which occurred while running the child. - -.. seealso:: :py:func:`setHtmlLog` -%End - - void setHtmlLog( const QString &log ); -%Docstring -Sets the HTML formatted contents of logged messages which occurred while running the child. - -.. seealso:: :py:func:`htmlLog` -%End - - bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; - bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const; - -}; - - - class QgsProcessingContext { %Docstring(signature="appended") diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 1127fa0d7f2ac..b70acd56a4684 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -551,6 +551,7 @@ %Include auto_generated/processing/models/qgsprocessingmodelgroupbox.sip %Include auto_generated/processing/models/qgsprocessingmodeloutput.sip %Include auto_generated/processing/models/qgsprocessingmodelparameter.sip +%Include auto_generated/processing/models/qgsprocessingmodelresult.sip %Include auto_generated/processing/qgsprocessing.sip %Include auto_generated/processing/qgsprocessingalgorithm.sip %Include auto_generated/processing/qgsprocessingalgrunnertask.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 85cbb80ae3000..20f1221a97776 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -281,6 +281,7 @@ set(QGIS_CORE_SRCS processing/models/qgsprocessingmodelgroupbox.cpp processing/models/qgsprocessingmodelparameter.cpp processing/models/qgsprocessingmodeloutput.cpp + processing/models/qgsprocessingmodelresult.cpp providers/qgsabstractproviderconnection.cpp providers/qgsabstractdatabaseproviderconnection.cpp @@ -1744,6 +1745,7 @@ set(QGIS_CORE_HDRS processing/models/qgsprocessingmodelgroupbox.h processing/models/qgsprocessingmodeloutput.h processing/models/qgsprocessingmodelparameter.h + processing/models/qgsprocessingmodelresult.h processing/qgsprocessing.h processing/qgsprocessingalgorithm.h processing/qgsprocessingalgrunnertask.h diff --git a/src/core/processing/models/qgsprocessingmodelresult.cpp b/src/core/processing/models/qgsprocessingmodelresult.cpp new file mode 100644 index 0000000000000..aa8f4ca3e69a7 --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelresult.cpp @@ -0,0 +1,25 @@ +/*************************************************************************** + qgsprocessingmodelresult.cpp + ---------------------- + begin : April 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "qgsprocessingmodelresult.h" + +// +// QgsProcessingModelChildResult +// + +QgsProcessingModelChildAlgorithmResult::QgsProcessingModelChildAlgorithmResult() = default; + diff --git a/src/core/processing/models/qgsprocessingmodelresult.h b/src/core/processing/models/qgsprocessingmodelresult.h new file mode 100644 index 0000000000000..a59cfdca40a94 --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelresult.h @@ -0,0 +1,119 @@ +/*************************************************************************** + qgsprocessingmodelresult.h + ---------------------- + begin : April 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef QGSPROCESSINGMODELRESULT_H +#define QGSPROCESSINGMODELRESULT_H + +#include "qgis_core.h" +#include "qgis.h" + +/** + * \class QgsProcessingModelChildResult + * \ingroup core + * \brief Encapsulates the results of running a child algorithm within a model + * + * \since QGIS 3.38 +*/ +class CORE_EXPORT QgsProcessingModelChildAlgorithmResult +{ + public: + + QgsProcessingModelChildAlgorithmResult(); + + /** + * Returns the status of executing the child algorithm. + * + * \see setExecutionStatus() + */ + Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const { return mExecutionStatus; } + + /** + * Sets the \a status of executing the child algorithm. + * + * \see executionStatus() + */ + void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ) { mExecutionStatus = status; } + + /** + * Returns the inputs used for the child algorithm. + * + * \see setInputs() + */ + QVariantMap inputs() const { return mInputs; } + + /** + * Sets the \a inputs used for the child algorithm. + * + * \see inputs() + */ + void setInputs( const QVariantMap &inputs ) { mInputs = inputs; } + + /** + * Returns the outputs generated by the child algorithm. + * + * \see setOutputs() + */ + QVariantMap outputs() const { return mOutputs; } + + /** + * Sets the \a outputs used for the child algorithm. + * + * \see outputs() + */ + void setOutputs( const QVariantMap &outputs ) { mOutputs = outputs; } + + /** + * Returns the HTML formatted contents of logged messages which occurred while running the child. + * + * \see setHtmlLog() + */ + QString htmlLog() const { return mHtmlLog; } + + /** + * Sets the HTML formatted contents of logged messages which occurred while running the child. + * + * \see htmlLog() + */ + void setHtmlLog( const QString &log ) { mHtmlLog = log; } + + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const + { + return mExecutionStatus == other.mExecutionStatus + && mHtmlLog == other.mHtmlLog + && mInputs == other.mInputs + && mOutputs == other.mOutputs; + } + bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const + { + return !( *this == other ); + } + + private: + + Qgis::ProcessingModelChildAlgorithmExecutionStatus mExecutionStatus = Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted; + QVariantMap mInputs; + QVariantMap mOutputs; + QString mHtmlLog; + +}; + + +#endif // QGSPROCESSINGMODELRESULT_H + + + + diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index de3a939bb04d9..dc9fe79158c80 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -21,13 +21,6 @@ #include "qgsproviderregistry.h" #include "qgsprocessing.h" -// -// QgsProcessingModelChildResult -// - -QgsProcessingModelChildAlgorithmResult::QgsProcessingModelChildAlgorithmResult() = default; - - // // QgsProcessingContext // diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index 378355d620a2e..dc03deb70b4df 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -24,104 +24,13 @@ #include "qgsexpressioncontext.h" #include "qgsprocessingfeedback.h" #include "qgsprocessingutils.h" - +#include "qgsprocessingmodelresult.h" #include #include class QgsProcessingLayerPostProcessorInterface; -/** - * \class QgsProcessingModelChildResult - * \ingroup core - * \brief Encapsulates the results of running a child algorithm within a model - * - * \since QGIS 3.38 -*/ -class CORE_EXPORT QgsProcessingModelChildAlgorithmResult -{ - public: - - QgsProcessingModelChildAlgorithmResult(); - - /** - * Returns the status of executing the child algorithm. - * - * \see setExecutionStatus() - */ - Qgis::ProcessingModelChildAlgorithmExecutionStatus executionStatus() const { return mExecutionStatus; } - - /** - * Sets the \a status of executing the child algorithm. - * - * \see executionStatus() - */ - void setExecutionStatus( Qgis::ProcessingModelChildAlgorithmExecutionStatus status ) { mExecutionStatus = status; } - - /** - * Returns the inputs used for the child algorithm. - * - * \see setInputs() - */ - QVariantMap inputs() const { return mInputs; } - - /** - * Sets the \a inputs used for the child algorithm. - * - * \see inputs() - */ - void setInputs( const QVariantMap &inputs ) { mInputs = inputs; } - - /** - * Returns the outputs generated by the child algorithm. - * - * \see setOutputs() - */ - QVariantMap outputs() const { return mOutputs; } - - /** - * Sets the \a outputs used for the child algorithm. - * - * \see outputs() - */ - void setOutputs( const QVariantMap &outputs ) { mOutputs = outputs; } - - /** - * Returns the HTML formatted contents of logged messages which occurred while running the child. - * - * \see setHtmlLog() - */ - QString htmlLog() const { return mHtmlLog; } - - /** - * Sets the HTML formatted contents of logged messages which occurred while running the child. - * - * \see htmlLog() - */ - void setHtmlLog( const QString &log ) { mHtmlLog = log; } - - bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const - { - return mExecutionStatus == other.mExecutionStatus - && mHtmlLog == other.mHtmlLog - && mInputs == other.mInputs - && mOutputs == other.mOutputs; - } - bool operator!=( const QgsProcessingModelChildAlgorithmResult &other ) const - { - return !( *this == other ); - } - - private: - - Qgis::ProcessingModelChildAlgorithmExecutionStatus mExecutionStatus = Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted; - QVariantMap mInputs; - QVariantMap mOutputs; - QString mHtmlLog; - -}; - - /** * \class QgsProcessingContext * \ingroup core From de0f5cf87c2de359f9148fc60a2f4157f7cb5b20 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 11:51:33 +1000 Subject: [PATCH 39/46] Make API more future proof --- .../models/qgsprocessingmodelresult.sip.in | 28 +++++++++++++ .../processing/qgsprocessingcontext.sip.in | 9 +---- .../models/qgsmodeldesignerdialog.sip.in | 4 +- .../models/qgsmodelgraphicsscene.sip.in | 4 +- .../models/qgsprocessingmodelresult.sip.in | 28 +++++++++++++ .../processing/qgsprocessingcontext.sip.in | 9 +---- .../models/qgsmodeldesignerdialog.sip.in | 4 +- .../models/qgsmodelgraphicsscene.sip.in | 4 +- .../models/qgsprocessingmodelalgorithm.cpp | 4 +- .../models/qgsprocessingmodelresult.cpp | 6 +++ .../models/qgsprocessingmodelresult.h | 40 ++++++++++++++++++- src/core/processing/qgsprocessingcontext.cpp | 2 +- src/core/processing/qgsprocessingcontext.h | 20 ++++------ .../models/qgsmodeldesignerdialog.cpp | 14 +++---- .../models/qgsmodeldesignerdialog.h | 6 +-- .../models/qgsmodelgraphicsscene.cpp | 11 ++--- .../processing/models/qgsmodelgraphicsscene.h | 6 +-- tests/src/analysis/testqgsprocessing.cpp | 24 +++++------ .../testqgsprocessingmodelalgorithm.cpp | 20 +++++----- 19 files changed, 164 insertions(+), 79 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index 60c61b0cd0c48..7feda672d51a5 100644 --- a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -85,6 +85,34 @@ Sets the HTML formatted contents of logged messages which occurred while running }; +class QgsProcessingModelResult +{ +%Docstring(signature="appended") +Encapsulates the results of running a Processing model + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsprocessingmodelresult.h" +%End + public: + + QgsProcessingModelResult(); + + + QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const; +%Docstring +Returns the map of child algorithm results. + +Map keys refer to the child algorithm IDs. + +.. versionadded:: 3.38 +%End + + +}; + diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index b7cbc067c6e85..6ca842b447369 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -657,14 +657,9 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End - QMap< QString, QgsProcessingModelChildAlgorithmResult > modelChildResults() const; + QgsProcessingModelResult modelResult() const; %Docstring -Returns the map of child algorithm results, populated when the context is used -to run a model algorithm. - -Map keys refer to the child algorithm IDs. - -.. seealso:: :py:func:`modelChildInputs` +Returns the model results, populated when the context is used to run a model algorithm. .. versionadded:: 3.38 %End diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 75523aabd4af8..b9682191f025d 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -117,9 +117,9 @@ Checks if there are unsaved changes in the model, and if so, prompts the user to Returns ``False`` if the cancel option was selected %End - void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); + void setLastRunResult( const QgsProcessingModelResult &result ); %Docstring -Sets the ``results`` of child algorithms for the last run of the model through the designer window. +Sets the ``result`` of the last run of the model through the designer window. %End void setModelName( const QString &name ); diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 5e0271b16f8ef..9ef5ef203d3d0 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -124,9 +124,9 @@ not correctly emit signals to allow the scene's model to update. Clears any selected items and sets ``item`` as the current selection. %End - void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); + void setLastRunResult( const QgsProcessingModelResult &result ); %Docstring -Sets the ``results`` of child algorithms for the last run of the model through the designer window. +Sets the ``result`` of the last run of the model through the designer window. %End QgsMessageBar *messageBar() const; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index 60c61b0cd0c48..7feda672d51a5 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -85,6 +85,34 @@ Sets the HTML formatted contents of logged messages which occurred while running }; +class QgsProcessingModelResult +{ +%Docstring(signature="appended") +Encapsulates the results of running a Processing model + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsprocessingmodelresult.h" +%End + public: + + QgsProcessingModelResult(); + + + QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const; +%Docstring +Returns the map of child algorithm results. + +Map keys refer to the child algorithm IDs. + +.. versionadded:: 3.38 +%End + + +}; + diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index e8127223c7ed7..38acae6b9dd9a 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -657,14 +657,9 @@ Returns list of the equivalent qgis_process arguments representing the settings .. versionadded:: 3.24 %End - QMap< QString, QgsProcessingModelChildAlgorithmResult > modelChildResults() const; + QgsProcessingModelResult modelResult() const; %Docstring -Returns the map of child algorithm results, populated when the context is used -to run a model algorithm. - -Map keys refer to the child algorithm IDs. - -.. seealso:: :py:func:`modelChildInputs` +Returns the model results, populated when the context is used to run a model algorithm. .. versionadded:: 3.38 %End diff --git a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 75523aabd4af8..b9682191f025d 100644 --- a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -117,9 +117,9 @@ Checks if there are unsaved changes in the model, and if so, prompts the user to Returns ``False`` if the cancel option was selected %End - void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); + void setLastRunResult( const QgsProcessingModelResult &result ); %Docstring -Sets the ``results`` of child algorithms for the last run of the model through the designer window. +Sets the ``result`` of the last run of the model through the designer window. %End void setModelName( const QString &name ); diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 91a6a9e6e9063..4635cbf8914a9 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -124,9 +124,9 @@ not correctly emit signals to allow the scene's model to update. Clears any selected items and sets ``item`` as the current selection. %End - void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); + void setLastRunResult( const QgsProcessingModelResult &result ); %Docstring -Sets the ``results`` of child algorithms for the last run of the model through the designer window. +Sets the ``result`` of the last run of the model through the designer window. %End QgsMessageBar *messageBar() const; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index a9efe94b4ca67..fd8e0d54bc45c 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -631,14 +631,14 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa const QString formattedRunTime = QStringLiteral( "%1
" ).arg( QObject::tr( "Failed after %1 s." ).arg( childTime.elapsed() / 1000.0 ).toHtmlEscaped() ).replace( '\n', QLatin1String( "
" ) ); childResult.setHtmlLog( thisAlgorithmHtmlLog + formattedException + formattedRunTime ); - context.modelChildResults().insert( childId, childResult ); + context.modelResult().childResults().insert( childId, childResult ); throw QgsProcessingException( error ); } else { childResult.setHtmlLog( thisAlgorithmHtmlLog ); - context.modelChildResults().insert( childId, childResult ); + context.modelResult().childResults().insert( childId, childResult ); } } diff --git a/src/core/processing/models/qgsprocessingmodelresult.cpp b/src/core/processing/models/qgsprocessingmodelresult.cpp index aa8f4ca3e69a7..932663cce706a 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.cpp +++ b/src/core/processing/models/qgsprocessingmodelresult.cpp @@ -23,3 +23,9 @@ QgsProcessingModelChildAlgorithmResult::QgsProcessingModelChildAlgorithmResult() = default; + +// +// QgsProcessingModelResult +// + +QgsProcessingModelResult::QgsProcessingModelResult() = default; diff --git a/src/core/processing/models/qgsprocessingmodelresult.h b/src/core/processing/models/qgsprocessingmodelresult.h index a59cfdca40a94..e3385591c8ddb 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.h +++ b/src/core/processing/models/qgsprocessingmodelresult.h @@ -22,7 +22,6 @@ #include "qgis.h" /** - * \class QgsProcessingModelChildResult * \ingroup core * \brief Encapsulates the results of running a child algorithm within a model * @@ -111,6 +110,45 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult }; +/** + * \ingroup core + * \brief Encapsulates the results of running a Processing model + * + * \since QGIS 3.38 +*/ +class CORE_EXPORT QgsProcessingModelResult +{ + public: + + QgsProcessingModelResult(); + + + /** + * Returns the map of child algorithm results. + * + * Map keys refer to the child algorithm IDs. + * + * \since QGIS 3.38 + */ + QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const { return mChildResults; } + + /** + * Returns a reference to the map of child algorithm results. + * + * Map keys refer to the child algorithm IDs. + * + * \note Not available in Python bindings + + * \since QGIS 3.38 + */ + QMap< QString, QgsProcessingModelChildAlgorithmResult > &childResults() SIP_SKIP { return mChildResults; } + + private: + + QMap< QString, QgsProcessingModelChildAlgorithmResult > mChildResults; + +}; + #endif // QGSPROCESSINGMODELRESULT_H diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index dc9fe79158c80..09a39ca4ae431 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -127,7 +127,7 @@ std::function QgsProcessingContext::defaultInvalidG void QgsProcessingContext::takeResultsFrom( QgsProcessingContext &context ) { setLayersToLoadOnCompletion( context.mLayersToLoadOnCompletion ); - mModelChildResults = context.mModelChildResults; + mModelResult = context.mModelResult; context.mLayersToLoadOnCompletion.clear(); tempLayerStore.transferLayersFromStore( context.temporaryLayerStore() ); } diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index dc03deb70b4df..2fe0ce74ece25 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -89,7 +89,7 @@ class CORE_EXPORT QgsProcessingContext mLogLevel = other.mLogLevel; mTemporaryFolderOverride = other.mTemporaryFolderOverride; mMaximumThreads = other.mMaximumThreads; - mModelChildResults = other.mModelChildResults; + mModelResult = other.mModelResult; } /** @@ -736,27 +736,21 @@ class CORE_EXPORT QgsProcessingContext QStringList asQgisProcessArguments( QgsProcessingContext::ProcessArgumentFlags flags = QgsProcessingContext::ProcessArgumentFlags() ) const; /** - * Returns the map of child algorithm results, populated when the context is used - * to run a model algorithm. - * - * Map keys refer to the child algorithm IDs. + * Returns the model results, populated when the context is used to run a model algorithm. * - * \see modelChildInputs() * \since QGIS 3.38 */ - QMap< QString, QgsProcessingModelChildAlgorithmResult > modelChildResults() const { return mModelChildResults; } + QgsProcessingModelResult modelResult() const { return mModelResult; } /** - * Returns a reference to the map of child algorithm results, populated when the context is used + * Returns a reference to the model results, populated when the context is used * to run a model algorithm. * - * Map keys refer to the child algorithm IDs. - * * \note Not available in Python bindings - * \see modelChildInputs() + * \since QGIS 3.38 */ - QMap< QString, QgsProcessingModelChildAlgorithmResult > &modelChildResults() SIP_SKIP { return mModelChildResults; } + QgsProcessingModelResult &modelResult() SIP_SKIP { return mModelResult; } private: @@ -792,7 +786,7 @@ class CORE_EXPORT QgsProcessingContext QString mTemporaryFolderOverride; int mMaximumThreads = QThread::idealThreadCount(); - QMap< QString, QgsProcessingModelChildAlgorithmResult > mModelChildResults; + QgsProcessingModelResult mModelResult; #ifdef SIP_RUN QgsProcessingContext( const QgsProcessingContext &other ); diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index d6271795ccddb..04ad9d39657c9 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -493,7 +493,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) mScene = scene; mScene->setParent( this ); - mScene->setLastRunChildAlgorithmResults( mChildResults ); + mScene->setLastRunResult( mLastResult ); mScene->setModel( mModel.get() ); mScene->setMessageBar( mMessageBar ); @@ -596,11 +596,11 @@ bool QgsModelDesignerDialog::checkForUnsavedChanges() } } -void QgsModelDesignerDialog::setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ) +void QgsModelDesignerDialog::setLastRunResult( const QgsProcessingModelResult &result ) { - mChildResults = results; + mLastResult = result; if ( mScene ) - mScene->setLastRunChildAlgorithmResults( mChildResults ); + mScene->setLastRunResult( mLastResult ); } void QgsModelDesignerDialog::setModelName( const QString &name ) @@ -1031,7 +1031,7 @@ void QgsModelDesignerDialog::run() { QgsProcessingContext *context = dialog->processingContext(); - setLastRunChildAlgorithmResults( context->modelChildResults() ); + setLastRunResult( context->modelResult() ); mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) ); @@ -1047,7 +1047,7 @@ void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) { const QString childDescription = mModel->childAlgorithm( childId ).description(); - const QgsProcessingModelChildAlgorithmResult result = mChildResults.value( childId ); + const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId ); const QVariantMap childAlgorithmOutputs = result.outputs(); if ( childAlgorithmOutputs.isEmpty() ) { @@ -1125,7 +1125,7 @@ void QgsModelDesignerDialog::showLog( const QString &childId ) { const QString childDescription = mModel->childAlgorithm( childId ).description(); - const QgsProcessingModelChildAlgorithmResult result = mChildResults.value( childId ); + const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId ); if ( result.htmlLog().isEmpty() ) { mMessageBar->pushWarning( QString(), tr( "No log is available for %1" ).arg( childDescription ) ); diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 462e3aeb09205..c2c7e65b4e57f 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -152,9 +152,9 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode bool checkForUnsavedChanges(); /** - * Sets the \a results of child algorithms for the last run of the model through the designer window. + * Sets the \a result of the last run of the model through the designer window. */ - void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); + void setLastRunResult( const QgsProcessingModelResult &result ); /** * Sets the model \a name. @@ -230,7 +230,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode int mBlockRepaints = 0; - QMap< QString, QgsProcessingModelChildAlgorithmResult > mChildResults; + QgsProcessingModelResult mLastResult; bool isDirty() const; diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 8cede3fe36599..213812ddcc5ee 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -140,7 +140,7 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs item->setPos( it.value().position().x(), it.value().position().y() ); const QString childId = it.value().childId(); - item->setResults( mChildResults.value( childId ) ); + item->setResults( mLastResult.childResults().value( childId ) ); mChildAlgorithmItems.insert( childId, item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); @@ -173,7 +173,7 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs QList< QgsProcessingModelChildParameterSource > sources; if ( it.value().parameterSources().contains( parameter->name() ) ) sources = it.value().parameterSources()[parameter->name()]; - for ( const QgsProcessingModelChildParameterSource &source : sources ) + for ( const QgsProcessingModelChildParameterSource &source : std::as_const( sources ) ) { const QList< LinkSource > sourceItems = linkSourcesForParameterValue( model, QVariant::fromValue( source ), it.value().childId(), context ); for ( const LinkSource &link : sourceItems ) @@ -363,11 +363,12 @@ void QgsModelGraphicsScene::setSelectedItem( QgsModelComponentGraphicItem *item emit selectedItemChanged( item ); } -void QgsModelGraphicsScene::setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ) +void QgsModelGraphicsScene::setLastRunResult( const QgsProcessingModelResult &result ) { - mChildResults = results; + mLastResult = result; - for ( auto it = mChildResults.constBegin(); it != mChildResults.constEnd(); ++it ) + const auto childResults = mLastResult.childResults(); + for ( auto it = childResults.constBegin(); it != childResults.constEnd(); ++it ) { if ( QgsModelChildAlgorithmGraphicItem *item = mChildAlgorithmItems.value( it.key() ) ) { diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index e3fa979cf727a..1b86e0818ba91 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -138,9 +138,9 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene void setSelectedItem( QgsModelComponentGraphicItem *item ); /** - * Sets the \a results of child algorithms for the last run of the model through the designer window. + * Sets the \a result of the last run of the model through the designer window. */ - void setLastRunChildAlgorithmResults( const QMap< QString, QgsProcessingModelChildAlgorithmResult > &results ); + void setLastRunResult( const QgsProcessingModelResult &result ); /** * Returns the message bar associated with the scene. @@ -249,7 +249,7 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene QMap< QString, QgsModelChildAlgorithmGraphicItem * > mChildAlgorithmItems; QMap< QString, QMap< QString, QgsModelComponentGraphicItem * > > mOutputItems; QMap< QString, QgsModelComponentGraphicItem * > mGroupBoxItems; - QMap< QString, QgsProcessingModelChildAlgorithmResult > mChildResults; + QgsProcessingModelResult mLastResult; QgsMessageBar *mMessageBar = nullptr; diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index 8dddccceffc94..5222c83f3612a 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -1317,8 +1317,8 @@ void TestQgsProcessing::context() res2.setInputs( {{ QStringLiteral( "INPUT2" ), 2 }} ); res2.setOutputs( {{ QStringLiteral( "RESULT2" ), 2 }} ); - context.modelChildResults().insert( QStringLiteral( "CHILD1" ), res1 ); - context.modelChildResults().insert( QStringLiteral( "CHILD2" ), res2 ); + context.modelResult().childResults().insert( QStringLiteral( "CHILD1" ), res1 ); + context.modelResult().childResults().insert( QStringLiteral( "CHILD2" ), res2 ); QgsProcessingContext context2; context2.copyThreadSafeSettings( context ); @@ -1326,11 +1326,11 @@ void TestQgsProcessing::context() QCOMPARE( context2.invalidGeometryCheck(), context.invalidGeometryCheck() ); QCOMPARE( context2.flags(), context.flags() ); QCOMPARE( context2.project(), context.project() ); - QCOMPARE( context2.modelChildResults().count(), 2 ); - QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD1" ) ).inputs().value( QStringLiteral( "INPUT1" ) ).toInt(), 1 ); - QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD2" ) ).inputs().value( QStringLiteral( "INPUT2" ) ).toInt(), 2 ); - QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD1" ) ).outputs().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); - QCOMPARE( context2.modelChildResults().value( QStringLiteral( "CHILD2" ) ).outputs().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); + QCOMPARE( context2.modelResult().childResults().count(), 2 ); + QCOMPARE( context2.modelResult().childResults().value( QStringLiteral( "CHILD1" ) ).inputs().value( QStringLiteral( "INPUT1" ) ).toInt(), 1 ); + QCOMPARE( context2.modelResult().childResults().value( QStringLiteral( "CHILD2" ) ).inputs().value( QStringLiteral( "INPUT2" ) ).toInt(), 2 ); + QCOMPARE( context2.modelResult().childResults().value( QStringLiteral( "CHILD1" ) ).outputs().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); + QCOMPARE( context2.modelResult().childResults().value( QStringLiteral( "CHILD2" ) ).outputs().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); QCOMPARE( static_cast< int >( context2.logLevel() ), static_cast< int >( Qgis::ProcessingLogLevel::Verbose ) ); // layers from temporaryLayerStore must not be copied by copyThreadSafeSettings QVERIFY( context2.temporaryLayerStore()->mapLayers().isEmpty() ); @@ -1410,11 +1410,11 @@ void TestQgsProcessing::context() QgsProcessingContext context3; context3.takeResultsFrom( context ); - QCOMPARE( context3.modelChildResults().count(), 2 ); - QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD1" ) ).inputs().value( QStringLiteral( "INPUT1" ) ).toInt(), 1 ); - QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD2" ) ).inputs().value( QStringLiteral( "INPUT2" ) ).toInt(), 2 ); - QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD1" ) ).outputs().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); - QCOMPARE( context3.modelChildResults().value( QStringLiteral( "CHILD2" ) ).outputs().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); + QCOMPARE( context3.modelResult().childResults().count(), 2 ); + QCOMPARE( context3.modelResult().childResults().value( QStringLiteral( "CHILD1" ) ).inputs().value( QStringLiteral( "INPUT1" ) ).toInt(), 1 ); + QCOMPARE( context3.modelResult().childResults().value( QStringLiteral( "CHILD2" ) ).inputs().value( QStringLiteral( "INPUT2" ) ).toInt(), 2 ); + QCOMPARE( context3.modelResult().childResults().value( QStringLiteral( "CHILD1" ) ).outputs().value( QStringLiteral( "RESULT1" ) ).toInt(), 1 ); + QCOMPARE( context3.modelResult().childResults().value( QStringLiteral( "CHILD2" ) ).outputs().value( QStringLiteral( "RESULT2" ) ).toInt(), 2 ); // make sure postprocessor is correctly deleted ppDeleted = false; diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index df094d16e7686..7df66218f3a02 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -2358,16 +2358,16 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QVERIFY( !DummyRaiseExceptionAlgorithm::postProcessAlgorithmCalled ); // results and inputs from buffer child should be available through the context - QCOMPARE( context.modelChildResults().value( "buffer" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); - QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); - QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); - QCOMPARE( context.modelChildResults().value( "buffer" ).htmlLog().left( 50 ), QStringLiteral( "Prepare algorithm: buffer" ) ); - QCOMPARE( context.modelChildResults().value( "buffer" ).htmlLog().right( 21 ), QStringLiteral( "s (1 output(s)).
" ) ); - QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); - - QCOMPARE( context.modelChildResults().value( "raise" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); - QCOMPARE( context.modelChildResults().value( "raise" ).htmlLog().left( 49 ), QStringLiteral( "Prepare algorithm: raise" ) ); - QVERIFY( context.modelChildResults().value( "raise" ).htmlLog().contains( QStringLiteral( "Error encountered while running my second step: something bad happened" ) ) ); + QCOMPARE( context.modelResult().childResults().value( "buffer" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); + QCOMPARE( context.modelResult().childResults().value( "buffer" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); + QCOMPARE( context.modelResult().childResults().value( "buffer" ).inputs().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); + QCOMPARE( context.modelResult().childResults().value( "buffer" ).htmlLog().left( 50 ), QStringLiteral( "Prepare algorithm: buffer" ) ); + QCOMPARE( context.modelResult().childResults().value( "buffer" ).htmlLog().right( 21 ), QStringLiteral( "s (1 output(s)).
" ) ); + QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelResult().childResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); + + QCOMPARE( context.modelResult().childResults().value( "raise" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); + QCOMPARE( context.modelResult().childResults().value( "raise" ).htmlLog().left( 49 ), QStringLiteral( "Prepare algorithm: raise" ) ); + QVERIFY( context.modelResult().childResults().value( "raise" ).htmlLog().contains( QStringLiteral( "Error encountered while running my second step: something bad happened" ) ) ); } void TestQgsProcessingModelAlgorithm::modelDependencies() From ae766b0d69402199523b8f1a26529ad3b52e9b7b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 13:17:24 +1000 Subject: [PATCH 40/46] Trim first child log --- src/core/processing/models/qgsprocessingmodelalgorithm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index fd8e0d54bc45c..9df13d6240d92 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -326,7 +326,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QVariantMap finalResults; QSet< QString > executed; bool executedAlg = true; - int previousHtmlLogLength = 0; + int previousHtmlLogLength = feedback->htmlLog().length(); while ( executedAlg && executed.count() < toExecute.count() ) { executedAlg = false; From 0a877ea4af97fa831bf5828a22d9f8e1d11cd454 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 20 Apr 2024 16:48:45 +1000 Subject: [PATCH 41/46] Documentation improvements --- .../processing/models/qgsprocessingmodelresult.sip.in | 4 +--- .../processing/models/qgsprocessingmodelresult.sip.in | 4 +--- src/core/processing/models/qgsprocessingmodelresult.h | 6 +----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index 7feda672d51a5..bff0a33a107a5 100644 --- a/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/PyQt6/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -61,7 +61,7 @@ Returns the outputs generated by the child algorithm. void setOutputs( const QVariantMap &outputs ); %Docstring -Sets the ``outputs`` used for the child algorithm. +Sets the ``outputs`` generated by child algorithm. .. seealso:: :py:func:`outputs` %End @@ -106,8 +106,6 @@ Encapsulates the results of running a Processing model Returns the map of child algorithm results. Map keys refer to the child algorithm IDs. - -.. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in index 7feda672d51a5..bff0a33a107a5 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelresult.sip.in @@ -61,7 +61,7 @@ Returns the outputs generated by the child algorithm. void setOutputs( const QVariantMap &outputs ); %Docstring -Sets the ``outputs`` used for the child algorithm. +Sets the ``outputs`` generated by child algorithm. .. seealso:: :py:func:`outputs` %End @@ -106,8 +106,6 @@ Encapsulates the results of running a Processing model Returns the map of child algorithm results. Map keys refer to the child algorithm IDs. - -.. versionadded:: 3.38 %End diff --git a/src/core/processing/models/qgsprocessingmodelresult.h b/src/core/processing/models/qgsprocessingmodelresult.h index e3385591c8ddb..df59884fe9bc4 100644 --- a/src/core/processing/models/qgsprocessingmodelresult.h +++ b/src/core/processing/models/qgsprocessingmodelresult.h @@ -69,7 +69,7 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult QVariantMap outputs() const { return mOutputs; } /** - * Sets the \a outputs used for the child algorithm. + * Sets the \a outputs generated by child algorithm. * * \see outputs() */ @@ -127,8 +127,6 @@ class CORE_EXPORT QgsProcessingModelResult * Returns the map of child algorithm results. * * Map keys refer to the child algorithm IDs. - * - * \since QGIS 3.38 */ QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const { return mChildResults; } @@ -138,8 +136,6 @@ class CORE_EXPORT QgsProcessingModelResult * Map keys refer to the child algorithm IDs. * * \note Not available in Python bindings - - * \since QGIS 3.38 */ QMap< QString, QgsProcessingModelChildAlgorithmResult > &childResults() SIP_SKIP { return mChildResults; } From a979931960e81bb317f1c534cb61bb031370270a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 20 Apr 2024 16:50:45 +1000 Subject: [PATCH 42/46] Function naming improvements --- .../processing/models/qgsmodelgraphicsscene.sip.in | 4 ++-- .../processing/models/qgsmodelgraphicsscene.sip.in | 4 ++-- src/gui/processing/models/qgsmodeldesignerdialog.cpp | 8 ++++---- src/gui/processing/models/qgsmodeldesignerdialog.h | 4 ++-- src/gui/processing/models/qgsmodelgraphicsscene.cpp | 4 ++-- src/gui/processing/models/qgsmodelgraphicsscene.h | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 9ef5ef203d3d0..8ec16930a4eba 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -174,14 +174,14 @@ Emitted whenever the selected item changes. If ``None``, no item is selected. %End - void showPreviousResults( const QString &childId ); + void showChildAlgorithmOutputs( const QString &childId ); %Docstring Emitted when the user opts to view previous results from the child algorithm with matching ID. .. versionadded:: 3.38 %End - void showLog( const QString &childId ); + void showChildAlgorithmLog( const QString &childId ); %Docstring Emitted when the user opts to view the previous log from the child algorithm with matching ID. diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index 4635cbf8914a9..27f5386024aaa 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -174,14 +174,14 @@ Emitted whenever the selected item changes. If ``None``, no item is selected. %End - void showPreviousResults( const QString &childId ); + void showChildAlgorithmOutputs( const QString &childId ); %Docstring Emitted when the user opts to view previous results from the child algorithm with matching ID. .. versionadded:: 3.38 %End - void showLog( const QString &childId ); + void showChildAlgorithmLog( const QString &childId ); %Docstring Emitted when the user opts to view the previous log from the child algorithm with matching ID. diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 04ad9d39657c9..6b20dba7c7e43 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -512,8 +512,8 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) } ); connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } ); connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } ); - connect( mScene, &QgsModelGraphicsScene::showPreviousResults, this, &QgsModelDesignerDialog::showPreviousResults ); - connect( mScene, &QgsModelGraphicsScene::showLog, this, &QgsModelDesignerDialog::showLog ); + connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmOutputs, this, &QgsModelDesignerDialog::showChildAlgorithmOutputs ); + connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmLog, this, &QgsModelDesignerDialog::showChildAlgorithmLog ); mView->centerOn( center ); @@ -1043,7 +1043,7 @@ void QgsModelDesignerDialog::run() dialog->exec(); } -void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) +void QgsModelDesignerDialog::showChildAlgorithmOutputs( const QString &childId ) { const QString childDescription = mModel->childAlgorithm( childId ).description(); @@ -1121,7 +1121,7 @@ void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) } } -void QgsModelDesignerDialog::showLog( const QString &childId ) +void QgsModelDesignerDialog::showChildAlgorithmLog( const QString &childId ) { const QString childDescription = mModel->childAlgorithm( childId ).description(); diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index c2c7e65b4e57f..cf5a9eee61850 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -185,8 +185,8 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void setPanelVisibility( bool hidden ); void editHelp(); void run(); - void showPreviousResults( const QString &childId ); - void showLog( const QString &childId ); + void showChildAlgorithmOutputs( const QString &childId ); + void showChildAlgorithmLog( const QString &childId ); private: diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 213812ddcc5ee..0b52ea83bcb24 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -147,11 +147,11 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs connect( item, &QgsModelComponentGraphicItem::aboutToChange, this, &QgsModelGraphicsScene::componentAboutToChange ); connect( item, &QgsModelChildAlgorithmGraphicItem::showPreviousResults, this, [this, childId] { - emit showPreviousResults( childId ); + emit showChildAlgorithmOutputs( childId ); } ); connect( item, &QgsModelChildAlgorithmGraphicItem::showLog, this, [this, childId] { - emit showLog( childId ); + emit showChildAlgorithmLog( childId ); } ); addCommentItemForComponent( model, it.value(), item ); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index 1b86e0818ba91..b02340fb4f78c 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -192,14 +192,14 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene * * \since QGIS 3.38 */ - void showPreviousResults( const QString &childId ); + void showChildAlgorithmOutputs( const QString &childId ); /** * Emitted when the user opts to view the previous log from the child algorithm with matching ID. * * \since QGIS 3.38 */ - void showLog( const QString &childId ); + void showChildAlgorithmLog( const QString &childId ); protected: From 2121644904493dfa9e6709a1425c0517da9b7d4f Mon Sep 17 00:00:00 2001 From: Andrea Giudiceandrea Date: Sat, 20 Apr 2024 17:53:31 +0200 Subject: [PATCH 43/46] [DB Manager] Avoid printing HTML code in the Python console --- python/plugins/db_manager/info_viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/plugins/db_manager/info_viewer.py b/python/plugins/db_manager/info_viewer.py index d691c8815a017..67d0c67309744 100644 --- a/python/plugins/db_manager/info_viewer.py +++ b/python/plugins/db_manager/info_viewer.py @@ -160,5 +160,5 @@ def setHtml(self, html): """ % html - print(">>>>>\n", html, "\n<<<<<<") + # print(">>>>>\n", html, "\n<<<<<<") return QTextBrowser.setHtml(self, html) From 6785fe45541084450e5de9cba274056f3b6e352f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 19 Apr 2024 08:24:44 +1000 Subject: [PATCH 44/46] [cmake] Use execute_process instead of deprecated EXEC_COMMAND See CMP0153 --- CMakeLists.txt | 2 +- cmake/FindGDAL.cmake | 15 ++++++--------- cmake/FindGEOS.cmake | 15 ++++++--------- cmake/FindPDAL.cmake | 5 ++--- cmake/FindPostgres.cmake | 15 ++++++--------- cmake_templates/cmake_uninstall.cmake.in | 8 ++++---- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e6f1278f7bf45..c0c02a97f25dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -544,7 +544,7 @@ if(WITH_CORE) # get the Qt plugins directory get_target_property(QMAKE_EXECUTABLE ${QT_VERSION_BASE}::qmake LOCATION) - EXEC_PROGRAM(${QMAKE_EXECUTABLE} ARGS "-query QT_INSTALL_PLUGINS" RETURN_VALUE return_code OUTPUT_VARIABLE DEFAULT_QT_PLUGINS_DIR ) + execute_process(COMMAND ${QMAKE_EXECUTABLE} -query QT_INSTALL_PLUGINS RESULT_VARIABLE return_code OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE DEFAULT_QT_PLUGINS_DIR ) set (QT_PLUGINS_DIR ${DEFAULT_QT_PLUGINS_DIR} CACHE STRING "Path to installation directory for Qt Plugins. Defaults to Qt native plugin directory") if (BUILD_WITH_QT6) diff --git a/cmake/FindGDAL.cmake b/cmake/FindGDAL.cmake index 5878e218440b0..2b2bff8c7e286 100644 --- a/cmake/FindGDAL.cmake +++ b/cmake/FindGDAL.cmake @@ -98,9 +98,8 @@ if(NOT GDAL_FOUND) IF (GDAL_CONFIG) ## extract gdal version - EXEC_PROGRAM(${GDAL_CONFIG} - ARGS --version - OUTPUT_VARIABLE GDAL_VERSION ) + execute_process(COMMAND ${GDAL_CONFIG} --version + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GDAL_VERSION ) STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\1" GDAL_VERSION_MAJOR "${GDAL_VERSION}") STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\2" GDAL_VERSION_MINOR "${GDAL_VERSION}") STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\3" GDAL_VERSION_MICRO "${GDAL_VERSION}") @@ -123,9 +122,8 @@ if(NOT GDAL_FOUND) ENDIF( (GDAL_VERSION_MAJOR EQUAL 3) AND (GDAL_VERSION_MINOR EQUAL 0) AND (GDAL_VERSION_MICRO LESS 3) ) # set INCLUDE_DIR to prefix+include - EXEC_PROGRAM(${GDAL_CONFIG} - ARGS --prefix - OUTPUT_VARIABLE GDAL_PREFIX) + execute_process(COMMAND ${GDAL_CONFIG} --prefix + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GDAL_PREFIX) #SET(GDAL_INCLUDE_DIR ${GDAL_PREFIX}/include CACHE STRING INTERNAL) FIND_PATH(GDAL_INCLUDE_DIR gdal.h @@ -136,9 +134,8 @@ if(NOT GDAL_FOUND) ) ## extract link dirs for rpath - EXEC_PROGRAM(${GDAL_CONFIG} - ARGS --libs - OUTPUT_VARIABLE GDAL_CONFIG_LIBS ) + execute_process(COMMAND ${GDAL_CONFIG} --libs + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GDAL_CONFIG_LIBS ) ## split off the link dirs (for rpath) ## use regular expression to match wildcard equivalent "-L*" diff --git a/cmake/FindGEOS.cmake b/cmake/FindGEOS.cmake index f5e2551949c42..61d3dada79971 100644 --- a/cmake/FindGEOS.cmake +++ b/cmake/FindGEOS.cmake @@ -86,9 +86,8 @@ if(NOT GEOS_FOUND) IF (GEOS_CONFIG) - EXEC_PROGRAM(${GEOS_CONFIG} - ARGS --version - OUTPUT_VARIABLE GEOS_VERSION) + execute_process(COMMAND ${GEOS_CONFIG} --version + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GEOS_VERSION) STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\1" GEOS_VERSION_MAJOR "${GEOS_VERSION}") STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\2" GEOS_VERSION_MINOR "${GEOS_VERSION}") @@ -97,9 +96,8 @@ if(NOT GEOS_FOUND) ENDIF (GEOS_VERSION_MAJOR LESS 3 OR (GEOS_VERSION_MAJOR EQUAL 3 AND GEOS_VERSION_MINOR LESS 9) ) # set INCLUDE_DIR to prefix+include - EXEC_PROGRAM(${GEOS_CONFIG} - ARGS --prefix - OUTPUT_VARIABLE GEOS_PREFIX) + execute_process(COMMAND ${GEOS_CONFIG} --prefix + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GEOS_PREFIX) FIND_PATH(GEOS_INCLUDE_DIR geos_c.h @@ -109,9 +107,8 @@ if(NOT GEOS_FOUND) ) ## extract link dirs for rpath - EXEC_PROGRAM(${GEOS_CONFIG} - ARGS --libs - OUTPUT_VARIABLE GEOS_CONFIG_LIBS ) + execute_process(COMMAND ${GEOS_CONFIG} --libs + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE GEOS_CONFIG_LIBS ) ## split off the link dirs (for rpath) ## use regular expression to match wildcard equivalent "-L*" diff --git a/cmake/FindPDAL.cmake b/cmake/FindPDAL.cmake index 32732d6e5bc54..098dcdd1dd59a 100644 --- a/cmake/FindPDAL.cmake +++ b/cmake/FindPDAL.cmake @@ -76,9 +76,8 @@ ENDIF (PDAL_INCLUDE_DIR AND PDAL_CPP_LIBRARY AND PDAL_UTIL_LIBRARY AND PDAL_BIN) IF (PDAL_FOUND) # extract PDAL version - EXEC_PROGRAM(${PDAL_BIN} - ARGS --version - OUTPUT_VARIABLE PDAL_VERSION_OUT ) + execute_process(COMMAND ${PDAL_BIN} --version + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE PDAL_VERSION_OUT ) STRING(REGEX REPLACE "^.*([0-9]+)\\.([0-9]+)\\.([0-9]+).*$" "\\1" PDAL_VERSION_MAJOR "${PDAL_VERSION_OUT}") STRING(REGEX REPLACE "^.*([0-9]+)\\.([0-9]+)\\.([0-9]+).*$" "\\2" PDAL_VERSION_MINOR "${PDAL_VERSION_OUT}") STRING(REGEX REPLACE "^.*([0-9]+)\\.([0-9]+)\\.([0-9]+).*$" "\\3" PDAL_VERSION_PATCH "${PDAL_VERSION_OUT}") diff --git a/cmake/FindPostgres.cmake b/cmake/FindPostgres.cmake index a2a85ac996719..29ddf2602ceff 100644 --- a/cmake/FindPostgres.cmake +++ b/cmake/FindPostgres.cmake @@ -68,21 +68,18 @@ ELSE(WIN32) IF (POSTGRES_CONFIG) # set INCLUDE_DIR - EXEC_PROGRAM(${POSTGRES_CONFIG} - ARGS --includedir - OUTPUT_VARIABLE PG_TMP) + execute_process(COMMAND ${POSTGRES_CONFIG} --includedir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE PG_TMP) SET(POSTGRES_INCLUDE_DIR ${PG_TMP} CACHE STRING INTERNAL) # set LIBRARY_DIR - EXEC_PROGRAM(${POSTGRES_CONFIG} - ARGS --libdir - OUTPUT_VARIABLE PG_TMP) + execute_process(COMMAND ${POSTGRES_CONFIG} --libdir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE PG_TMP) IF (APPLE) SET(POSTGRES_LIBRARY ${PG_TMP}/libpq.dylib CACHE STRING INTERNAL) ELSEIF (CYGWIN) - EXEC_PROGRAM(${POSTGRES_CONFIG} - ARGS --libs - OUTPUT_VARIABLE PG_TMP) + execute_process(COMMAND ${POSTGRES_CONFIG} --libs + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE PG_TMP) STRING(REGEX MATCHALL "[-][L]([^ ;])+" _LDIRS "${PG_TMP}") STRING(REGEX MATCHALL "[-][l]([^ ;])+" _LLIBS "${PG_TMP}") diff --git a/cmake_templates/cmake_uninstall.cmake.in b/cmake_templates/cmake_uninstall.cmake.in index 03137d5aff74e..08e81cb3a7504 100644 --- a/cmake_templates/cmake_uninstall.cmake.in +++ b/cmake_templates/cmake_uninstall.cmake.in @@ -7,10 +7,10 @@ STRING(REGEX REPLACE "\n" ";" files "${files}") FOREACH(file ${files}) MESSAGE(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") IF(EXISTS "$ENV{DESTDIR}${file}") - EXEC_PROGRAM( - "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" - OUTPUT_VARIABLE rm_out - RETURN_VALUE rm_retval + execute_process( + COMMAND "@CMAKE_COMMAND@" -E remove "$ENV{DESTDIR}${file}" + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE rm_out + RESULT_VARIABLE rm_retval ) IF("${rm_retval}" STREQUAL 0) ELSE("${rm_retval}" STREQUAL 0) From 7b9756ec8bb0a6fb63b7610407f6d1896accdbbf Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Fri, 19 Apr 2024 14:22:43 +0200 Subject: [PATCH 45/46] add osgeo4w workflow --- .github/workflows/{ => off}/backport.yml | 0 .../{ => off}/build_artifact_comment.yml | 0 .github/workflows/{ => off}/code_layout.yml | 0 .github/workflows/{ => off}/flake8.yml | 0 .github/workflows/{ => off}/macos-build.yml | 0 .../workflows/{ => off}/mingw-w64-msys2.yml | 0 .github/workflows/{ => off}/mingw64.yml | 0 .github/workflows/{ => off}/ogc.yml | 0 .github/workflows/{ => off}/pr-auto-label.yml | 0 .../workflows/{ => off}/pr-auto-milestone.yml | 0 .../{ => off}/pr-needs-documentation.yml | 0 .../workflows/{ => off}/pr_unstale_commit.yml | 0 .github/workflows/{ => off}/release.yml | 0 .github/workflows/{ => off}/run-tests.yml | 0 .github/workflows/{ => off}/stale.yml | 0 .github/workflows/{ => off}/unstale.yml | 0 .../{ => off}/write_failure_comment.yml | 0 .github/workflows/osgeo4w.yml | 122 ++++++++++++++++++ 18 files changed, 122 insertions(+) rename .github/workflows/{ => off}/backport.yml (100%) rename .github/workflows/{ => off}/build_artifact_comment.yml (100%) rename .github/workflows/{ => off}/code_layout.yml (100%) rename .github/workflows/{ => off}/flake8.yml (100%) rename .github/workflows/{ => off}/macos-build.yml (100%) rename .github/workflows/{ => off}/mingw-w64-msys2.yml (100%) rename .github/workflows/{ => off}/mingw64.yml (100%) rename .github/workflows/{ => off}/ogc.yml (100%) rename .github/workflows/{ => off}/pr-auto-label.yml (100%) rename .github/workflows/{ => off}/pr-auto-milestone.yml (100%) rename .github/workflows/{ => off}/pr-needs-documentation.yml (100%) rename .github/workflows/{ => off}/pr_unstale_commit.yml (100%) rename .github/workflows/{ => off}/release.yml (100%) rename .github/workflows/{ => off}/run-tests.yml (100%) rename .github/workflows/{ => off}/stale.yml (100%) rename .github/workflows/{ => off}/unstale.yml (100%) rename .github/workflows/{ => off}/write_failure_comment.yml (100%) create mode 100644 .github/workflows/osgeo4w.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/off/backport.yml similarity index 100% rename from .github/workflows/backport.yml rename to .github/workflows/off/backport.yml diff --git a/.github/workflows/build_artifact_comment.yml b/.github/workflows/off/build_artifact_comment.yml similarity index 100% rename from .github/workflows/build_artifact_comment.yml rename to .github/workflows/off/build_artifact_comment.yml diff --git a/.github/workflows/code_layout.yml b/.github/workflows/off/code_layout.yml similarity index 100% rename from .github/workflows/code_layout.yml rename to .github/workflows/off/code_layout.yml diff --git a/.github/workflows/flake8.yml b/.github/workflows/off/flake8.yml similarity index 100% rename from .github/workflows/flake8.yml rename to .github/workflows/off/flake8.yml diff --git a/.github/workflows/macos-build.yml b/.github/workflows/off/macos-build.yml similarity index 100% rename from .github/workflows/macos-build.yml rename to .github/workflows/off/macos-build.yml diff --git a/.github/workflows/mingw-w64-msys2.yml b/.github/workflows/off/mingw-w64-msys2.yml similarity index 100% rename from .github/workflows/mingw-w64-msys2.yml rename to .github/workflows/off/mingw-w64-msys2.yml diff --git a/.github/workflows/mingw64.yml b/.github/workflows/off/mingw64.yml similarity index 100% rename from .github/workflows/mingw64.yml rename to .github/workflows/off/mingw64.yml diff --git a/.github/workflows/ogc.yml b/.github/workflows/off/ogc.yml similarity index 100% rename from .github/workflows/ogc.yml rename to .github/workflows/off/ogc.yml diff --git a/.github/workflows/pr-auto-label.yml b/.github/workflows/off/pr-auto-label.yml similarity index 100% rename from .github/workflows/pr-auto-label.yml rename to .github/workflows/off/pr-auto-label.yml diff --git a/.github/workflows/pr-auto-milestone.yml b/.github/workflows/off/pr-auto-milestone.yml similarity index 100% rename from .github/workflows/pr-auto-milestone.yml rename to .github/workflows/off/pr-auto-milestone.yml diff --git a/.github/workflows/pr-needs-documentation.yml b/.github/workflows/off/pr-needs-documentation.yml similarity index 100% rename from .github/workflows/pr-needs-documentation.yml rename to .github/workflows/off/pr-needs-documentation.yml diff --git a/.github/workflows/pr_unstale_commit.yml b/.github/workflows/off/pr_unstale_commit.yml similarity index 100% rename from .github/workflows/pr_unstale_commit.yml rename to .github/workflows/off/pr_unstale_commit.yml diff --git a/.github/workflows/release.yml b/.github/workflows/off/release.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/off/release.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/off/run-tests.yml similarity index 100% rename from .github/workflows/run-tests.yml rename to .github/workflows/off/run-tests.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/off/stale.yml similarity index 100% rename from .github/workflows/stale.yml rename to .github/workflows/off/stale.yml diff --git a/.github/workflows/unstale.yml b/.github/workflows/off/unstale.yml similarity index 100% rename from .github/workflows/unstale.yml rename to .github/workflows/off/unstale.yml diff --git a/.github/workflows/write_failure_comment.yml b/.github/workflows/off/write_failure_comment.yml similarity index 100% rename from .github/workflows/write_failure_comment.yml rename to .github/workflows/off/write_failure_comment.yml diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml new file mode 100644 index 0000000000000..184b6b7c4b74a --- /dev/null +++ b/.github/workflows/osgeo4w.yml @@ -0,0 +1,122 @@ +name: OSGeo4W Windows Build + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - master + - release-** + - queued_ltr_backports + paths: + - 'src/**' + - 'external/**' + - 'python/**' + - 'tests/**' + - 'ms-windows/**' + - 'CMakeLists.txt' + - '.github/workflows/osgeo4w.yml' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + osgeo4w-build: + name: OSGeo4W Windows Build + runs-on: windows-latest + env: + CCACHE_DIR: build/ccache + PATH: C:\WINDOWS\system32;C:\Windows + + steps: + - name: dump environment + run: | + echo ::group::environment + set + echo ::endgroup:: + + - name: keep original line endings + run: + git config --global core.autocrlf false + env: + PATH: C:\Program Files\Git\bin;C:\WINDOWS\system32;C:\Windows + + - uses: actions/checkout@v4 + with: + repository: 'jef-n/o4w-playground' + env: + PATH: C:\Program Files\Git\bin;C:\WINDOWS\system32;C:\Windows + + - name: 'setup environment' + shell: cmd + run: | + call bootstrap.cmd none + cygwin\bin\cygpath -aw /bin >>%GITHUB_PATH% + + - name: 'determine package name' + shell: bash {0} + env: + SHELLOPTS: igncr + run: | + export PATH=/bin:/usr/bin + export HOME=$(cygpath -au "$USERPROFILE") + + set -xeo pipefail + + REPO=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY + + read RELBRANCH < <(git ls-remote --heads $REPO "refs/heads/release-*_*" | sed -e '/\^{}$/d' -ne 's#^.*refs/heads/release-#release-#p' | sort -V | tail -1) + read OLTRBRANCH LTRBRANCH < <(git ls-remote --tags $REPO | sed -e '/\^{}$/d' -ne 's#^.*refs/tags/ltr-#release-#p' | sort -V | tail -2 | paste -s) + [ "$RELBRANCH" != "$LTRBRANCH" ] || LTRBRANCH=$OLTRBRANCH + + if [ -n "$GITHUB_BASE_REF" ]; then + branch=$GITHUB_BASE_REF + BUILDNAMEPREFIX="PR${GITHUB_EVENT_NUMBER}-" + else + branch=${GITHUB_REF##*/} + fi + + case "$branch" in + $LTRBRANCH|queued_ltr_backports) + p=qgis-ltr-dev + ;; + $RELBRANCH) + p=qgis-rel-dev + ;; + master) + p="qgis-dev qgis-qt6-dev" + ;; + *) + echo "Could not determine package name" + exit 1 + ;; + esac + + echo PKGS=$p >>$GITHUB_ENV + echo REPO=$REPO >>$GITHUB_ENV + echo REF=$GITHUB_REF >>$GITHUB_ENV + echo PKGDESC="QGIS build of $REF" >>$GITHUB_ENV + echo BUILDNAMEPREFIX=$BUILDNAMEPREFIX >>$GITHUB_ENV + + - name: Restore build cache + uses: actions/cache/restore@v4 + with: + path: build + key: build-ccache-osgeo4w-${{ github.event.pull_request.base.ref || github.ref_name }} + restore-keys: | + build-ccache-osgeo4w-master + + - name: Build QGIS + shell: cmd + run: bootstrap.cmd %PKGS% + + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: build + key: build-ccache-osgeo4w-${{ github.ref_name }}-${{ github.run_id }} From 7185043964eb87e9435e6566c335d5f3ef4f10e3 Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Sun, 21 Apr 2024 18:16:40 +0200 Subject: [PATCH 46/46] site osgeo4w --- .github/workflows/osgeo4w.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 184b6b7c4b74a..e36002e1e0f4d 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -96,6 +96,7 @@ jobs: ;; esac + echo SITE=osgeo4w >>$GITHUB_ENV echo PKGS=$p >>$GITHUB_ENV echo REPO=$REPO >>$GITHUB_ENV echo REF=$GITHUB_REF >>$GITHUB_ENV