mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-08 00:05:09 -04:00
[feature] Run batch processing steps in tasks
Instead of forcing all steps in the batch processing dialog to execute in the main thread, we now run each step as a separate task whenever possible. This keeps the UI nice and responsive, and permits responsive cancelation and progress reporting. Individual steps are still run sequentially, not in parallel (yet!)
This commit is contained in:
parent
5dd473d682
commit
092279e90d
@ -35,6 +35,13 @@ and processing ``context``.
|
||||
virtual void cancel();
|
||||
|
||||
|
||||
bool algorithmCanceled();
|
||||
%Docstring
|
||||
Returns ``True`` if the algorithm was canceled.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
signals:
|
||||
|
||||
void executed( bool successful, const QVariantMap &results );
|
||||
|
@ -28,7 +28,7 @@ task manager.
|
||||
%End
|
||||
public:
|
||||
|
||||
QgsProxyProgressTask( const QString &description );
|
||||
QgsProxyProgressTask( const QString &description, bool canCancel = false );
|
||||
%Docstring
|
||||
Constructor for QgsProxyProgressTask, with the specified ``description``.
|
||||
%End
|
||||
@ -49,6 +49,18 @@ to remove this proxy task from the task manager.
|
||||
Sets the ``progress`` (from 0 to 100) for the proxied operation.
|
||||
|
||||
This method is safe to call from the main thread.
|
||||
%End
|
||||
|
||||
virtual void cancel();
|
||||
|
||||
|
||||
signals:
|
||||
|
||||
void canceled();
|
||||
%Docstring
|
||||
Emitted when the task is canceled.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
};
|
||||
|
@ -333,6 +333,13 @@ by the dialog. Ownership of ``task`` is transferred to the dialog.
|
||||
Formats an input ``string`` for display in the log tab.
|
||||
|
||||
.. versionadded:: 3.0.1
|
||||
%End
|
||||
|
||||
virtual bool isFinalized();
|
||||
%Docstring
|
||||
Returns ``True`` if the dialog is all finalized and can be safely deleted.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
signals:
|
||||
@ -354,6 +361,13 @@ Called when the algorithm has finished executing.
|
||||
virtual void runAlgorithm();
|
||||
%Docstring
|
||||
Called when the dialog's algorithm should be run. Must be overridden by subclasses.
|
||||
%End
|
||||
|
||||
virtual void algExecuted( bool successful, const QVariantMap &results );
|
||||
%Docstring
|
||||
Called when an algorithm task has completed.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
};
|
||||
|
@ -0,0 +1,97 @@
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
* src/gui/processing/qgsprocessingbatchalgorithmdialogbase.h *
|
||||
* *
|
||||
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
|
||||
************************************************************************/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class QgsProcessingBatchAlgorithmDialogBase : QgsProcessingAlgorithmDialogBase
|
||||
{
|
||||
%Docstring(signature="appended")
|
||||
Base class for processing batch algorithm dialogs.
|
||||
|
||||
.. note::
|
||||
|
||||
This is not considered stable API and may change in future QGIS versions.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
%TypeHeaderCode
|
||||
#include "qgsprocessingbatchalgorithmdialogbase.h"
|
||||
%End
|
||||
public:
|
||||
|
||||
QgsProcessingBatchAlgorithmDialogBase( QWidget *parent /TransferThis/ = 0, Qt::WindowFlags flags = Qt::WindowFlags() );
|
||||
%Docstring
|
||||
Constructor for QgsProcessingBatchAlgorithmDialogBase.
|
||||
%End
|
||||
~QgsProcessingBatchAlgorithmDialogBase();
|
||||
|
||||
virtual void resetAdditionalGui();
|
||||
|
||||
virtual void blockAdditionalControlsWhileRunning();
|
||||
|
||||
|
||||
public slots:
|
||||
|
||||
virtual void runAsSingle() = 0;
|
||||
%Docstring
|
||||
Will be called when the "Run as Single" button is clicked.
|
||||
%End
|
||||
|
||||
protected slots:
|
||||
|
||||
virtual void algExecuted( bool successful, const QVariantMap &results );
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
virtual bool isFinalized();
|
||||
|
||||
|
||||
void execute( const QList< QVariantMap > ¶meters );
|
||||
%Docstring
|
||||
Starts the batch execution, where the ``parameters`` list dictates the parameters for each component
|
||||
step of the batch.
|
||||
%End
|
||||
|
||||
virtual QgsProcessingContext *createContext( QgsProcessingFeedback *feedback ) = 0 /Factory/;
|
||||
%Docstring
|
||||
Creates a new Processing context.
|
||||
|
||||
(Each step in the batch processing will use a new Processing context)
|
||||
%End
|
||||
|
||||
virtual void handleAlgorithmResults( QgsProcessingAlgorithm *algorithm, QgsProcessingContext &context, QgsProcessingFeedback *feedback, const QVariantMap ¶meters ) = 0;
|
||||
%Docstring
|
||||
Called when the dialog should handle the results of an algorithm, e.g. by loading layers into the current project.
|
||||
%End
|
||||
|
||||
virtual void loadHtmlResults( const QVariantMap &results, int index ) = 0;
|
||||
%Docstring
|
||||
Populates the HTML results dialog as a result of a successful algorithm execution.
|
||||
%End
|
||||
|
||||
virtual void createSummaryTable( const QList< QVariantMap > &results, const QList< QVariantMap > &errors ) = 0;
|
||||
%Docstring
|
||||
Creates a summary table of the results of a batch execution.
|
||||
%End
|
||||
|
||||
};
|
||||
|
||||
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
* src/gui/processing/qgsprocessingbatchalgorithmdialogbase.h *
|
||||
* *
|
||||
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
|
||||
************************************************************************/
|
@ -366,6 +366,7 @@
|
||||
%Include auto_generated/processing/qgsprocessingaggregatewidgets.sip
|
||||
%Include auto_generated/processing/qgsprocessingalgorithmconfigurationwidget.sip
|
||||
%Include auto_generated/processing/qgsprocessingalgorithmdialogbase.sip
|
||||
%Include auto_generated/processing/qgsprocessingbatchalgorithmdialogbase.sip
|
||||
%Include auto_generated/processing/qgsprocessinggui.sip
|
||||
%Include auto_generated/processing/qgsprocessingguiregistry.sip
|
||||
%Include auto_generated/processing/qgsprocessinghistoryprovider.sip
|
||||
|
@ -366,7 +366,6 @@ class ProcessingPlugin(QObject):
|
||||
|
||||
if as_batch:
|
||||
dlg = BatchAlgorithmDialog(alg, iface.mainWindow())
|
||||
dlg.setAttribute(Qt.WA_DeleteOnClose)
|
||||
dlg.show()
|
||||
dlg.exec_()
|
||||
else:
|
||||
|
@ -21,53 +21,36 @@ __author__ = 'Victor Olaya'
|
||||
__date__ = 'August 2012'
|
||||
__copyright__ = '(C) 2012, Victor Olaya'
|
||||
|
||||
from pprint import pformat
|
||||
import codecs
|
||||
import time
|
||||
|
||||
from qgis.PyQt.QtWidgets import QPushButton, QDialogButtonBox
|
||||
from qgis.PyQt.QtCore import Qt, QCoreApplication
|
||||
|
||||
from qgis.core import (QgsProcessingOutputHtml,
|
||||
QgsProcessingOutputNumber,
|
||||
QgsProcessingOutputString,
|
||||
QgsProcessingOutputBoolean,
|
||||
QgsProject,
|
||||
QgsScopedProxyProgressTask,
|
||||
QgsProcessingBatchFeedback,
|
||||
QgsProcessingException)
|
||||
|
||||
from qgis.gui import QgsProcessingAlgorithmDialogBase
|
||||
from qgis.utils import OverrideCursor, iface
|
||||
|
||||
from processing.gui.BatchPanel import BatchPanel
|
||||
from processing.gui.AlgorithmExecutor import execute
|
||||
from processing.gui.Postprocessing import handleAlgorithmResults
|
||||
QgsProject)
|
||||
from qgis.gui import QgsProcessingBatchAlgorithmDialogBase
|
||||
from qgis.utils import iface
|
||||
|
||||
from processing.core.ProcessingResults import resultsList
|
||||
|
||||
from processing.tools.system import getTempFilename
|
||||
from processing.gui.BatchPanel import BatchPanel
|
||||
from processing.gui.Postprocessing import handleAlgorithmResults
|
||||
from processing.tools import dataobjects
|
||||
|
||||
import codecs
|
||||
from processing.tools.system import getTempFilename
|
||||
|
||||
|
||||
class BatchAlgorithmDialog(QgsProcessingAlgorithmDialogBase):
|
||||
class BatchAlgorithmDialog(QgsProcessingBatchAlgorithmDialogBase):
|
||||
|
||||
def __init__(self, alg, parent=None):
|
||||
super().__init__(parent, mode=QgsProcessingAlgorithmDialogBase.DialogMode.Batch)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setAlgorithm(alg)
|
||||
|
||||
self.setWindowTitle(self.tr('Batch Processing - {0}').format(self.algorithm().displayName()))
|
||||
self.setMainWidget(BatchPanel(self, self.algorithm()))
|
||||
self.hideShortHelp()
|
||||
|
||||
self.btnRunSingle = QPushButton(QCoreApplication.translate('BatchAlgorithmDialog', "Run as Single Process…"))
|
||||
self.btnRunSingle.clicked.connect(self.runAsSingle)
|
||||
self.buttonBox().addButton(self.btnRunSingle, QDialogButtonBox.ResetRole) # reset role to ensure left alignment
|
||||
|
||||
self.context = None
|
||||
self.updateRunButtonVisibility()
|
||||
self.hideShortHelp()
|
||||
|
||||
def runAsSingle(self):
|
||||
self.close()
|
||||
@ -77,12 +60,6 @@ class BatchAlgorithmDialog(QgsProcessingAlgorithmDialogBase):
|
||||
dlg.show()
|
||||
dlg.exec_()
|
||||
|
||||
def resetAdditionalGui(self):
|
||||
self.btnRunSingle.setEnabled(True)
|
||||
|
||||
def blockAdditionalControlsWhileRunning(self):
|
||||
self.btnRunSingle.setEnabled(False)
|
||||
|
||||
def processingContext(self):
|
||||
if self.context is None:
|
||||
self.feedback = self.createFeedback()
|
||||
@ -90,11 +67,12 @@ class BatchAlgorithmDialog(QgsProcessingAlgorithmDialogBase):
|
||||
self.context.setLogLevel(self.logLevel())
|
||||
return self.context
|
||||
|
||||
def createContext(self, feedback):
|
||||
return dataobjects.createContext(feedback)
|
||||
|
||||
def runAlgorithm(self):
|
||||
alg_parameters = []
|
||||
|
||||
feedback = self.createFeedback()
|
||||
|
||||
load_layers = self.mainWidget().checkLoadLayersOnCompletion.isChecked()
|
||||
project = QgsProject.instance() if load_layers else None
|
||||
|
||||
@ -105,90 +83,12 @@ class BatchAlgorithmDialog(QgsProcessingAlgorithmDialogBase):
|
||||
if not alg_parameters:
|
||||
return
|
||||
|
||||
task = QgsScopedProxyProgressTask(self.tr('Batch Processing - {0}').format(self.algorithm().displayName()))
|
||||
batch_feedback = QgsProcessingBatchFeedback(len(alg_parameters), feedback)
|
||||
feedback.progressChanged.connect(task.setProgress)
|
||||
self.execute(alg_parameters)
|
||||
|
||||
algorithm_results = []
|
||||
errors = []
|
||||
def handleAlgorithmResults(self, algorithm, context, feedback, parameters):
|
||||
handleAlgorithmResults(algorithm, context, feedback, False, parameters)
|
||||
|
||||
with OverrideCursor(Qt.WaitCursor):
|
||||
|
||||
self.blockControlsWhileRunning()
|
||||
self.setExecutedAnyResult(True)
|
||||
self.cancelButton().setEnabled(True)
|
||||
|
||||
# Make sure the Log tab is visible before executing the algorithm
|
||||
try:
|
||||
self.showLog()
|
||||
self.repaint()
|
||||
except Exception: # FIXME which one?
|
||||
pass
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for count, parameters in enumerate(alg_parameters):
|
||||
if feedback.isCanceled():
|
||||
break
|
||||
self.setProgressText(
|
||||
QCoreApplication.translate('BatchAlgorithmDialog', '\nProcessing algorithm {0}/{1}…').format(
|
||||
count + 1, len(alg_parameters)))
|
||||
self.setInfo(self.tr('<b>Algorithm {0} starting…</b>').format(self.algorithm().displayName()),
|
||||
escapeHtml=False)
|
||||
batch_feedback.setCurrentStep(count)
|
||||
|
||||
parameters = self.algorithm().preprocessParameters(parameters)
|
||||
|
||||
feedback.pushInfo(self.tr('Input parameters:'))
|
||||
feedback.pushCommandInfo(pformat(parameters))
|
||||
feedback.pushInfo('')
|
||||
|
||||
# important - we create a new context for each iteration
|
||||
# this avoids holding onto resources and layers from earlier iterations,
|
||||
# and allows batch processing of many more items then is possible
|
||||
# if we hold on to these layers
|
||||
context = dataobjects.createContext(feedback)
|
||||
|
||||
alg_start_time = time.time()
|
||||
results, ok = self.algorithm().run(parameters, context, batch_feedback)
|
||||
if ok:
|
||||
self.setInfo(
|
||||
QCoreApplication.translate('BatchAlgorithmDialog', 'Algorithm {0} correctly executed…').format(
|
||||
self.algorithm().displayName()), escapeHtml=False)
|
||||
feedback.pushInfo(
|
||||
self.tr('Execution completed in {0:0.2f} seconds'.format(time.time() - alg_start_time)))
|
||||
feedback.pushInfo(self.tr('Results:'))
|
||||
feedback.pushCommandInfo(pformat(results))
|
||||
feedback.pushInfo('')
|
||||
algorithm_results.append({'parameters': parameters, 'results': results})
|
||||
|
||||
handleAlgorithmResults(self.algorithm(), context, batch_feedback, False, parameters)
|
||||
else:
|
||||
task_errors = batch_feedback.popErrors()
|
||||
self.setInfo(
|
||||
QCoreApplication.translate('BatchAlgorithmDialog', 'Algorithm {0} failed…').format(
|
||||
self.algorithm().displayName()), escapeHtml=False)
|
||||
feedback.reportError(
|
||||
self.tr('Execution failed after {0:0.2f} seconds'.format(time.time() - alg_start_time)),
|
||||
fatalError=False)
|
||||
errors.append({'parameters': parameters, 'errors': task_errors})
|
||||
|
||||
feedback.pushInfo(self.tr('Batch execution completed in {0:0.2f} seconds'.format(time.time() - start_time)))
|
||||
if errors:
|
||||
feedback.reportError(self.tr('{} executions failed. See log for further details.').format(len(errors)), fatalError=True)
|
||||
task = None
|
||||
|
||||
self.finish(algorithm_results, errors)
|
||||
self.cancelButton().setEnabled(False)
|
||||
|
||||
def finish(self, algorithm_results, errors):
|
||||
for count, results in enumerate(algorithm_results):
|
||||
self.loadHTMLResults(results['results'], count)
|
||||
|
||||
self.createSummaryTable(algorithm_results, errors)
|
||||
self.resetGui()
|
||||
|
||||
def loadHTMLResults(self, results, num):
|
||||
def loadHtmlResults(self, results, num):
|
||||
for out in self.algorithm().outputDefinitions():
|
||||
if isinstance(out, QgsProcessingOutputHtml) and out.name() in results and results[out.name()]:
|
||||
resultsList.addResult(icon=self.algorithm().icon(), name='{} [{}]'.format(out.description(), num),
|
||||
|
@ -49,6 +49,13 @@ class CORE_EXPORT QgsProcessingAlgRunnerTask : public QgsTask
|
||||
|
||||
void cancel() override;
|
||||
|
||||
/**
|
||||
* Returns TRUE if the algorithm was canceled.
|
||||
*
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
bool algorithmCanceled() { return isCanceled(); }
|
||||
|
||||
signals:
|
||||
|
||||
/**
|
||||
|
@ -18,8 +18,8 @@
|
||||
#include "qgsproxyprogresstask.h"
|
||||
#include "qgsapplication.h"
|
||||
|
||||
QgsProxyProgressTask::QgsProxyProgressTask( const QString &description )
|
||||
: QgsTask( description, QgsTask::Flags() )
|
||||
QgsProxyProgressTask::QgsProxyProgressTask( const QString &description, bool canCancel )
|
||||
: QgsTask( description, canCancel ? QgsTask::CanCancel : QgsTask::Flags() )
|
||||
{
|
||||
}
|
||||
|
||||
@ -49,6 +49,13 @@ void QgsProxyProgressTask::setProxyProgress( double progress )
|
||||
QMetaObject::invokeMethod( this, "setProgress", Qt::AutoConnection, Q_ARG( double, progress ) );
|
||||
}
|
||||
|
||||
void QgsProxyProgressTask::cancel()
|
||||
{
|
||||
emit canceled();
|
||||
|
||||
QgsTask::cancel();
|
||||
}
|
||||
|
||||
//
|
||||
// QgsScopedProxyProgressTask
|
||||
//
|
||||
|
@ -43,7 +43,7 @@ class CORE_EXPORT QgsProxyProgressTask : public QgsTask
|
||||
/**
|
||||
* Constructor for QgsProxyProgressTask, with the specified \a description.
|
||||
*/
|
||||
QgsProxyProgressTask( const QString &description );
|
||||
QgsProxyProgressTask( const QString &description, bool canCancel = false );
|
||||
|
||||
/**
|
||||
* Finalizes the task, with the specified \a result.
|
||||
@ -62,6 +62,17 @@ class CORE_EXPORT QgsProxyProgressTask : public QgsTask
|
||||
*/
|
||||
void setProxyProgress( double progress );
|
||||
|
||||
void cancel() override;
|
||||
|
||||
signals:
|
||||
|
||||
/**
|
||||
* Emitted when the task is canceled.
|
||||
*
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
void canceled();
|
||||
|
||||
private:
|
||||
|
||||
QWaitCondition mNotFinishedWaitCondition;
|
||||
|
@ -323,6 +323,7 @@ set(QGIS_GUI_SRCS
|
||||
processing/qgsprocessingaggregatewidgetwrapper.cpp
|
||||
processing/qgsprocessingalgorithmconfigurationwidget.cpp
|
||||
processing/qgsprocessingalgorithmdialogbase.cpp
|
||||
processing/qgsprocessingbatchalgorithmdialogbase.cpp
|
||||
processing/qgsprocessingconfigurationwidgets.cpp
|
||||
processing/qgsprocessingdxflayerswidgetwrapper.cpp
|
||||
processing/qgsprocessingenummodelerwidget.cpp
|
||||
@ -1159,6 +1160,7 @@ set(QGIS_GUI_HDRS
|
||||
processing/qgsprocessingaggregatewidgetwrapper.h
|
||||
processing/qgsprocessingalgorithmconfigurationwidget.h
|
||||
processing/qgsprocessingalgorithmdialogbase.h
|
||||
processing/qgsprocessingbatchalgorithmdialogbase.h
|
||||
processing/qgsprocessingconfigurationwidgets.h
|
||||
processing/qgsprocessingdxflayerswidgetwrapper.h
|
||||
processing/qgsprocessingenummodelerwidget.h
|
||||
|
@ -510,7 +510,7 @@ void QgsProcessingAlgorithmDialogBase::algExecuted( bool successful, const QVari
|
||||
else
|
||||
{
|
||||
// delete dialog if closed
|
||||
if ( !isVisible() )
|
||||
if ( isFinalized() && !isVisible() )
|
||||
{
|
||||
deleteLater();
|
||||
}
|
||||
@ -658,7 +658,7 @@ void QgsProcessingAlgorithmDialogBase::closeEvent( QCloseEvent *e )
|
||||
|
||||
QDialog::closeEvent( e );
|
||||
|
||||
if ( !mAlgorithmTask )
|
||||
if ( !mAlgorithmTask && isFinalized() )
|
||||
{
|
||||
// when running a background task, the dialog is kept around and deleted only when the task
|
||||
// completes. But if not running a task, we auto cleanup (later - gotta give callers a chance
|
||||
@ -810,6 +810,11 @@ QString QgsProcessingAlgorithmDialogBase::formatStringForLog( const QString &str
|
||||
return s;
|
||||
}
|
||||
|
||||
bool QgsProcessingAlgorithmDialogBase::isFinalized()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void QgsProcessingAlgorithmDialogBase::setInfo( const QString &message, bool isError, bool escapeHtml, bool isWarning )
|
||||
{
|
||||
constexpr int MESSAGE_COUNT_LIMIT = 10000;
|
||||
@ -834,7 +839,7 @@ void QgsProcessingAlgorithmDialogBase::setInfo( const QString &message, bool isE
|
||||
|
||||
void QgsProcessingAlgorithmDialogBase::reject()
|
||||
{
|
||||
if ( !mAlgorithmTask )
|
||||
if ( !mAlgorithmTask && isFinalized() )
|
||||
{
|
||||
setAttribute( Qt::WA_DeleteOnClose );
|
||||
}
|
||||
|
@ -383,6 +383,13 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, public QgsPr
|
||||
*/
|
||||
static QString formatStringForLog( const QString &string );
|
||||
|
||||
/**
|
||||
* Returns TRUE if the dialog is all finalized and can be safely deleted.
|
||||
*
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
virtual bool isFinalized();
|
||||
|
||||
signals:
|
||||
|
||||
/**
|
||||
@ -404,6 +411,13 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, public QgsPr
|
||||
*/
|
||||
virtual void runAlgorithm();
|
||||
|
||||
/**
|
||||
* Called when an algorithm task has completed.
|
||||
*
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
virtual void algExecuted( bool successful, const QVariantMap &results );
|
||||
|
||||
private slots:
|
||||
|
||||
void openHelp();
|
||||
@ -412,7 +426,6 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, public QgsPr
|
||||
void splitterChanged( int pos, int index );
|
||||
void mTabWidget_currentChanged( int index );
|
||||
void linkClicked( const QUrl &url );
|
||||
void algExecuted( bool successful, const QVariantMap &results );
|
||||
void taskTriggered( QgsTask *task );
|
||||
void closeClicked();
|
||||
|
||||
|
225
src/gui/processing/qgsprocessingbatchalgorithmdialogbase.cpp
Normal file
225
src/gui/processing/qgsprocessingbatchalgorithmdialogbase.cpp
Normal file
@ -0,0 +1,225 @@
|
||||
/***************************************************************************
|
||||
qgsprocessingbatchalgorithmdialogbase.cpp
|
||||
------------------------------------
|
||||
Date : March 2022
|
||||
Copyright : (C) 2022 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 "qgsprocessingbatchalgorithmdialogbase.h"
|
||||
#include "qgsprocessingbatch.h"
|
||||
#include "qgsproxyprogresstask.h"
|
||||
#include "qgsprocessingalgorithm.h"
|
||||
#include "qgsjsonutils.h"
|
||||
#include "qgsprocessingalgrunnertask.h"
|
||||
#include "qgsapplication.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
///@cond NOT_STABLE
|
||||
QgsProcessingBatchAlgorithmDialogBase::QgsProcessingBatchAlgorithmDialogBase( QWidget *parent, Qt::WindowFlags flags )
|
||||
: QgsProcessingAlgorithmDialogBase( parent, flags, QgsProcessingAlgorithmDialogBase::DialogMode::Batch )
|
||||
{
|
||||
mButtonRunSingle = new QPushButton( tr( "Run as Single Process…" ) );
|
||||
connect( mButtonRunSingle, &QPushButton::clicked, this, &QgsProcessingBatchAlgorithmDialogBase::runAsSingle );
|
||||
buttonBox()->addButton( mButtonRunSingle, QDialogButtonBox::ResetRole ); // reset role to ensure left alignment
|
||||
|
||||
connect( QgsApplication::taskManager(), &QgsTaskManager::taskTriggered, this, &QgsProcessingBatchAlgorithmDialogBase::taskTriggered );
|
||||
|
||||
updateRunButtonVisibility();
|
||||
}
|
||||
|
||||
QgsProcessingBatchAlgorithmDialogBase::~QgsProcessingBatchAlgorithmDialogBase() = default;
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::resetAdditionalGui()
|
||||
{
|
||||
mButtonRunSingle->setEnabled( true );
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::blockAdditionalControlsWhileRunning()
|
||||
{
|
||||
mButtonRunSingle->setEnabled( false );
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::execute( const QList<QVariantMap> ¶meters )
|
||||
{
|
||||
mQueuedParameters = parameters;
|
||||
mCurrentStep = 0;
|
||||
mTotalSteps = mQueuedParameters.size();
|
||||
mResults.clear();
|
||||
mErrors.clear();
|
||||
|
||||
mFeedback.reset( createFeedback() );
|
||||
|
||||
mBatchFeedback = std::make_unique< QgsProcessingBatchFeedback >( mTotalSteps, mFeedback.get() );
|
||||
|
||||
mProxyTask = new QgsProxyProgressTask( tr( "Batch Processing - %1" ).arg( algorithm()->displayName() ), true );
|
||||
connect( mProxyTask, &QgsProxyProgressTask::canceled, mBatchFeedback.get(), &QgsFeedback::cancel );
|
||||
connect( mFeedback.get(), &QgsFeedback::progressChanged, mProxyTask, &QgsProxyProgressTask::setProxyProgress );
|
||||
QgsApplication::taskManager()->addTask( mProxyTask );
|
||||
|
||||
blockControlsWhileRunning();
|
||||
setExecutedAnyResult( true );
|
||||
cancelButton()->setEnabled( true );
|
||||
|
||||
// Make sure the Log tab is visible before executing the algorithm
|
||||
showLog();
|
||||
repaint();
|
||||
|
||||
mTotalTimer.restart();
|
||||
if ( mTotalSteps > 0 )
|
||||
executeNext();
|
||||
}
|
||||
|
||||
bool QgsProcessingBatchAlgorithmDialogBase::isFinalized()
|
||||
{
|
||||
return mQueuedParameters.empty();
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::executeNext()
|
||||
{
|
||||
if ( mQueuedParameters.empty() || mFeedback->isCanceled() )
|
||||
{
|
||||
allTasksComplete( false );
|
||||
return;
|
||||
}
|
||||
|
||||
mBatchFeedback->setCurrentStep( mCurrentStep++ );
|
||||
setProgressText( QStringLiteral( "\n" ) + tr( "Processing algorithm %1/%2…" ).arg( mCurrentStep ).arg( mTotalSteps ) );
|
||||
setInfo( tr( "<b>Algorithm %1 starting…</b>" ).arg( algorithm()->displayName() ), false, false );
|
||||
|
||||
pushInfo( tr( "Input parameters:" ) );
|
||||
|
||||
// important - we create a new context for each iteration
|
||||
// this avoids holding onto resources and layers from earlier iterations,
|
||||
// and allows batch processing of many more items then is possible
|
||||
// if we hold on to these layers
|
||||
mTaskContext.reset( createContext( mBatchFeedback.get() ) );
|
||||
|
||||
const QVariantMap paramsJson = algorithm()->asMap( mQueuedParameters.constFirst(), *mTaskContext ).value( QStringLiteral( "inputs" ) ).toMap();
|
||||
pushCommandInfo( QString::fromStdString( QgsJsonUtils::jsonFromVariant( paramsJson ).dump() ) );
|
||||
pushInfo( QString() );
|
||||
|
||||
mCurrentParameters = algorithm()->preprocessParameters( mQueuedParameters.constFirst() );
|
||||
mQueuedParameters.pop_front();
|
||||
|
||||
mCurrentStepTimer.restart();
|
||||
if ( !( algorithm()->flags() & QgsProcessingAlgorithm::FlagNoThreading ) )
|
||||
{
|
||||
QgsProcessingAlgRunnerTask *task = new QgsProcessingAlgRunnerTask( algorithm(), mCurrentParameters, *mTaskContext, mBatchFeedback.get() );
|
||||
if ( task->algorithmCanceled() )
|
||||
onTaskComplete( false, {} );
|
||||
else
|
||||
{
|
||||
setCurrentTask( task );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// have to execute in main thread, no tasks allowed
|
||||
bool ok = false;
|
||||
const QVariantMap results = algorithm()->run( mCurrentParameters, *mTaskContext, mBatchFeedback.get(), &ok );
|
||||
onTaskComplete( ok, results );
|
||||
}
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::algExecuted( bool successful, const QVariantMap &results )
|
||||
{
|
||||
// parent class cleanup first!
|
||||
QgsProcessingAlgorithmDialogBase::algExecuted( successful, results );
|
||||
onTaskComplete( successful, results );
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::onTaskComplete( bool ok, const QVariantMap &results )
|
||||
{
|
||||
if ( ok )
|
||||
{
|
||||
setInfo( tr( "Algorithm %1 correctly executed…" ).arg( algorithm()->displayName() ), false, false );
|
||||
pushInfo( tr( "Execution completed in %1 seconds" ).arg( mCurrentStepTimer.elapsed() / 1000.0, 2 ) );
|
||||
pushInfo( tr( "Results:" ) );
|
||||
|
||||
pushCommandInfo( QString::fromStdString( QgsJsonUtils::jsonFromVariant( results ).dump() ) );
|
||||
pushInfo( QString() );
|
||||
|
||||
mResults.append( QVariantMap(
|
||||
{
|
||||
{ QStringLiteral( "parameters" ), mCurrentParameters },
|
||||
{ QStringLiteral( "results" ), results }
|
||||
} ) );
|
||||
|
||||
handleAlgorithmResults( algorithm(), *mTaskContext, mBatchFeedback.get(), mCurrentParameters );
|
||||
executeNext();
|
||||
}
|
||||
else if ( mBatchFeedback->isCanceled() )
|
||||
{
|
||||
setInfo( tr( "Algorithm %1 canceled…" ).arg( algorithm()->displayName() ), false, false );
|
||||
pushInfo( tr( "Execution canceled after %1 seconds" ).arg( mCurrentStepTimer.elapsed() / 1000.0, 2 ) );
|
||||
allTasksComplete( true );
|
||||
}
|
||||
else
|
||||
{
|
||||
const QStringList taskErrors = mBatchFeedback->popErrors();
|
||||
setInfo( tr( "Algorithm %1 failed…" ).arg( algorithm()->displayName() ), false, false );
|
||||
reportError( tr( "Execution failed after %1 seconds" ).arg( mCurrentStepTimer.elapsed() / 1000.0, 2 ), false );
|
||||
|
||||
mErrors.append( QVariantMap(
|
||||
{
|
||||
{ QStringLiteral( "parameters" ), mCurrentParameters },
|
||||
{ QStringLiteral( "errors" ), taskErrors }
|
||||
} ) );
|
||||
executeNext();
|
||||
}
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::taskTriggered( QgsTask *task )
|
||||
{
|
||||
if ( task == mProxyTask )
|
||||
{
|
||||
show();
|
||||
raise();
|
||||
setWindowState( ( windowState() & ~Qt::WindowMinimized ) | Qt::WindowActive );
|
||||
activateWindow();
|
||||
showLog();
|
||||
}
|
||||
}
|
||||
|
||||
void QgsProcessingBatchAlgorithmDialogBase::allTasksComplete( bool canceled )
|
||||
{
|
||||
mBatchFeedback.reset();
|
||||
mFeedback.reset();
|
||||
mTaskContext.reset();
|
||||
mQueuedParameters.clear();
|
||||
if ( mProxyTask )
|
||||
{
|
||||
mProxyTask->finalize( true );
|
||||
mProxyTask = nullptr;
|
||||
}
|
||||
|
||||
if ( !canceled )
|
||||
{
|
||||
pushInfo( tr( "Batch execution completed in %1 seconds" ).arg( mTotalTimer.elapsed() / 1000.0, 2 ) );
|
||||
if ( !mErrors.empty() )
|
||||
{
|
||||
reportError( tr( "%1 executions failed. See log for further details." ).arg( mErrors.size() ), true );
|
||||
}
|
||||
|
||||
for ( int i = 0; i < mResults.size(); ++i )
|
||||
{
|
||||
loadHtmlResults( mResults.at( i ).value( QStringLiteral( "results" ) ).toMap(), i );
|
||||
}
|
||||
|
||||
createSummaryTable( mResults, mErrors );
|
||||
}
|
||||
|
||||
resetGui();
|
||||
cancelButton()->setEnabled( false );
|
||||
}
|
||||
|
||||
|
||||
///@endcond
|
123
src/gui/processing/qgsprocessingbatchalgorithmdialogbase.h
Normal file
123
src/gui/processing/qgsprocessingbatchalgorithmdialogbase.h
Normal file
@ -0,0 +1,123 @@
|
||||
/***************************************************************************
|
||||
qgsprocessingalgorithmdialogbase.h
|
||||
----------------------------------
|
||||
Date : November 2017
|
||||
Copyright : (C) 2017 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 QGSPROCESSINGBATCHALGORITHMDIALOGBASE_H
|
||||
#define QGSPROCESSINGBATCHALGORITHMDIALOGBASE_H
|
||||
|
||||
#include "qgis.h"
|
||||
#include "qgis_gui.h"
|
||||
#include "qgsprocessingalgorithmdialogbase.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
class QgsProcessingBatchFeedback;
|
||||
class QgsProxyProgressTask;
|
||||
|
||||
///@cond NOT_STABLE
|
||||
|
||||
|
||||
/**
|
||||
* \ingroup gui
|
||||
* \brief Base class for processing batch algorithm dialogs.
|
||||
* \note This is not considered stable API and may change in future QGIS versions.
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
class GUI_EXPORT QgsProcessingBatchAlgorithmDialogBase : public QgsProcessingAlgorithmDialogBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Constructor for QgsProcessingBatchAlgorithmDialogBase.
|
||||
*/
|
||||
QgsProcessingBatchAlgorithmDialogBase( QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags flags = Qt::WindowFlags() );
|
||||
~QgsProcessingBatchAlgorithmDialogBase() override;
|
||||
|
||||
void resetAdditionalGui() override;
|
||||
void blockAdditionalControlsWhileRunning() override;
|
||||
|
||||
public slots:
|
||||
|
||||
/**
|
||||
* Will be called when the "Run as Single" button is clicked.
|
||||
*/
|
||||
virtual void runAsSingle() = 0;
|
||||
|
||||
protected slots:
|
||||
|
||||
void algExecuted( bool successful, const QVariantMap &results ) override;
|
||||
|
||||
protected:
|
||||
|
||||
bool isFinalized() override;
|
||||
|
||||
/**
|
||||
* Starts the batch execution, where the \a parameters list dictates the parameters for each component
|
||||
* step of the batch.
|
||||
*/
|
||||
void execute( const QList< QVariantMap > ¶meters );
|
||||
|
||||
/**
|
||||
* Creates a new Processing context.
|
||||
*
|
||||
* (Each step in the batch processing will use a new Processing context)
|
||||
*/
|
||||
virtual QgsProcessingContext *createContext( QgsProcessingFeedback *feedback ) = 0 SIP_FACTORY;
|
||||
|
||||
/**
|
||||
* Called when the dialog should handle the results of an algorithm, e.g. by loading layers into the current project.
|
||||
*/
|
||||
virtual void handleAlgorithmResults( QgsProcessingAlgorithm *algorithm, QgsProcessingContext &context, QgsProcessingFeedback *feedback, const QVariantMap ¶meters ) = 0;
|
||||
|
||||
/**
|
||||
* Populates the HTML results dialog as a result of a successful algorithm execution.
|
||||
*/
|
||||
virtual void loadHtmlResults( const QVariantMap &results, int index ) = 0;
|
||||
|
||||
/**
|
||||
* Creates a summary table of the results of a batch execution.
|
||||
*/
|
||||
virtual void createSummaryTable( const QList< QVariantMap > &results, const QList< QVariantMap > &errors ) = 0;
|
||||
|
||||
private slots:
|
||||
|
||||
void onTaskComplete( bool ok, const QVariantMap &results );
|
||||
void taskTriggered( QgsTask *task );
|
||||
|
||||
private:
|
||||
|
||||
void executeNext();
|
||||
void allTasksComplete( bool canceled );
|
||||
|
||||
QPushButton *mButtonRunSingle = nullptr;
|
||||
|
||||
int mCurrentStep = 0;
|
||||
int mTotalSteps = 0;
|
||||
QList< QVariantMap > mQueuedParameters;
|
||||
QVariantMap mCurrentParameters;
|
||||
QPointer< QgsProxyProgressTask > mProxyTask;
|
||||
std::unique_ptr< QgsProcessingFeedback > mFeedback;
|
||||
std::unique_ptr< QgsProcessingBatchFeedback > mBatchFeedback;
|
||||
std::unique_ptr< QgsProcessingContext > mTaskContext;
|
||||
QList< QVariantMap > mResults;
|
||||
QList< QVariantMap > mErrors;
|
||||
QElapsedTimer mTotalTimer;
|
||||
QElapsedTimer mCurrentStepTimer;
|
||||
};
|
||||
|
||||
///@endcond
|
||||
|
||||
#endif // QGSPROCESSINGBATCHALGORITHMDIALOGBASE_H
|
Loading…
x
Reference in New Issue
Block a user