RasterCalculatorWidgets.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. """
  2. ***************************************************************************
  3. RasterCalculatorWidgets.py
  4. ---------------------
  5. Date : November 2016
  6. Copyright : (C) 2016 by Victor Olaya
  7. Email : volayaf 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__ = 'Victor Olaya'
  18. __date__ = 'November 2016'
  19. __copyright__ = '(C) 2016, Victor Olaya'
  20. import os
  21. from functools import partial
  22. import re
  23. import json
  24. from qgis.utils import iface
  25. from qgis.PyQt import uic
  26. from qgis.PyQt.QtCore import Qt
  27. from qgis.PyQt.QtGui import QTextCursor
  28. from qgis.PyQt.QtWidgets import (QLineEdit, QPushButton, QLabel,
  29. QComboBox, QSpacerItem, QSizePolicy,
  30. QListWidgetItem)
  31. from qgis.core import (QgsProcessingUtils,
  32. QgsProcessingParameterDefinition,
  33. QgsProcessingParameterRasterLayer,
  34. QgsProcessingOutputRasterLayer,
  35. QgsProject)
  36. from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_BATCH
  37. from processing.gui.BatchInputSelectionPanel import BatchInputSelectionPanel
  38. from processing.tools import dataobjects
  39. from processing.tools.system import userFolder
  40. from processing.gui.wrappers import InvalidParameterValue
  41. from qgis.analysis import QgsRasterCalculatorEntry, QgsRasterCalcNode
  42. pluginPath = os.path.dirname(__file__)
  43. WIDGET_ADD_NEW, BASE_ADD_NEW = uic.loadUiType(
  44. os.path.join(pluginPath, 'AddNewExpressionDialog.ui'))
  45. class AddNewExpressionDialog(BASE_ADD_NEW, WIDGET_ADD_NEW):
  46. def __init__(self, expression):
  47. super().__init__()
  48. self.setupUi(self)
  49. self.name = None
  50. self.expression = None
  51. self.txtExpression.setPlainText(expression)
  52. self.buttonBox.rejected.connect(self.cancelPressed)
  53. self.buttonBox.accepted.connect(self.okPressed)
  54. def cancelPressed(self):
  55. self.close()
  56. def okPressed(self):
  57. self.name = self.txtName.text()
  58. self.expression = self.txtExpression.toPlainText()
  59. self.close()
  60. WIDGET_DLG, BASE_DLG = uic.loadUiType(
  61. os.path.join(pluginPath, 'PredefinedExpressionDialog.ui'))
  62. class PredefinedExpressionDialog(BASE_DLG, WIDGET_DLG):
  63. def __init__(self, expression, options):
  64. super().__init__()
  65. self.setupUi(self)
  66. self.filledExpression = None
  67. self.options = options
  68. self.expression = expression
  69. self.variables = set(re.findall(r'\[.*?\]', expression))
  70. self.comboBoxes = {}
  71. for variable in self.variables:
  72. label = QLabel(variable[1:-1])
  73. combo = QComboBox()
  74. for opt in self.options.keys():
  75. combo.addItem(opt)
  76. self.comboBoxes[variable] = combo
  77. self.groupBox.layout().addWidget(label)
  78. self.groupBox.layout().addWidget(combo)
  79. verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
  80. self.groupBox.layout().addItem(verticalSpacer)
  81. self.buttonBox.rejected.connect(self.cancelPressed)
  82. self.buttonBox.accepted.connect(self.okPressed)
  83. def cancelPressed(self):
  84. self.close()
  85. def okPressed(self):
  86. self.filledExpression = self.expression
  87. for name, combo in self.comboBoxes.items():
  88. self.filledExpression = self.filledExpression.replace(name,
  89. self.options[combo.currentText()])
  90. self.close()
  91. WIDGET, BASE = uic.loadUiType(
  92. os.path.join(pluginPath, 'RasterCalculatorWidget.ui'))
  93. class ExpressionWidget(BASE, WIDGET):
  94. _expressions = {"NDVI": "([NIR] - [Red]) / ([NIR] + [Red])"}
  95. def __init__(self, options):
  96. super().__init__(None)
  97. self.setupUi(self)
  98. self.setList(options)
  99. def doubleClicked(item):
  100. self.text.insertPlainText(f'"{self.options[item.text()]}"')
  101. def addButtonText(text):
  102. if any(c for c in text if c.islower()):
  103. self.text.insertPlainText(f" {text}()")
  104. self.text.moveCursor(QTextCursor.PreviousCharacter, QTextCursor.MoveAnchor)
  105. else:
  106. self.text.insertPlainText(f" {text} ")
  107. buttons = [b for b in self.buttonsGroupBox.children()if isinstance(b, QPushButton)]
  108. for button in buttons:
  109. button.clicked.connect(partial(addButtonText, button.text()))
  110. self.listWidget.itemDoubleClicked.connect(doubleClicked)
  111. self.expressions = {}
  112. if os.path.exists(self.expsFile()):
  113. with open(self.expsFile()) as f:
  114. self.expressions.update(json.load(f))
  115. self.expressions.update(self._expressions)
  116. self.fillPredefined()
  117. self.buttonAddPredefined.clicked.connect(self.addPredefined)
  118. self.buttonSavePredefined.clicked.connect(self.savePredefined)
  119. self.text.textChanged.connect(self.expressionValid)
  120. def expressionValid(self):
  121. errorString = ''
  122. testNode = QgsRasterCalcNode.parseRasterCalcString(self.text.toPlainText(), errorString)
  123. if not self.text.toPlainText():
  124. self.expressionErrorLabel.setText(self.tr('Expression is empty'))
  125. self.expressionErrorLabel.setStyleSheet("QLabel { color: black; }")
  126. return False
  127. if testNode:
  128. self.expressionErrorLabel.setText(self.tr('Expression is valid'))
  129. self.expressionErrorLabel.setStyleSheet("QLabel { color: green; font-weight: bold; }")
  130. return True
  131. self.expressionErrorLabel.setText(self.tr('Expression is not valid ') + errorString)
  132. self.expressionErrorLabel.setStyleSheet("QLabel { color : red; font-weight: bold; }")
  133. return False
  134. def expsFile(self):
  135. return os.path.join(userFolder(), 'rastercalcexpressions.json')
  136. def addPredefined(self):
  137. expression = self.expressions[self.comboPredefined.currentText()]
  138. dlg = PredefinedExpressionDialog(expression, self.options)
  139. dlg.exec_()
  140. if dlg.filledExpression:
  141. self.text.setPlainText(dlg.filledExpression)
  142. def savePredefined(self):
  143. exp = self.text.toPlainText()
  144. used = [v for v in self.options.values() if v in exp]
  145. for i, v in enumerate(used):
  146. exp = exp.replace(v, f'[{chr(97 + i)}]')
  147. dlg = AddNewExpressionDialog(exp)
  148. dlg.exec_()
  149. if dlg.name:
  150. self.expressions[dlg.name] = dlg.expression
  151. with open(self.expsFile(), "w") as f:
  152. f.write(json.dumps(self.expressions))
  153. def fillPredefined(self):
  154. self.comboPredefined.clear()
  155. for expression in self.expressions:
  156. self.comboPredefined.addItem(expression)
  157. def setList(self, options):
  158. self.options = options
  159. self.listWidget.clear()
  160. entries = QgsRasterCalculatorEntry.rasterEntries()
  161. def _find_source(name):
  162. for entry in entries:
  163. if entry.ref == name:
  164. return entry.raster.source()
  165. return ''
  166. for name in options.keys():
  167. item = QListWidgetItem(name, self.listWidget)
  168. tooltip = _find_source(name)
  169. if tooltip:
  170. item.setData(Qt.ToolTipRole, tooltip)
  171. self.listWidget.addItem(item)
  172. def setValue(self, value):
  173. self.text.setPlainText(value)
  174. def value(self):
  175. return self.text.toPlainText()
  176. class ExpressionWidgetWrapper(WidgetWrapper):
  177. def _panel(self, options):
  178. return ExpressionWidget(options)
  179. def _get_options(self):
  180. entries = QgsRasterCalculatorEntry.rasterEntries()
  181. options = {}
  182. for entry in entries:
  183. options[entry.ref] = entry.ref
  184. return options
  185. def createWidget(self):
  186. if self.dialogType == DIALOG_STANDARD:
  187. if iface is not None and iface.layerTreeView() is not None and iface.layerTreeView().layerTreeModel() is not None:
  188. iface.layerTreeView().layerTreeModel().dataChanged.connect(self.refresh)
  189. return self._panel(self._get_options())
  190. elif self.dialogType == DIALOG_BATCH:
  191. return QLineEdit()
  192. else:
  193. layers = self.dialog.getAvailableValuesOfType([QgsProcessingParameterRasterLayer], [QgsProcessingOutputRasterLayer])
  194. options = {self.dialog.resolveValueDescription(lyr): f"{self.dialog.resolveValueDescription(lyr)}@1" for lyr in layers}
  195. self.widget = self._panel(options)
  196. return self.widget
  197. def refresh(self, *args):
  198. self.widget.setList(self._get_options())
  199. def setValue(self, value):
  200. if self.dialogType == DIALOG_STANDARD:
  201. pass # TODO
  202. elif self.dialogType == DIALOG_BATCH:
  203. return self.widget.setText(value)
  204. else:
  205. self.widget.setValue(value)
  206. def value(self):
  207. if self.dialogType == DIALOG_STANDARD:
  208. return self.widget.value()
  209. elif self.dialogType == DIALOG_BATCH:
  210. return self.widget.text()
  211. else:
  212. return self.widget.value()
  213. class LayersListWidgetWrapper(WidgetWrapper):
  214. def createWidget(self):
  215. if self.dialogType == DIALOG_BATCH:
  216. widget = BatchInputSelectionPanel(self.parameterDefinition(), self.row, self.col, self.dialog)
  217. widget.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self))
  218. return widget
  219. else:
  220. return None
  221. def setValue(self, value):
  222. if self.dialogType == DIALOG_BATCH:
  223. return self.widget.setText(value)
  224. def value(self):
  225. if self.dialogType == DIALOG_STANDARD:
  226. if self.param.datatype == dataobjects.TYPE_FILE:
  227. return self.param.setValue(self.widget.selectedoptions)
  228. else:
  229. if self.param.datatype == dataobjects.TYPE_RASTER:
  230. options = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance(), False)
  231. elif self.param.datatype == dataobjects.TYPE_VECTOR_ANY:
  232. options = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), [], False)
  233. else:
  234. options = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), [self.param.datatype], False)
  235. return [options[i] for i in self.widget.selectedoptions]
  236. elif self.dialogType == DIALOG_BATCH:
  237. return self.widget.getText()
  238. else:
  239. options = self._getOptions()
  240. values = [options[i] for i in self.widget.selectedoptions]
  241. if len(values) == 0 and not self.parameterDefinition().flags() & QgsProcessingParameterDefinition.FlagOptional:
  242. raise InvalidParameterValue()
  243. return values