Exemple #1
0
 def validate(self):
     """
     Verifies contents displayed on mapping table in order to infer its validity
     as datasource conversion map.
     :return: (bool) map validity status.
     """
     # validate map
     msg = self.invalidatedReason()
     if msg:
         # if an invalidation reason was given, warn user and nothing else.
         msgBar = QgsMessageBar(self)
         # if window is resized, msgBar stays, not ideal, but works for now
         # maybe we should connect to some parent resizing signal or something...
         msgBar.resize(
             QSize(self.geometry().size().width(),
                   msgBar.geometry().height()))
         msgBar.pushMessage(self.tr('Warning!'),
                            msg,
                            level=Qgis.Warning,
                            duration=5)
         QgsMessageLog.logMessage(msg, 'DSGTools Plugin', Qgis.Critical)
     return msg == ''
Exemple #2
0
class FMEManagerWidget(QWidget, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(FMEManagerWidget, self).__init__(parent=parent)
        self.setupUi(self)
        self._workspaceList = list()
        self.paramWidgetMap = dict()
        self.versionComboBox.addItems(["v1", "v2"])
        self.messageBar = QgsMessageBar(self)

    def clearLayout(self):
        """
        Removes all inserted widgets from workspace's parameters from GUI.
        """
        for i in range(self.verticalLayout_2.count()-1, -1, -1):
            self.verticalLayout_2.itemAt(i).widget().setParent(None)

    @pyqtSlot(int)
    def on_workspaceComboBox_currentIndexChanged(self):
        """
        Fetch necessary parameters from selected workspace and insert a
        QLineEdit for each parameter.
        """
        self.clearLayout()
        workspace = self.getCurrentWorkspace()
        key = "parameters" if self.version() == "v1" else "parametros"
        _parameters =  workspace.get(key, list())
        parameters = filter(lambda x: x != "LOG_FILE", _parameters)
        for parameter in parameters:
            newLabel = QLabel(parameter)
            self.verticalLayout_2.addWidget(newLabel)
            newLineEdit = QLineEdit()
            self.paramWidgetMap[parameter] = newLineEdit
            self.verticalLayout_2.addWidget(newLineEdit)

    def resizeEvent(self, e):
        """
        Resize QgsMessageBar to widget's width
        """
        self.messageBar.resize(
            QSize(
                self.geometry().size().width(),
                30
            )
        )

    def getProxyInfo(self):
        """
        Reads Proxy settings as registered on QGIS settings.
        :return: (tuple) the QGIS proxy mapping per schema and its
                 authentication object.
        """
        if self.useProxy():
            return Utils().get_proxy_config()
        else:
            return (None, None)

    def version(self):
        """
        Identifies the selected FME Manager from GUI.
        :return: (str) FME Manager selected version.
        """
        return self.versionComboBox.currentText()

    def server(self):
        """
        Identifies server provided by user from GUI.
        :return: (str) server from which FME routines are read from.
        """
        return self.serverLineEdit.text()

    def useSsl(self):
        """
        Identifies whether user intends to use SSL to request routines from FME
        Manager server.
        :return: (bool) whether SSL is going to be used as from GUI.
        """
        return self.sslCheckBox.isChecked()

    def useProxy(self):
        """
        Identifies whether user intends to connect to FME Manager server behind
        a proxy.
        :return: (bool) whether Proxy is set. Proxy setup is read from
                 QSettings (QGIS settings).
        """
        return self.sslCheckBox.isChecked()

    def getWorkspacesFromServer(self):
        """
        Reads all available workspaces from a filled server.
        :return: (list-of-dict) list of available workspaces and its metadata.
        """
        if self.version() == "v1":
            url = "{server}/versions?last=true".format(server=self.server())
            jsonKey = "data"
        else:
            url = "{server}/api/rotinas".format(server=self.server())
            jsonKey = "dados"
        try:
            if not self.useProxy():
                workspaceList = requests.get(
                    url,
                    timeout=8
                ).json()[jsonKey]
            else:
                proxyInfo, proxyAuth = self.getProxyInfo()
                workspaceList = requests.get(
                    url,
                    proxies=proxyInfo,
                    auth=proxyAuth,
                    timeout=8
                ).json()[jsonKey]
        except ReadTimeout:
            self.messageBar.pushMessage(
                self.tr("Connection timed out."), level=Qgis.Warning)
            workspaceList = list()
        except ConnectTimeout:
            self.messageBar.pushMessage(
                self.tr("Connection timed out (max attempts)."),
                level=Qgis.Warning
            )
            workspaceList = list()
        except InvalidSchema:
            self.messageBar.pushMessage(
                self.tr("Missing connection schema (e.g.'http', etc)."),
                level=Qgis.Warning
            )
            workspaceList = list()
        except BaseException as e:
            self.messageBar.pushMessage(
                self.tr("Unexpected error while trying to reach server. "
                        "Check your parameters. Error message: {}".format(e)),
                level=Qgis.Warning
            )
            workspaceList = list()
        return workspaceList

    def setWorkspaces(self, workspaces):
        """
        Sets a list of workspaces to the GUI.
        :param workspaces: (list-of-str) 
        """
        self.workspaceComboBox.clear()
        if workspaces:
            self.workspaceComboBox.addItems(workspaces)

    def getCurrentWorkspace(self):
        """
        Reads currently selected workspace from GUI.
        :return: (dict) selected workspace's metadata map.
        """
        idx = self.workspaceComboBox.currentIndex()
        try:
            return self._workspaceList[idx]
        except IndexError:
            return dict()

    @pyqtSlot(bool)
    def on_loadPushButton_clicked(self):
        """
        Sync available workspaces from server and display
        these workspaces on workspaceComboBox
        """
        self.workspaceComboBox.clear()
        self._workspaceList = self.getWorkspacesFromServer()
        if self.version() == "v1":
            workspaceNameKey = "workspace_name"
            workspaceDescKey = "workspace_description"
        else:
            workspaceNameKey = "rotina"
            workspaceDescKey = "descricao"
        for workspace in self._workspaceList:
            self.workspaceComboBox.addItem(
                "{name} ({description})".format(
                    name=workspace[workspaceNameKey],
                    description=workspace[workspaceDescKey]
                )
            )

    def validate(self, pushAlert=False):
        """
        Validates fields. Returns True if all information are filled correctly.
        :param pushAlert: (bool) whether invalidation reason should be
                          displayed on the widget.
        :return: (bool) whether set of filled parameters if valid.
        """
        proxyInfo, _ = self.getProxyInfo()
        if self.useProxy() and not proxyInfo:
            if pushAlert:
                self.messageBar.pushMessage(
                    self.tr("Proxy usage is set but no QGIS proxy settings "
                            "was found."),
                    level=Qgis.Warning
                )
            return False
        if self.server() == "":
            if pushAlert:
                self.messageBar.pushMessage(
                    self.tr("URL to FME Manager server was not provided."),
                    level=Qgis.Warning
                )
            return False
        return True

    def getParameters(self):
        """
        Returns necessary parameters for running the algorithm
        """
        workspace = self.getCurrentWorkspace()
        workspaceId = workspace.get("id", None)
        version = self.version()
        parameters = {
            "parameters" if version == "v1" else "parametros": {
                key: value.text() for key, value in self.paramWidgetMap.items()
            }
        }
        proxyInfo, proxyAuth = self.getProxyInfo()
        return {
            "version": version,
            "server": self.server(),
            "workspace_id": workspaceId,
            "parameters": parameters,
            "auth": proxyAuth,
            "proxy_dict": proxyInfo,
            "use_ssl": self.useSsl(),
            "use_proxy": self.useProxy()
        }
Exemple #3
0
class ButtonSetupWidget(QDialog, FORM_CLASS):
    def __init__(self, parent=None, buttonSetup=None):
        """
        Class constructor.
        :param parent: (QtWidgets.*) any widget that 'contains' this tool.
        :param buttonSetup: (CustomButtonSetup) object that handles all
                            buttons displayed and configured through this GUI.
        """
        super(ButtonSetupWidget, self).__init__(parent)
        self.setupUi(self)
        self.messageBar = QgsMessageBar(self)
        self.setup = CustomButtonSetup()
        if buttonSetup:
            self.setSetup(buttonSetup)
        self.buttonComboBox.addItem(self.tr("No button selected"))
        self.tableWidget.verticalHeader().sectionDoubleClicked.connect(
            self.setButtonFromRow)
        bEnabled = self.buttonComboBox.currentIndex() > 0
        for w in ("savePushButton", "undoPushButton", "removePushButton",
                  "buttonPropWidget"):
            getattr(self, w).setEnabled(bEnabled)

        # making the button selection to stand out a little bit
        if "Night Mapping" in app.activeThemePath():
            ss = """QHeaderView::section:checked
                { color:black; background-color:white; }"""
        else:
            ss = """QHeaderView::section:checked
                { color:gray; background-color:black; }"""
        self.tableWidget.setStyleSheet(ss)

    def raiseWarning(self, msg, title=None, lvl=None, duration=None):
        """
        Raises a warning message to the user on a message bar and logs it to
        QGIS logger.
        :param msg: (str) message to be displayed.
        :param title: (str) pre-message on the warning bar.
        :param lvl: (int) warning level enumerator as from Qgis module.
        :param duration: (int) warning message display time.
        """
        self.messageBar.pushMessage(title or self.tr('Invalid workflow'),
                                    msg,
                                    level=lvl or Qgis.Warning,
                                    duration=duration or 5)
        # msg = self.tr("Buttons setup definion invalid: {m}").format(m=msg)
        QgsMessageLog.logMessage(msg, 'DSGTools Plugin', Qgis.Warning)

    def resizeEvent(self, e):
        """
        Reimplementation in order to use this window's resize event.
        On this object, this method makes sure that message bar is always the
        same size as the window.
        :param e: (QResizeEvent) resize event.
        """
        self.messageBar.resize(
            QSize(
                self.geometry().size().width(),
                40  # this felt nicer than the original height (30)
            ))

    def clear(self):
        """
        Clears all data filled into GUI.
        """
        self.buttonComboBox.clear()
        for button in self.registeredButtonNames():
            self.removeButtonFromTable(self.getButtonByName(button))
            self.setup.removeButton(button)
            self.buttonComboBox.removeItem(
                self.buttonComboBox.findText(button))

    def setSetupName(self, name):
        """
        Defines setup's name on GUI.
        :param name: (str) name to be set.
        """
        self.setupNameLineEdit.setText(name)

    def setupName(self):
        """
        Retrieves button's setup name read from GUI.
        :return: (str) name for button's setup.
        """
        return self.setupNameLineEdit.text()

    def setCurrentSetupName(self, name):
        """
        Defines current button's setup name.
        :param name: (str) name for button's setup.
        """
        return self.setup.setName(name)

    def currentSetupName(self):
        """
        Retrieves current button's setup name.
        :param name: (str) name for button's setup.
        """
        return self.setup.name()

    def setDescription(self, desc):
        """
        Defines setup's description on GUI.
        :param desc: (str) description to be set.
        """
        self.textEdit.setText(desc)

    def description(self):
        """
        Reads button's setup description from GUI.
        :return: (str) description for button's setup.
        """
        return self.textEdit.toPlainText()

    def setCurrentDescription(self, name):
        """
        Defines current button's setup description.
        :param name: (str) description for button's setup.
        """
        self.setup.setDescription(name)

    def currentDescription(self):
        """
        Retrieves current button's description.
        :param name: (str) description for button's setup.
        """
        return self.setup.description()

    def setDynamicShortcut(self, ds):
        """
        Defines setup's dynamic shortcut option on GUI.
        :param ds: (bool) dynamic shortcut assignment option.
        """
        self.dsCheckBox.setChecked(ds)

    def dynamicShortcut(self):
        """
        Retrieves button's setup dynamic shortcut option read from GUI.
        :param ds: (bool) dynamic shortcut assignment option.
        """
        return self.dsCheckBox.isChecked()

    def setCurrentDynamicShortcut(self, ds):
        """
        Defines current button's setup dynamic shortcut option.
        :param ds: (bool) dynamic shortcut assignment option.
        """
        self.setup.setDynamicShortcut(ds)

    def currentDynamicShortcut(self):
        """
        Retrieves current button's description.
        :return: (bool) dynamic shortcut assignment option.
        :param name: (str) description for button's setup.
        """
        return self.setup.dynamicShortcut()

    def readSetup(self):
        """
        Reads setup from GUI.
        :return: (CustomButtonSetup) reads all data from GUI as a setup.
        """
        s = CustomButtonSetup()
        s.setName(self.setupName())
        s.setDescription(self.description())
        for row in range(self.tableWidget.rowCount()):
            s.addButton(self.buttonFromRow(row).properties())
        s.setDynamicShortcut(self.dynamicShortcut())
        return s

    def setSetup(self, newSetup):
        """
        Imports buttons setup definitions from another buttons setup.
        :param newSetup: (CustomButtonSetup) setup to be imported. 
        """
        self.buttonComboBox.blockSignals(True)
        self.setSetupName(newSetup.name())
        self.setCurrentSetupName(newSetup.name())
        self.setDescription(newSetup.description())
        self.setCurrentDescription(newSetup.description())
        self.setDynamicShortcut(newSetup.dynamicShortcut())
        self.setCurrentDynamicShortcut(newSetup.dynamicShortcut())
        for button in newSetup.buttons():
            self.addButton(button)
        self.buttonComboBox.blockSignals(False)

    def getButtonByName(self, name):
        """
        Retrieves a registered button from its name.
        :param name: (str) name for the requested button.
        """
        return self.setup.button(name)

    def setButtonName(self, name):
        """
        Sets button name to GUI.
        :param name: (str) name to be set to GUI.
        """
        self.buttonPropWidget.setButtonName(name)

    def buttonName(self):
        """
        Reads button name from GUI.
        :return: (str) button name read from GUI.
        """
        return self.buttonPropWidget.buttonName()

    def registeredButtonNames(self):
        """
        All names for registered buttons on current profile.
        :return: (list-of-str) list of button names.
        """
        return self.setup.buttonNames()

    def setDigitizingTool(self, tool):
        """
        Sets button's digitizing tool to GUI.
        :param tool: (str) a supported digitizing tool to be set.
        """
        self.buttonPropWidget.setDigitizingTool(tool)

    def digitizingTool(self):
        """
        Reads current digitizing tool from GUI.
        :return: (str) current digitizing tool.
        """
        return self.buttonPropWidget.digitizingTool()

    def setUseColor(self, useColor):
        """
        Sets button's digitizing tool to GUI.
        :param useColor: (bool) whether button should use a custom color
                         palette.
        """
        self.buttonPropWidget.setUseColor(useColor)

    def useColor(self):
        """
        Reads whether button will have a custom color from GUI.
        :return: (bool) whether button should use a custom color
                        palette.
        """
        return self.buttonPropWidget.useColor()

    def setColor(self, color):
        """
        Sets custom color to the color widget.
        :param color: (str/tuple) color to be set.
        """
        self.buttonPropWidget.setColor(color)

    def color(self):
        """
        Reads custom color to be set to widget as read from GUI.
        :return: (tuple) color to be used.
        """
        return self.buttonPropWidget.color()

    def setUseToolTip(self, useToolTip):
        """
        Sets button's digitizing tool to GUI.
        :param useToolTip: (bool) whether button will have a tool tip assigned.
        """
        self.buttonPropWidget.setUseToolTip(useToolTip)

    def useToolTip(self):
        """
        Reads if the button will have a tool tip assigned to it from GUI.
        :return: (bool) whether the button will have a tool tip assigned.
        """
        return self.buttonPropWidget.useToolTip()

    def setToolTip(self, tooltip):
        """
        Sets a tool tip for the active button widget.
        :param tooltip: (str) tool tip to be set.
        """
        self.buttonPropWidget.setToolTip(tooltip)

    def toolTip(self):
        """
        Reads the tool tip for the button from GUI.
        :param tooltip: (str) tool tip to be used.
        """
        return self.buttonPropWidget.toolTip()

    def setUseCategory(self, useCat):
        """
        Sets whether button should be assigned a category/group on GUI.
        :param useCat: (bool) whether button will have a category assigned.
        """
        self.buttonPropWidget.setUseCategory(useCat)

    def useCategory(self):
        """
        Reads button's category/group from GUI.
        :return: (bool) whether button will have a category assigned.
        """
        return self.buttonPropWidget.useCategory()

    def setCategory(self, cat):
        """
        Assigns a group to the active button.
        :param cat: (str) category to be set.
        """
        self.buttonPropWidget.setCategory(cat)

    def category(self):
        """
        Reads the assigned category/group to the active button from GUI.
        :return: (str) category to be used.
        """
        return self.buttonPropWidget.category()

    def setUseKeywords(self, useKw):
        """
        Sets whether active button should have keywords for button searching.
        :param useKw: (bool) whether button will have keywords assigned to it.
        """
        self.buttonPropWidget.setUseKeywords(useKw)

    def useKeywords(self):
        """
        Reads whether active button should have keywords for button searching
        from GUI.
        :return: (bool) whether button will have keywords assigned to it.
        """
        return self.buttonPropWidget.useKeywords()

    def setKeywords(self, kws):
        """
        Sets button's keywords for button searching.
        :param kws: (set-of-str) set of keywords to be assigned to the button.
        """
        self.buttonPropWidget.setKeywords(kws)

    def keywords(self):
        """
        Reads button's keywords for button searching from GUI.
        :return: (set-of-str) set of keywords to be assigned to the button.
        """
        return self.buttonPropWidget.keywords()

    def setUseShortcut(self, useShortcut):
        """
        Sets whether active button should have a shortcut assigned to it.
        :param useShortcut: (bool) whether button will have a shortcut assigned.
        """
        self.buttonPropWidget.setUseShortcut(useShortcut)

    def useShortcut(self):
        """
        Reads whether active button should have a shortcut assigned to it from GUI.
        :return: (bool) whether button will have a shortcut assigned.
        """
        return self.buttonPropWidget.useShortcut()

    def setShortcurt(self, s, autoReplace):
        """
        Assigns a shortcut to trigger active button's action.
        :param s: (str) new shortcut to be set.
        :param autoReplace: (bool) whether a confirmation from the user is
                            necessary in order to replace existing shortcuts.
        """
        self.buttonPropWidget.setShortcurt(s, autoReplace)

    def shortcut(self):
        """
        Assigned shortcut read from GUI.
        :return: (str) shortcut to be used.
        """
        return self.buttonPropWidget.shorcut()

    def setOpenForm(self, openForm):
        """
        Defines whether (re)classification tool will open feature form while
        being used.
        :param openForm: (bool) whether feature form should be opened.
        """
        self.buttonPropWidget.setOpenForm(openForm)

    def openForm(self):
        """
        Defines whether (re)classification tool will open feature form while
        being used.
        :return: (bool) whether feature form should be opened.
        """
        return self.buttonPropWidget.openForm()

    def setAttributeMap(self, attrMap):
        """
        Sets the attribute value map for current button to GUI.
        :param attrMap: (dict) a map from each field and its value to be set. 
        """
        self.buttonPropWidget.setAttributeMap(attrMap)

    def attributeMap(self):
        """
        Reads the field map data and set it to a button attribute map format.
        :return: (dict) read attribute map. 
        """
        return self.buttonPropWidget.attributeMap()

    def setLayer(self, layer):
        """
        Sets current layer selection on GUI.
        :param layer: (str) name for the layer to be set.
        """
        self.buttonPropWidget.setLayer(layer)

    def layer(self):
        """
        Reads current layer selection from GUI.
        :return: (str) name for the selected layer.
        """
        self.buttonPropWidget.layer()

    def currentButton(self):
        """
        Retrives button active on GUI that has a saved state (on its last saved
        state).
        :return: (CustomFeatureButton) current button.
        """
        return self.buttonPropWidget.currentButton()

    def readButton(self):
        """
        Reads current data on GUI and gets a button with those properties.
        :return: (CustomFeatureButton) current button as from GUI.
        """
        return self.buttonPropWidget.readButton()

    def buttonIsModified(self):
        """
        Checks whether current button is modified.
        :return: (bool) whether button is modified.
        """
        return self.currentButton() != self.readButton()

    def updateButton(self, buttonName, newProps):
        """
        Updates a registered button with a new set of properties.
        :param button: (str) name for the button to be updated.
        :param newProps: (dict) new set of properties.
        """
        self.setup.updateButton(buttonName, newProps)

    def confirmAction(self, msg, title=None, showNo=True):
        """
        Raises a message box that asks for user confirmation.
        :param msg: (str) message requesting for confirmation to be shown.
        :param showNo: (bool) whether No button should be exposed.
        :return: (bool) whether action was confirmed.
        """
        return self.buttonPropWidget.confirmAction(msg, title, showNo)

    def validate(self):
        """
        Validates current input data, giving invalidation reason.
        :return: (str) invalidation reason.
        """
        if self.setupName() == "":
            return self.tr("No name provided for current setup.")
        buttons = self.readSetup().buttons()
        if not buttons:
            return self.tr("Please register at least one button.")
        # for button in buttons:
        #     # validate attribute map
        #     pass
        return ""

    def isValid(self):
        """
        Validates current input data, giving invalidation reason.
        :return: (bool) current input data validity.
        """
        if self.setupName() == "":
            return False
        buttons = self.readSetup().buttons()
        if not buttons:
            return False
        # for button in buttons:
        #     # validate attribute map
        #     pass
        return True

    @pyqtSlot(int, name="on_buttonComboBox_currentIndexChanged")
    def setCurrentButton(self, button):
        """
        Sets button properties to the GUI.
        :param button:  (CustomFeatureButton) button to be set to the GUI.
        """
        if isinstance(button, int):
            if button <= 0:
                button = CustomFeatureButton()
                button.setName("")
            else:
                # table row is less 1 due to "no button" option
                button = self.buttonFromRow(button - 1)
        if button.name() not in self.registeredButtonNames():
            # create a new one with that button?
            pass
        self.buttonComboBox.setCurrentText(button.name())
        bEnabled = self.buttonComboBox.currentIndex() > 0
        for w in ("savePushButton", "undoPushButton", "removePushButton",
                  "buttonPropWidget"):
            getattr(self, w).setEnabled(bEnabled)
        self.buttonPropWidget.setButton(button)

    @pyqtSlot(bool, name="on_savePushButton_clicked")
    def updateCurrentButton(self, props=None):
        """
        Current data will be stored as current button's properties.
        :param props: (dict) a map to button's properties to be updated.
        """
        if isinstance(props, bool) or props is None:
            # if button pressing was the triggering event, current data will be
            # store into current button
            props = self.readButton().properties()
        prevName = self.currentButton().name()
        button = self.getButtonByName(prevName)
        self.updateButton(prevName, props)
        newName = button.name()
        self.buttonPropWidget.button = button
        if prevName != newName:
            idx = self.buttonComboBox.findText(prevName)
            self.buttonComboBox.setItemText(idx, newName)
        self.setCurrentButton(button)

    @pyqtSlot(bool, name="on_undoPushButton_clicked")
    def undoButtonModifications(self):
        """
        Restores stored data from current button and sets it to GUI.
        """
        self.buttonPropWidget.setButton(self.currentButton())

    @pyqtSlot(bool, name="on_addPushButton_clicked")
    def addButton(self, button=None):
        """
        Adds a button to the setup.
        :param button: (CustomFeatureButton) a pre-existent button to be set.
        :return: (CustomFeatureButton) added button.
        """
        if button is not None and not isinstance(button, bool):
            buttonName = button.name()
            if buttonName in self.registeredButtonNames():
                msg = self.tr("Button {b} already exists. Would you like to "
                              "replace it?").format(b=buttonName)
                cnf = self.confirmAction(msg,
                                         self.tr("Replace existing button"))
                if not cnf:
                    return self.getButtonByName(buttonName)
                self.updateButton(buttonName, button.properties())
            else:
                props = button.properties()
                button = self.setup.newButton()
                self.buttonComboBox.addItem(buttonName)
                self.updateButton(button.name(), props)
            # we want the button passed by reference from setup
            button = self.getButtonByName(buttonName)
        else:
            button = self.setup.newButton()
            self.buttonComboBox.addItem(button.name())
        self.setCurrentButton(button)
        self.addButtonToTable(button)
        return button

    @pyqtSlot(bool, name="on_removePushButton_clicked")
    def removeButton(self):
        """
        Removes the current button from setup.
        """
        name = self.buttonName()
        txt = self.tr("Confirm button '{b}' removal?").format(b=name)
        if name == "":
            # ignore the "Select a button..."
            return
        self.removeButtonFromTable(self.currentButton())
        self.setup.removeButton(name)
        self.buttonComboBox.removeItem(self.buttonComboBox.findText(name))

    def buttonFromRow(self, row):
        """
        Retrieves the button object from table row.
        :param row: (int) target row to get its button.
        :return: (CustomFeatureButton) retrieved button.
        """
        # combo box includes "no button", table does not -> row + 1
        return self.getButtonByName(self.buttonComboBox.itemText(row + 1))

    def setButtonFromRow(self, row):
        """
        Fills GUI with a button's properties from its row.
        :param row: (int) target row to get its button.
        """
        b = self.getButtonByName(self.buttonComboBox.itemText(row + 1))
        self.setCurrentButton(b)

    def addButtonToTable(self, button):
        """
        Adds widget to table widget.
        :param button: (CustomFeatureButton) button to have its widget added.
        """
        row = self.tableWidget.rowCount()
        self.tableWidget.insertRow(row)
        self.tableWidget.setCellWidget(row, 0, button.newWidget())

    def removeButtonFromTable(self, button):
        """
        Removes the button widget from buttons table.
        :param button: (CustomFeatureButton) button to be removed.
        """
        name = button.displayName()
        for row in range(self.tableWidget.rowCount()):
            bName = self.tableWidget.cellWidget(row, 0).text().replace("&", "")
            if bName == name:
                self.tableWidget.removeRow(row)
                return

    def readButtonTable(self):
        """
        Reads all registered buttons from button table and returns it, in
        order.
        :return: (list-of-CustomFeature) ordered buttons.
        """
        buttons = list()
        count = self.tableWidget.rowCount()
        if count > 0:
            for row in range(count):
                buttons.append(
                    self.tableWidget.cellWidget(row, 0).text()\
                        .rsplit(" [", 1)[0].replace("&", "")
                ) # Qt mnemonic shortcut for it widgets introduces "&"...
        return buttons

    def buttonsOrder(self):
        """
        Retrieves button order to be used for button setup.
        :return: (dict) a map from button name to its position on GUI.
        """
        buttons = dict()
        for row in range(self.tableWidget.rowCount()):
            button = self.tableWidget.cellWidget(row, 0).text()\
                         .rsplit(" [", 1)[0].replace("&", "")
            buttons[button] = row
        return buttons

    def selectedIndexes(self):
        """
        :return: (list-of-QModelIndex) table's selected indexes.
        """
        return self.tableWidget.selectedIndexes()

    def selectedRows(self, reverseOrder=False):
        """
        List of all rows that have selected items on the table.
        :param reverOrder: (bool) indicates if the row order is reversed.
        :return: (list-of-int) ordered list of selected rows' indexes.
        """
        rows = self.tableWidget.selectionModel().selectedRows()
        return sorted(set(i.row() for i in rows), reverse=reverseOrder)

    def selectedColumns(self, reverseOrder=False):
        """
        List of all columns that have selected items on the table.
        :param reverOrder: (bool) indicates if the column order is reversed.
        :return: (list-of-int) ordered list of selected columns' indexes.
        """
        return sorted(set(i.column() for i in self.selectedIndexes()),
                      reverse=reverseOrder)

    def selectRow(self, row):
        """
        Clears all selected rows and selects row.
        :param row: (int) index for the row to be select.
        """
        self.clearRowSelection()
        self.addRowToSelection(row)

    def addRowToSelection(self, row):
        """
        Adds a row to selection.
        :param row: (int) index for the row to be added to selection.
        """
        if row not in self.selectedRows():
            self.tableWidget.setSelectionMode(QAbstractItemView.MultiSelection)
            self.tableWidget.selectRow(row)
            self.tableWidget.setSelectionMode(
                QAbstractItemView.ExtendedSelection)

    def removeRowFromSelection(self, row):
        """
        Removes a row from selection.
        :param row: (int) index for the row to be removed from selection.
        """
        if row in self.selectedRows():
            self.tableWidget.setSelectionMode(QAbstractItemView.MultiSelection)
            self.tableWidget.selectRow(row)
            self.tableWidget.setSelectionMode(
                QAbstractItemView.ExtendedSelection)

    def clearRowSelection(self):
        """
        Removes all selected rows from selection.
        """
        for row in self.selectedRows():
            self.removeRowFromSelection(row)

    def moveRowUp(self, row):
        """
        Moves a row one position up, if possible.
        :param row: (int) row be moved.
        """
        if row <= 0:
            return
        button = self.buttonFromRow(row)
        upperButton = self.buttonFromRow(row - 1)
        self.tableWidget.setCellWidget(row - 1, 0, button.newWidget())
        self.buttonComboBox.setItemText(row, button.name())
        self.tableWidget.setCellWidget(row, 0, upperButton.newWidget())
        self.buttonComboBox.setItemText(row + 1, upperButton.name())
        self.addRowToSelection(row - 1)
        self.removeRowFromSelection(row)

    def moveRowDown(self, row):
        """
        Moves a row one position up, if possible.
        :param row: (int) row be moved.
        """
        if row >= self.tableWidget.rowCount() - 1:
            return
        button = self.buttonFromRow(row)
        lowerButton = self.buttonFromRow(row + 1)
        self.tableWidget.setCellWidget(row + 1, 0, button.newWidget())
        self.buttonComboBox.setItemText(row + 1 + 1, button.name())
        self.tableWidget.setCellWidget(row, 0, lowerButton.newWidget())
        self.buttonComboBox.setItemText(row + 1, lowerButton.name())
        self.addRowToSelection(row + 1)
        self.removeRowFromSelection(row)

    @pyqtSlot()
    def on_moveUpPushButton_clicked(self):
        """
        Method triggered when move row up button is clicked.
        """
        rows = self.selectedRows()
        if not rows:
            return
        for row in self.selectedRows():
            if row - 1 in rows:
                # rows is a copy of selected rows that is updated after the
                # item is moved
                continue
            self.moveRowUp(row)
            if row != 0:
                # this row is never aftected, hence it is "fixed"
                rows.remove(row)
        row = max(self.selectedRows())
        self.setCurrentButton(self.buttonFromRow(row))

    @pyqtSlot()
    def on_moveDownPushButton_clicked(self):
        """
        Method triggered when move row down button is clicked.
        """
        rows = self.selectedRows(True)
        if not rows:
            return
        lastRow = self.tableWidget.rowCount() - 1
        for row in self.selectedRows(True):
            if row + 1 in rows:
                continue
            self.moveRowDown(row)
            if row != lastRow:
                rows.remove(row)
        row = max(self.selectedRows())
        self.setCurrentButton(self.buttonFromRow(row))

    @pyqtSlot()
    def on_okPushButton_clicked(self):
        """
        Closes setup dialog and returns a confirmation code.
        :return: (int) confirmation code.
        """
        if not self.isValid():
            msg = self.tr("Invalid input data: {r}").format(r=self.validate())
            self.raiseWarning(msg)
            return
        msg = self.tr("Current button has been modified and not saved. Would "
                      "you like to save it?")
        title = self.tr("Unsaved modifications")
        if self.buttonIsModified() and self.confirmAction(msg, title):
            self.updateCurrentButton()
        self.setCurrentSetupName(self.setupName())
        self.setCurrentDescription(self.description())
        self.setCurrentDynamicShortcut(self.dynamicShortcut())
        self.done(1)

    @pyqtSlot()
    def on_cancelPushButton_clicked(self):
        """
        Closes setup dialog and returns a refusal code.
        :return: (int) confirmation code.
        """
        self.done(0)

    def state(self):
        """
        Exports current setup's state as read from the GUI.
        :return: (dict) a map to tool's current state.
        """
        state = self.readSetup().state()
        # buttons' keywords are stored as sets, which are not seriallizable
        for idx, props in enumerate(state["buttons"]):
            kws = props["keywords"]
            state["buttons"][idx]["keywords"] = tuple(kws)
        return {"state": state, "order": self.buttonsOrder()}

    def setState(self, state):
        """
        Restores the GUI to a given state.
        :param state: (dict) a map to tool's state.
        """
        self.setup.setState(state)

    @pyqtSlot(bool, name="on_importPushButton_clicked")
    def importSetup(self):
        """
        Imports a setup from a file.
        """
        fd = QFileDialog()
        filename = fd.getOpenFileName(
            caption=self.tr("Import a DSGTools Button Setup (set of buttons)"),
            filter=self.tr("DSGTools Buttons Setup (*.setup)"))
        filename = filename[0] if isinstance(filename, tuple) else filename
        if not filename:
            return
        with open(filename, "r", encoding="utf-8") as fp:
            state = json.load(fp)
            order = [b[0] for b in \
                        sorted(state["order"].items(), key=lambda i: i[1])]
            state = state["state"]
        # buttons' keywords are stored as tuple in order to be seriallizable
        for idx, props in enumerate(state["buttons"]):
            kws = props["keywords"]
            state["buttons"][idx]["keywords"] = set(kws)
            # tuples and list are misinterpreted when exported
            col = props["color"]
            state["buttons"][idx]["color"] = tuple(col)
        self.clear()
        self.setState(state)
        self.buttonComboBox.blockSignals(True)
        self.buttonComboBox.addItem(self.tr("No button selected"))
        self.buttonComboBox.addItems(order)
        for btn in order:
            self.addButtonToTable(self.setup.button(btn))
        self.buttonComboBox.blockSignals(False)
        if order:
            self.setCurrentButton(self.setup.button(order[0]))
        else:
            self.buttonComboBox.setCurrentIndex(0)
            self.setCurrentButton(None)
        self.setSetupName(self.setup.name())
        self.setDescription(self.setup.description())
        self.setDynamicShortcut(self.setup.dynamicShortcut())
        msg = self.tr('Setup "{0}" imported from "{1}"')\
                  .format(self.setup.name(), filename)
        self.raiseWarning(msg,
                          title=self.tr("Imported workflow"),
                          lvl=Qgis.Success)

    @pyqtSlot(bool, name="on_exportPushButton_clicked")
    def exportSetup(self):
        """
        Exports current setup's saved state to a file.
        :return: (bool) whether setup was exported.
        """
        if not self.isValid():
            msg = self.tr("Invalid input data: {r}").format(r=self.validate())
            self.raiseWarning(msg)
            return False
        # add check of modified button in here
        s = self.readSetup()
        fd = QFileDialog()
        filename = fd.getSaveFileName(
            caption=self.tr("Export setup - {0}").format(s.name()),
            filter=self.tr("DSGTools Buttons Setup (*.setup)"))
        filename = filename[0] if isinstance(filename, tuple) else filename
        if not filename:
            return False
        with open(filename, "w", encoding="utf-8") as fp:
            fp.write(json.dumps(self.state(), sort_keys=True, indent=4))
        res = os.path.exists(filename)
        if res:
            msg = self.tr('Setup "{0}" exported to "{1}"')\
                      .format(s.name(), filename)
            self.raiseWarning(msg,
                              title=self.tr("Exported workflow"),
                              lvl=Qgis.Success)
        return res
Exemple #4
0
class CustomFeatureForm(QDialog, FORM_CLASS):
    """
    Customized form tailor made for reclassification mode of DSGTools: Custom
    Feature Tool Box. This form was copied from `Ferramentas de Produção` and
    modified for the DSGTools plugin.
    """
    def __init__(self, layer, layerMap, attributeMap=None, valueMaps=None):
        """
        Class constructor.
        :param layer: (QgsVectorLayer) layer that will receive the reclassified
                      features.
        :param layerMap: (dict) a map from vector layer to feature list to be
                         reclassified (allocated to another layer).
        :param attributeMap: (dict) a map from attribute name to its
                             (reclassified) value.
        :param valueMaps: (dict) map of all value/relations maps set to layer's
                          fields. These maps will be used for domain checking
                          operations. 
        """
        super(CustomFeatureForm, self).__init__()
        self.setupUi(self)
        self._layer = layer
        self.layerMap = layerMap
        self.valueMaps = valueMaps or LayerHandler().valueMaps(layer)
        self.attributeMap = attributeMap or dict()
        self._layersWidgets = dict()
        self.setupReclassifiedLayers()
        self.widgetsLayout = QGridLayout(self.scrollAreaWidgetContents)
        self._fieldsWidgets = dict()
        self.setupFields()
        self.setWindowTitle(self.tr("DSGTools Feature Reclassification Form"))
        self.messageBar = QgsMessageBar(self)

    def resizeEvent(self, e):
        """
        Just make sure if any alert box is being displayed, it matches the
        dialog size. Method reimplementation.
        :param e: (QResizeEvent) resize event related to this widget resizing.
        """
        self.messageBar.resize(
            QSize(
                self.geometry().size().width(),
                40  # this felt nicer than the original height (30)
            ))

    def setupReclassifiedLayers(self):
        """
        Fills GUI with the data for reclassified data. Dialog should allow user
        to choose whether he wants to reclassify all identified layers or just
        part of it.
        """
        layout = self.vLayout
        for l, fl in self.layerMap.items():
            cb = QCheckBox()
            size = len(fl)
            fCount = self.tr("features") if size > 1 else self.tr("feature")
            cb.setText("{0} ({1} {2})".format(l.name(), size, fCount))
            cb.setChecked(True)
            # just to avoid reading the layout when reading the form
            self._layersWidgets[l.name()] = cb
            layout.addWidget(cb)

    def fieldHasDomainMap(self, field):
        """
        Identifies whether a given field has a value/relations map.
        :param field: (QgsField) field to be checked.
        :return: (bool) whether field has a value/relations map set.
        """
        return field.name() in self.valueMaps

    def getFieldComboBox(self, field):
        """
        Provides a combo box containing all values possible for a field set to
        have value/relations map value. This method also set a getValue and
        setValue proxy that handles value management through its 'real' value.
        E.G. a given domain {"Not available": 1, "Available": 2} shall have its
        values set and read as '1' and '2', instead of either combo box's text
        and index usually used. This also handles setting invalid value setting
        attempts.
        :param field: (QgsField) field to have its field's values exposed.
        :return: (QComboBox) combo box widget filled with all possible values.
        """
        cb = QComboBox()
        # this methods assumes that if a field is used, than it has a value map
        domain = self.valueMaps.get(field.name(), None)
        inverseDomain = {v: k for k, v in domain.items()}
        if not domain:
            raise ValueError(
                self.tr("Field {0} does not have a value/relations map")\
                    .format(field.name())
            )
        cb.addItems(list(domain.keys()))

        def setValue(val):
            """val: field's real value"""
            if val not in domain.values():
                return
            cb.setCurrentText(inverseDomain[val])

        def value():
            return domain[cb.currentText()]

        cb.setValue = setValue
        cb.value = value
        return cb

    def layer(self):
        """
        Retrieves layer set to receive the reclassified features from the
        other layers.
        :return: (QgsVectorLayer) layer to get the newly reclassified features.
        """
        return self._layer

    def setupFields(self):
        """
        Sets up all fields and fill up with available data on the attribute
        map.
        """
        utils = Utils()
        row = 0  # in case no fields are provided
        for row, f in enumerate(self.layer().fields()):
            fName = f.name()
            fMap = self.attributeMap.get(fName, None)
            if fName in self.attributeMap:
                fMap = self.attributeMap[fName]
                if fMap["ignored"]:
                    w = QLineEdit()
                    w.setText(self.tr("Field is set to be ignored"))
                    value = None
                    enabled = False
                else:
                    value = fMap["value"]
                    enabled = fMap["editable"]
                if fMap["isPk"]:
                    # visually identify primary key attributes
                    text = '<p>{0} <img src=":/plugins/DsgTools/icons/key.png" '\
                           'width="16" height="16"></p>'.format(fName)
                else:
                    text = fName
            else:
                value = None
                enabled = True
                text = fName
            if fName in self.attributeMap and self.attributeMap[fName][
                    "ignored"]:
                pass
            if self.fieldHasDomainMap(f):
                # this will provide the combo box already filled with the
                # possible values for the provided field
                w = self.getFieldComboBox(f)
                # proxy method added on customized qcombobox
                w.setValue(value)
            elif utils.fieldIsBool(f):
                w = QCheckBox()
                w.setChecked(False if value is None else value)
            elif utils.fieldIsFloat(f):
                w = QDoubleSpinBox()
                w.setValue(0 if value is None else value)
            elif utils.fieldIsInt(f):
                w = QSpinBox()
                w.setValue(0 if value is None else value)
            else:
                w = QLineEdit()
                w.setText("" if value is None else value)
            w.setEnabled(enabled)
            # also to make easier to read data
            self._fieldsWidgets[fName] = w
            label = QLabel(text)
            label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
            w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
            self.widgetsLayout.addWidget(label, row, 0)
            self.widgetsLayout.addWidget(w, row, 1)
        self.widgetsLayout.addItem(
            QSpacerItem(20, 40, QSizePolicy.Expanding, QSizePolicy.Expanding),
            row + 1, 1, 1, 2)  # row, col, rowSpan, colSpan

    def updateAttributeMap(self):
        """
        Reads all filled data and updated attribute map with such values.
        """
        for fName, w in self._fieldsWidgets.items():
            if not fName in self.attributeMap:
                self.attributeMap[fName] = dict()
                self.attributeMap[fName]["ignored"] = False
                self.attributeMap[fName]["editable"] = True
            if self.attributeMap[fName]["ignored"]:
                continue
            w = self._fieldsWidgets[fName]
            if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox):
                self.attributeMap[fName]["value"] = w.value()
            elif isinstance(w, QCheckBox):
                self.attributeMap[fName]["value"] = w.isChecked()
            elif isinstance(w, QComboBox):
                self.attributeMap[fName]["value"] = w.value()
            else:
                self.attributeMap[fName]["value"] = w.text()

    def readSelectedLayers(self):
        """
        Applies a filter over the layer/feature list map based on user
        selection.
        :return: (dict) filtered layer-feature list map.
        """
        filtered = dict()
        for l, fl in self.layerMap.items():
            if self._layersWidgets[l.name()].isChecked():
                filtered[l] = fl
        return filtered

    def readFieldMap(self):
        """
        Reads filled data into the form and sets it to a map from field name to
        field value to be set. Only fields allowed to be reclassified shall be
        exported in this method.
        :return: (dict) a map from field name to its output value.
        """
        fMap = dict()
        for fName, w in self._fieldsWidgets.items():
            if not fName in self.attributeMap:
                continue
            w = self._fieldsWidgets[fName]
            if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox):
                fMap[fName] = w.value()
            elif isinstance(w, QCheckBox):
                fMap[fName] = w.isChecked()
            elif isinstance(w, QComboBox):
                fMap[fName] = w.value()
            else:
                fMap[fName] = w.text()
        return fMap

    @pyqtSlot()
    def on_okPushButton_clicked(self):
        """
        Verifies if at least one layer is selected and either warn user to
        select one, or closes with status 1 ("OK").
        """
        if len(self.readSelectedLayers()) > 0:
            self.updateAttributeMap()
            self.done(1)
        else:
            self.messageBar.pushMessage(
                self.tr('Invalid layer selection'),
                self.tr("select at least one layer for reclassification!"),
                level=Qgis.Warning,
                duration=5)
Exemple #5
0
class CustomFeatureForm(QDialog, FORM_CLASS):
    """
    Customized form tailor made for reclassification mode of DSGTools: Custom
    Feature Tool Box. This form was copied from `Ferramentas de Produção` and
    modified for the DSGTools plugin.
    """
    def __init__(self, fields, layerMap, attributeMap=None):
        """
        Class constructor.
        :param fields: (QgsFields) set of fields that will be applied to new
                       feature(s).
        :param layerMap: (dict) a map from vector layer to feature list to be
                         reclassified (allocated to another layer).
        :param attributeMap: (dict) a map from attribute name to its
                             (reclassified) value.
        """
        super(CustomFeatureForm, self).__init__()
        self.setupUi(self)
        self.fields = fields
        self.layerMap = layerMap
        self.attributeMap = attributeMap or dict()
        self._layersWidgets = dict()
        self.setupReclassifiedLayers()
        self.widgetsLayout = QGridLayout(self.scrollAreaWidgetContents)
        self._fieldsWidgets = dict()
        self.setupFields()
        self.setWindowTitle(self.tr("DSGTools Feature Reclassification Form"))
        self.messageBar = QgsMessageBar(self)

    def resizeEvent(self, e):
        """
        Just make sure if any alert box is being displayed, it matches the
        dialog size. Method reimplementation.
        :param e: (QResizeEvent) resize event related to this widget resizing.
        """
        self.messageBar.resize(
            QSize(
                self.geometry().size().width(),
                40  # this felt nicer than the original height (30)
            ))

    def setupReclassifiedLayers(self):
        """
        Fills GUI with the data for reclassified data. Dialog should allow user
        to choose whether he wants to reclassify all identified layers or just
        part of it.
        """
        layout = self.vLayout
        for l, fl in self.layerMap.items():
            cb = QCheckBox()
            size = len(fl)
            fCount = self.tr("features") if size > 1 else self.tr("feature")
            cb.setText("{0} ({1} {2})".format(l.name(), size, fCount))
            cb.setChecked(True)
            # just to avoid reading the layout when reading the form
            self._layersWidgets[l.name()] = cb
            layout.addWidget(cb)

    def setupFields(self):
        """
        Setups up all fields and fill up with available data on the attribute
        map.
        """
        utils = Utils()
        row = 0  # in case no fields are provided
        for row, f in enumerate(self.fields):
            fName = f.name()
            fMap = self.attributeMap[fName] if fName in self.attributeMap \
                        else None
            if fName in self.attributeMap:
                fMap = self.attributeMap[fName]
                if fMap["ignored"]:
                    w = QLineEdit()
                    w.setText(self.tr("Field is set to be ignored"))
                    value = None
                    enabled = False
                else:
                    value = fMap["value"]
                    enabled = fMap["editable"]
                if fMap["isPk"]:
                    # visually identify primary key attributes
                    text = '<p>{0} <img src=":/plugins/DsgTools/icons/key.png" '\
                           'width="16" height="16"></p>'.format(fName)
                else:
                    text = fName
            else:
                value = None
                enabled = True
                text = fName
            if fName in self.attributeMap and self.attributeMap[fName][
                    "ignored"]:
                pass
            elif utils.fieldIsFloat(f):
                w = QDoubleSpinBox()
                w.setValue(0 if value is None else value)
            elif utils.fieldIsInt(f):
                w = QSpinBox()
                w.setValue(0 if value is None else value)
            else:
                w = QLineEdit()
                w.setText("" if value is None else value)
            w.setEnabled(enabled)
            # also to make easier to read data
            self._fieldsWidgets[fName] = w
            label = QLabel(text)
            label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
            w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
            self.widgetsLayout.addWidget(label, row, 0)
            self.widgetsLayout.addWidget(w, row, 1)
        self.widgetsLayout.addItem(
            QSpacerItem(20, 40, QSizePolicy.Expanding, QSizePolicy.Expanding),
            row + 1, 1, 1, 2)  # row, col, rowSpan, colSpan

    def updateAttributeMap(self):
        """
        Reads all filled data and updated attribute map with such values.
        """
        for fName, w in self._fieldsWidgets.items():
            if not fName in self.attributeMap:
                self.attributeMap[fName] = dict()
                self.attributeMap[fName]["ignored"] = False
                self.attributeMap[fName]["editable"] = True
            if self.attributeMap[fName]["ignored"]:
                continue
            w = self._fieldsWidgets[fName]
            if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox):
                self.attributeMap[fName]["value"] = w.value()
            else:
                self.attributeMap[fName]["value"] = w.text()

    def readSelectedLayers(self):
        """
        Applies a filter over the layer/feature list map based on user
        selection.
        :return: (dict) filtered layer-feature list map.
        """
        filtered = dict()
        for l, fl in self.layerMap.items():
            if self._layersWidgets[l.name()].isChecked():
                filtered[l] = fl
        return filtered

    def readFieldMap(self):
        """
        Reads filled data into the form and sets it to a map from field name to
        field value to be set. Only fields allowed to be reclassified shall be
        exported in this method.
        :return: (dict) a map from field name to its output value.
        """
        fMap = dict()
        for fName, w in self._fieldsWidgets.items():
            if not fName in self.attributeMap:
                continue
            w = self._fieldsWidgets[fName]
            if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox):
                fMap[fName] = w.value()
            else:
                fMap[fName] = w.text()
        return fMap

    @pyqtSlot()
    def on_okPushButton_clicked(self):
        """
        Verifies if at least one layer is selected and either warn user to
        select one, or closes with status 1 ("OK").
        """
        if len(self.readSelectedLayers()) > 0:
            self.updateAttributeMap()
            self.done(1)
        else:
            self.messageBar.pushMessage(
                self.tr('Invalid layer selection'),
                self.tr("select at least one layer for reclassification!"),
                level=Qgis.Warning,
                duration=5)
Exemple #6
0
class EnforceSpatialRuleWrapper(WidgetWrapper):
    __ATTRIBUTE_MAP_VERSION = 0.2

    def __init__(self, *args, **kwargs):
        super(EnforceSpatialRuleWrapper, self).__init__(*args, **kwargs)
        self.messageBar = QgsMessageBar(self.panel)
        self.panel.resizeEvent = self.resizeEvent
        self._lastError = ""

    def resizeEvent(self, e):
        """
        Resize QgsMessageBar to widget's width
        """
        self.messageBar.resize(
            QSize(self.panel.parent().geometry().size().width(), 30))

    def ruleNameWidget(self):
        """
        Retrieves the widget for reading/setting rule name.
        :return: (QLineEdit)
        """
        le = QLineEdit()
        le.setPlaceholderText(self.tr("Set a name for this spatial rule..."))
        return le

    def mapLayerComboBox(self):
        """
        Retrieves the configured map layer selection combo box.
        :return: (QgsMapLayerComboBox) configured layer selection widget. 
        """
        cb = QgsMapLayerComboBox()
        cb.setFilters(QgsMapLayerProxyModel.VectorLayer)
        return cb

    def mapLayerModelDialog(self):
        """
        Retrieves widget for map layer selection in a model dialog setup.
        :return: (QLineEdit) map layer setter widget for processing dialog
                 mode.
        """
        le = QLineEdit()
        le.setPlaceholderText(self.tr("Type a vector layer's name..."))
        return le

    def filterExpressionWidget(self):
        """
        Retrieves a new widget for filtering expression setting.
        :return: (QgsFieldExpressionWidget) snap mode selection widget.
        """
        fe = QgsFieldExpressionWidget()

        def setValueProxy(exp):
            layer = fe.layer()
            if layer and exp.strip() in layer.fields().names():
                # if a layer is set and the expression is the name purely a
                # field, it will be ignore. single names are causing a weird
                # crash when running the algorithm. this seems to solve it.
                exp = ""
            fe.setExpression(exp)

        def getValueProxy():
            layer = fe.layer()
            exp = fe.currentText()
            if layer and exp.strip() in layer.fields().names():
                exp = ""
            return exp

        fe.setExpression_ = setValueProxy
        fe.currentText_ = getValueProxy
        return fe

    def predicateComboBox(self):
        """
        Retrieves widget for spatial predicate selection.
        :return: (QComboBox) a combo box with all available predicates.
        """
        cb = QComboBox()
        cb.addItems(
            list(SpatialRelationsHandler().availablePredicates().values()))
        return cb

    def de9imWidget(self):
        """
        Creates a new widget to handle DE-9IM masks as input.
        :return: (QLineEdit) a line edit with a DE-9IM text validator.
        """
        le = QLineEdit()
        regex = QRegExp("[FfTt012\*]{9}")
        le.setValidator(QRegExpValidator(regex, le))
        le.setPlaceholderText(self.tr("Type in a DE-9IM as 'T*F0*F21*'..."))
        return le

    def cardinalityWidget(self):
        """
        Retrieves a widget for cardinality setting.
        :return: (QLineEdit) cardinality widget with its content validation
                 applied.
        """
        le = QLineEdit()
        regex = QRegExp("[0-9\*]\.\.[0-9\*]")
        le.setValidator(QRegExpValidator(regex, le))
        le.setPlaceholderText("1..*")
        return le

    def useDE9IM(self):
        """
        Identifies whether user chose to input predicate as a DE-9IM mask.
        :return: (bool) whether GUI should handle the DE-9IM mask widget over
                 the combo box selection.
        """
        return self.panel.cb.isChecked()

    def _checkCardinalityAvailability(self, row):
        """
        Checks if the cardinality for the rule at a given row is available.
        Cardinality is only handled when predicate is provided through the
        combo box options and are not available for the "NOT" options.
        :param row: (int) row to have its cardinality checked.
        :return: (bool) whether cardinality is available
        """
        otw = self.panel.otw
        if self.useDE9IM():
            # if user is using the DE-9IM input, cardinality won't be
            # managed
            otw.itemAt(row, 7).setEnabled(True)
            return True
        predicate = otw.getValue(row, 3)
        handler = SpatialRelationsHandler()
        noCardinality = predicate in (handler.DISJOINT, handler.NOTEQUALS,
                                      handler.NOTINTERSECTS,
                                      handler.NOTTOUCHES, handler.NOTCROSSES,
                                      handler.NOTWITHIN, handler.NOTOVERLAPS,
                                      handler.NOTCONTAINS)
        otw.itemAt(row, 7).setEnabled(not noCardinality)
        if noCardinality:
            otw.setValue(row, 7, "")
        return not noCardinality

    def postAddRowStandard(self, row):
        """
        Sets up widgets to work as expected right after they are added to GUI.
        :param row: (int) row to have its widgets setup.
        """
        # in standard GUI, the layer selectors are QgsMapLayerComboBox, and its
        # layer changed signal should be connected to the filter expression
        # widget setup
        otw = self.panel.otw
        for col in [1, 5]:
            mapLayerComboBox = otw.itemAt(row, col)
            filterWidget = otw.itemAt(row, col + 1)
            mapLayerComboBox.layerChanged.connect(filterWidget.setLayer)
            mapLayerComboBox.layerChanged.connect(
                partial(filterWidget.setExpression, ""))
            # first setup is manual though
            vl = mapLayerComboBox.currentLayer()
            if vl:
                filterWidget.setLayer(vl)
        predicateWidget = otw.itemAt(row, 3)
        predicateWidget.currentIndexChanged.connect(
            partial(self._checkCardinalityAvailability, row))
        # also triggers the action for the first time it is open
        self._checkCardinalityAvailability(row)

    def postAddRowModeler(self, row):
        """
        Sets up widgets to work as expected right after they are added to GUI.
        :param row: (int) row to have its widgets setup.
        """
        otw = self.panel.otw

        def checkLayerBeforeConnect(le, filterExp):
            lName = le.text().strip()
            for layer in QgsProject.instance().mapLayersByName(lName):
                if isinstance(layer, QgsVectorLayer) and layer.name() == lName:
                    filterExp.setLayer(layer)
                    return
            filterExp.setLayer(None)

        for col in [1, 5]:
            le = otw.itemAt(row, col)
            filterWidget = otw.itemAt(row, col + 1)
            le.editingFinished.connect(
                partial(checkLayerBeforeConnect, le, filterWidget))
        predicateWidget = otw.itemAt(row, 3)
        predicateWidget.currentIndexChanged.connect(
            partial(self._checkCardinalityAvailability, row))
        self._checkCardinalityAvailability(row)

    def standardPanel(self):
        """
        Returns the table prepared for the standard Processing GUI.
        :return: (OrderedTableWidget) DSGTools customized table widget.
        """
        widget = QWidget()
        layout = QVBoxLayout()
        # added as an attribute in order to make it easier to be read
        widget.cb = QCheckBox()
        widget.cb.setText(self.tr("Use DE-9IM inputs"))
        layout.addWidget(widget.cb)
        widget.otw = OrderedTableWidget(
            headerMap={
                0: {
                    "header": self.tr("Rule name"),
                    "type": "widget",
                    "widget": self.ruleNameWidget,
                    "setter": "setText",
                    "getter": "text"
                },
                1: {
                    "header": self.tr("Layer A"),
                    "type": "widget",
                    "widget": self.mapLayerComboBox,
                    "setter": "setCurrentText",
                    "getter": "currentText"
                },
                2: {
                    "header": self.tr("Filter A"),
                    "type": "widget",
                    "widget": self.filterExpressionWidget,
                    "setter": "setExpression_",
                    "getter": "currentText_"
                },
                3: {
                    "header": self.tr("Predicate"),
                    "type": "widget",
                    "widget": self.predicateComboBox,
                    "setter": "setCurrentIndex",
                    "getter": "currentIndex"
                },
                4: {
                    "header": self.tr("DE-9IM mask predicate"),
                    "type": "widget",
                    "widget": self.de9imWidget,
                    "setter": "setText",
                    "getter": "text"
                },
                5: {
                    "header": self.tr("Layer B"),
                    "type": "widget",
                    "widget": self.mapLayerComboBox,
                    "setter": "setCurrentText",
                    "getter": "currentText"
                },
                6: {
                    "header": self.tr("Filter B"),
                    "type": "widget",
                    "widget": self.filterExpressionWidget,
                    "setter": "setExpression_",
                    "getter": "currentText_"
                },
                7: {
                    "header": self.tr("Cardinality"),
                    "type": "widget",
                    "widget": self.cardinalityWidget,
                    "setter": "setText",
                    "getter": "text"
                }
            })

        def handlePredicateColumns(checked):
            """
            Predicate input widgets are mutually exclusively: the user may only
            input data through either of them. This method manages hiding and
            showing correct columns in accord to the user selection.
            :param checked: (bool) whether the DE-9IM usage checkbox is ticked.
            """
            widget.otw.tableWidget.hideColumn(3 if checked else 4)
            widget.otw.tableWidget.showColumn(4 if checked else 3)

        widget.cb.toggled.connect(handlePredicateColumns)
        widget.cb.toggled.emit(widget.cb.isChecked())
        widget.otw.setHeaderDoubleClickBehaviour("replicate")
        widget.otw.rowAdded.connect(self.postAddRowStandard)
        layout.addWidget(widget.otw)
        widget.setLayout(layout)
        return widget

    def batchPanel(self):
        """
        Returns the table prepared for the batch Processing GUI.
        :return: (OrderedTableWidget) DSGTools customized table widget.
        """
        return self.standardPanel()

    def modelerPanel(self):
        """
        Returns the table prepared for the modeler Processing GUI.
        :return: (OrderedTableWidget) DSGTools customized table widget.
        """
        widget = QWidget()
        layout = QVBoxLayout()
        # added as an attribute in order to make it easier to be read
        widget.cb = QCheckBox()
        widget.cb.setText(self.tr("Use DE-9IM inputs"))
        layout.addWidget(widget.cb)
        widget.otw = OrderedTableWidget(
            headerMap={
                0: {
                    "header": self.tr("Rule name"),
                    "type": "widget",
                    "widget": self.ruleNameWidget,
                    "setter": "setText",
                    "getter": "text"
                },
                1: {
                    "header": self.tr("Layer A"),
                    "type": "widget",
                    "widget": self.mapLayerModelDialog,
                    "setter": "setText",
                    "getter": "text"
                },
                2: {
                    "header": self.tr("Filter A"),
                    "type": "widget",
                    "widget": self.filterExpressionWidget,
                    "setter": "setExpression_",
                    "getter": "currentText_"
                },
                3: {
                    "header": self.tr("Predicate"),
                    "type": "widget",
                    "widget": self.predicateComboBox,
                    "setter": "setCurrentIndex",
                    "getter": "currentIndex"
                },
                4: {
                    "header": self.tr("DE-9IM mask predicate"),
                    "type": "widget",
                    "widget": self.de9imWidget,
                    "setter": "setText",
                    "getter": "text"
                },
                5: {
                    "header": self.tr("Layer B"),
                    "type": "widget",
                    "widget": self.mapLayerModelDialog,
                    "setter": "setText",
                    "getter": "text"
                },
                6: {
                    "header": self.tr("Filter B"),
                    "type": "widget",
                    "widget": self.filterExpressionWidget,
                    "setter": "setExpression_",
                    "getter": "currentText_"
                },
                7: {
                    "header": self.tr("Cardinality"),
                    "type": "widget",
                    "widget": self.cardinalityWidget,
                    "setter": "setText",
                    "getter": "text"
                }
            })

        def handlePredicateColumns(checked):
            """
            Predicate input widgets are mutually exclusively: the user may only
            input data through either of them. This method manages hiding and
            showing correct columns in accord to the user selection.
            :param checked: (bool) whether the DE-9IM usage checkbox is ticked.
            """
            widget.otw.tableWidget.hideColumn(3 if checked else 4)
            widget.otw.tableWidget.showColumn(4 if checked else 3)

        widget.cb.toggled.connect(handlePredicateColumns)
        widget.cb.toggled.emit(widget.cb.isChecked())
        widget.otw.setHeaderDoubleClickBehaviour("replicate")
        widget.otw.rowAdded.connect(self.postAddRowModeler)
        layout.addWidget(widget.otw)
        widget.setLayout(layout)
        return widget

    def createPanel(self):
        return {
            DIALOG_MODELER: self.modelerPanel,
            DIALOG_STANDARD: self.standardPanel,
            DIALOG_BATCH: self.batchPanel
        }[self.dialogType]()

    def createWidget(self):
        self.panel = self.createPanel()
        self.panel.otw.showSaveLoadButtons(True)
        self.panel.otw.extension = ".rules"
        self.panel.otw.fileType = self.tr("Set of DSGTools Spatial Rules")
        self.panel.otw.setMetadata({"version": self.__ATTRIBUTE_MAP_VERSION})
        return self.panel

    def parentLayerChanged(self, layer=None):
        pass

    def setLayer(self, layer):
        pass

    def showLoadingMsg(self, invalidRules=None, msgType=None):
        """
        Shows a message box to user if successfully loaded data or not.
        If not, shows to user a list of not loaded layers and allows user
        to choice between ignore and continue or cancel the importation.
        :param lyrList: (list) a list of not loaded layers.
        :param msgType: (str) type of message box - warning or information.
        :return: (signal) value returned from the clicked button.
        """
        msg = QMessageBox()
        msg.setWindowTitle(self.tr("DSGTools: importing spatial rules"))
        if invalidRules and msgType == "warning":
            msg.setIcon(QMessageBox.Warning)
            msg.setText(self.tr("Some rules have not been loaded"))
            msg.setInformativeText(
                self.tr("Do you want to ignore and continue or cancel?"))
            msgString = "\n".join((r.ruleName() for r in invalidRules))
            formatedMsgString = self.tr(
                "The following layers have not been loaded:\n{0}").format(
                    msgString)
            msg.setDetailedText(formatedMsgString)
            msg.setStandardButtons(QMessageBox.Ignore | QMessageBox.Cancel)
            msg.setDefaultButton(QMessageBox.Cancel)
        else:
            msg.setIcon(QMessageBox.Information)
            msg.setText(self.tr("Successfully loaded rules!"))
        choice = msg.exec_()
        return choice

    def setValue(self, value):
        """
        Sets back parameters to the GUI. Method reimplementation.
        :param value: (list-of-SpatialRule) list of spatial rules to be set.
        """
        if not value:
            return
        otw = self.panel.otw
        useDE9IM = value[0].get("useDE9IM", False)
        self.panel.cb.setChecked(useDE9IM)
        # signal must be triggered to adjust the correct column display
        self.panel.cb.toggled.emit(useDE9IM)
        isNotModeler = self.dialogType != DIALOG_MODELER
        invalids = list()
        for rule in value:
            # GUI was crashing when passing SpatialRule straight up
            rule = SpatialRule(**rule, checkLoadedLayer=False)
            # we want to check whether the layer is loaded as this does not
            # work properly with the map layer combobox. on the modeler it
            # won't matter as it is a line edit
            if not rule.isValid(checkLoaded=isNotModeler):
                invalids.append(rule)
                continue
            otw.addRow({
                0: rule.ruleName(),
                1: rule.layerA(),
                2: rule.filterA(),
                3: rule.predicateEnum(),
                4: rule.predicateDE9IM(),
                5: rule.layerB(),
                6: rule.filterB(),
                7: rule.cardinality()
            })
        choice = self.showLoadingMsg(invalids, "warning" if invalids else "")
        if choice == QMessageBox.Cancel:
            otw.clear()

    def readStandardPanel(self):
        """
        Reads widget's contents when process' parameters are set from an 
        algorithm call (e.g. Processing toolbox).
        """
        ruleList = list()
        otw = self.panel.otw
        useDe9im = self.useDE9IM()
        for row in range(otw.rowCount()):
            ruleList.append(
                SpatialRule(
                    name=otw.getValue(row, 0).strip(),  # or \
                    # self.tr("Spatial Rule #{n}".format(n=row + 1)),
                    layer_a=otw.getValue(row, 1),
                    filter_a=otw.getValue(row, 2),
                    predicate=otw.getValue(row, 3),
                    de9im_predicate=otw.getValue(row, 4),
                    layer_b=otw.getValue(row, 5),
                    filter_b=otw.getValue(row, 6),
                    cardinality=otw.getValue(row, 7) or "1..*",
                    useDE9IM=useDe9im,
                    checkLoadedLayer=False).asDict())
        return ruleList

    def readModelerPanel(self):
        """
        Reads widget's contents when process' parameters are set from a modeler
        instance.
        """
        return self.readStandardPanel()

    def readBatchPanel(self):
        """
        Reads widget's contents when process' parameters are set from a batch
        processing instance.
        """
        return self.readStandardPanel()

    def validate(self, pushAlert=False):
        """
        Validates fields. Returns True if all information are filled correctly.
        :param pushAlert: (bool) whether invalidation reason should be
                          displayed on the widget.
        :return: (bool) whether set of filled parameters if valid.
        """
        inputMap = {
            DIALOG_STANDARD: self.readStandardPanel,
            DIALOG_MODELER: self.readModelerPanel,
            DIALOG_BATCH: self.readBatchPanel
        }[self.dialogType]()
        if len(inputMap) == 0:
            if pushAlert:
                self.messageBar.pushMessage(
                    self.tr("Please provide at least 1 spatial rule."),
                    level=Qgis.Warning,
                    duration=5)
            return False
        for row, rule in enumerate(inputMap):
            # GUI was crashing when passing SpatialRule straight up
            rule = SpatialRule(**rule)
            if not rule.isValid():
                if pushAlert:
                    self.messageBar.pushMessage(
                        self.tr("{0} (row {1}).")\
                            .format(rule.validate(), row + 1),
                        level=Qgis.Warning,
                        duration=5
                    )
                return False
        return True

    def value(self):
        """
        Retrieves parameters from current widget. Method reimplementation.
        :return: (dict) value currently set to the GUI.
        """
        if self.validate(pushAlert=True):
            return {
                DIALOG_STANDARD: self.readStandardPanel,
                DIALOG_MODELER: self.readModelerPanel,
                DIALOG_BATCH: self.readBatchPanel
            }[self.dialogType]()

    def postInitialize(self, wrappers):
        pass
class WorkflowSetupDialog(QDialog, FORM_CLASS):
    __qgisModelPath__ = ModelerUtils.modelsFolders()[0]
    ON_FLAGS_HALT, ON_FLAGS_WARN, ON_FLAGS_IGNORE = range(3)
    onFlagsDisplayNameMap = {
        ON_FLAGS_HALT:
        QCoreApplication.translate('WorkflowSetupDialog', "Halt"),
        ON_FLAGS_WARN:
        QCoreApplication.translate('WorkflowSetupDialog', "Warn"),
        ON_FLAGS_IGNORE:
        QCoreApplication.translate('WorkflowSetupDialog', "Ignore")
    }
    onFlagsValueMap = {
        ON_FLAGS_HALT: "halt",
        ON_FLAGS_WARN: "warn",
        ON_FLAGS_IGNORE: "ignore"
    }
    MODEL_NAME_HEADER, MODEL_SOURCE_HEADER, ON_FLAGS_HEADER, LOAD_OUT_HEADER = range(
        4)

    def __init__(self, parent=None):
        """
        Class constructor.
        :param headerMap: (dict) a map from each header to be shown and type of
                           cell content (e.g. widget or item).
        :param parent: (QtWidgets.*) any widget parent to current instance.
        """
        super(WorkflowSetupDialog, self).__init__(parent)
        self.parent = parent
        self.setupUi(self)
        self.messageBar = QgsMessageBar(self)
        self.orderedTableWidget.setHeaders({
            self.MODEL_NAME_HEADER: {
                "header": self.tr("Model name"),
                "type": "widget",
                "widget": self.modelNameWidget,
                "setter": "setText",
                "getter": "text"
            },
            self.MODEL_SOURCE_HEADER: {
                "header": self.tr("Model source"),
                "type": "widget",
                "widget": self.modelWidget,
                "setter": "setText",
                "getter": "text"
            },
            self.ON_FLAGS_HEADER: {
                "header": self.tr("On flags"),
                "type": "widget",
                "widget": self.onFlagsWidget,
                "setter": "setCurrentIndex",
                "getter": "currentIndex"
            },
            self.LOAD_OUT_HEADER: {
                "header": self.tr("Load output"),
                "type": "widget",
                "widget": self.loadOutputWidget,
                "setter": "setChecked",
                "getter": "isChecked"
            }
        })
        self.orderedTableWidget.setHeaderDoubleClickBehaviour("replicate")

    def resizeTable(self):
        """
        Adjusts table columns sizes.
        """
        dSize = self.orderedTableWidget.geometry().width() - \
                self.orderedTableWidget.horizontalHeader().geometry().width()
        onFlagsColSize = self.orderedTableWidget.sectionSize(2)
        loadOutColSize = self.orderedTableWidget.sectionSize(3)
        missingBarSize = self.geometry().size().width() - dSize\
                         - onFlagsColSize - loadOutColSize
        # the "-11" is empiric: it makes it fit header to table
        self.orderedTableWidget.tableWidget.horizontalHeader().resizeSection(
            0,
            int(0.4 * missingBarSize) - 11)
        self.orderedTableWidget.tableWidget.horizontalHeader().resizeSection(
            1, missingBarSize - int(0.4 * missingBarSize) - 11)

    def resizeEvent(self, e):
        """
        Reimplementation in order to use this window's resize event.
        On this object, this method makes sure that message bar is always the
        same size as the window.
        :param e: (QResizeEvent) resize event.
        """
        self.messageBar.resize(
            QSize(
                self.geometry().size().width(),
                40  # this felt nicer than the original height (30)
            ))
        self.resizeTable()

    def confirmAction(self, msg, showCancel=True):
        """
        Raises a message box for confirmation before executing an action.
        :param msg: (str) message to be exposed.
        :param showCancel: (bool) whether Cancel button should be exposed.
        :return: (bool) whether action was confirmed.
        """
        if showCancel:
            return QMessageBox.question(self, self.tr('Confirm Action'), msg,
                                        QMessageBox.Ok
                                        | QMessageBox.Cancel) == QMessageBox.Ok
        else:
            return QMessageBox.question(self, self.tr('Confirm Action'), msg,
                                        QMessageBox.Ok) == QMessageBox.Ok

    def clear(self):
        """
        Clears all input data from GUI.
        """
        self.authorLineEdit.setText("")
        self.nameLineEdit.setText("")
        self.versionLineEdit.setText("")
        self.orderedTableWidget.clear()

    def modelNameWidget(self, name=None):
        """
        Gets a new instance of model name's setter widget.
        :param name: (str) model name to be filled.
        :return: (QLineEdit) widget for model's name setting.
        """
        # no need to pass parenthood as it will be set to the table when added
        # to a cell
        le = QLineEdit()
        # setPlace"h"older, with a lower case "h"...
        le.setPlaceholderText(self.tr("Set a name for the model..."))
        if name is not None:
            le.setText(name)
        le.setFrame(False)
        return le

    def modelWidget(self, filepath=None):
        """
        Gets a new instance of model settter's widget.
        :parma filepath: (str) path to a model.
        :return: (SelectFileWidget) DSGTools custom file selection widget.
        """
        widget = SelectFileWidget()
        widget.label.hide()
        widget.selectFilePushButton.setText("...")
        widget.selectFilePushButton.setMaximumWidth(32)
        widget.lineEdit.setPlaceholderText(self.tr("Select a model..."))
        widget.lineEdit.setFrame(False)
        widget.setCaption(self.tr("Select a QGIS Processing model file"))
        widget.setFilter(
            self.tr("Select a QGIS Processing model (*.model *.model3)"))
        # defining setter and getter methods for composed widgets into OTW
        widget.setText = widget.lineEdit.setText
        widget.text = widget.lineEdit.text
        if filepath is not None:
            widget.setText(filepath)
        return widget

    def onFlagsWidget(self, option=None):
        """
        Gets a new instance for the widget that sets model's behaviour when
        flags are raised.
        :param option: (str) on flags raised behaviour (non translatable text).
        :return: (QComboBox) model's behaviour selection widget. 
        """
        combo = QComboBox()
        combo.addItems([
            self.onFlagsDisplayNameMap[self.ON_FLAGS_HALT],
            self.onFlagsDisplayNameMap[self.ON_FLAGS_WARN],
            self.onFlagsDisplayNameMap[self.ON_FLAGS_IGNORE]
        ])
        if option is not None:
            optIdx = None
            for idx, txt in self.onFlagsValueMap.items():
                if option == txt:
                    optIdx = idx
                    break
            optIdx = optIdx if optIdx is not None else 0
            combo.setCurrentIndex(optIdx)
        return combo

    def loadOutputWidget(self, option=None):
        """
        Gets a new instance for the widget that sets output layer loading
        definitions.
        :param option: (bool) if output should be loaded.
        :return: (QWidget) widget for output layer loading behaviour
                 definition.
        """
        cb = QCheckBox()
        cb.setStyleSheet("margin:auto;")
        if option is not None:
            cb.setChecked(option)
        return cb

    def now(self):
        """
        Gets time and date from the system. Format: "dd/mm/yyyy HH:MM:SS".
        :return: (str) current's date and time
        """
        paddle = lambda n: str(n) if n > 9 else "0{0}".format(n)
        now = datetime.now()
        return "{day}/{month}/{year} {hour}:{minute}:{second}".format(
            year=now.year,
            month=paddle(now.month),
            day=paddle(now.day),
            hour=paddle(now.hour),
            minute=paddle(now.minute),
            second=paddle(now.second))

    def workflowName(self):
        """
        Reads filled workflow name from GUI.
        :return: (str) workflow's name.
        """
        return self.nameLineEdit.text().strip()

    def setWorkflowName(self, name):
        """
        Sets workflow name to GUI.
        :param name: (str) workflow's name.
        """
        self.nameLineEdit.setText(name)

    def author(self):
        """
        Reads filled workflow name from GUI.
        :return: (str) workflow's author.
        """
        return self.authorLineEdit.text().strip()

    def setWorkflowAuthor(self, author):
        """
        Sets workflow author name to GUI.
        :param author: (str) workflow's author name.
        """
        self.authorLineEdit.setText(author)

    def version(self):
        """
        Reads filled workflow name from GUI.
        :return: (str) workflow's version.
        """
        return self.versionLineEdit.text().strip()

    def setWorkflowVersion(self, version):
        """
        Sets workflow version to GUI.
        :param version: (str) workflow's version.
        """
        self.versionLineEdit.setText(version)

    def modelCount(self):
        """
        Reads the amount of models (rows added) the user intend to use.
        :return: (int) model count.
        """
        return self.orderedTableWidget.rowCount()

    def readRow(self, row):
        """
        Reads a row's contents and set it as a DsgToolsProcessingModel set of
        parameters.
        :return: (dict) parameters map.
        """
        contents = self.orderedTableWidget.row(row)
        filepath = contents[self.MODEL_SOURCE_HEADER].strip()
        onFlagsIdx = contents[self.ON_FLAGS_HEADER]
        name = contents[self.MODEL_NAME_HEADER].strip()
        loadOutput = contents[self.LOAD_OUT_HEADER]
        if not os.path.exists(filepath):
            xml = ""
        else:
            with open(filepath, "r", encoding="utf-8") as f:
                xml = f.read()
        return {
            "displayName": name,
            "flags": {
                "onFlagsRaised": self.onFlagsValueMap[onFlagsIdx],
                "loadOutput": loadOutput
            },
            "source": {
                "type": "xml",
                "data": xml
            },
            "metadata": {
                "originalName": os.path.basename(filepath)
            }
        }

    def setModelToRow(self, row, model):
        """
        Reads model's parameters from model parameters default map.
        :param row: (int) row to have its widgets filled with model's
                    parameters.
        :param model: (DsgToolsProcessingModel) model object.
        """
        # all model files handled by this tool are read/written on QGIS model dir
        data = model.data()
        if model.source() == "file" and os.path.exists(data):
            with open(data, "r", encoding="utf-8") as f:
                xml = f.read()
            originalName = os.path.basename(data)
        elif model.source() == "xml":
            xml = data
            meta = model.metadata()
            originalName = model.originalName() if model.originalName() \
                            else "temp_{0}.model3".format(hash(time()))
        else:
            return False
        path = os.path.join(self.__qgisModelPath__, originalName)
        msg = self.tr(
            "Model '{0}' is already imported would you like to overwrite it?"
        ).format(path)
        if os.path.exists(path) and self.confirmAction(msg):
            os.remove(path)
        if not os.path.exists(path):
            with open(path, "w") as f:
                f.write(xml)
        self.orderedTableWidget.addRow(
            contents={
                self.MODEL_NAME_HEADER: model.displayName(),
                self.MODEL_SOURCE_HEADER: path,
                self.ON_FLAGS_HEADER: {
                    "halt": self.ON_FLAGS_HALT,
                    "warn": self.ON_FLAGS_WARN,
                    "ignore": self.ON_FLAGS_IGNORE
                }[model.onFlagsRaised()],
                self.LOAD_OUT_HEADER: model.loadOutput()
            })
        return True

    def validateRowContents(self, contents):
        """
        Checks if all attributes read from a row are valid.
        :param contents: (dict) map to (already read) row contents.
        :return: (str) invalidation reason
        """
        if contents["displayName"] == "":
            return self.tr("Missing model's name.")
        if contents["source"]["data"] == "":
            return self.tr("Model is empty or file was not found.")
        return ""

    def models(self):
        """
        Reads all table contents and sets it as a DsgToolsProcessingAlgorithm's
        set of parameters.
        :return: (dict) map to each model's set of parameters.
        """
        models = dict()
        for row in range(self.modelCount()):
            contents = self.readRow(row)
            models[contents["displayName"]] = contents
        return models

    def validateModels(self):
        """
        Check if each row on table has a valid input.
        :return: (str) invalidation reason.
        """
        for row in range(self.modelCount()):
            msg = self.validateRowContents(self.readRow(row))
            if msg:
                return "Row {row}: '{error}'".format(row=row + 1, error=msg)
        if len(self.models()) != self.modelCount():
            return self.tr("Check if no model name is repeated.")
        return ""

    def workflowParameterMap(self):
        """
        Generates a Workflow map from input data.
        """
        return {
            "displayName": self.workflowName(),
            "models": self.models(),
            "metadata": {
                "author": self.author(),
                "version": self.version(),
                "lastModified": self.now()
            }
        }

    def currentWorkflow(self):
        """
        Returns current workflow object as read from GUI.
        :return: (QualityAssuranceWorkflow) current workflow object.
        """
        try:
            return QualityAssuranceWorkflow(self.workflowParameterMap())
        except:
            return None

    def validate(self):
        """
        Checks if all filled data generates a valid Workflow object.
        :return: (bool) validation status.
        """
        if self.workflowName() == "":
            return self.tr("Workflow's name needs to be filled.")
        if self.author() == "":
            return self.tr("Workflow's author needs to be filled.")
        if self.version() == "":
            return self.tr("Workflow's version needs to be filled.")
        msg = self.validateModels()
        if msg != "":
            return msg
        return ""

    def exportWorkflow(self, filepath):
        """
        Exports current data to a JSON file.
        :param filepath: (str) output file directory.
        """
        QualityAssuranceWorkflow(self.workflowParameterMap()).export(filepath)

    @pyqtSlot(bool, name="on_exportPushButton_clicked")
    def export(self):
        """
        Exports current input data as a workflow JSON, IF input is valid.
        :return: (bool) operation success.
        """
        msg = self.validate()
        if msg != "":
            self.messageBar.pushMessage(self.tr('Invalid workflow'),
                                        msg,
                                        level=Qgis.Warning,
                                        duration=5)
            return False
        fd = QFileDialog()
        filename = fd.getSaveFileName(
            caption=self.tr("Export DSGTools Workflow"),
            filter=self.tr("DSGTools Workflow (*.workflow)"))
        filename = filename[0] if isinstance(filename, tuple) else ""
        if filename == "":
            return False
        filename = filename if filename.lower().endswith(".workflow") \
                    else "{0}.workflow".format(filename)
        try:
            self.exportWorkflow(filename)
        except Exception as e:
            self.messageBar.pushMessage(
                self.tr('Invalid workflow'),
                self.tr(
                    "Unable to export workflow to '{fp}' ({error}).").format(
                        fp=filename, error=str(e)),
                level=Qgis.Warning,
                duration=5)
            return False
        result = os.path.exists(filename)
        msg = (self.tr("Workflow exported to {fp}") if result else \
                self.tr("Unable to export workflow to '{fp}'")).format(fp=filename)
        lvl = Qgis.Success if result else Qgis.Warning
        self.messageBar.pushMessage(self.tr('Workflow exportation'),
                                    msg,
                                    level=lvl,
                                    duration=5)
        return result

    def importWorkflow(self, filepath):
        """
        Sets workflow contents from an imported DSGTools Workflow dump file.
        :param filepath: (str) workflow file to be imported.
        """
        with open(filepath, "r", encoding="utf-8") as f:
            xml = json.load(f)
        workflow = QualityAssuranceWorkflow(xml)
        self.clear()
        self.setWorkflowAuthor(workflow.author())
        self.setWorkflowVersion(workflow.version())
        self.setWorkflowName(workflow.displayName())
        for row, modelParam in enumerate(xml["models"].values()):
            self.setModelToRow(row, DsgToolsProcessingModel(modelParam, ""))

    @pyqtSlot(bool, name="on_importPushButton_clicked")
    def import_(self):
        """
        Request a file for Workflow importation and sets it to GUI.
        :return: (bool) operation status.
        """
        fd = QFileDialog()
        filename = fd.getOpenFileName(
            caption=self.tr('Select a Workflow file'),
            filter=self.tr('DSGTools Workflow (*.workflow *.json)'))
        filename = filename[0] if isinstance(filename, tuple) else ""
        if not filename:
            return False
        try:
            self.importWorkflow(filename)
        except Exception as e:
            self.messageBar.pushMessage(
                self.tr('Invalid workflow'),
                self.tr(
                    "Unable to export workflow to '{fp}' ({error}).").format(
                        fp=filename, error=str(e)),
                level=Qgis.Critical,
                duration=5)
            return False
        self.messageBar.pushMessage(
            self.tr('Success'),
            self.tr("Workflow '{fp}' imported!").format(fp=filename),
            level=Qgis.Info,
            duration=5)
        return True

    @pyqtSlot(bool, name="on_okPushButton_clicked")
    def ok(self):
        """
        Closes dialog and checks if current workflow is valid.
        """
        msg = self.validate()
        if msg == "" and self.currentWorkflow():
            self.done(1)
        else:
            self.messageBar.pushMessage(self.tr('Invalid workflow'),
                                        self.validate(),
                                        level=Qgis.Warning,
                                        duration=5)

    @pyqtSlot(bool, name="on_cancelPushButton_clicked")
    def cancel(self):
        """
        Restores GUI to last state and closes it.
        """
        self.done(0)