| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 | """***************************************************************************    RasterCalculatorWidgets.py    ---------------------    Date                 : November 2016    Copyright            : (C) 2016 by Victor Olaya    Email                : volayaf 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__ = 'Victor Olaya'__date__ = 'November 2016'__copyright__ = '(C) 2016, Victor Olaya'import osfrom functools import partialimport reimport jsonfrom qgis.utils import ifacefrom qgis.PyQt import uicfrom qgis.PyQt.QtCore import Qtfrom qgis.PyQt.QtGui import QTextCursorfrom qgis.PyQt.QtWidgets import (QLineEdit, QPushButton, QLabel,                                 QComboBox, QSpacerItem, QSizePolicy,                                 QListWidgetItem)from qgis.core import (QgsProcessingUtils,                       QgsProcessingParameterDefinition,                       QgsProcessingParameterRasterLayer,                       QgsProcessingOutputRasterLayer,                       QgsProject)from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_BATCHfrom processing.gui.BatchInputSelectionPanel import BatchInputSelectionPanelfrom processing.tools import dataobjectsfrom processing.tools.system import userFolderfrom processing.gui.wrappers import InvalidParameterValuefrom qgis.analysis import QgsRasterCalculatorEntry, QgsRasterCalcNodepluginPath = os.path.dirname(__file__)WIDGET_ADD_NEW, BASE_ADD_NEW = uic.loadUiType(    os.path.join(pluginPath, 'AddNewExpressionDialog.ui'))class AddNewExpressionDialog(BASE_ADD_NEW, WIDGET_ADD_NEW):    def __init__(self, expression):        super().__init__()        self.setupUi(self)        self.name = None        self.expression = None        self.txtExpression.setPlainText(expression)        self.buttonBox.rejected.connect(self.cancelPressed)        self.buttonBox.accepted.connect(self.okPressed)    def cancelPressed(self):        self.close()    def okPressed(self):        self.name = self.txtName.text()        self.expression = self.txtExpression.toPlainText()        self.close()WIDGET_DLG, BASE_DLG = uic.loadUiType(    os.path.join(pluginPath, 'PredefinedExpressionDialog.ui'))class PredefinedExpressionDialog(BASE_DLG, WIDGET_DLG):    def __init__(self, expression, options):        super().__init__()        self.setupUi(self)        self.filledExpression = None        self.options = options        self.expression = expression        self.variables = set(re.findall(r'\[.*?\]', expression))        self.comboBoxes = {}        for variable in self.variables:            label = QLabel(variable[1:-1])            combo = QComboBox()            for opt in self.options.keys():                combo.addItem(opt)            self.comboBoxes[variable] = combo            self.groupBox.layout().addWidget(label)            self.groupBox.layout().addWidget(combo)        verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)        self.groupBox.layout().addItem(verticalSpacer)        self.buttonBox.rejected.connect(self.cancelPressed)        self.buttonBox.accepted.connect(self.okPressed)    def cancelPressed(self):        self.close()    def okPressed(self):        self.filledExpression = self.expression        for name, combo in self.comboBoxes.items():            self.filledExpression = self.filledExpression.replace(name,                                                                  self.options[combo.currentText()])        self.close()WIDGET, BASE = uic.loadUiType(    os.path.join(pluginPath, 'RasterCalculatorWidget.ui'))class ExpressionWidget(BASE, WIDGET):    _expressions = {"NDVI": "([NIR] - [Red]) / ([NIR] + [Red])"}    def __init__(self, options):        super().__init__(None)        self.setupUi(self)        self.setList(options)        def doubleClicked(item):            self.text.insertPlainText(f'"{self.options[item.text()]}"')        def addButtonText(text):            if any(c for c in text if c.islower()):                self.text.insertPlainText(f" {text}()")                self.text.moveCursor(QTextCursor.PreviousCharacter, QTextCursor.MoveAnchor)            else:                self.text.insertPlainText(f" {text} ")        buttons = [b for b in self.buttonsGroupBox.children()if isinstance(b, QPushButton)]        for button in buttons:            button.clicked.connect(partial(addButtonText, button.text()))        self.listWidget.itemDoubleClicked.connect(doubleClicked)        self.expressions = {}        if os.path.exists(self.expsFile()):            with open(self.expsFile()) as f:                self.expressions.update(json.load(f))        self.expressions.update(self._expressions)        self.fillPredefined()        self.buttonAddPredefined.clicked.connect(self.addPredefined)        self.buttonSavePredefined.clicked.connect(self.savePredefined)        self.text.textChanged.connect(self.expressionValid)    def expressionValid(self):        errorString = ''        testNode = QgsRasterCalcNode.parseRasterCalcString(self.text.toPlainText(), errorString)        if not self.text.toPlainText():            self.expressionErrorLabel.setText(self.tr('Expression is empty'))            self.expressionErrorLabel.setStyleSheet("QLabel { color: black; }")            return False        if testNode:            self.expressionErrorLabel.setText(self.tr('Expression is valid'))            self.expressionErrorLabel.setStyleSheet("QLabel { color: green; font-weight: bold; }")            return True        self.expressionErrorLabel.setText(self.tr('Expression is not valid ') + errorString)        self.expressionErrorLabel.setStyleSheet("QLabel { color : red; font-weight: bold; }")        return False    def expsFile(self):        return os.path.join(userFolder(), 'rastercalcexpressions.json')    def addPredefined(self):        expression = self.expressions[self.comboPredefined.currentText()]        dlg = PredefinedExpressionDialog(expression, self.options)        dlg.exec_()        if dlg.filledExpression:            self.text.setPlainText(dlg.filledExpression)    def savePredefined(self):        exp = self.text.toPlainText()        used = [v for v in self.options.values() if v in exp]        for i, v in enumerate(used):            exp = exp.replace(v, f'[{chr(97 + i)}]')        dlg = AddNewExpressionDialog(exp)        dlg.exec_()        if dlg.name:            self.expressions[dlg.name] = dlg.expression        with open(self.expsFile(), "w") as f:            f.write(json.dumps(self.expressions))    def fillPredefined(self):        self.comboPredefined.clear()        for expression in self.expressions:            self.comboPredefined.addItem(expression)    def setList(self, options):        self.options = options        self.listWidget.clear()        entries = QgsRasterCalculatorEntry.rasterEntries()        def _find_source(name):            for entry in entries:                if entry.ref == name:                    return entry.raster.source()            return ''        for name in options.keys():            item = QListWidgetItem(name, self.listWidget)            tooltip = _find_source(name)            if tooltip:                item.setData(Qt.ToolTipRole, tooltip)            self.listWidget.addItem(item)    def setValue(self, value):        self.text.setPlainText(value)    def value(self):        return self.text.toPlainText()class ExpressionWidgetWrapper(WidgetWrapper):    def _panel(self, options):        return ExpressionWidget(options)    def _get_options(self):        entries = QgsRasterCalculatorEntry.rasterEntries()        options = {}        for entry in entries:            options[entry.ref] = entry.ref        return options    def createWidget(self):        if self.dialogType == DIALOG_STANDARD:            if iface is not None and iface.layerTreeView() is not None and iface.layerTreeView().layerTreeModel() is not None:                iface.layerTreeView().layerTreeModel().dataChanged.connect(self.refresh)            return self._panel(self._get_options())        elif self.dialogType == DIALOG_BATCH:            return QLineEdit()        else:            layers = self.dialog.getAvailableValuesOfType([QgsProcessingParameterRasterLayer], [QgsProcessingOutputRasterLayer])            options = {self.dialog.resolveValueDescription(lyr): f"{self.dialog.resolveValueDescription(lyr)}@1" for lyr in layers}            self.widget = self._panel(options)            return self.widget    def refresh(self, *args):        self.widget.setList(self._get_options())    def setValue(self, value):        if self.dialogType == DIALOG_STANDARD:            pass  # TODO        elif self.dialogType == DIALOG_BATCH:            return self.widget.setText(value)        else:            self.widget.setValue(value)    def value(self):        if self.dialogType == DIALOG_STANDARD:            return self.widget.value()        elif self.dialogType == DIALOG_BATCH:            return self.widget.text()        else:            return self.widget.value()class LayersListWidgetWrapper(WidgetWrapper):    def createWidget(self):        if self.dialogType == DIALOG_BATCH:            widget = BatchInputSelectionPanel(self.parameterDefinition(), self.row, self.col, self.dialog)            widget.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self))            return widget        else:            return None    def setValue(self, value):        if self.dialogType == DIALOG_BATCH:            return self.widget.setText(value)    def value(self):        if self.dialogType == DIALOG_STANDARD:            if self.param.datatype == dataobjects.TYPE_FILE:                return self.param.setValue(self.widget.selectedoptions)            else:                if self.param.datatype == dataobjects.TYPE_RASTER:                    options = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance(), False)                elif self.param.datatype == dataobjects.TYPE_VECTOR_ANY:                    options = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), [], False)                else:                    options = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), [self.param.datatype], False)                return [options[i] for i in self.widget.selectedoptions]        elif self.dialogType == DIALOG_BATCH:            return self.widget.getText()        else:            options = self._getOptions()            values = [options[i] for i in self.widget.selectedoptions]            if len(values) == 0 and not self.parameterDefinition().flags() & QgsProcessingParameterDefinition.FlagOptional:                raise InvalidParameterValue()            return values
 |