mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-24 00:47:57 -05:00
815 lines
34 KiB
Python
815 lines
34 KiB
Python
"""
|
|
***************************************************************************
|
|
BatchPanel.py
|
|
---------------------
|
|
Date : November 2014
|
|
Copyright : (C) 2014 by Alexander Bruy
|
|
Email : alexander dot bruy 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. *
|
|
* *
|
|
***************************************************************************
|
|
"""
|
|
|
|
__author__ = 'Alexander Bruy'
|
|
__date__ = 'November 2014'
|
|
__copyright__ = '(C) 2014, Alexander Bruy'
|
|
|
|
import os
|
|
import json
|
|
import warnings
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from qgis.PyQt import uic
|
|
from qgis.PyQt.QtWidgets import (
|
|
QTableWidgetItem,
|
|
QComboBox,
|
|
QHeaderView,
|
|
QFileDialog,
|
|
QMessageBox,
|
|
QToolButton,
|
|
QMenu,
|
|
QAction
|
|
)
|
|
|
|
# adding to this list? also update the QgsProcessingHistoryProvider executeAlgorithm imports!!
|
|
|
|
from qgis.PyQt.QtCore import (
|
|
QTime, # NOQA - must be here for saved file evaluation
|
|
QDate, # NOQA - must be here for saved file evaluation
|
|
QDateTime # NOQA - must be here for saved file evaluation
|
|
)
|
|
from qgis.PyQt.QtGui import (
|
|
QPalette,
|
|
QColor, # NOQA - must be here for saved file evaluation
|
|
)
|
|
|
|
from qgis.PyQt.QtCore import (
|
|
QDir,
|
|
QFileInfo,
|
|
QCoreApplication
|
|
)
|
|
from qgis.core import (
|
|
Qgis,
|
|
QgsApplication,
|
|
QgsSettings,
|
|
QgsProperty, # NOQA - must be here for saved file evaluation
|
|
QgsProject,
|
|
QgsFeatureRequest, # NOQA - must be here for saved file evaluation
|
|
QgsProcessingFeatureSourceDefinition, # NOQA - must be here for saved file evaluation
|
|
QgsCoordinateReferenceSystem, # NOQA - must be here for saved file evaluation
|
|
QgsProcessingParameterDefinition,
|
|
QgsProcessingModelAlgorithm,
|
|
QgsProcessingParameterFile,
|
|
QgsProcessingParameterMapLayer,
|
|
QgsProcessingParameterRasterLayer,
|
|
QgsProcessingParameterMeshLayer,
|
|
QgsProcessingParameterPointCloudLayer,
|
|
QgsProcessingParameterVectorLayer,
|
|
QgsProcessingParameterFeatureSource,
|
|
QgsProcessingParameterRasterDestination,
|
|
QgsProcessingParameterVectorDestination,
|
|
QgsProcessingParameterMultipleLayers,
|
|
QgsProcessingParameterFeatureSink,
|
|
QgsProcessingOutputLayerDefinition,
|
|
QgsExpressionContextUtils,
|
|
QgsProcessing,
|
|
QgsExpression,
|
|
QgsRasterLayer,
|
|
QgsProcessingUtils,
|
|
QgsFileFilterGenerator,
|
|
QgsProcessingContext
|
|
)
|
|
from qgis.gui import (
|
|
QgsProcessingParameterWidgetContext,
|
|
QgsProcessingContextGenerator,
|
|
QgsFindFilesByPatternDialog,
|
|
QgsExpressionBuilderDialog,
|
|
QgsPanelWidget
|
|
)
|
|
from qgis.utils import iface
|
|
|
|
from processing.gui.wrappers import WidgetWrapperFactory, WidgetWrapper
|
|
from processing.gui.BatchOutputSelectionPanel import BatchOutputSelectionPanel
|
|
|
|
from processing.tools import dataobjects
|
|
from processing.tools.dataobjects import createContext
|
|
from processing.gui.MultipleInputDialog import MultipleInputDialog
|
|
|
|
pluginPath = os.path.split(os.path.dirname(__file__))[0]
|
|
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
WIDGET, BASE = uic.loadUiType(
|
|
os.path.join(pluginPath, 'ui', 'widgetBatchPanel.ui'))
|
|
|
|
|
|
class BatchPanelFillWidget(QToolButton):
|
|
|
|
def __init__(self, parameterDefinition, column, panel, parent=None):
|
|
super().__init__(parent)
|
|
|
|
self.setBackgroundRole(QPalette.ColorRole.Button)
|
|
self.setAutoFillBackground(True)
|
|
|
|
self.parameterDefinition = parameterDefinition
|
|
self.column = column
|
|
self.panel = panel
|
|
|
|
self.setText(QCoreApplication.translate('BatchPanel', 'Autofill…'))
|
|
f = self.font()
|
|
f.setItalic(True)
|
|
self.setFont(f)
|
|
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
|
self.setAutoRaise(True)
|
|
|
|
self.menu = QMenu()
|
|
self.menu.aboutToShow.connect(self.createMenu)
|
|
self.setMenu(self.menu)
|
|
|
|
def createMenu(self):
|
|
self.menu.clear()
|
|
self.menu.setMinimumWidth(self.width())
|
|
|
|
fill_down_action = QAction(self.tr('Fill Down'), self.menu)
|
|
fill_down_action.triggered.connect(self.fillDown)
|
|
fill_down_action.setToolTip(self.tr('Copy the first value down to all other rows'))
|
|
self.menu.addAction(fill_down_action)
|
|
|
|
calculate_by_expression = QAction(QCoreApplication.translate('BatchPanel', 'Calculate by Expression…'),
|
|
self.menu)
|
|
calculate_by_expression.setIcon(QgsApplication.getThemeIcon('/mActionCalculateField.svg'))
|
|
calculate_by_expression.triggered.connect(self.calculateByExpression)
|
|
calculate_by_expression.setToolTip(self.tr('Calculates parameter values by evaluating an expression'))
|
|
self.menu.addAction(calculate_by_expression)
|
|
|
|
add_by_expression = QAction(QCoreApplication.translate('BatchPanel', 'Add Values by Expression…'),
|
|
self.menu)
|
|
add_by_expression.triggered.connect(self.addByExpression)
|
|
add_by_expression.setToolTip(self.tr('Adds new parameter values by evaluating an expression'))
|
|
self.menu.addAction(add_by_expression)
|
|
|
|
if not self.parameterDefinition.isDestination() and isinstance(self.parameterDefinition, QgsFileFilterGenerator):
|
|
self.menu.addSeparator()
|
|
find_by_pattern_action = QAction(QCoreApplication.translate('BatchPanel', 'Add Files by Pattern…'),
|
|
self.menu)
|
|
find_by_pattern_action.triggered.connect(self.addFilesByPattern)
|
|
find_by_pattern_action.setToolTip(self.tr('Adds files by a file pattern match'))
|
|
self.menu.addAction(find_by_pattern_action)
|
|
|
|
select_file_action = QAction(
|
|
QCoreApplication.translate('BatchInputSelectionPanel', 'Select Files…'), self.menu)
|
|
select_file_action.triggered.connect(self.showFileSelectionDialog)
|
|
self.menu.addAction(select_file_action)
|
|
|
|
select_directory_action = QAction(
|
|
QCoreApplication.translate('BatchInputSelectionPanel', 'Add All Files from a Directory…'), self.menu)
|
|
select_directory_action.triggered.connect(self.showDirectorySelectionDialog)
|
|
self.menu.addAction(select_directory_action)
|
|
|
|
if not isinstance(self.parameterDefinition, QgsProcessingParameterFile):
|
|
select_layer_action = QAction(
|
|
QCoreApplication.translate('BatchInputSelectionPanel', 'Select from Open Layers…'), self.menu)
|
|
select_layer_action.triggered.connect(self.showLayerSelectionDialog)
|
|
self.menu.addAction(select_layer_action)
|
|
|
|
def findStartingRow(self):
|
|
first_row = 0
|
|
for row in range(self.panel.batchRowCount()):
|
|
wrapper = self.panel.wrappers[row][self.column]
|
|
if wrapper is None:
|
|
break
|
|
else:
|
|
value = wrapper.parameterValue()
|
|
if value is None:
|
|
break
|
|
first_row += 1
|
|
return first_row
|
|
|
|
def fillDown(self):
|
|
"""
|
|
Copy the top value down
|
|
"""
|
|
context = dataobjects.createContext()
|
|
|
|
wrapper = self.panel.wrappers[0][self.column]
|
|
if wrapper is None:
|
|
# e.g. double clicking on a destination header
|
|
widget = self.panel.tblParameters.cellWidget(1, self.column)
|
|
value = widget.getValue()
|
|
else:
|
|
value = wrapper.parameterValue()
|
|
|
|
for row in range(1, self.panel.batchRowCount()):
|
|
self.setRowValue(row, value, context)
|
|
|
|
def setRowValue(self, row, value, context):
|
|
"""
|
|
Sets the value for a row, in the current column
|
|
"""
|
|
if self.panel.batchRowCount() <= row:
|
|
self.panel.addRow()
|
|
|
|
wrapper = self.panel.wrappers[row][self.column]
|
|
if wrapper is None:
|
|
# e.g. destination header
|
|
self.panel.tblParameters.cellWidget(row + 1, self.column).setValue(str(value))
|
|
else:
|
|
wrapper.setParameterValue(value, context)
|
|
|
|
def addFilesByPattern(self):
|
|
"""
|
|
Populates the dialog using a file pattern match
|
|
"""
|
|
dlg = QgsFindFilesByPatternDialog()
|
|
dlg.setWindowTitle(self.tr("Add Files by Pattern"))
|
|
if dlg.exec():
|
|
files = dlg.files()
|
|
context = dataobjects.createContext()
|
|
|
|
first_row = self.findStartingRow()
|
|
self.panel.tblParameters.setUpdatesEnabled(False)
|
|
for row, file in enumerate(files):
|
|
self.setRowValue(first_row + row, file, context)
|
|
self.panel.tblParameters.setUpdatesEnabled(True)
|
|
|
|
def showFileSelectionDialog(self):
|
|
settings = QgsSettings()
|
|
if settings.contains('/Processing/LastInputPath'):
|
|
path = str(settings.value('/Processing/LastInputPath'))
|
|
else:
|
|
path = QDir.homePath()
|
|
|
|
files, selected_filter = QFileDialog.getOpenFileNames(
|
|
self, self.tr('Select Files'), path, self.parameterDefinition.createFileFilter()
|
|
)
|
|
|
|
if not files:
|
|
return
|
|
|
|
settings.setValue('/Processing/LastInputPath', os.path.dirname(str(files[0])))
|
|
|
|
context = dataobjects.createContext()
|
|
|
|
first_row = self.findStartingRow()
|
|
self.panel.tblParameters.setUpdatesEnabled(False)
|
|
for row, file in enumerate(files):
|
|
self.setRowValue(first_row + row, file, context)
|
|
self.panel.tblParameters.setUpdatesEnabled(True)
|
|
|
|
def showDirectorySelectionDialog(self):
|
|
settings = QgsSettings()
|
|
if settings.contains('/Processing/LastInputPath'):
|
|
path = str(settings.value('/Processing/LastInputPath'))
|
|
else:
|
|
path = QDir.homePath()
|
|
|
|
folder = QFileDialog.getExistingDirectory(self, self.tr('Select Directory'), path)
|
|
if not folder:
|
|
return
|
|
|
|
settings.setValue('/Processing/LastInputPath', folder)
|
|
|
|
files = []
|
|
for pp in Path(folder).rglob("*"):
|
|
if not pp.is_file():
|
|
continue
|
|
|
|
p = pp.as_posix()
|
|
|
|
if isinstance(self.parameterDefinition, QgsProcessingParameterRasterLayer) or (
|
|
isinstance(self.parameterDefinition, QgsProcessingParameterMultipleLayers)
|
|
and self.parameterDefinition.layerType() == QgsProcessing.SourceType.TypeRaster
|
|
):
|
|
if not QgsRasterLayer.isValidRasterFileName(p):
|
|
continue
|
|
|
|
files.append(p)
|
|
|
|
if not files:
|
|
return
|
|
|
|
context = dataobjects.createContext()
|
|
|
|
first_row = self.findStartingRow()
|
|
self.panel.tblParameters.setUpdatesEnabled(False)
|
|
for row, file in enumerate(files):
|
|
self.setRowValue(first_row + row, file, context)
|
|
self.panel.tblParameters.setUpdatesEnabled(True)
|
|
|
|
def showLayerSelectionDialog(self):
|
|
layers = []
|
|
if isinstance(self.parameterDefinition, QgsProcessingParameterRasterLayer):
|
|
layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition,
|
|
QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.SourceType.TypeRaster:
|
|
layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition, QgsProcessingParameterVectorLayer):
|
|
layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition, QgsProcessingParameterMapLayer):
|
|
layers = QgsProcessingUtils.compatibleLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition, QgsProcessingParameterMeshLayer):
|
|
layers = QgsProcessingUtils.compatibleMeshLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition,
|
|
QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.SourceType.TypeMesh:
|
|
layers = QgsProcessingUtils.compatibleMeshLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition, QgsProcessingParameterPointCloudLayer):
|
|
layers = QgsProcessingUtils.compatiblePointCloudLayers(QgsProject.instance())
|
|
elif isinstance(self.parameterDefinition,
|
|
QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.SourceType.TypePointCloud:
|
|
layers = QgsProcessingUtils.compatiblePointCloudLayers(QgsProject.instance())
|
|
else:
|
|
datatypes = [QgsProcessing.SourceType.TypeVectorAnyGeometry]
|
|
if isinstance(self.parameterDefinition, QgsProcessingParameterFeatureSource):
|
|
datatypes = self.parameterDefinition.dataTypes()
|
|
elif isinstance(self.parameterDefinition, QgsProcessingParameterMultipleLayers):
|
|
datatypes = [self.parameterDefinition.layerType()]
|
|
|
|
if QgsProcessing.SourceType.TypeVectorAnyGeometry not in datatypes:
|
|
layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), datatypes)
|
|
else:
|
|
layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance())
|
|
|
|
dlg = MultipleInputDialog([layer.name() for layer in layers])
|
|
dlg.exec()
|
|
|
|
if not dlg.selectedoptions:
|
|
return
|
|
|
|
selected = dlg.selectedoptions
|
|
|
|
context = dataobjects.createContext()
|
|
|
|
first_row = self.findStartingRow()
|
|
for row, selected_idx in enumerate(selected):
|
|
value = layers[selected_idx].id()
|
|
self.setRowValue(first_row + row, value, context)
|
|
|
|
def calculateByExpression(self):
|
|
"""
|
|
Calculates parameter values by evaluating expressions.
|
|
"""
|
|
self.populateByExpression(adding=False)
|
|
|
|
def addByExpression(self):
|
|
"""
|
|
Adds new parameter values by evaluating an expression
|
|
"""
|
|
self.populateByExpression(adding=True)
|
|
|
|
def populateByExpression(self, adding=False):
|
|
"""
|
|
Populates the panel using an expression
|
|
"""
|
|
context = dataobjects.createContext()
|
|
expression_context = context.expressionContext()
|
|
|
|
# use the first row parameter values as a preview during expression creation
|
|
params, ok = self.panel.parametersForRow(row=0,
|
|
context=context,
|
|
warnOnInvalid=False)
|
|
alg_scope = QgsExpressionContextUtils.processingAlgorithmScope(self.panel.alg, params, context)
|
|
|
|
# create explicit variables corresponding to every parameter
|
|
for k, v in params.items():
|
|
alg_scope.setVariable(k, v, True)
|
|
|
|
# add batchCount in the alg scope to be used in the expressions. 0 is only an example value
|
|
alg_scope.setVariable('row_number', 0, False)
|
|
|
|
expression_context.appendScope(alg_scope)
|
|
|
|
# mark the parameter variables as highlighted for discoverability
|
|
highlighted_vars = expression_context.highlightedVariables()
|
|
highlighted_vars.extend(list(params.keys()))
|
|
highlighted_vars.append('row_number')
|
|
expression_context.setHighlightedVariables(highlighted_vars)
|
|
|
|
dlg = QgsExpressionBuilderDialog(layer=None, context=context.expressionContext())
|
|
if adding:
|
|
dlg.setExpectedOutputFormat(self.tr('An array of values corresponding to each new row to add'))
|
|
|
|
if not dlg.exec():
|
|
return
|
|
|
|
if adding:
|
|
exp = QgsExpression(dlg.expressionText())
|
|
res = exp.evaluate(expression_context)
|
|
|
|
if type(res) is not list:
|
|
res = [res]
|
|
|
|
first_row = self.findStartingRow()
|
|
self.panel.tblParameters.setUpdatesEnabled(False)
|
|
for row, value in enumerate(res):
|
|
self.setRowValue(row + first_row, value, context)
|
|
self.panel.tblParameters.setUpdatesEnabled(True)
|
|
else:
|
|
self.panel.tblParameters.setUpdatesEnabled(False)
|
|
for row in range(self.panel.batchRowCount()):
|
|
params, ok = self.panel.parametersForRow(row=row,
|
|
context=context,
|
|
warnOnInvalid=False)
|
|
|
|
# remove previous algorithm scope -- we need to rebuild this completely, using the
|
|
# other parameter values from the current row
|
|
expression_context.popScope()
|
|
alg_scope = QgsExpressionContextUtils.processingAlgorithmScope(self.panel.alg, params, context)
|
|
|
|
for k, v in params.items():
|
|
alg_scope.setVariable(k, v, True)
|
|
|
|
# add batch row number as evaluable variable in algorithm scope
|
|
alg_scope.setVariable('row_number', row, False)
|
|
|
|
expression_context.appendScope(alg_scope)
|
|
|
|
# rebuild a new expression every time -- we don't want the expression compiler to replace
|
|
# variables with precompiled values
|
|
exp = QgsExpression(dlg.expressionText())
|
|
value = exp.evaluate(expression_context)
|
|
self.setRowValue(row, value, context)
|
|
self.panel.tblParameters.setUpdatesEnabled(True)
|
|
|
|
|
|
class BatchPanel(QgsPanelWidget, WIDGET):
|
|
PARAMETERS = "PARAMETERS"
|
|
OUTPUTS = "OUTPUTS"
|
|
|
|
def __init__(self, parent, alg):
|
|
super().__init__(None)
|
|
self.setupUi(self)
|
|
|
|
self.wrappers = []
|
|
|
|
self.btnAdvanced.hide()
|
|
|
|
# Set icons
|
|
self.btnAdd.setIcon(QgsApplication.getThemeIcon('/symbologyAdd.svg'))
|
|
self.btnRemove.setIcon(QgsApplication.getThemeIcon('/symbologyRemove.svg'))
|
|
self.btnOpen.setIcon(QgsApplication.getThemeIcon('/mActionFileOpen.svg'))
|
|
self.btnSave.setIcon(QgsApplication.getThemeIcon('/mActionFileSave.svg'))
|
|
self.btnAdvanced.setIcon(QgsApplication.getThemeIcon("/processingAlgorithm.svg"))
|
|
|
|
self.alg = alg
|
|
self.parent = parent
|
|
|
|
self.btnAdd.clicked.connect(lambda: self.addRow(1))
|
|
self.btnRemove.clicked.connect(self.removeRows)
|
|
self.btnOpen.clicked.connect(self.load)
|
|
self.btnSave.clicked.connect(self.save)
|
|
self.btnAdvanced.toggled.connect(self.toggleAdvancedMode)
|
|
|
|
self.tblParameters.horizontalHeader().resizeSections(QHeaderView.ResizeMode.ResizeToContents)
|
|
self.tblParameters.horizontalHeader().setDefaultSectionSize(250)
|
|
self.tblParameters.horizontalHeader().setMinimumSectionSize(150)
|
|
|
|
self.processing_context = createContext()
|
|
|
|
class ContextGenerator(QgsProcessingContextGenerator):
|
|
|
|
def __init__(self, context):
|
|
super().__init__()
|
|
self.processing_context = context
|
|
|
|
def processingContext(self):
|
|
return self.processing_context
|
|
|
|
self.context_generator = ContextGenerator(self.processing_context)
|
|
|
|
self.column_to_parameter_definition = {}
|
|
self.parameter_to_column = {}
|
|
|
|
self.initWidgets()
|
|
|
|
def layerRegistryChanged(self):
|
|
pass
|
|
|
|
def initWidgets(self):
|
|
# If there are advanced parameters — show corresponding button
|
|
for param in self.alg.parameterDefinitions():
|
|
if param.flags() & QgsProcessingParameterDefinition.Flag.FlagAdvanced:
|
|
self.btnAdvanced.show()
|
|
break
|
|
|
|
# Determine column count
|
|
self.tblParameters.setColumnCount(
|
|
len(self.alg.parameterDefinitions()))
|
|
|
|
# Table headers
|
|
column = 0
|
|
for param in self.alg.parameterDefinitions():
|
|
if param.isDestination():
|
|
continue
|
|
self.tblParameters.setHorizontalHeaderItem(
|
|
column, QTableWidgetItem(param.description()))
|
|
if param.flags() & QgsProcessingParameterDefinition.Flag.FlagAdvanced or param.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
|
|
self.tblParameters.setColumnHidden(column, True)
|
|
|
|
self.column_to_parameter_definition[column] = param.name()
|
|
self.parameter_to_column[param.name()] = column
|
|
column += 1
|
|
|
|
for out in self.alg.destinationParameterDefinitions():
|
|
if not out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
|
|
self.tblParameters.setHorizontalHeaderItem(
|
|
column, QTableWidgetItem(out.description()))
|
|
self.column_to_parameter_definition[column] = out.name()
|
|
self.parameter_to_column[out.name()] = column
|
|
column += 1
|
|
|
|
self.addFillRow()
|
|
|
|
# Add an empty row to begin
|
|
self.addRow()
|
|
|
|
self.tblParameters.horizontalHeader().resizeSections(QHeaderView.ResizeMode.ResizeToContents)
|
|
self.tblParameters.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
|
self.tblParameters.horizontalHeader().setStretchLastSection(True)
|
|
|
|
def batchRowCount(self):
|
|
"""
|
|
Returns the number of rows corresponding to execution iterations
|
|
"""
|
|
return len(self.wrappers)
|
|
|
|
def clear(self):
|
|
self.tblParameters.setRowCount(1)
|
|
self.wrappers = []
|
|
|
|
def load(self):
|
|
context = dataobjects.createContext()
|
|
settings = QgsSettings()
|
|
last_path = settings.value("/Processing/LastBatchPath", QDir.homePath())
|
|
filename, selected_filter = QFileDialog.getOpenFileName(self,
|
|
self.tr('Open Batch'), last_path,
|
|
self.tr('JSON files (*.json)'))
|
|
if filename:
|
|
last_path = QFileInfo(filename).path()
|
|
settings.setValue('/Processing/LastBatchPath', last_path)
|
|
with open(filename) as f:
|
|
values = json.load(f)
|
|
else:
|
|
# If the user clicked on the cancel button.
|
|
return
|
|
|
|
self.clear()
|
|
try:
|
|
for row, alg in enumerate(values):
|
|
self.addRow()
|
|
params = alg[self.PARAMETERS]
|
|
outputs = alg[self.OUTPUTS]
|
|
|
|
for param in self.alg.parameterDefinitions():
|
|
if param.isDestination():
|
|
continue
|
|
if param.name() in params:
|
|
column = self.parameter_to_column[param.name()]
|
|
value = eval(params[param.name()])
|
|
wrapper = self.wrappers[row][column]
|
|
wrapper.setParameterValue(value, context)
|
|
|
|
for out in self.alg.destinationParameterDefinitions():
|
|
if out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
|
|
continue
|
|
if out.name() in outputs:
|
|
column = self.parameter_to_column[out.name()]
|
|
value = outputs[out.name()].strip("'")
|
|
widget = self.tblParameters.cellWidget(row + 1, column)
|
|
widget.setValue(value)
|
|
except TypeError:
|
|
QMessageBox.critical(
|
|
self,
|
|
self.tr('Error'),
|
|
self.tr('An error occurred while reading your file.'))
|
|
|
|
def save(self):
|
|
toSave = []
|
|
context = dataobjects.createContext()
|
|
for row in range(self.batchRowCount()):
|
|
algParams = {}
|
|
algOutputs = {}
|
|
alg = self.alg
|
|
for param in alg.parameterDefinitions():
|
|
if param.isDestination():
|
|
continue
|
|
|
|
col = self.parameter_to_column[param.name()]
|
|
wrapper = self.wrappers[row][col]
|
|
|
|
# For compatibility with 3.x API, we need to check whether the wrapper is
|
|
# the deprecated WidgetWrapper class. If not, it's the newer
|
|
# QgsAbstractProcessingParameterWidgetWrapper class
|
|
# TODO QGIS 4.0 - remove
|
|
if issubclass(wrapper.__class__, WidgetWrapper):
|
|
widget = wrapper.widget
|
|
else:
|
|
widget = wrapper.wrappedWidget()
|
|
|
|
value = wrapper.parameterValue()
|
|
|
|
if not param.checkValueIsAcceptable(value, context):
|
|
msg = self.tr('Wrong or missing parameter value: {0} (row {1})').format(
|
|
param.description(), row + 2)
|
|
self.parent.messageBar().pushMessage("", msg, level=Qgis.MessageLevel.Warning, duration=5)
|
|
return
|
|
algParams[param.name()] = param.valueAsPythonString(value, context)
|
|
|
|
for out in alg.destinationParameterDefinitions():
|
|
if out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
|
|
continue
|
|
col = self.parameter_to_column[out.name()]
|
|
widget = self.tblParameters.cellWidget(row + 1, col)
|
|
text = widget.getValue()
|
|
if text.strip() != '':
|
|
algOutputs[out.name()] = text.strip()
|
|
else:
|
|
self.parent.messageBar().pushMessage("",
|
|
self.tr('Wrong or missing output value: {0} (row {1})').format(
|
|
out.description(), row + 2),
|
|
level=Qgis.MessageLevel.Warning, duration=5)
|
|
return
|
|
toSave.append({self.PARAMETERS: algParams, self.OUTPUTS: algOutputs})
|
|
|
|
settings = QgsSettings()
|
|
last_path = settings.value("/Processing/LastBatchPath", QDir.homePath())
|
|
filename, __ = QFileDialog.getSaveFileName(self,
|
|
self.tr('Save Batch'),
|
|
last_path,
|
|
self.tr('JSON files (*.json)'))
|
|
if filename:
|
|
if not filename.endswith('.json'):
|
|
filename += '.json'
|
|
last_path = QFileInfo(filename).path()
|
|
settings.setValue('/Processing/LastBatchPath', last_path)
|
|
with open(filename, 'w') as f:
|
|
json.dump(toSave, f)
|
|
|
|
def setCellWrapper(self, row, column, wrapper, context):
|
|
self.wrappers[row - 1][column] = wrapper
|
|
|
|
widget_context = QgsProcessingParameterWidgetContext()
|
|
widget_context.setProject(QgsProject.instance())
|
|
if iface is not None:
|
|
widget_context.setActiveLayer(iface.activeLayer())
|
|
widget_context.setMapCanvas(iface.mapCanvas())
|
|
|
|
widget_context.setMessageBar(self.parent.messageBar())
|
|
|
|
if isinstance(self.alg, QgsProcessingModelAlgorithm):
|
|
widget_context.setModel(self.alg)
|
|
wrapper.setWidgetContext(widget_context)
|
|
wrapper.registerProcessingContextGenerator(self.context_generator)
|
|
|
|
# For compatibility with 3.x API, we need to check whether the wrapper is
|
|
# the deprecated WidgetWrapper class. If not, it's the newer
|
|
# QgsAbstractProcessingParameterWidgetWrapper class
|
|
# TODO QGIS 4.0 - remove
|
|
is_cpp_wrapper = not issubclass(wrapper.__class__, WidgetWrapper)
|
|
if is_cpp_wrapper:
|
|
widget = wrapper.createWrappedWidget(context)
|
|
else:
|
|
widget = wrapper.widget
|
|
|
|
self.tblParameters.setCellWidget(row, column, widget)
|
|
|
|
def addFillRow(self):
|
|
self.tblParameters.setRowCount(1)
|
|
for col, name in self.column_to_parameter_definition.items():
|
|
param_definition = self.alg.parameterDefinition(self.column_to_parameter_definition[col])
|
|
self.tblParameters.setCellWidget(0, col, BatchPanelFillWidget(param_definition, col, self))
|
|
|
|
def addRow(self, nb=1):
|
|
self.tblParameters.setUpdatesEnabled(False)
|
|
self.tblParameters.setRowCount(self.tblParameters.rowCount() + nb)
|
|
|
|
context = dataobjects.createContext()
|
|
|
|
wrappers = {}
|
|
row = self.tblParameters.rowCount() - nb
|
|
while row < self.tblParameters.rowCount():
|
|
self.wrappers.append([None] * self.tblParameters.columnCount())
|
|
for param in self.alg.parameterDefinitions():
|
|
if param.isDestination():
|
|
continue
|
|
|
|
column = self.parameter_to_column[param.name()]
|
|
wrapper = WidgetWrapperFactory.create_wrapper(param, self.parent, row, column)
|
|
wrappers[param.name()] = wrapper
|
|
self.setCellWrapper(row, column, wrapper, context)
|
|
|
|
for out in self.alg.destinationParameterDefinitions():
|
|
if out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
|
|
continue
|
|
|
|
column = self.parameter_to_column[out.name()]
|
|
self.tblParameters.setCellWidget(
|
|
row, column, BatchOutputSelectionPanel(
|
|
out, self.alg, row, column, self))
|
|
|
|
for wrapper in list(wrappers.values()):
|
|
wrapper.postInitialize(list(wrappers.values()))
|
|
row += 1
|
|
|
|
self.tblParameters.setUpdatesEnabled(True)
|
|
|
|
def removeRows(self):
|
|
rows = set()
|
|
for index in self.tblParameters.selectedIndexes():
|
|
if index.row() == 0:
|
|
continue
|
|
rows.add(index.row())
|
|
|
|
for row in sorted(rows, reverse=True):
|
|
if self.tblParameters.rowCount() <= 2:
|
|
break
|
|
|
|
del self.wrappers[row - 1]
|
|
self.tblParameters.removeRow(row)
|
|
|
|
# resynchronize stored row numbers for table widgets
|
|
for row in range(1, self.tblParameters.rowCount()):
|
|
for col in range(0, self.tblParameters.columnCount()):
|
|
cell_widget = self.tblParameters.cellWidget(row, col)
|
|
if not cell_widget:
|
|
continue
|
|
|
|
if isinstance(cell_widget, BatchOutputSelectionPanel):
|
|
cell_widget.row = row
|
|
|
|
def toggleAdvancedMode(self, checked):
|
|
for param in self.alg.parameterDefinitions():
|
|
if param.flags() & QgsProcessingParameterDefinition.Flag.FlagAdvanced and not (param.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden):
|
|
self.tblParameters.setColumnHidden(self.parameter_to_column[param.name()], not checked)
|
|
|
|
def valueForParameter(self, row, parameter_name):
|
|
"""
|
|
Returns the current value for a parameter in a row
|
|
"""
|
|
wrapper = self.wrappers[row][self.parameter_to_column[parameter_name]]
|
|
return wrapper.parameterValue()
|
|
|
|
def parametersForRow(self,
|
|
row: int,
|
|
context: QgsProcessingContext,
|
|
destinationProject: Optional[QgsProject] = None,
|
|
warnOnInvalid: bool = True):
|
|
"""
|
|
Returns the parameters dictionary corresponding to a row in the batch table
|
|
"""
|
|
parameters = {}
|
|
for param in self.alg.parameterDefinitions():
|
|
if param.isDestination():
|
|
continue
|
|
col = self.parameter_to_column[param.name()]
|
|
wrapper = self.wrappers[row][col]
|
|
parameters[param.name()] = wrapper.parameterValue()
|
|
if warnOnInvalid and not param.checkValueIsAcceptable(wrapper.parameterValue()):
|
|
self.parent.messageBar().pushMessage("",
|
|
self.tr('Wrong or missing parameter value: {0} (row {1})').format(
|
|
param.description(), row + 2),
|
|
level=Qgis.MessageLevel.Warning, duration=5)
|
|
return {}, False
|
|
|
|
count_visible_outputs = 0
|
|
for out in self.alg.destinationParameterDefinitions():
|
|
if out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
|
|
continue
|
|
|
|
col = self.parameter_to_column[out.name()]
|
|
|
|
count_visible_outputs += 1
|
|
widget = self.tblParameters.cellWidget(row + 1, col)
|
|
text = widget.getValue()
|
|
if warnOnInvalid:
|
|
if not out.checkValueIsAcceptable(text):
|
|
msg = self.tr(
|
|
'Wrong or missing output value: {0} (row {1})').format(
|
|
out.description(), row + 2)
|
|
self.parent.messageBar().pushMessage("", msg,
|
|
level=Qgis.MessageLevel.Warning,
|
|
duration=5)
|
|
return {}, False
|
|
|
|
ok, error = out.isSupportedOutputValue(text, context)
|
|
if not ok:
|
|
self.parent.messageBar().pushMessage("", error,
|
|
level=Qgis.MessageLevel.Warning,
|
|
duration=5)
|
|
return {}, False
|
|
|
|
if isinstance(out, (QgsProcessingParameterRasterDestination,
|
|
QgsProcessingParameterVectorDestination,
|
|
QgsProcessingParameterFeatureSink)):
|
|
# load rasters and sinks on completion
|
|
parameters[out.name()] = QgsProcessingOutputLayerDefinition(text, destinationProject)
|
|
else:
|
|
parameters[out.name()] = text
|
|
|
|
return parameters, True
|