BatchPanel.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. """
  2. ***************************************************************************
  3. BatchPanel.py
  4. ---------------------
  5. Date : November 2014
  6. Copyright : (C) 2014 by Alexander Bruy
  7. Email : alexander dot bruy at gmail dot com
  8. ***************************************************************************
  9. * *
  10. * This program is free software; you can redistribute it and/or modify *
  11. * it under the terms of the GNU General Public License as published by *
  12. * the Free Software Foundation; either version 2 of the License, or *
  13. * (at your option) any later version. *
  14. * *
  15. ***************************************************************************
  16. """
  17. __author__ = 'Alexander Bruy'
  18. __date__ = 'November 2014'
  19. __copyright__ = '(C) 2014, Alexander Bruy'
  20. import os
  21. import json
  22. import warnings
  23. from pathlib import Path
  24. from typing import Optional
  25. from qgis.PyQt import uic
  26. from qgis.PyQt.QtWidgets import (
  27. QTableWidgetItem,
  28. QComboBox,
  29. QHeaderView,
  30. QFileDialog,
  31. QMessageBox,
  32. QToolButton,
  33. QMenu,
  34. QAction
  35. )
  36. # adding to this list? also update the QgsProcessingHistoryProvider executeAlgorithm imports!!
  37. from qgis.PyQt.QtCore import (
  38. QTime, # NOQA - must be here for saved file evaluation
  39. QDate, # NOQA - must be here for saved file evaluation
  40. QDateTime # NOQA - must be here for saved file evaluation
  41. )
  42. from qgis.PyQt.QtGui import (
  43. QPalette,
  44. QColor, # NOQA - must be here for saved file evaluation
  45. )
  46. from qgis.PyQt.QtCore import (
  47. QDir,
  48. QFileInfo,
  49. QCoreApplication
  50. )
  51. from qgis.core import (
  52. Qgis,
  53. QgsApplication,
  54. QgsSettings,
  55. QgsProperty, # NOQA - must be here for saved file evaluation
  56. QgsProject,
  57. QgsFeatureRequest, # NOQA - must be here for saved file evaluation
  58. QgsProcessingFeatureSourceDefinition, # NOQA - must be here for saved file evaluation
  59. QgsCoordinateReferenceSystem, # NOQA - must be here for saved file evaluation
  60. QgsProcessingParameterDefinition,
  61. QgsProcessingModelAlgorithm,
  62. QgsProcessingParameterFile,
  63. QgsProcessingParameterMapLayer,
  64. QgsProcessingParameterRasterLayer,
  65. QgsProcessingParameterMeshLayer,
  66. QgsProcessingParameterPointCloudLayer,
  67. QgsProcessingParameterVectorLayer,
  68. QgsProcessingParameterFeatureSource,
  69. QgsProcessingParameterRasterDestination,
  70. QgsProcessingParameterVectorDestination,
  71. QgsProcessingParameterMultipleLayers,
  72. QgsProcessingParameterFeatureSink,
  73. QgsProcessingOutputLayerDefinition,
  74. QgsExpressionContextUtils,
  75. QgsProcessing,
  76. QgsExpression,
  77. QgsRasterLayer,
  78. QgsProcessingUtils,
  79. QgsFileFilterGenerator,
  80. QgsProcessingContext
  81. )
  82. from qgis.gui import (
  83. QgsProcessingParameterWidgetContext,
  84. QgsProcessingContextGenerator,
  85. QgsFindFilesByPatternDialog,
  86. QgsExpressionBuilderDialog,
  87. QgsPanelWidget
  88. )
  89. from qgis.utils import iface
  90. from processing.gui.wrappers import WidgetWrapperFactory, WidgetWrapper
  91. from processing.gui.BatchOutputSelectionPanel import BatchOutputSelectionPanel
  92. from processing.tools import dataobjects
  93. from processing.tools.dataobjects import createContext
  94. from processing.gui.MultipleInputDialog import MultipleInputDialog
  95. pluginPath = os.path.split(os.path.dirname(__file__))[0]
  96. with warnings.catch_warnings():
  97. warnings.filterwarnings("ignore", category=DeprecationWarning)
  98. WIDGET, BASE = uic.loadUiType(
  99. os.path.join(pluginPath, 'ui', 'widgetBatchPanel.ui'))
  100. class BatchPanelFillWidget(QToolButton):
  101. def __init__(self, parameterDefinition, column, panel, parent=None):
  102. super().__init__(parent)
  103. self.setBackgroundRole(QPalette.Button)
  104. self.setAutoFillBackground(True)
  105. self.parameterDefinition = parameterDefinition
  106. self.column = column
  107. self.panel = panel
  108. self.setText(QCoreApplication.translate('BatchPanel', 'Autofill…'))
  109. f = self.font()
  110. f.setItalic(True)
  111. self.setFont(f)
  112. self.setPopupMode(QToolButton.InstantPopup)
  113. self.setAutoRaise(True)
  114. self.menu = QMenu()
  115. self.menu.aboutToShow.connect(self.createMenu)
  116. self.setMenu(self.menu)
  117. def createMenu(self):
  118. self.menu.clear()
  119. self.menu.setMinimumWidth(self.width())
  120. fill_down_action = QAction(self.tr('Fill Down'), self.menu)
  121. fill_down_action.triggered.connect(self.fillDown)
  122. fill_down_action.setToolTip(self.tr('Copy the first value down to all other rows'))
  123. self.menu.addAction(fill_down_action)
  124. calculate_by_expression = QAction(QCoreApplication.translate('BatchPanel', 'Calculate by Expression…'),
  125. self.menu)
  126. calculate_by_expression.setIcon(QgsApplication.getThemeIcon('/mActionCalculateField.svg'))
  127. calculate_by_expression.triggered.connect(self.calculateByExpression)
  128. calculate_by_expression.setToolTip(self.tr('Calculates parameter values by evaluating an expression'))
  129. self.menu.addAction(calculate_by_expression)
  130. add_by_expression = QAction(QCoreApplication.translate('BatchPanel', 'Add Values by Expression…'),
  131. self.menu)
  132. add_by_expression.triggered.connect(self.addByExpression)
  133. add_by_expression.setToolTip(self.tr('Adds new parameter values by evaluating an expression'))
  134. self.menu.addAction(add_by_expression)
  135. if not self.parameterDefinition.isDestination() and isinstance(self.parameterDefinition, QgsFileFilterGenerator):
  136. self.menu.addSeparator()
  137. find_by_pattern_action = QAction(QCoreApplication.translate('BatchPanel', 'Add Files by Pattern…'),
  138. self.menu)
  139. find_by_pattern_action.triggered.connect(self.addFilesByPattern)
  140. find_by_pattern_action.setToolTip(self.tr('Adds files by a file pattern match'))
  141. self.menu.addAction(find_by_pattern_action)
  142. select_file_action = QAction(
  143. QCoreApplication.translate('BatchInputSelectionPanel', 'Select Files…'), self.menu)
  144. select_file_action.triggered.connect(self.showFileSelectionDialog)
  145. self.menu.addAction(select_file_action)
  146. select_directory_action = QAction(
  147. QCoreApplication.translate('BatchInputSelectionPanel', 'Add All Files from a Directory…'), self.menu)
  148. select_directory_action.triggered.connect(self.showDirectorySelectionDialog)
  149. self.menu.addAction(select_directory_action)
  150. if not isinstance(self.parameterDefinition, QgsProcessingParameterFile):
  151. select_layer_action = QAction(
  152. QCoreApplication.translate('BatchInputSelectionPanel', 'Select from Open Layers…'), self.menu)
  153. select_layer_action.triggered.connect(self.showLayerSelectionDialog)
  154. self.menu.addAction(select_layer_action)
  155. def findStartingRow(self):
  156. first_row = 0
  157. for row in range(self.panel.batchRowCount()):
  158. wrapper = self.panel.wrappers[row][self.column]
  159. if wrapper is None:
  160. break
  161. else:
  162. value = wrapper.parameterValue()
  163. if value is None:
  164. break
  165. first_row += 1
  166. return first_row
  167. def fillDown(self):
  168. """
  169. Copy the top value down
  170. """
  171. context = dataobjects.createContext()
  172. wrapper = self.panel.wrappers[0][self.column]
  173. if wrapper is None:
  174. # e.g. double clicking on a destination header
  175. widget = self.panel.tblParameters.cellWidget(1, self.column)
  176. value = widget.getValue()
  177. else:
  178. value = wrapper.parameterValue()
  179. for row in range(1, self.panel.batchRowCount()):
  180. self.setRowValue(row, value, context)
  181. def setRowValue(self, row, value, context):
  182. """
  183. Sets the value for a row, in the current column
  184. """
  185. if self.panel.batchRowCount() <= row:
  186. self.panel.addRow()
  187. wrapper = self.panel.wrappers[row][self.column]
  188. if wrapper is None:
  189. # e.g. destination header
  190. self.panel.tblParameters.cellWidget(row + 1, self.column).setValue(str(value))
  191. else:
  192. wrapper.setParameterValue(value, context)
  193. def addFilesByPattern(self):
  194. """
  195. Populates the dialog using a file pattern match
  196. """
  197. dlg = QgsFindFilesByPatternDialog()
  198. dlg.setWindowTitle(self.tr("Add Files by Pattern"))
  199. if dlg.exec_():
  200. files = dlg.files()
  201. context = dataobjects.createContext()
  202. first_row = self.findStartingRow()
  203. self.panel.tblParameters.setUpdatesEnabled(False)
  204. for row, file in enumerate(files):
  205. self.setRowValue(first_row + row, file, context)
  206. self.panel.tblParameters.setUpdatesEnabled(True)
  207. def showFileSelectionDialog(self):
  208. settings = QgsSettings()
  209. if settings.contains('/Processing/LastInputPath'):
  210. path = str(settings.value('/Processing/LastInputPath'))
  211. else:
  212. path = QDir.homePath()
  213. files, selected_filter = QFileDialog.getOpenFileNames(
  214. self, self.tr('Select Files'), path, self.parameterDefinition.createFileFilter()
  215. )
  216. if not files:
  217. return
  218. settings.setValue('/Processing/LastInputPath', os.path.dirname(str(files[0])))
  219. context = dataobjects.createContext()
  220. first_row = self.findStartingRow()
  221. self.panel.tblParameters.setUpdatesEnabled(False)
  222. for row, file in enumerate(files):
  223. self.setRowValue(first_row + row, file, context)
  224. self.panel.tblParameters.setUpdatesEnabled(True)
  225. def showDirectorySelectionDialog(self):
  226. settings = QgsSettings()
  227. if settings.contains('/Processing/LastInputPath'):
  228. path = str(settings.value('/Processing/LastInputPath'))
  229. else:
  230. path = QDir.homePath()
  231. folder = QFileDialog.getExistingDirectory(self, self.tr('Select Directory'), path)
  232. if not folder:
  233. return
  234. settings.setValue('/Processing/LastInputPath', folder)
  235. files = []
  236. for pp in Path(folder).rglob("*"):
  237. if not pp.is_file():
  238. continue
  239. p = pp.as_posix()
  240. if isinstance(self.parameterDefinition, QgsProcessingParameterRasterLayer) or (
  241. isinstance(self.parameterDefinition, QgsProcessingParameterMultipleLayers)
  242. and self.parameterDefinition.layerType() == QgsProcessing.TypeRaster
  243. ):
  244. if not QgsRasterLayer.isValidRasterFileName(p):
  245. continue
  246. files.append(p)
  247. if not files:
  248. return
  249. context = dataobjects.createContext()
  250. first_row = self.findStartingRow()
  251. self.panel.tblParameters.setUpdatesEnabled(False)
  252. for row, file in enumerate(files):
  253. self.setRowValue(first_row + row, file, context)
  254. self.panel.tblParameters.setUpdatesEnabled(True)
  255. def showLayerSelectionDialog(self):
  256. layers = []
  257. if isinstance(self.parameterDefinition, QgsProcessingParameterRasterLayer):
  258. layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance())
  259. elif isinstance(self.parameterDefinition,
  260. QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.TypeRaster:
  261. layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance())
  262. elif isinstance(self.parameterDefinition, QgsProcessingParameterVectorLayer):
  263. layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance())
  264. elif isinstance(self.parameterDefinition, QgsProcessingParameterMapLayer):
  265. layers = QgsProcessingUtils.compatibleLayers(QgsProject.instance())
  266. elif isinstance(self.parameterDefinition, QgsProcessingParameterMeshLayer):
  267. layers = QgsProcessingUtils.compatibleMeshLayers(QgsProject.instance())
  268. elif isinstance(self.parameterDefinition,
  269. QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.TypeMesh:
  270. layers = QgsProcessingUtils.compatibleMeshLayers(QgsProject.instance())
  271. elif isinstance(self.parameterDefinition, QgsProcessingParameterPointCloudLayer):
  272. layers = QgsProcessingUtils.compatiblePointCloudLayers(QgsProject.instance())
  273. elif isinstance(self.parameterDefinition,
  274. QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.TypePointCloud:
  275. layers = QgsProcessingUtils.compatiblePointCloudLayers(QgsProject.instance())
  276. else:
  277. datatypes = [QgsProcessing.TypeVectorAnyGeometry]
  278. if isinstance(self.parameterDefinition, QgsProcessingParameterFeatureSource):
  279. datatypes = self.parameterDefinition.dataTypes()
  280. elif isinstance(self.parameterDefinition, QgsProcessingParameterMultipleLayers):
  281. datatypes = [self.parameterDefinition.layerType()]
  282. if QgsProcessing.TypeVectorAnyGeometry not in datatypes:
  283. layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), datatypes)
  284. else:
  285. layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance())
  286. dlg = MultipleInputDialog([layer.name() for layer in layers])
  287. dlg.exec_()
  288. if not dlg.selectedoptions:
  289. return
  290. selected = dlg.selectedoptions
  291. context = dataobjects.createContext()
  292. first_row = self.findStartingRow()
  293. for row, selected_idx in enumerate(selected):
  294. value = layers[selected_idx].id()
  295. self.setRowValue(first_row + row, value, context)
  296. def calculateByExpression(self):
  297. """
  298. Calculates parameter values by evaluating expressions.
  299. """
  300. self.populateByExpression(adding=False)
  301. def addByExpression(self):
  302. """
  303. Adds new parameter values by evaluating an expression
  304. """
  305. self.populateByExpression(adding=True)
  306. def populateByExpression(self, adding=False):
  307. """
  308. Populates the panel using an expression
  309. """
  310. context = dataobjects.createContext()
  311. expression_context = context.expressionContext()
  312. # use the first row parameter values as a preview during expression creation
  313. params, ok = self.panel.parametersForRow(row=0,
  314. context=context,
  315. warnOnInvalid=False)
  316. alg_scope = QgsExpressionContextUtils.processingAlgorithmScope(self.panel.alg, params, context)
  317. # create explicit variables corresponding to every parameter
  318. for k, v in params.items():
  319. alg_scope.setVariable(k, v, True)
  320. # add batchCount in the alg scope to be used in the expressions. 0 is only an example value
  321. alg_scope.setVariable('row_number', 0, False)
  322. expression_context.appendScope(alg_scope)
  323. # mark the parameter variables as highlighted for discoverability
  324. highlighted_vars = expression_context.highlightedVariables()
  325. highlighted_vars.extend(list(params.keys()))
  326. highlighted_vars.append('row_number')
  327. expression_context.setHighlightedVariables(highlighted_vars)
  328. dlg = QgsExpressionBuilderDialog(layer=None, context=context.expressionContext())
  329. if adding:
  330. dlg.setExpectedOutputFormat(self.tr('An array of values corresponding to each new row to add'))
  331. if not dlg.exec_():
  332. return
  333. if adding:
  334. exp = QgsExpression(dlg.expressionText())
  335. res = exp.evaluate(expression_context)
  336. if type(res) is not list:
  337. res = [res]
  338. first_row = self.findStartingRow()
  339. self.panel.tblParameters.setUpdatesEnabled(False)
  340. for row, value in enumerate(res):
  341. self.setRowValue(row + first_row, value, context)
  342. self.panel.tblParameters.setUpdatesEnabled(True)
  343. else:
  344. self.panel.tblParameters.setUpdatesEnabled(False)
  345. for row in range(self.panel.batchRowCount()):
  346. params, ok = self.panel.parametersForRow(row=row,
  347. context=context,
  348. warnOnInvalid=False)
  349. # remove previous algorithm scope -- we need to rebuild this completely, using the
  350. # other parameter values from the current row
  351. expression_context.popScope()
  352. alg_scope = QgsExpressionContextUtils.processingAlgorithmScope(self.panel.alg, params, context)
  353. for k, v in params.items():
  354. alg_scope.setVariable(k, v, True)
  355. # add batch row number as evaluable variable in algorithm scope
  356. alg_scope.setVariable('row_number', row, False)
  357. expression_context.appendScope(alg_scope)
  358. # rebuild a new expression every time -- we don't want the expression compiler to replace
  359. # variables with precompiled values
  360. exp = QgsExpression(dlg.expressionText())
  361. value = exp.evaluate(expression_context)
  362. self.setRowValue(row, value, context)
  363. self.panel.tblParameters.setUpdatesEnabled(True)
  364. class BatchPanel(QgsPanelWidget, WIDGET):
  365. PARAMETERS = "PARAMETERS"
  366. OUTPUTS = "OUTPUTS"
  367. def __init__(self, parent, alg):
  368. super().__init__(None)
  369. self.setupUi(self)
  370. self.wrappers = []
  371. self.btnAdvanced.hide()
  372. # Set icons
  373. self.btnAdd.setIcon(QgsApplication.getThemeIcon('/symbologyAdd.svg'))
  374. self.btnRemove.setIcon(QgsApplication.getThemeIcon('/symbologyRemove.svg'))
  375. self.btnOpen.setIcon(QgsApplication.getThemeIcon('/mActionFileOpen.svg'))
  376. self.btnSave.setIcon(QgsApplication.getThemeIcon('/mActionFileSave.svg'))
  377. self.btnAdvanced.setIcon(QgsApplication.getThemeIcon("/processingAlgorithm.svg"))
  378. self.alg = alg
  379. self.parent = parent
  380. self.btnAdd.clicked.connect(lambda: self.addRow(1))
  381. self.btnRemove.clicked.connect(self.removeRows)
  382. self.btnOpen.clicked.connect(self.load)
  383. self.btnSave.clicked.connect(self.save)
  384. self.btnAdvanced.toggled.connect(self.toggleAdvancedMode)
  385. self.tblParameters.horizontalHeader().resizeSections(QHeaderView.ResizeToContents)
  386. self.tblParameters.horizontalHeader().setDefaultSectionSize(250)
  387. self.tblParameters.horizontalHeader().setMinimumSectionSize(150)
  388. self.processing_context = createContext()
  389. class ContextGenerator(QgsProcessingContextGenerator):
  390. def __init__(self, context):
  391. super().__init__()
  392. self.processing_context = context
  393. def processingContext(self):
  394. return self.processing_context
  395. self.context_generator = ContextGenerator(self.processing_context)
  396. self.column_to_parameter_definition = {}
  397. self.parameter_to_column = {}
  398. self.initWidgets()
  399. def layerRegistryChanged(self):
  400. pass
  401. def initWidgets(self):
  402. # If there are advanced parameters — show corresponding button
  403. for param in self.alg.parameterDefinitions():
  404. if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced:
  405. self.btnAdvanced.show()
  406. break
  407. # Determine column count
  408. self.tblParameters.setColumnCount(
  409. len(self.alg.parameterDefinitions()))
  410. # Table headers
  411. column = 0
  412. for param in self.alg.parameterDefinitions():
  413. if param.isDestination():
  414. continue
  415. self.tblParameters.setHorizontalHeaderItem(
  416. column, QTableWidgetItem(param.description()))
  417. if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced or param.flags() & QgsProcessingParameterDefinition.FlagHidden:
  418. self.tblParameters.setColumnHidden(column, True)
  419. self.column_to_parameter_definition[column] = param.name()
  420. self.parameter_to_column[param.name()] = column
  421. column += 1
  422. for out in self.alg.destinationParameterDefinitions():
  423. if not out.flags() & QgsProcessingParameterDefinition.FlagHidden:
  424. self.tblParameters.setHorizontalHeaderItem(
  425. column, QTableWidgetItem(out.description()))
  426. self.column_to_parameter_definition[column] = out.name()
  427. self.parameter_to_column[out.name()] = column
  428. column += 1
  429. self.addFillRow()
  430. # Add an empty row to begin
  431. self.addRow()
  432. self.tblParameters.horizontalHeader().resizeSections(QHeaderView.ResizeToContents)
  433. self.tblParameters.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
  434. self.tblParameters.horizontalHeader().setStretchLastSection(True)
  435. def batchRowCount(self):
  436. """
  437. Returns the number of rows corresponding to execution iterations
  438. """
  439. return len(self.wrappers)
  440. def clear(self):
  441. self.tblParameters.setRowCount(1)
  442. self.wrappers = []
  443. def load(self):
  444. context = dataobjects.createContext()
  445. settings = QgsSettings()
  446. last_path = settings.value("/Processing/LastBatchPath", QDir.homePath())
  447. filename, selected_filter = QFileDialog.getOpenFileName(self,
  448. self.tr('Open Batch'), last_path,
  449. self.tr('JSON files (*.json)'))
  450. if filename:
  451. last_path = QFileInfo(filename).path()
  452. settings.setValue('/Processing/LastBatchPath', last_path)
  453. with open(filename) as f:
  454. values = json.load(f)
  455. else:
  456. # If the user clicked on the cancel button.
  457. return
  458. self.clear()
  459. try:
  460. for row, alg in enumerate(values):
  461. self.addRow()
  462. params = alg[self.PARAMETERS]
  463. outputs = alg[self.OUTPUTS]
  464. for param in self.alg.parameterDefinitions():
  465. if param.isDestination():
  466. continue
  467. if param.name() in params:
  468. column = self.parameter_to_column[param.name()]
  469. value = eval(params[param.name()])
  470. wrapper = self.wrappers[row][column]
  471. wrapper.setParameterValue(value, context)
  472. for out in self.alg.destinationParameterDefinitions():
  473. if out.flags() & QgsProcessingParameterDefinition.FlagHidden:
  474. continue
  475. if out.name() in outputs:
  476. column = self.parameter_to_column[out.name()]
  477. value = outputs[out.name()].strip("'")
  478. widget = self.tblParameters.cellWidget(row + 1, column)
  479. widget.setValue(value)
  480. except TypeError:
  481. QMessageBox.critical(
  482. self,
  483. self.tr('Error'),
  484. self.tr('An error occurred while reading your file.'))
  485. def save(self):
  486. toSave = []
  487. context = dataobjects.createContext()
  488. for row in range(self.batchRowCount()):
  489. algParams = {}
  490. algOutputs = {}
  491. alg = self.alg
  492. for param in alg.parameterDefinitions():
  493. if param.isDestination():
  494. continue
  495. col = self.parameter_to_column[param.name()]
  496. wrapper = self.wrappers[row][col]
  497. # For compatibility with 3.x API, we need to check whether the wrapper is
  498. # the deprecated WidgetWrapper class. If not, it's the newer
  499. # QgsAbstractProcessingParameterWidgetWrapper class
  500. # TODO QGIS 4.0 - remove
  501. if issubclass(wrapper.__class__, WidgetWrapper):
  502. widget = wrapper.widget
  503. else:
  504. widget = wrapper.wrappedWidget()
  505. value = wrapper.parameterValue()
  506. if not param.checkValueIsAcceptable(value, context):
  507. msg = self.tr('Wrong or missing parameter value: {0} (row {1})').format(
  508. param.description(), row + 2)
  509. self.parent.messageBar().pushMessage("", msg, level=Qgis.Warning, duration=5)
  510. return
  511. algParams[param.name()] = param.valueAsPythonString(value, context)
  512. for out in alg.destinationParameterDefinitions():
  513. if out.flags() & QgsProcessingParameterDefinition.FlagHidden:
  514. continue
  515. col = self.parameter_to_column[out.name()]
  516. widget = self.tblParameters.cellWidget(row + 1, col)
  517. text = widget.getValue()
  518. if text.strip() != '':
  519. algOutputs[out.name()] = text.strip()
  520. else:
  521. self.parent.messageBar().pushMessage("",
  522. self.tr('Wrong or missing output value: {0} (row {1})').format(
  523. out.description(), row + 2),
  524. level=Qgis.Warning, duration=5)
  525. return
  526. toSave.append({self.PARAMETERS: algParams, self.OUTPUTS: algOutputs})
  527. settings = QgsSettings()
  528. last_path = settings.value("/Processing/LastBatchPath", QDir.homePath())
  529. filename, __ = QFileDialog.getSaveFileName(self,
  530. self.tr('Save Batch'),
  531. last_path,
  532. self.tr('JSON files (*.json)'))
  533. if filename:
  534. if not filename.endswith('.json'):
  535. filename += '.json'
  536. last_path = QFileInfo(filename).path()
  537. settings.setValue('/Processing/LastBatchPath', last_path)
  538. with open(filename, 'w') as f:
  539. json.dump(toSave, f)
  540. def setCellWrapper(self, row, column, wrapper, context):
  541. self.wrappers[row - 1][column] = wrapper
  542. widget_context = QgsProcessingParameterWidgetContext()
  543. widget_context.setProject(QgsProject.instance())
  544. if iface is not None:
  545. widget_context.setActiveLayer(iface.activeLayer())
  546. widget_context.setMapCanvas(iface.mapCanvas())
  547. widget_context.setMessageBar(self.parent.messageBar())
  548. if isinstance(self.alg, QgsProcessingModelAlgorithm):
  549. widget_context.setModel(self.alg)
  550. wrapper.setWidgetContext(widget_context)
  551. wrapper.registerProcessingContextGenerator(self.context_generator)
  552. # For compatibility with 3.x API, we need to check whether the wrapper is
  553. # the deprecated WidgetWrapper class. If not, it's the newer
  554. # QgsAbstractProcessingParameterWidgetWrapper class
  555. # TODO QGIS 4.0 - remove
  556. is_cpp_wrapper = not issubclass(wrapper.__class__, WidgetWrapper)
  557. if is_cpp_wrapper:
  558. widget = wrapper.createWrappedWidget(context)
  559. else:
  560. widget = wrapper.widget
  561. self.tblParameters.setCellWidget(row, column, widget)
  562. def addFillRow(self):
  563. self.tblParameters.setRowCount(1)
  564. for col, name in self.column_to_parameter_definition.items():
  565. param_definition = self.alg.parameterDefinition(self.column_to_parameter_definition[col])
  566. self.tblParameters.setCellWidget(0, col, BatchPanelFillWidget(param_definition, col, self))
  567. def addRow(self, nb=1):
  568. self.tblParameters.setUpdatesEnabled(False)
  569. self.tblParameters.setRowCount(self.tblParameters.rowCount() + nb)
  570. context = dataobjects.createContext()
  571. wrappers = {}
  572. row = self.tblParameters.rowCount() - nb
  573. while row < self.tblParameters.rowCount():
  574. self.wrappers.append([None] * self.tblParameters.columnCount())
  575. for param in self.alg.parameterDefinitions():
  576. if param.isDestination():
  577. continue
  578. column = self.parameter_to_column[param.name()]
  579. wrapper = WidgetWrapperFactory.create_wrapper(param, self.parent, row, column)
  580. wrappers[param.name()] = wrapper
  581. self.setCellWrapper(row, column, wrapper, context)
  582. for out in self.alg.destinationParameterDefinitions():
  583. if out.flags() & QgsProcessingParameterDefinition.FlagHidden:
  584. continue
  585. column = self.parameter_to_column[out.name()]
  586. self.tblParameters.setCellWidget(
  587. row, column, BatchOutputSelectionPanel(
  588. out, self.alg, row, column, self))
  589. for wrapper in list(wrappers.values()):
  590. wrapper.postInitialize(list(wrappers.values()))
  591. row += 1
  592. self.tblParameters.setUpdatesEnabled(True)
  593. def removeRows(self):
  594. rows = set()
  595. for index in self.tblParameters.selectedIndexes():
  596. if index.row() == 0:
  597. continue
  598. rows.add(index.row())
  599. for row in sorted(rows, reverse=True):
  600. if self.tblParameters.rowCount() <= 2:
  601. break
  602. del self.wrappers[row - 1]
  603. self.tblParameters.removeRow(row)
  604. # resynchronize stored row numbers for table widgets
  605. for row in range(1, self.tblParameters.rowCount()):
  606. for col in range(0, self.tblParameters.columnCount()):
  607. cell_widget = self.tblParameters.cellWidget(row, col)
  608. if not cell_widget:
  609. continue
  610. if isinstance(cell_widget, BatchOutputSelectionPanel):
  611. cell_widget.row = row
  612. def toggleAdvancedMode(self, checked):
  613. for param in self.alg.parameterDefinitions():
  614. if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced and not (param.flags() & QgsProcessingParameterDefinition.FlagHidden):
  615. self.tblParameters.setColumnHidden(self.parameter_to_column[param.name()], not checked)
  616. def valueForParameter(self, row, parameter_name):
  617. """
  618. Returns the current value for a parameter in a row
  619. """
  620. wrapper = self.wrappers[row][self.parameter_to_column[parameter_name]]
  621. return wrapper.parameterValue()
  622. def parametersForRow(self,
  623. row: int,
  624. context: QgsProcessingContext,
  625. destinationProject: Optional[QgsProject] = None,
  626. warnOnInvalid: bool = True):
  627. """
  628. Returns the parameters dictionary corresponding to a row in the batch table
  629. """
  630. parameters = {}
  631. for param in self.alg.parameterDefinitions():
  632. if param.isDestination():
  633. continue
  634. col = self.parameter_to_column[param.name()]
  635. wrapper = self.wrappers[row][col]
  636. parameters[param.name()] = wrapper.parameterValue()
  637. if warnOnInvalid and not param.checkValueIsAcceptable(wrapper.parameterValue()):
  638. self.parent.messageBar().pushMessage("",
  639. self.tr('Wrong or missing parameter value: {0} (row {1})').format(
  640. param.description(), row + 2),
  641. level=Qgis.Warning, duration=5)
  642. return {}, False
  643. count_visible_outputs = 0
  644. for out in self.alg.destinationParameterDefinitions():
  645. if out.flags() & QgsProcessingParameterDefinition.FlagHidden:
  646. continue
  647. col = self.parameter_to_column[out.name()]
  648. count_visible_outputs += 1
  649. widget = self.tblParameters.cellWidget(row + 1, col)
  650. text = widget.getValue()
  651. if warnOnInvalid:
  652. if not out.checkValueIsAcceptable(text):
  653. msg = self.tr(
  654. 'Wrong or missing output value: {0} (row {1})').format(
  655. out.description(), row + 2)
  656. self.parent.messageBar().pushMessage("", msg,
  657. level=Qgis.Warning,
  658. duration=5)
  659. return {}, False
  660. ok, error = out.isSupportedOutputValue(text, context)
  661. if not ok:
  662. self.parent.messageBar().pushMessage("", error,
  663. level=Qgis.Warning,
  664. duration=5)
  665. return {}, False
  666. if isinstance(out, (QgsProcessingParameterRasterDestination,
  667. QgsProcessingParameterVectorDestination,
  668. QgsProcessingParameterFeatureSink)):
  669. # load rasters and sinks on completion
  670. parameters[out.name()] = QgsProcessingOutputLayerDefinition(text, destinationProject)
  671. else:
  672. parameters[out.name()] = text
  673. return parameters, True