class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """Dialog implementation class for the InaSAFE keywords editor."""

    def __init__(self, parent, iface, dock=None, layer=None):
        """Constructor for the dialog.

        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        :param parent: Parent widget of this dialog.
        :type parent: QWidget

        :param iface: Quantum GIS QGisAppInterface instance.
        :type iface: QGisAppInterface

        :param dock: Dock widget instance that we can notify of changes to
            the keywords. Optional.
        :type dock: Dock
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr("InaSAFE %s Keywords Editor") % (get_version()))
        self.keywordIO = KeywordIO()
        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standardExposureList = OrderedDict(
            [
                ("population", self.tr("population")),
                ("structure", self.tr("structure")),
                ("Not Set", self.tr("Not Set")),
            ]
        )
        self.standardHazardList = OrderedDict(
            [
                ("earthquake [MMI]", self.tr("earthquake [MMI]")),
                ("tsunami [m]", self.tr("tsunami [m]")),
                ("tsunami [wet/dry]", self.tr("tsunami [wet/dry]")),
                ("tsunami [feet]", self.tr("tsunami [feet]")),
                ("flood [m]", self.tr("flood [m]")),
                ("flood [wet/dry]", self.tr("flood [wet/dry]")),
                ("flood [feet]", self.tr("flood [feet]")),
                ("tephra [kg2/m2]", self.tr("tephra [kg2/m2]")),
                ("volcano", self.tr("volcano")),
                ("Not Set", self.tr("Not Set")),
            ]
        )
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = dock

        self.lstKeywords.itemClicked.connect(self.edit_key_value_pair)

        # Set up help dialog showing logic.
        self.helpDialog = None
        helpButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        helpButton.clicked.connect(self.show_help)

        # set some inital ui state:
        self.defaults = breakdown_defaults()
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults["FEM_RATIO"])
        self.dsbFemaleRatioDefault.blockSignals(False)
        # myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        # myButton.setEnabled(False)
        if layer is None:
            self.layer = self.iface.activeLayer()
        else:
            self.layer = layer
        if self.layer:
            self.load_state_from_keywords()

        # add a reload from keywords button
        reloadButton = self.buttonBox.addButton(self.tr("Reload"), QtGui.QDialogButtonBox.ActionRole)
        reloadButton.clicked.connect(self.load_state_from_keywords)

    def set_layer(self, layer):
        """Set the layer associated with the keyword editor.

        :param layer: Layer whose keywords should be edited.
        :type layer: QgsMapLayer
        """
        self.layer = layer
        self.load_state_from_keywords()

    def show_help(self):
        """Load the help text for the keywords dialog."""
        show_context_help(context="keywords")

    def toggle_postprocessing_widgets(self):
        """Hide or show the post processing widgets depending on context.
        """
        LOGGER.debug("togglePostprocessingWidgets")
        isPostprocessingOn = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not isPostprocessingOn)
        self.lblSubcategory.setVisible(not isPostprocessingOn)
        self.show_aggregation_attribute(isPostprocessingOn)
        self.show_female_ratio_attribute(isPostprocessingOn)
        self.show_female_ratio_default(isPostprocessingOn)

    def show_aggregation_attribute(self, visible_flag):
        """Hide or show the aggregation attribute in the keyword editor dialog.

        :param visible_flag: Flag indicating if the aggregation attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        theBox = self.cboAggregationAttribute
        theBox.blockSignals(True)
        theBox.clear()
        theBox.blockSignals(False)
        if visible_flag:
            currentKeyword = self.get_value_for_key(self.defaults["AGGR_ATTR_KEY"])
            fields, attributePosition = layer_attribute_names(
                self.layer, [QtCore.QVariant.Int, QtCore.QVariant.String], currentKeyword
            )
            theBox.addItems(fields)
            if attributePosition is None:
                theBox.setCurrentIndex(0)
            else:
                theBox.setCurrentIndex(attributePosition)

        theBox.setVisible(visible_flag)
        self.lblAggregationAttribute.setVisible(visible_flag)

    def show_female_ratio_attribute(self, visible_flag):
        """Hide or show the female ratio attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        theBox = self.cboFemaleRatioAttribute
        theBox.blockSignals(True)
        theBox.clear()
        theBox.blockSignals(False)
        if visible_flag:
            currentKeyword = self.get_value_for_key(self.defaults["FEM_RATIO_ATTR_KEY"])
            fields, attributePosition = layer_attribute_names(self.layer, [QtCore.QVariant.Double], currentKeyword)
            fields.insert(0, self.tr("Use default"))
            fields.insert(1, self.tr("Don't use"))
            theBox.addItems(fields)
            if currentKeyword == self.tr("Use default"):
                theBox.setCurrentIndex(0)
            elif currentKeyword == self.tr("Don't use"):
                theBox.setCurrentIndex(1)
            elif attributePosition is None:
                # currentKeyword was not found in the attribute table.
                # Use default
                theBox.setCurrentIndex(0)
            else:
                # + 2 is because we add use defaults and don't use
                theBox.setCurrentIndex(attributePosition + 2)
        theBox.setVisible(visible_flag)
        self.lblFemaleRatioAttribute.setVisible(visible_flag)

    def show_female_ratio_default(self, visible_flag):
        """Hide or show the female ratio default attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio
            default attribute should be hidden or shown.
        :type visible_flag: bool
        """
        theBox = self.dsbFemaleRatioDefault
        if visible_flag:
            currentValue = self.get_value_for_key(self.defaults["FEM_RATIO_KEY"])
            if currentValue is None:
                val = self.defaults["FEM_RATIO"]
            else:
                val = float(currentValue)
            theBox.setValue(val)

        theBox.setVisible(visible_flag)
        self.lblFemaleRatioDefault.setVisible(visible_flag)

    # prevents actions being handled twice
    @pyqtSignature("int")
    def on_cboAggregationAttribute_currentIndexChanged(self, index=None):
        """Handler for aggregation attribute combo change.

        :param index: Not used but required for slot.
        """
        del index
        self.add_list_entry(self.defaults["AGGR_ATTR_KEY"], self.cboAggregationAttribute.currentText())

    # prevents actions being handled twice
    @pyqtSignature("int")
    def on_cboFemaleRatioAttribute_currentIndexChanged(self, index=None):
        """Handler for female ratio attribute change.

        :param index: Not used but required for slot.
        """
        del index
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr("Use default"):
            self.dsbFemaleRatioDefault.setEnabled(True)
            currentDefault = self.get_value_for_key(self.defaults["FEM_RATIO_KEY"])
            if currentDefault is None:
                self.add_list_entry(self.defaults["FEM_RATIO_KEY"], self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.remove_item_by_key(self.defaults["FEM_RATIO_KEY"])
        self.add_list_entry(self.defaults["FEM_RATIO_ATTR_KEY"], text)

    # prevents actions being handled twice
    @pyqtSignature("double")
    def on_dsbFemaleRatioDefault_valueChanged(self, value):
        """Handler for female ration default value changing.

        :param value: Not used but required for slot.
        """
        del value
        box = self.dsbFemaleRatioDefault
        if box.isEnabled():
            self.add_list_entry(self.defaults["FEM_RATIO_KEY"], box.value())

    # prevents actions being handled twice
    @pyqtSignature("bool")
    def on_pbnAdvanced_toggled(self, flag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        self.toggle_advanced(flag)

    def toggle_advanced(self, flag):
        """Hide or show advanced editor.

        :param flag: Desired state for advanced editor visibility.
        :type flag: bool
        """
        if flag:
            self.pbnAdvanced.setText(self.tr("Hide advanced editor"))
        else:
            self.pbnAdvanced.setText(self.tr("Show advanced editor"))
        self.grpAdvanced.setVisible(flag)
        self.resize_dialog()

    # prevents actions being handled twice
    @pyqtSignature("bool")
    def on_radHazard_toggled(self, flag):
        """Automatic slot executed when the hazard radio is toggled.

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        if not flag:
            return
        self.set_category("hazard")
        self.update_controls_from_list()

    # prevents actions being handled twice
    @pyqtSignature("bool")
    def on_radExposure_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled on.

        :param theFlag: Flag indicating the new checked state of the button.
        :type theFlag: bool
        """
        if not theFlag:
            return
        self.set_category("exposure")
        self.update_controls_from_list()

    # prevents actions being handled twice
    @pyqtSignature("bool")
    def on_radPostprocessing_toggled(self, flag):
        """Automatic slot executed when the hazard radio is toggled on.

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        if not flag:
            self.remove_item_by_key(self.defaults["AGGR_ATTR_KEY"])
            self.remove_item_by_key(self.defaults["FEM_RATIO_ATTR_KEY"])
            self.remove_item_by_key(self.defaults["FEM_RATIO_KEY"])
            return
        self.set_category("postprocessing")
        self.update_controls_from_list()

    # prevents actions being handled twice
    @pyqtSignature("int")
    def on_cboSubcategory_currentIndexChanged(self, index=None):
        """Automatic slot executed when the subcategory is changed.

        When the user changes the subcategory, we will extract the
        subcategory and dataype or unit (depending on if it is a hazard
        or exposure subcategory) from the [] after the name.

        :param index: Not used but required for Qt slot.
        """
        del index
        myItem = self.cboSubcategory.itemData(self.cboSubcategory.currentIndex())
        myText = str(myItem)
        # I found that myText is 'Not Set' for every language
        if myText == self.tr("Not Set") or myText == "Not Set":
            self.remove_item_by_key("subcategory")
            return
        myTokens = myText.split(" ")
        if len(myTokens) < 1:
            self.remove_item_by_key("subcategory")
            return
        mySubcategory = myTokens[0]
        self.add_list_entry("subcategory", mySubcategory)

        # Some subcategories e.g. roads have no units or datatype
        if len(myTokens) == 1:
            return
        if myTokens[1].find("[") < 0:
            return
        myCategory = self.get_value_for_key("category")
        if "hazard" == myCategory:
            myUnits = myTokens[1].replace("[", "").replace("]", "")
            self.add_list_entry("unit", myUnits)
        if "exposure" == myCategory:
            myDataType = myTokens[1].replace("[", "").replace("]", "")
            self.add_list_entry("datatype", myDataType)
            # prevents actions being handled twice

    def set_subcategory_list(self, entries, selected_item=None):
        """Helper to populate the subcategory list based on category context.

        :param entries: An OrderedDict of subcategories. The dict entries
             should be in the form ('earthquake', self.tr('earthquake')). See
             http://www.voidspace.org.uk/python/odict.html for info on
             OrderedDict.
        :type entries: OrderedDict

        :param selected_item: Which item should be selected in the combo. If
            the selected item is not in entries, it will be appended to it.
            This is optional.
        :type selected_item: str
        """
        # To avoid triggering on_cboSubcategory_currentIndexChanged
        # we block signals from the combo while updating it
        self.cboSubcategory.blockSignals(True)
        self.cboSubcategory.clear()
        theSelectedItemNone = selected_item is not None
        theSelectedItemInValues = selected_item not in entries.values()
        theSelectedItemInKeys = selected_item not in entries.keys()
        if theSelectedItemNone and theSelectedItemInValues and theSelectedItemInKeys:
            # Add it to the OrderedList
            entries[selected_item] = selected_item
        myIndex = 0
        mySelectedIndex = 0
        for myKey, myValue in entries.iteritems():
            if myValue == selected_item or myKey == selected_item:
                mySelectedIndex = myIndex
            myIndex += 1
            self.cboSubcategory.addItem(myValue, myKey)
        self.cboSubcategory.setCurrentIndex(mySelectedIndex)
        self.cboSubcategory.blockSignals(False)

    # prevents actions being handled twice
    @pyqtSignature("")
    def on_pbnAddToList1_clicked(self):
        """Automatic slot executed when the pbnAddToList1 button is pressed.
        """
        if self.lePredefinedValue.text() != "" and self.cboKeyword.currentText() != "":
            myCurrentKey = self.tr(self.cboKeyword.currentText())
            myCurrentValue = self.lePredefinedValue.text()
            self.add_list_entry(myCurrentKey, myCurrentValue)
            self.lePredefinedValue.setText("")
            self.update_controls_from_list()

    # prevents actions being handled twice
    @pyqtSignature("")
    def on_pbnAddToList2_clicked(self):
        """Automatic slot executed when the pbnAddToList2 button is pressed.
        """

        myCurrentKey = self.leKey.text()
        myCurrentValue = self.leValue.text()
        if myCurrentKey == "category" and myCurrentValue == "hazard":
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.set_subcategory_list(self.standardHazardList)
            self.radHazard.blockSignals(False)
        elif myCurrentKey == "category" and myCurrentValue == "exposure":
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.set_subcategory_list(self.standardExposureList)
            self.radExposure.blockSignals(False)
        elif myCurrentKey == "category":
            # .. todo:: notify the user their category is invalid
            pass
        self.add_list_entry(myCurrentKey, myCurrentValue)
        self.leKey.setText("")
        self.leValue.setText("")
        self.update_controls_from_list()

    # prevents actions being handled twice
    @pyqtSignature("")
    def on_pbnRemove_clicked(self):
        """Automatic slot executed when the pbnRemove button is pressed.

        Any selected items in the keywords list will be removed.
        """
        for myItem in self.lstKeywords.selectedItems():
            self.lstKeywords.takeItem(self.lstKeywords.row(myItem))
        self.leKey.setText("")
        self.leValue.setText("")
        self.update_controls_from_list()

    def add_list_entry(self, key, value):
        """Add an item to the keywords list given its key/value.

        The key and value must both be valid, non empty strings
        or an InvalidKVPError will be raised.

        If an entry with the same key exists, it's value will be
        replaced with value.

        It will add the current key/value pair to the list if it is not
        already present. The kvp will also be stored in the data of the
        listwidgetitem as a simple string delimited with a bar ('|').

        :param key: The key part of the key value pair (kvp).
        :type key: str

        :param value: Value part of the key value pair (kvp).
        :type value: str
        """
        if key is None or key == "":
            return
        if value is None or value == "":
            return

        # make sure that both key and value is string
        key = str(key)
        value = str(value)
        myMessage = ""
        if ":" in key:
            key = key.replace(":", ".")
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if ":" in value:
            value = value.replace(":", ".")
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if myMessage == "":
            self.lblMessage.setText("")
            self.lblMessage.hide()
        else:
            self.lblMessage.setText(myMessage)
            self.lblMessage.show()
        myItem = QtGui.QListWidgetItem(key + ":" + value)
        # We are going to replace, so remove it if it exists already
        self.remove_item_by_key(key)
        myData = key + "|" + value
        myItem.setData(QtCore.Qt.UserRole, myData)
        self.lstKeywords.insertItem(0, myItem)

    def set_category(self, category):
        """Set the category radio button based on category.

        :param category: Either 'hazard', 'exposure' or 'postprocessing'.
        :type category: str

        :returns: False if radio button could not be updated, otherwise True.
        :rtype: bool
        """
        # convert from QString if needed
        myCategory = str(category)
        if self.get_value_for_key("category") == myCategory:
            # nothing to do, go home
            return True
        if myCategory not in ["hazard", "exposure", "postprocessing"]:
            # .. todo:: report an error to the user
            return False
            # Special case when category changes, we start on a new slate!

        if myCategory == "hazard":
            # only cause a toggle if we actually changed the category
            # This will only really be apparent if user manually enters
            # category as a keyword
            self.reset()
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.radHazard.blockSignals(False)
            self.remove_item_by_key("subcategory")
            self.remove_item_by_key("datatype")
            self.add_list_entry("category", "hazard")
            myList = self.standardHazardList
            self.set_subcategory_list(myList)

        elif myCategory == "exposure":
            self.reset()
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.radExposure.blockSignals(False)
            self.remove_item_by_key("subcategory")
            self.remove_item_by_key("unit")
            self.add_list_entry("category", "exposure")
            myList = self.standardExposureList
            self.set_subcategory_list(myList)

        else:
            self.reset()
            self.radPostprocessing.blockSignals(True)
            self.radPostprocessing.setChecked(True)
            self.radPostprocessing.blockSignals(False)
            self.remove_item_by_key("subcategory")
            self.add_list_entry("category", "postprocessing")

        return True

    def reset(self, primary_keywords_only=True):
        """Reset all controls to a blank state.

        :param primary_keywords_only: If True (the default), only reset
            Subcategory, datatype and units.
        :type primary_keywords_only: bool
        """

        self.cboSubcategory.clear()
        self.remove_item_by_key("subcategory")
        self.remove_item_by_key("datatype")
        self.remove_item_by_key("unit")
        self.remove_item_by_key("source")
        if not primary_keywords_only:
            # Clear everything else too
            self.lstKeywords.clear()
            self.leKey.clear()
            self.leValue.clear()
            self.lePredefinedValue.clear()
            self.leTitle.clear()

    def remove_item_by_key(self, key):
        """Remove an item from the kvp list given its key.

        :param key: Key of item to be removed.
        :type key: str
        """
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            if len(myTokens) < 2:
                break
            myKey = myTokens[0]
            if myKey == key:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def remove_item_by_value(self, value):
        """Remove an item from the kvp list given its key.

        :param value: Value of item to be removed.
        :type value: str
        """
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            myValue = myTokens[1]
            if myValue == value:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def get_value_for_key(self, key):
        """If key list contains a specific key, return its value.

        :param key: The key to search for
        :type key: str

        :returns: Value of key if matched otherwise none.
        :rtype: str
        """
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            if myKey == key:
                return myValue
        return None

    def load_state_from_keywords(self):
        """Set the ui state to match the keywords of the active layer.

        In case the layer has no keywords or any problem occurs reading them,
        start with a blank slate so that subcategory gets populated nicely &
        we will assume exposure to start with.
        """
        myKeywords = {"category": "exposure"}

        try:
            # Now read the layer with sub layer if needed
            myKeywords = self.keywordIO.read_keywords(self.layer)
        except (InvalidParameterError, HashNotFoundError, NoKeywordsFoundError):
            pass

        myLayerName = self.layer.name()
        if "title" not in myKeywords:
            self.leTitle.setText(myLayerName)
        self.lblLayerName.setText(self.tr("Keywords for %s" % myLayerName))
        # if we have a category key, unpack it first
        # so radio button etc get set
        if "category" in myKeywords:
            self.set_category(myKeywords["category"])
            myKeywords.pop("category")

        for myKey in myKeywords.iterkeys():
            self.add_list_entry(myKey, str(myKeywords[myKey]))

        # now make the rest of the safe_qgis reflect the list entries
        self.update_controls_from_list()

    def update_controls_from_list(self):
        """Set the ui state to match the keywords of the active layer."""
        mySubcategory = self.get_value_for_key("subcategory")
        myUnits = self.get_value_for_key("unit")
        myType = self.get_value_for_key("datatype")
        myTitle = self.get_value_for_key("title")
        if myTitle is not None:
            self.leTitle.setText(myTitle)
        elif self.layer is not None:
            myLayerName = self.layer.name()
            self.lblLayerName.setText(self.tr("Keywords for %s" % myLayerName))
        else:
            self.lblLayerName.setText("")

        if not is_polygon_layer(self.layer):
            self.radPostprocessing.setEnabled(False)

        # adapt gui if we are in postprocessing category
        self.toggle_postprocessing_widgets()

        if self.radExposure.isChecked():
            if mySubcategory is not None and myType is not None:
                self.set_subcategory_list(self.standardExposureList, mySubcategory + " [" + myType + "]")
            elif mySubcategory is not None:
                self.set_subcategory_list(self.standardExposureList, mySubcategory)
            else:
                self.set_subcategory_list(self.standardExposureList, self.tr("Not Set"))
        elif self.radHazard.isChecked():
            if mySubcategory is not None and myUnits is not None:
                self.set_subcategory_list(self.standardHazardList, mySubcategory + " [" + myUnits + "]")
            elif mySubcategory is not None:
                self.set_subcategory_list(self.standardHazardList, mySubcategory)
            else:
                self.set_subcategory_list(self.standardHazardList, self.tr("Not Set"))

        self.resize_dialog()

    def resize_dialog(self):
        """Resize the dialog to fit its contents."""
        # noinspection PyArgumentList
        QtCore.QCoreApplication.processEvents()
        LOGGER.debug("adjust ing dialog size")
        self.adjustSize()

    # prevents actions being handled twice
    @pyqtSignature("QString")
    def on_leTitle_textEdited(self, title):
        """Update the keywords list whenever the user changes the title.

        This slot is not called if the title is changed programmatically.

        :param title: New title keyword for the layer.
        :type title: str
        """
        self.add_list_entry("title", str(title))

    def get_keywords(self):
        """Obtain the state of the dialog as a keywords dict.

        :returns: Keywords reflecting the state of the dialog.
        :rtype: dict
        """
        # make sure title is listed
        if str(self.leTitle.text()) != "":
            self.add_list_entry("title", str(self.leTitle.text()))

        myKeywords = {}
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            myKeywords[myKey] = myValue
        return myKeywords

    def accept(self):
        """Automatic slot executed when the ok button is pressed.

        It will write out the keywords for the layer that is active.
        """
        self.apply_changes()
        myKeywords = self.get_keywords()
        try:
            self.keywordIO.write_keywords(layer=self.layer, keywords=myKeywords)
        except InaSAFEError, e:
            myErrorMessage = get_error_message(e)
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QtGui.QMessageBox.warning(
                self,
                self.tr("InaSAFE"),
                ((self.tr("An error was encountered when saving the keywords:\n" "%s" % myErrorMessage.to_html()))),
            )
        if self.dock is not None:
            self.dock.get_layers()
        self.done(QtGui.QDialog.Accepted)
Example #2
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """Dialog implementation class for the InaSAFE keywords editor."""
    def __init__(self, parent, iface, dock=None, layer=None):
        """Constructor for the dialog.

        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        :param parent: Parent widget of this dialog.
        :type parent: QWidget

        :param iface: Quantum GIS QGisAppInterface instance.
        :type iface: QGisAppInterface

        :param dock: Dock widget instance that we can notify of changes to
            the keywords. Optional.
        :type dock: Dock
        """
        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(
            self.tr('InaSAFE %s Keywords Editor' % get_version()))
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = dock

        if layer is None:
            self.layer = iface.activeLayer()
        else:
            self.layer = layer

        self.keyword_io = KeywordIO()

        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standard_exposure_list = OrderedDict([
            ('population', self.tr('population')),
            ('structure', self.tr('structure')), ('road', self.tr('road')),
            ('Not Set', self.tr('Not Set'))
        ])
        self.standard_hazard_list = OrderedDict([
            ('earthquake [MMI]', self.tr('earthquake [MMI]')),
            ('tsunami [m]', self.tr('tsunami [m]')),
            ('tsunami [wet/dry]', self.tr('tsunami [wet/dry]')),
            ('tsunami [feet]', self.tr('tsunami [feet]')),
            ('flood [m]', self.tr('flood [m]')),
            ('flood [wet/dry]', self.tr('flood [wet/dry]')),
            ('flood [feet]', self.tr('flood [feet]')),
            ('tephra [kg2/m2]', self.tr('tephra [kg2/m2]')),
            ('volcano', self.tr('volcano')), ('Not Set', self.tr('Not Set'))
        ])

        self.lstKeywords.itemClicked.connect(self.edit_key_value_pair)

        # Set up help dialog showing logic.
        help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        # set some initial ui state:
        self.defaults = breakdown_defaults()
        self.pbnAdvanced.setChecked(False)
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults['FEM_RATIO'])
        self.dsbFemaleRatioDefault.blockSignals(False)

        if self.layer:
            self.load_state_from_keywords()

        # add a reload from keywords button
        reload_button = self.buttonBox.addButton(
            self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole)
        reload_button.clicked.connect(self.load_state_from_keywords)
        self.grpAdvanced.setVisible(False)
        self.resize_dialog()

    def set_layer(self, layer):
        """Set the layer associated with the keyword editor.

        :param layer: Layer whose keywords should be edited.
        :type layer: QgsMapLayer
        """
        self.layer = layer
        self.load_state_from_keywords()

    #noinspection PyMethodMayBeStatic
    def show_help(self):
        """Load the help text for the keywords dialog."""
        show_context_help(context='keywords')

    def toggle_postprocessing_widgets(self):
        """Hide or show the post processing widgets depending on context."""
        LOGGER.debug('togglePostprocessingWidgets')
        postprocessing_flag = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not postprocessing_flag)
        self.lblSubcategory.setVisible(not postprocessing_flag)
        self.show_aggregation_attribute(postprocessing_flag)
        self.show_female_ratio_attribute(postprocessing_flag)
        self.show_female_ratio_default(postprocessing_flag)

    def show_aggregation_attribute(self, visible_flag):
        """Hide or show the aggregation attribute in the keyword editor dialog.

        :param visible_flag: Flag indicating if the aggregation attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboAggregationAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            current_keyword = self.get_value_for_key(
                self.defaults['AGGR_ATTR_KEY'])
            fields, attribute_position = layer_attribute_names(
                self.layer, [QtCore.QVariant.Int, QtCore.QVariant.String],
                current_keyword)
            box.addItems(fields)
            if attribute_position is None:
                box.setCurrentIndex(0)
            else:
                box.setCurrentIndex(attribute_position)

        box.setVisible(visible_flag)
        self.lblAggregationAttribute.setVisible(visible_flag)

    def show_female_ratio_attribute(self, visible_flag):
        """Hide or show the female ratio attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboFemaleRatioAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            current_keyword = self.get_value_for_key(
                self.defaults['FEM_RATIO_ATTR_KEY'])
            fields, attribute_position = layer_attribute_names(
                self.layer, [QtCore.QVariant.Double], current_keyword)
            fields.insert(0, self.tr('Use default'))
            fields.insert(1, self.tr('Don\'t use'))
            box.addItems(fields)
            if current_keyword == self.tr('Use default'):
                box.setCurrentIndex(0)
            elif current_keyword == self.tr('Don\'t use'):
                box.setCurrentIndex(1)
            elif attribute_position is None:
                # current_keyword was not found in the attribute table.
                # Use default
                box.setCurrentIndex(0)
            else:
                # + 2 is because we add use defaults and don't use
                box.setCurrentIndex(attribute_position + 2)
        box.setVisible(visible_flag)
        self.lblFemaleRatioAttribute.setVisible(visible_flag)

    def show_female_ratio_default(self, visible_flag):
        """Hide or show the female ratio default attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio
            default attribute should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.dsbFemaleRatioDefault
        if visible_flag:
            current_value = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_value is None:
                val = self.defaults['FEM_RATIO']
            else:
                val = float(current_value)
            box.setValue(val)

        box.setVisible(visible_flag)
        self.lblFemaleRatioDefault.setVisible(visible_flag)

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('int')
    def on_cboAggregationAttribute_currentIndexChanged(self, index=None):
        """Handler for aggregation attribute combo change.

        :param index: Not used but required for slot.
        """
        del index
        self.add_list_entry(self.defaults['AGGR_ATTR_KEY'],
                            self.cboAggregationAttribute.currentText())

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('int')
    def on_cboFemaleRatioAttribute_currentIndexChanged(self, index=None):
        """Handler for female ratio attribute change.

        :param index: Not used but required for slot.
        """
        del index
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr('Use default'):
            self.dsbFemaleRatioDefault.setEnabled(True)
            current_default = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_default is None:
                self.add_list_entry(self.defaults['FEM_RATIO_KEY'],
                                    self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
        self.add_list_entry(self.defaults['FEM_RATIO_ATTR_KEY'], text)

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('double')
    def on_dsbFemaleRatioDefault_valueChanged(self, value):
        """Handler for female ration default value changing.

        :param value: Not used but required for slot.
        """
        del value
        box = self.dsbFemaleRatioDefault
        if box.isEnabled():
            self.add_list_entry(self.defaults['FEM_RATIO_KEY'], box.value())

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_pbnAdvanced_toggled(self, flag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        self.toggle_advanced(flag)

    def toggle_advanced(self, flag):
        """Hide or show advanced editor.

        :param flag: Desired state for advanced editor visibility.
        :type flag: bool
        """
        if flag:
            self.pbnAdvanced.setText(self.tr('Hide advanced editor'))
        else:
            self.pbnAdvanced.setText(self.tr('Show advanced editor'))
        self.grpAdvanced.setVisible(flag)
        self.resize_dialog()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_radHazard_toggled(self, flag):
        """Automatic slot executed when the hazard radio is toggled.

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        if not flag:
            return
        self.set_category('hazard')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_radExposure_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled on.

        :param theFlag: Flag indicating the new checked state of the button.
        :type theFlag: bool
        """
        if not theFlag:
            return
        self.set_category('exposure')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_radPostprocessing_toggled(self, flag):
        """Automatic slot executed when the hazard radio is toggled on.

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        if not flag:
            self.remove_item_by_key(self.defaults['AGGR_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
            return
        self.set_category('postprocessing')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('int')
    def on_cboSubcategory_currentIndexChanged(self, index=None):
        """Automatic slot executed when the subcategory is changed.

        When the user changes the subcategory, we will extract the
        subcategory and dataype or unit (depending on if it is a hazard
        or exposure subcategory) from the [] after the name.

        :param index: Not used but required for Qt slot.
        """
        if index == -1:
            self.remove_item_by_key('subcategory')
            return

        text = self.cboSubcategory.itemData(self.cboSubcategory.currentIndex())

        # I found that myText is 'Not Set' for every language
        if text == self.tr('Not Set') or text == 'Not Set':
            self.remove_item_by_key('subcategory')
            return

        tokens = text.split(' ')
        if len(tokens) < 1:
            self.remove_item_by_key('subcategory')
            return

        subcategory = tokens[0]
        self.add_list_entry('subcategory', subcategory)

        # Some subcategories e.g. roads have no units or datatype
        if len(tokens) == 1:
            return
        if tokens[1].find('[') < 0:
            return
        category = self.get_value_for_key('category')
        if 'hazard' == category:
            units = tokens[1].replace('[', '').replace(']', '')
            self.add_list_entry('unit', units)
        if 'exposure' == category:
            data_type = tokens[1].replace('[', '').replace(']', '')
            self.add_list_entry('datatype', data_type)
            # prevents actions being handled twice

    def set_subcategory_list(self, entries, selected_item=None):
        """Helper to populate the subcategory list based on category context.

        :param entries: An OrderedDict of subcategories. The dict entries
             should be in the form ('earthquake', self.tr('earthquake')). See
             http://www.voidspace.org.uk/python/odict.html for info on
             OrderedDict.
        :type entries: OrderedDict

        :param selected_item: Which item should be selected in the combo. If
            the selected item is not in entries, it will be appended to it.
            This is optional.
        :type selected_item: str
        """
        # To avoid triggering on_cboSubcategory_currentIndexChanged
        # we block signals from the combo while updating it
        self.cboSubcategory.blockSignals(True)
        self.cboSubcategory.clear()
        item_selected_flag = selected_item is not None
        selected_item_values = selected_item not in entries.values()
        selected_item_keys = selected_item not in entries.keys()
        if (item_selected_flag and selected_item_values
                and selected_item_keys):
            # Add it to the OrderedList
            entries[selected_item] = selected_item
        index = 0
        selected_index = 0
        for key, value in entries.iteritems():
            if value == selected_item or key == selected_item:
                selected_index = index
            index += 1
            self.cboSubcategory.addItem(value, key)
        self.cboSubcategory.setCurrentIndex(selected_index)
        self.cboSubcategory.blockSignals(False)

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('')
    def on_pbnAddToList1_clicked(self):
        """Automatic slot executed when the pbnAddToList1 button is pressed.
        """
        if (self.lePredefinedValue.text() != ""
                and self.cboKeyword.currentText() != ""):
            current_key = self.tr(self.cboKeyword.currentText())
            current_value = self.lePredefinedValue.text()
            self.add_list_entry(current_key, current_value)
            self.lePredefinedValue.setText('')
            self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('')
    def on_pbnAddToList2_clicked(self):
        """Automatic slot executed when the pbnAddToList2 button is pressed.
        """

        current_key = self.leKey.text()
        current_value = self.leValue.text()
        if current_key == 'category' and current_value == 'hazard':
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.set_subcategory_list(self.standard_hazard_list)
            self.radHazard.blockSignals(False)
        elif current_key == 'category' and current_value == 'exposure':
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.set_subcategory_list(self.standard_exposure_list)
            self.radExposure.blockSignals(False)
        elif current_key == 'category':
            #.. todo:: notify the user their category is invalid
            pass
        self.add_list_entry(current_key, current_value)
        self.leKey.setText('')
        self.leValue.setText('')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('')
    def on_pbnRemove_clicked(self):
        """Automatic slot executed when the pbnRemove button is pressed.

        Any selected items in the keywords list will be removed.
        """
        for item in self.lstKeywords.selectedItems():
            self.lstKeywords.takeItem(self.lstKeywords.row(item))
        self.leKey.setText('')
        self.leValue.setText('')
        self.update_controls_from_list()

    def add_list_entry(self, key, value):
        """Add an item to the keywords list given its key/value.

        The key and value must both be valid, non empty strings
        or an InvalidKVPError will be raised.

        If an entry with the same key exists, it's value will be
        replaced with value.

        It will add the current key/value pair to the list if it is not
        already present. The kvp will also be stored in the data of the
        listwidgetitem as a simple string delimited with a bar ('|').

        :param key: The key part of the key value pair (kvp).
        :type key: str

        :param value: Value part of the key value pair (kvp).
        :type value: str
        """
        if key is None or key == '':
            return
        if value is None or value == '':
            return

        # make sure that both key and value is string
        key = str(key)
        value = str(value)
        message = ''
        if ':' in key:
            key = key.replace(':', '.')
            message = self.tr('Colons are not allowed, replaced with "."')
        if ':' in value:
            value = value.replace(':', '.')
            message = self.tr('Colons are not allowed, replaced with "."')
        if message == '':
            self.lblMessage.setText('')
            self.lblMessage.hide()
        else:
            self.lblMessage.setText(message)
            self.lblMessage.show()
        item = QtGui.QListWidgetItem(key + ':' + value)
        # We are going to replace, so remove it if it exists already
        self.remove_item_by_key(key)
        data = key + '|' + value
        item.setData(QtCore.Qt.UserRole, data)
        self.lstKeywords.insertItem(0, item)

    def set_category(self, category):
        """Set the category radio button based on category.

        :param category: Either 'hazard', 'exposure' or 'postprocessing'.
        :type category: str

        :returns: False if radio button could not be updated, otherwise True.
        :rtype: bool
        """
        # convert from QString if needed
        category = str(category)
        if self.get_value_for_key('category') == category:
            #nothing to do, go home
            return True
        if category not in ['hazard', 'exposure', 'postprocessing']:
            # .. todo:: report an error to the user
            return False
            # Special case when category changes, we start on a new slate!

        if category == 'hazard':
            # only cause a toggle if we actually changed the category
            # This will only really be apparent if user manually enters
            # category as a keyword
            self.reset()
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.radHazard.blockSignals(False)
            self.remove_item_by_key('subcategory')
            self.remove_item_by_key('datatype')
            self.add_list_entry('category', 'hazard')
            hazard_list = self.standard_hazard_list
            self.set_subcategory_list(hazard_list)

        elif category == 'exposure':
            self.reset()
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.radExposure.blockSignals(False)
            self.remove_item_by_key('subcategory')
            self.remove_item_by_key('unit')
            self.add_list_entry('category', 'exposure')
            exposure_list = self.standard_exposure_list
            self.set_subcategory_list(exposure_list)

        else:
            self.reset()
            self.radPostprocessing.blockSignals(True)
            self.radPostprocessing.setChecked(True)
            self.radPostprocessing.blockSignals(False)
            self.remove_item_by_key('subcategory')
            self.add_list_entry('category', 'postprocessing')

        return True

    def reset(self, primary_keywords_only=True):
        """Reset all controls to a blank state.

        :param primary_keywords_only: If True (the default), only reset
            Subcategory, datatype and units.
        :type primary_keywords_only: bool
        """

        self.cboSubcategory.clear()
        self.remove_item_by_key('subcategory')
        self.remove_item_by_key('datatype')
        self.remove_item_by_key('unit')
        self.remove_item_by_key('source')
        if not primary_keywords_only:
            # Clear everything else too
            self.lstKeywords.clear()
            self.leKey.clear()
            self.leValue.clear()
            self.lePredefinedValue.clear()
            self.leTitle.clear()
            self.leSource.clear()

    def remove_item_by_key(self, removal_key):
        """Remove an item from the kvp list given its key.

        :param removal_key: Key of item to be removed.
        :type removal_key: str
        """
        for myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            text = existing_item.text()
            tokens = text.split(':')
            if len(tokens) < 2:
                break
            key = tokens[0]
            if removal_key == key:
                # remove it since the removal_key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def remove_item_by_value(self, removal_value):
        """Remove an item from the kvp list given its key.

        :param removal_value: Value of item to be removed.
        :type removal_value: str
        """
        for counter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(counter)
            text = existing_item.text()
            tokens = text.split(':')
            value = tokens[1]
            if removal_value == value:
                # remove it since the key is already present
                self.lstKeywords.takeItem(counter)
                break

    def get_value_for_key(self, lookup_key):
        """If key list contains a specific key, return its value.

        :param lookup_key: The key to search for
        :type lookup_key: str

        :returns: Value of key if matched otherwise none.
        :rtype: str
        """
        for counter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(counter)
            text = existing_item.text()
            tokens = text.split(':')
            key = str(tokens[0]).strip()
            value = str(tokens[1]).strip()
            if lookup_key == key:
                return value
        return None

    def load_state_from_keywords(self):
        """Set the ui state to match the keywords of the active layer.

        In case the layer has no keywords or any problem occurs reading them,
        start with a blank slate so that subcategory gets populated nicely &
        we will assume exposure to start with.

        Also if only title is set we use similar logic (title is added by
        default in dock and other defaults need to be explicitly added
        when opening this dialog). See #751

        """
        keywords = {'category': 'exposure'}

        try:
            # Now read the layer with sub layer if needed
            keywords = self.keyword_io.read_keywords(self.layer)
        except (InvalidParameterError, HashNotFoundError,
                NoKeywordsFoundError):
            pass

        layer_name = self.layer.name()
        if 'title' not in keywords:
            self.leTitle.setText(layer_name)
        self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name))

        if 'source' in keywords:
            self.leSource.setText(keywords['source'])
        else:
            self.leSource.setText('')

        # if we have a category key, unpack it first
        # so radio button etc get set
        if 'category' in keywords:
            self.set_category(keywords['category'])
            keywords.pop('category')
        else:
            # assume exposure to match ui. See issue #751
            self.add_list_entry('category', 'exposure')

        for key in keywords.iterkeys():
            self.add_list_entry(key, str(keywords[key]))

        # now make the rest of the safe_qgis reflect the list entries
        self.update_controls_from_list()

    def update_controls_from_list(self):
        """Set the ui state to match the keywords of the active layer."""
        subcategory = self.get_value_for_key('subcategory')
        units = self.get_value_for_key('unit')
        data_type = self.get_value_for_key('datatype')
        title = self.get_value_for_key('title')

        if title is not None:
            self.leTitle.setText(title)
        elif self.layer is not None:
            layer_name = self.layer.name()
            self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name))
        else:
            self.lblLayerName.setText('')

        if not is_polygon_layer(self.layer):
            self.radPostprocessing.setEnabled(False)

        # adapt gui if we are in postprocessing category
        self.toggle_postprocessing_widgets()

        if self.radExposure.isChecked():
            if subcategory is not None and data_type is not None:
                self.set_subcategory_list(self.standard_exposure_list,
                                          subcategory + ' [' + data_type + ']')
            elif subcategory is not None:
                self.set_subcategory_list(self.standard_exposure_list,
                                          subcategory)
            else:
                self.set_subcategory_list(self.standard_exposure_list,
                                          self.tr('Not Set'))
        elif self.radHazard.isChecked():
            if subcategory is not None and units is not None:
                self.set_subcategory_list(self.standard_hazard_list,
                                          subcategory + ' [' + units + ']')
            elif subcategory is not None:
                self.set_subcategory_list(self.standard_hazard_list,
                                          subcategory)
            else:
                self.set_subcategory_list(self.standard_hazard_list,
                                          self.tr('Not Set'))

        self.resize_dialog()

    def resize_dialog(self):
        """Resize the dialog to fit its contents."""
        # noinspection PyArgumentList
        QtCore.QCoreApplication.processEvents()
        LOGGER.debug('adjust ing dialog size')
        self.adjustSize()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('QString')
    def on_leTitle_textEdited(self, title):
        """Update the keywords list whenever the user changes the title.

        This slot is not called if the title is changed programmatically.

        :param title: New title keyword for the layer.
        :type title: str
        """
        self.add_list_entry('title', str(title))

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('QString')
    def on_leSource_textEdited(self, source):
        """Update the keywords list whenever the user changes the source.

        This slot is not called if the source is changed programmatically.

        :param source: New source keyword for the layer.
        :type source: str
        """
        if source is None or source == '':
            self.remove_item_by_key('source')
        else:
            self.add_list_entry('source', str(source))

    def get_keywords(self):
        """Obtain the state of the dialog as a keywords dict.

        :returns: Keywords reflecting the state of the dialog.
        :rtype: dict
        """
        #make sure title is listed
        if str(self.leTitle.text()) != '':
            self.add_list_entry('title', str(self.leTitle.text()))

        # make sure the source is listed too
        if str(self.leSource.text()) != '':
            self.add_list_entry('source', str(self.leSource.text()))

        keywords = {}
        for myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            text = existing_item.text()
            tokens = text.split(':')
            key = str(tokens[0]).strip()
            value = str(tokens[1]).strip()
            keywords[key] = value
        return keywords

    def accept(self):
        """Automatic slot executed when the ok button is pressed.

        It will write out the keywords for the layer that is active.
        """
        self.apply_changes()
        keywords = self.get_keywords()
        try:
            self.keyword_io.write_keywords(layer=self.layer, keywords=keywords)
        except InaSAFEError, e:
            error_message = get_error_message(e)
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QtGui.QMessageBox.warning(self, self.tr('InaSAFE'), ((self.tr(
                'An error was encountered when saving the keywords:\n'
                '%s' % error_message.to_html()))))
        if self.dock is not None:
            self.dock.get_layers()
        self.done(QtGui.QDialog.Accepted)
Example #3
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """Dialog implementation class for the InaSAFE keywords editor."""

    def __init__(self, parent, iface, dock=None, layer=None):
        """Constructor for the dialog.

        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        :param parent: Parent widget of this dialog.
        :type parent: QWidget

        :param iface: Quantum GIS QGisAppInterface instance.
        :type iface: QGisAppInterface

        :param dock: Dock widget instance that we can notify of changes to
            the keywords. Optional.
        :type dock: Dock
        """
        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr(
            'InaSAFE %s Keywords Editor' % get_version()))
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = dock

        if layer is None:
            self.layer = iface.activeLayer()
        else:
            self.layer = layer

        self.keyword_io = KeywordIO()

        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standard_exposure_list = OrderedDict(
            [('population', self.tr('population')),
             ('structure', self.tr('structure')),
             ('road', self.tr('road')),
             ('Not Set', self.tr('Not Set'))])
        self.standard_hazard_list = OrderedDict(
            [('earthquake [MMI]', self.tr('earthquake [MMI]')),
             ('tsunami [m]', self.tr('tsunami [m]')),
             ('tsunami [wet/dry]', self.tr('tsunami [wet/dry]')),
             ('tsunami [feet]', self.tr('tsunami [feet]')),
             ('flood [m]', self.tr('flood [m]')),
             ('flood [wet/dry]', self.tr('flood [wet/dry]')),
             ('flood [feet]', self.tr('flood [feet]')),
             ('tephra [kg2/m2]', self.tr('tephra [kg2/m2]')),
             ('volcano', self.tr('volcano')),
             ('Not Set', self.tr('Not Set'))])

        self.lstKeywords.itemClicked.connect(self.edit_key_value_pair)

        # Set up help dialog showing logic.
        help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        # set some initial ui state:
        self.defaults = breakdown_defaults()
        self.pbnAdvanced.setChecked(False)
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults['FEM_RATIO'])
        self.dsbFemaleRatioDefault.blockSignals(False)

        if self.layer:
            self.load_state_from_keywords()

        # add a reload from keywords button
        reload_button = self.buttonBox.addButton(
            self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole)
        reload_button.clicked.connect(self.load_state_from_keywords)
        self.grpAdvanced.setVisible(False)
        self.resize_dialog()

    def set_layer(self, layer):
        """Set the layer associated with the keyword editor.

        :param layer: Layer whose keywords should be edited.
        :type layer: QgsMapLayer
        """
        self.layer = layer
        self.load_state_from_keywords()

    #noinspection PyMethodMayBeStatic
    def show_help(self):
        """Load the help text for the keywords dialog."""
        show_context_help(context='keywords')

    def toggle_postprocessing_widgets(self):
        """Hide or show the post processing widgets depending on context."""
        LOGGER.debug('togglePostprocessingWidgets')
        postprocessing_flag = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not postprocessing_flag)
        self.lblSubcategory.setVisible(not postprocessing_flag)
        self.show_aggregation_attribute(postprocessing_flag)
        self.show_female_ratio_attribute(postprocessing_flag)
        self.show_female_ratio_default(postprocessing_flag)

    def show_aggregation_attribute(self, visible_flag):
        """Hide or show the aggregation attribute in the keyword editor dialog.

        :param visible_flag: Flag indicating if the aggregation attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboAggregationAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            current_keyword = self.get_value_for_key(
                self.defaults['AGGR_ATTR_KEY'])
            fields, attribute_position = layer_attribute_names(
                self.layer,
                [QtCore.QVariant.Int, QtCore.QVariant.String],
                current_keyword)
            box.addItems(fields)
            if attribute_position is None:
                box.setCurrentIndex(0)
            else:
                box.setCurrentIndex(attribute_position)

        box.setVisible(visible_flag)
        self.lblAggregationAttribute.setVisible(visible_flag)

    def show_female_ratio_attribute(self, visible_flag):
        """Hide or show the female ratio attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboFemaleRatioAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            current_keyword = self.get_value_for_key(
                self.defaults['FEM_RATIO_ATTR_KEY'])
            fields, attribute_position = layer_attribute_names(
                self.layer,
                [QtCore.QVariant.Double],
                current_keyword)
            fields.insert(0, self.tr('Use default'))
            fields.insert(1, self.tr('Don\'t use'))
            box.addItems(fields)
            if current_keyword == self.tr('Use default'):
                box.setCurrentIndex(0)
            elif current_keyword == self.tr('Don\'t use'):
                box.setCurrentIndex(1)
            elif attribute_position is None:
                # current_keyword was not found in the attribute table.
                # Use default
                box.setCurrentIndex(0)
            else:
                # + 2 is because we add use defaults and don't use
                box.setCurrentIndex(attribute_position + 2)
        box.setVisible(visible_flag)
        self.lblFemaleRatioAttribute.setVisible(visible_flag)

    def show_female_ratio_default(self, visible_flag):
        """Hide or show the female ratio default attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio
            default attribute should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.dsbFemaleRatioDefault
        if visible_flag:
            current_value = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_value is None:
                val = self.defaults['FEM_RATIO']
            else:
                val = float(current_value)
            box.setValue(val)

        box.setVisible(visible_flag)
        self.lblFemaleRatioDefault.setVisible(visible_flag)

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('int')
    def on_cboAggregationAttribute_currentIndexChanged(self, index=None):
        """Handler for aggregation attribute combo change.

        :param index: Not used but required for slot.
        """
        del index
        self.add_list_entry(
            self.defaults['AGGR_ATTR_KEY'],
            self.cboAggregationAttribute.currentText())

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('int')
    def on_cboFemaleRatioAttribute_currentIndexChanged(self, index=None):
        """Handler for female ratio attribute change.

        :param index: Not used but required for slot.
        """
        del index
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr('Use default'):
            self.dsbFemaleRatioDefault.setEnabled(True)
            current_default = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_default is None:
                self.add_list_entry(
                    self.defaults['FEM_RATIO_KEY'],
                    self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
        self.add_list_entry(self.defaults['FEM_RATIO_ATTR_KEY'], text)

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('double')
    def on_dsbFemaleRatioDefault_valueChanged(self, value):
        """Handler for female ration default value changing.

        :param value: Not used but required for slot.
        """
        del value
        box = self.dsbFemaleRatioDefault
        if box.isEnabled():
            self.add_list_entry(
                self.defaults['FEM_RATIO_KEY'],
                box.value())

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_pbnAdvanced_toggled(self, flag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        self.toggle_advanced(flag)

    def toggle_advanced(self, flag):
        """Hide or show advanced editor.

        :param flag: Desired state for advanced editor visibility.
        :type flag: bool
        """
        if flag:
            self.pbnAdvanced.setText(self.tr('Hide advanced editor'))
        else:
            self.pbnAdvanced.setText(self.tr('Show advanced editor'))
        self.grpAdvanced.setVisible(flag)
        self.resize_dialog()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_radHazard_toggled(self, flag):
        """Automatic slot executed when the hazard radio is toggled.

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        if not flag:
            return
        self.set_category('hazard')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_radExposure_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled on.

        :param theFlag: Flag indicating the new checked state of the button.
        :type theFlag: bool
        """
        if not theFlag:
            return
        self.set_category('exposure')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_radPostprocessing_toggled(self, flag):
        """Automatic slot executed when the hazard radio is toggled on.

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        if not flag:
            self.remove_item_by_key(self.defaults['AGGR_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
            return
        self.set_category('postprocessing')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('int')
    def on_cboSubcategory_currentIndexChanged(self, index=None):
        """Automatic slot executed when the subcategory is changed.

        When the user changes the subcategory, we will extract the
        subcategory and dataype or unit (depending on if it is a hazard
        or exposure subcategory) from the [] after the name.

        :param index: Not used but required for Qt slot.
        """
        if index == -1:
            self.remove_item_by_key('subcategory')
            return

        text = self.cboSubcategory.itemData(
            self.cboSubcategory.currentIndex())

        # I found that myText is 'Not Set' for every language
        if text == self.tr('Not Set') or text == 'Not Set':
            self.remove_item_by_key('subcategory')
            return

        tokens = text.split(' ')
        if len(tokens) < 1:
            self.remove_item_by_key('subcategory')
            return

        subcategory = tokens[0]
        self.add_list_entry('subcategory', subcategory)

        # Some subcategories e.g. roads have no units or datatype
        if len(tokens) == 1:
            return
        if tokens[1].find('[') < 0:
            return
        category = self.get_value_for_key('category')
        if 'hazard' == category:
            units = tokens[1].replace('[', '').replace(']', '')
            self.add_list_entry('unit', units)
        if 'exposure' == category:
            data_type = tokens[1].replace('[', '').replace(']', '')
            self.add_list_entry('datatype', data_type)
            # prevents actions being handled twice

    def set_subcategory_list(self, entries, selected_item=None):
        """Helper to populate the subcategory list based on category context.

        :param entries: An OrderedDict of subcategories. The dict entries
             should be in the form ('earthquake', self.tr('earthquake')). See
             http://www.voidspace.org.uk/python/odict.html for info on
             OrderedDict.
        :type entries: OrderedDict

        :param selected_item: Which item should be selected in the combo. If
            the selected item is not in entries, it will be appended to it.
            This is optional.
        :type selected_item: str
        """
        # To avoid triggering on_cboSubcategory_currentIndexChanged
        # we block signals from the combo while updating it
        self.cboSubcategory.blockSignals(True)
        self.cboSubcategory.clear()
        item_selected_flag = selected_item is not None
        selected_item_values = selected_item not in entries.values()
        selected_item_keys = selected_item not in entries.keys()
        if (item_selected_flag and selected_item_values and
                selected_item_keys):
            # Add it to the OrderedList
            entries[selected_item] = selected_item
        index = 0
        selected_index = 0
        for key, value in entries.iteritems():
            if value == selected_item or key == selected_item:
                selected_index = index
            index += 1
            self.cboSubcategory.addItem(value, key)
        self.cboSubcategory.setCurrentIndex(selected_index)
        self.cboSubcategory.blockSignals(False)

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('')
    def on_pbnAddToList1_clicked(self):
        """Automatic slot executed when the pbnAddToList1 button is pressed.
        """
        if (self.lePredefinedValue.text() != "" and
                self.cboKeyword.currentText() != ""):
            current_key = self.tr(self.cboKeyword.currentText())
            current_value = self.lePredefinedValue.text()
            self.add_list_entry(current_key, current_value)
            self.lePredefinedValue.setText('')
            self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('')
    def on_pbnAddToList2_clicked(self):
        """Automatic slot executed when the pbnAddToList2 button is pressed.
        """

        current_key = self.leKey.text()
        current_value = self.leValue.text()
        if current_key == 'category' and current_value == 'hazard':
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.set_subcategory_list(self.standard_hazard_list)
            self.radHazard.blockSignals(False)
        elif current_key == 'category' and current_value == 'exposure':
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.set_subcategory_list(self.standard_exposure_list)
            self.radExposure.blockSignals(False)
        elif current_key == 'category':
            #.. todo:: notify the user their category is invalid
            pass
        self.add_list_entry(current_key, current_value)
        self.leKey.setText('')
        self.leValue.setText('')
        self.update_controls_from_list()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('')
    def on_pbnRemove_clicked(self):
        """Automatic slot executed when the pbnRemove button is pressed.

        Any selected items in the keywords list will be removed.
        """
        for item in self.lstKeywords.selectedItems():
            self.lstKeywords.takeItem(self.lstKeywords.row(item))
        self.leKey.setText('')
        self.leValue.setText('')
        self.update_controls_from_list()

    def add_list_entry(self, key, value):
        """Add an item to the keywords list given its key/value.

        The key and value must both be valid, non empty strings
        or an InvalidKVPError will be raised.

        If an entry with the same key exists, it's value will be
        replaced with value.

        It will add the current key/value pair to the list if it is not
        already present. The kvp will also be stored in the data of the
        listwidgetitem as a simple string delimited with a bar ('|').

        :param key: The key part of the key value pair (kvp).
        :type key: str

        :param value: Value part of the key value pair (kvp).
        :type value: str
        """
        if key is None or key == '':
            return
        if value is None or value == '':
            return

        # make sure that both key and value is string
        key = str(key)
        value = str(value)
        message = ''
        if ':' in key:
            key = key.replace(':', '.')
            message = self.tr('Colons are not allowed, replaced with "."')
        if ':' in value:
            value = value.replace(':', '.')
            message = self.tr('Colons are not allowed, replaced with "."')
        if message == '':
            self.lblMessage.setText('')
            self.lblMessage.hide()
        else:
            self.lblMessage.setText(message)
            self.lblMessage.show()
        item = QtGui.QListWidgetItem(key + ':' + value)
        # We are going to replace, so remove it if it exists already
        self.remove_item_by_key(key)
        data = key + '|' + value
        item.setData(QtCore.Qt.UserRole, data)
        self.lstKeywords.insertItem(0, item)

    def set_category(self, category):
        """Set the category radio button based on category.

        :param category: Either 'hazard', 'exposure' or 'postprocessing'.
        :type category: str

        :returns: False if radio button could not be updated, otherwise True.
        :rtype: bool
        """
        # convert from QString if needed
        category = str(category)
        if self.get_value_for_key('category') == category:
            #nothing to do, go home
            return True
        if category not in ['hazard', 'exposure', 'postprocessing']:
            # .. todo:: report an error to the user
            return False
            # Special case when category changes, we start on a new slate!

        if category == 'hazard':
            # only cause a toggle if we actually changed the category
            # This will only really be apparent if user manually enters
            # category as a keyword
            self.reset()
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.radHazard.blockSignals(False)
            self.remove_item_by_key('subcategory')
            self.remove_item_by_key('datatype')
            self.add_list_entry('category', 'hazard')
            hazard_list = self.standard_hazard_list
            self.set_subcategory_list(hazard_list)

        elif category == 'exposure':
            self.reset()
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.radExposure.blockSignals(False)
            self.remove_item_by_key('subcategory')
            self.remove_item_by_key('unit')
            self.add_list_entry('category', 'exposure')
            exposure_list = self.standard_exposure_list
            self.set_subcategory_list(exposure_list)

        else:
            self.reset()
            self.radPostprocessing.blockSignals(True)
            self.radPostprocessing.setChecked(True)
            self.radPostprocessing.blockSignals(False)
            self.remove_item_by_key('subcategory')
            self.add_list_entry('category', 'postprocessing')

        return True

    def reset(self, primary_keywords_only=True):
        """Reset all controls to a blank state.

        :param primary_keywords_only: If True (the default), only reset
            Subcategory, datatype and units.
        :type primary_keywords_only: bool
        """

        self.cboSubcategory.clear()
        self.remove_item_by_key('subcategory')
        self.remove_item_by_key('datatype')
        self.remove_item_by_key('unit')
        self.remove_item_by_key('source')
        if not primary_keywords_only:
            # Clear everything else too
            self.lstKeywords.clear()
            self.leKey.clear()
            self.leValue.clear()
            self.lePredefinedValue.clear()
            self.leTitle.clear()
            self.leSource.clear()

    def remove_item_by_key(self, removal_key):
        """Remove an item from the kvp list given its key.

        :param removal_key: Key of item to be removed.
        :type removal_key: str
        """
        for myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            text = existing_item.text()
            tokens = text.split(':')
            if len(tokens) < 2:
                break
            key = tokens[0]
            if removal_key == key:
                # remove it since the removal_key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def remove_item_by_value(self, removal_value):
        """Remove an item from the kvp list given its key.

        :param removal_value: Value of item to be removed.
        :type removal_value: str
        """
        for counter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(counter)
            text = existing_item.text()
            tokens = text.split(':')
            value = tokens[1]
            if removal_value == value:
                # remove it since the key is already present
                self.lstKeywords.takeItem(counter)
                break

    def get_value_for_key(self, lookup_key):
        """If key list contains a specific key, return its value.

        :param lookup_key: The key to search for
        :type lookup_key: str

        :returns: Value of key if matched otherwise none.
        :rtype: str
        """
        for counter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(counter)
            text = existing_item.text()
            tokens = text.split(':')
            key = str(tokens[0]).strip()
            value = str(tokens[1]).strip()
            if lookup_key == key:
                return value
        return None

    def load_state_from_keywords(self):
        """Set the ui state to match the keywords of the active layer.

        In case the layer has no keywords or any problem occurs reading them,
        start with a blank slate so that subcategory gets populated nicely &
        we will assume exposure to start with.

        Also if only title is set we use similar logic (title is added by
        default in dock and other defaults need to be explicitly added
        when opening this dialog). See #751

        """
        keywords = {'category': 'exposure'}

        try:
            # Now read the layer with sub layer if needed
            keywords = self.keyword_io.read_keywords(self.layer)
        except (InvalidParameterError,
                HashNotFoundError,
                NoKeywordsFoundError):
            pass

        layer_name = self.layer.name()
        if 'title' not in keywords:
            self.leTitle.setText(layer_name)
        self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name))

        if 'source' in keywords:
            self.leSource.setText(keywords['source'])
        else:
            self.leSource.setText('')

        # if we have a category key, unpack it first
        # so radio button etc get set
        if 'category' in keywords:
            self.set_category(keywords['category'])
            keywords.pop('category')
        else:
            # assume exposure to match ui. See issue #751
            self.add_list_entry('category', 'exposure')

        for key in keywords.iterkeys():
            self.add_list_entry(key, str(keywords[key]))

        # now make the rest of the safe_qgis reflect the list entries
        self.update_controls_from_list()

    def update_controls_from_list(self):
        """Set the ui state to match the keywords of the active layer."""
        subcategory = self.get_value_for_key('subcategory')
        units = self.get_value_for_key('unit')
        data_type = self.get_value_for_key('datatype')
        title = self.get_value_for_key('title')

        if title is not None:
            self.leTitle.setText(title)
        elif self.layer is not None:
            layer_name = self.layer.name()
            self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name))
        else:
            self.lblLayerName.setText('')

        if not is_polygon_layer(self.layer):
            self.radPostprocessing.setEnabled(False)

        # adapt gui if we are in postprocessing category
        self.toggle_postprocessing_widgets()

        if self.radExposure.isChecked():
            if subcategory is not None and data_type is not None:
                self.set_subcategory_list(
                    self.standard_exposure_list,
                    subcategory + ' [' + data_type + ']')
            elif subcategory is not None:
                self.set_subcategory_list(
                    self.standard_exposure_list, subcategory)
            else:
                self.set_subcategory_list(
                    self.standard_exposure_list,
                    self.tr('Not Set'))
        elif self.radHazard.isChecked():
            if subcategory is not None and units is not None:
                self.set_subcategory_list(
                    self.standard_hazard_list,
                    subcategory + ' [' + units + ']')
            elif subcategory is not None:
                self.set_subcategory_list(
                    self.standard_hazard_list,
                    subcategory)
            else:
                self.set_subcategory_list(
                    self.standard_hazard_list,
                    self.tr('Not Set'))

        self.resize_dialog()

    def resize_dialog(self):
        """Resize the dialog to fit its contents."""
        # noinspection PyArgumentList
        QtCore.QCoreApplication.processEvents()
        LOGGER.debug('adjust ing dialog size')
        self.adjustSize()

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('QString')
    def on_leTitle_textEdited(self, title):
        """Update the keywords list whenever the user changes the title.

        This slot is not called if the title is changed programmatically.

        :param title: New title keyword for the layer.
        :type title: str
        """
        self.add_list_entry('title', str(title))

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('QString')
    def on_leSource_textEdited(self, source):
        """Update the keywords list whenever the user changes the source.

        This slot is not called if the source is changed programmatically.

        :param source: New source keyword for the layer.
        :type source: str
        """
        if source is None or source == '':
            self.remove_item_by_key('source')
        else:
            self.add_list_entry('source', str(source))

    def get_keywords(self):
        """Obtain the state of the dialog as a keywords dict.

        :returns: Keywords reflecting the state of the dialog.
        :rtype: dict
        """
        #make sure title is listed
        if str(self.leTitle.text()) != '':
            self.add_list_entry('title', str(self.leTitle.text()))

        # make sure the source is listed too
        if str(self.leSource.text()) != '':
            self.add_list_entry('source', str(self.leSource.text()))

        keywords = {}
        for myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            text = existing_item.text()
            tokens = text.split(':')
            key = str(tokens[0]).strip()
            value = str(tokens[1]).strip()
            keywords[key] = value
        return keywords

    def accept(self):
        """Automatic slot executed when the ok button is pressed.

        It will write out the keywords for the layer that is active.
        """
        self.apply_changes()
        keywords = self.get_keywords()
        try:
            self.keyword_io.write_keywords(
                layer=self.layer, keywords=keywords)
        except InaSAFEError, e:
            error_message = get_error_message(e)
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QtGui.QMessageBox.warning(
                self, self.tr('InaSAFE'),
                ((self.tr(
                    'An error was encountered when saving the keywords:\n'
                    '%s' % error_message.to_html()))))
        if self.dock is not None:
            self.dock.get_layers()
        self.done(QtGui.QDialog.Accepted)
Example #4
0
class Aggregator(QtCore.QObject):
    """The aggregator class facilitates aggregation of impact function results.
    """

    def __init__(
            self,
            iface,
            theAggregationLayer):
        """Director for aggregation based operations.
        Args:
          theAggregationLayer: QgsMapLayer representing clipped
              aggregation. This will be converted to a memory layer inside
              this class. see self.layer
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtCore.QObject.__init__(self)

        self.hazardLayer = None
        self.exposureLayer = None
        self.safeLayer = None

        self.prefix = 'aggr_'
        self.attributes = {}
        self.attributeTitle = None

        self.iface = iface
        self.keywordIO = KeywordIO()
        self.defaults = defaults()
        self.errorMessage = None
        self.targetField = None
        self.impactLayerAttributes = []
        self.aoiMode = True

        # If this flag is not True, no aggregation or postprocessing will run
        # this is set as True by validateKeywords()
        self.isValid = False
        self.showIntermediateLayers = False

        # This is used to hold an *in memory copy* of the aggregation layer
        # or None if the clip extents should be used.
        if theAggregationLayer is None:
            self.aoiMode = True
            # Will be completed in _prepareLayer just before deintersect call
            self.layer = self._createPolygonLayer()
        else:
            self.aoiMode = False
            self.layer = theAggregationLayer

    def validateKeywords(self):
        """Check if the postprocessing layer has all needed attribute keywords.

        This is only applicable in the case where were are not using the AOI
        (in other words self.aoiMode is False). When self.aoiMode is True
        then we always use just the defaults and dont allow the user to
        create custom aggregation field mappings.

        This method is called on instance creation and should always be
        called if you change any state of the aggregator class.

        On completion of this method the self.isValid flag is set. If this
        flag is not True, then no aggregation or postprocessing work will be
        carried out (these methods will raise an InvalidAggregatorError).

        Args:
            None

        Returns:
            None

        Raises:
            Errors are propogated
        """

        # Otherwise get the attributes for the aggregation layer.
        # noinspection PyBroadException
        try:
            myKeywords = self.keywordIO.read_keywords(self.layer)
        #discussed with Tim,in this case its ok to be generic
        except Exception:  # pylint: disable=W0703
            myKeywords = {}

        if self.aoiMode:
            myKeywords[self.defaults['FEM_RATIO_ATTR_KEY']] = self.tr(
                'Use default')
            self.keywordIO.update_keywords(self.layer, myKeywords)
            self.isValid = True
            return
        else:
            myMessage = m.Message(
                m.Heading(
                    self.tr('Select attribute'), **PROGRESS_UPDATE_STYLE),
                m.Paragraph(self.tr(
                    'Please select which attribute you want to use as ID for '
                    'the aggregated results')))
            self._sendMessage(myMessage)

            #myKeywords are already complete
            category = myKeywords['category']
            aggregationAttribute = self.defaults['AGGR_ATTR_KEY']
            femaleRatio = self.defaults['FEM_RATIO_ATTR_KEY']
            femaleRatioKey = self.defaults['FEM_RATIO_KEY']
            if ('category' in myKeywords and
                category == 'postprocessing' and
                aggregationAttribute in myKeywords and
                femaleRatio in myKeywords and
                (femaleRatio != self.tr('Use default') or
                 femaleRatioKey in myKeywords)):
                self.isValid = True
            #some keywords are needed
            else:
                #set the default values by writing to the myKeywords
                myKeywords['category'] = 'postprocessing'

                myAttributes, _ = layer_attribute_names(
                    self.layer,
                    [QtCore.QVariant.Int, QtCore.QVariant.String])
                if self.defaults['AGGR_ATTR_KEY'] not in myKeywords:
                    myKeywords[self.defaults['AGGR_ATTR_KEY']] = \
                        myAttributes[0]

                if self.defaults['FEM_RATIO_ATTR_KEY'] not in myKeywords:
                    myKeywords[self.defaults['FEM_RATIO_ATTR_KEY']] = self.tr(
                        'Use default')

                if self.defaults['FEM_RATIO_KEY'] not in myKeywords:
                    myKeywords[self.defaults['FEM_RATIO_KEY']] = \
                        self.defaults['FEM_RATIO']

                self.keywordIO.update_keywords(self.layer, myKeywords)
                self.isValid = False

    def deintersect(self, theHazardLayer, theExposureLayer):
        """Ensure there are no intersecting features with self.layer.

        This should only happen after initial checks have been made.

        Buildings are not split up by this method.

        """

        if not self.isValid:
            raise InvalidAggregatorError

        # These should have already been clipped to analysis extents
        self.hazardLayer = theHazardLayer
        self.exposureLayer = theExposureLayer
        self._prepareLayer()

        if not self.aoiMode:
            # This is a safe version of the aggregation layer
            self.safeLayer = safe_read_layer(str(self.layer.source()))

            if is_polygon_layer(self.hazardLayer):
                self.hazardLayer = self._preparePolygonLayer(self.hazardLayer)

            if is_polygon_layer(self.exposureLayer):
                # Find out the subcategory for this layer
                mySubcategory = self.keywordIO.read_keywords(
                    self.exposureLayer, 'subcategory')
                # We dont want to chop up buildings!
                if mySubcategory != 'structure':
                    self.exposureLayer = self._preparePolygonLayer(
                        self.exposureLayer)

    def aggregate(self, theSafeImpactLayer):
        """Do any requested aggregation post processing.

        Performs Aggregation postprocessing step by

            * creating a copy of the dataset clipped by the impactlayer
              bounding box
            * stripping all attributes beside the aggregation attribute
            * delegating to the appropriate aggregator for raster and vectors

        :raises: ReadLayerError
        """

        if not self.isValid:
            raise InvalidAggregatorError

        myMessage = m.Message(
            m.Heading(self.tr('Aggregating results'), **PROGRESS_UPDATE_STYLE),
            m.Paragraph(self.tr(
                'This may take a little while - we are aggregating the impact'
                ' by %1').arg(self.layer.name())))
        self._sendMessage(myMessage)

        myQGISImpactLayer = safe_to_qgis_layer(theSafeImpactLayer)
        if not myQGISImpactLayer.isValid():
            myMessage = self.tr('Error when reading %1').arg(myQGISImpactLayer)
            # noinspection PyExceptionInherit
            raise ReadLayerError(myMessage)
        myLayerName = str(self.tr('%1 aggregated to %2').arg(
            myQGISImpactLayer.name()).arg(self.layer.name()))

        #delete unwanted fields
        myProvider = self.layer.dataProvider()
        myFields = myProvider.fields()

        #mark important attributes as needed
        self._setPersistantAttributes()
        myUnneededAttributes = []

        for i in myFields:
            if (myFields[i].name() not in
                    self.attributes.values()):
                myUnneededAttributes.append(i)
        LOGGER.debug('Removing this attributes: ' + str(myUnneededAttributes))
        # noinspection PyBroadException
        try:
            self.layer.startEditing()
            myProvider.deleteAttributes(myUnneededAttributes)
            self.layer.commitChanges()
        # FIXME (Ole): Disable pylint check for the moment
        # Need to work out what exceptions we will catch here, though.
        except:  # pylint: disable=W0702
            myMessage = self.tr('Could not remove the unneeded fields')
            LOGGER.debug(myMessage)

        del myUnneededAttributes, myProvider, myFields
        self.keywordIO.update_keywords(
            self.layer, {'title': myLayerName})

        self.statisticsType, self.statisticsClasses = (
            self.keywordIO.get_statistics(myQGISImpactLayer))

        #call the correct aggregator
        if myQGISImpactLayer.type() == QgsMapLayer.VectorLayer:
            self._aggregateVectorImpact(myQGISImpactLayer, theSafeImpactLayer)
        elif myQGISImpactLayer.type() == QgsMapLayer.RasterLayer:
            self._aggregateRasterImpact(myQGISImpactLayer)
        else:
            myMessage = self.tr('%1 is %2 but it should be either vector or '
                                'raster').\
                arg(myQGISImpactLayer.name()).arg(myQGISImpactLayer.type())
            # noinspection PyExceptionInherit
            raise ReadLayerError(myMessage)

        # show a styled aggregation layer
        if self.showIntermediateLayers:
            if self.statisticsType == 'sum':
                #style layer if we are summing
                myProvider = self.layer.dataProvider()
                myAttr = self._sumFieldName()
                myAttrIndex = myProvider.fieldNameIndex(myAttr)
                myProvider.select([myAttrIndex], QgsRectangle(), False)
                myFeature = QgsFeature()
                myHighestVal = 0

                while myProvider.nextFeature(myFeature):
                    myAttrMap = myFeature.attributeMap()
                    myVal, ok = myAttrMap[myAttrIndex].toInt()
                    if ok and myVal > myHighestVal:
                        myHighestVal = myVal

                myClasses = []
                myColors = ['#fecc5c', '#fd8d3c', '#f31a1c']
                myStep = int(myHighestVal / len(myColors))
                myCounter = 0
                for myColor in myColors:
                    myMin = myCounter
                    myCounter += myStep
                    myMax = myCounter

                    myClasses.append(
                        {'min': myMin,
                         'max': myMax,
                         'colour': myColor,
                         'transparency': 30,
                         'label': '%s - %s' % (myMin, myMax)})
                    myCounter += 1

                myStyle = {'target_field': myAttr,
                           'style_classes': myClasses}
                set_vector_graduated_style(self.layer, myStyle)
            else:
                #make style of layer pretty much invisible
                myProps = {'style': 'no',
                           'color_border': '0,0,0,127',
                           'width_border': '0.0'
                           }
                # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
                mySymbol = QgsFillSymbolV2.createSimple(myProps)
                myRenderer = QgsSingleSymbolRendererV2(mySymbol)
                self.layer.setRendererV2(myRenderer)
                self.layer.saveDefaultStyle()

    def _aggregateVectorImpact(self, theQGISImpactLayer, theSafeImpactLayer):
        """Performs Aggregation postprocessing step on vector impact layers.

        Args:
            myQGISImpactLayer a valid QgsRasterLayer

        Returns:
            None
        """
        #TODO (MB) implement line aggregation

        myAggrFieldMap = {}
        myAggrFieldIndex = None

        try:
            self.targetField = self.keywordIO.read_keywords(theQGISImpactLayer,
                                                           'target_field')
        except KeywordNotFoundError:
            myMessage = m.Paragraph(
                self.tr(
                    'No "target_field" keyword found in the impact layer %1 '
                    'keywords. The impact function should define this.').arg(
                        theQGISImpactLayer.name()))
            LOGGER.debug('Skipping postprocessing due to: %s' % myMessage)
            self.errorMessage = myMessage
            return
        myImpactProvider = theQGISImpactLayer.dataProvider()
        myTargetFieldIndex = theQGISImpactLayer.fieldNameIndex(
            self.targetField)
        #if a feature has no field called
        if myTargetFieldIndex == -1:
            myMessage = m.Paragraph(
                self.tr('No attribute "%1" was found in the attribute table '
                        'for layer "%2". The impact function must define this'
                        ' attribute for postprocessing to work.').arg(
                            self.targetField, theQGISImpactLayer.name()))
            LOGGER.debug('Skipping postprocessing due to: %s' % myMessage)
            self.errorMessage = myMessage
            return

        # start data retreival: fetch no geometry and
        # 1 attr for each feature
        myImpactProvider.select([myTargetFieldIndex], QgsRectangle(), False)
        myTotal = 0

        myAggregationProvider = self.layer.dataProvider()
        self.layer.startEditing()

        if self.statisticsType == 'class_count':
            #add the class count fields to the layer
            myFields = [QgsField('%s_%s' % (f, self.targetField),
                                 QtCore.QVariant.String) for f in
                        self.statisticsClasses]
            myAggregationProvider.addAttributes(myFields)
            self.layer.commitChanges()

            myTmpAggrFieldMap = myAggregationProvider.fieldNameMap()
            for k, v in myTmpAggrFieldMap.iteritems():
                myAggrFieldMap[str(k)] = v

        elif self.statisticsType == 'sum':
            #add the total field to the layer
            myAggrField = self._sumFieldName()
            myAggregationProvider.addAttributes([QgsField(
                myAggrField, QtCore.QVariant.Int)])

            self.layer.commitChanges()
            myAggrFieldIndex = self.layer.fieldNameIndex(
                myAggrField)

        self.layer.startEditing()

        myImpactGeoms = theSafeImpactLayer.get_geometry()
        myImpactValues = theSafeImpactLayer.get_data()

        if not self.aoiMode:
            myAggregtionUnits = self.safeLayer.get_geometry()

            if (theSafeImpactLayer.is_point_data or
                    theSafeImpactLayer.is_polygon_data):
                LOGGER.debug('Doing point in polygon aggregation')

                myRemainingValues = myImpactValues

                if theSafeImpactLayer.is_polygon_data:
                    # Using centroids to do polygon in polygon aggregation
                    # this is always ok because
                    # deintersect() took care of splitting
                    # polygons that spawn across multiple postprocessing
                    # polygons. After deintersect()
                    # each impact polygon will never be contained by more than
                    # one aggregation polygon

                    # Calculate points for each polygon
                    myCentroids = []
                    for myPolygon in myImpactGeoms:
                        if hasattr(myPolygon, 'outer_ring'):
                            outer_ring = myPolygon.outer_ring
                        else:
                            # Assume it is an array
                            outer_ring = myPolygon
                        c = calculate_polygon_centroid(outer_ring)
                        myCentroids.append(c)
                    myRemainingPoints = myCentroids

                else:
                    #this are already points data
                    myRemainingPoints = myImpactGeoms

                #iterate over the aggregation units
                for myPolygonIndex, myPolygon in enumerate(myAggregtionUnits):
                    if hasattr(myPolygon, 'outer_ring'):
                        outer_ring = myPolygon.outer_ring
                        inner_rings = myPolygon.inner_rings
                    else:
                        # Assume it is an array
                        outer_ring = myPolygon
                        inner_rings = None

                    inside, outside = points_in_and_outside_polygon(
                        myRemainingPoints,
                        outer_ring,
                        holes=inner_rings,
                        closed=True,
                        check_input=True)

                    #self.impactLayerAttributes is a list of list of dict
                    #[
                    #   [{...},{...},{...}],
                    #   [{...},{...},{...}]
                    #]
                    self.impactLayerAttributes.append([])
                    if self.statisticsType == 'class_count':
                        myResults = OrderedDict()
                        for myClass in self.statisticsClasses:
                            myResults[myClass] = 0

                        for i in inside:
                            myKey = myRemainingValues[i][self.targetField]
                            try:
                                myResults[myKey] += 1
                            except KeyError:
                                myError = ('StatisticsClasses %s does not '
                                           'include the %s class which was '
                                           'found in the data. This is a '
                                           'problem in the %s '
                                           'statistics_classes definition' %
                                           (self.statisticsClasses,
                                            myKey,
                                            self.getFunctionID()))
                                raise KeyError(myError)

                            self.impactLayerAttributes[myPolygonIndex].append(
                                myRemainingValues[i])
                        myAttrs = {}
                        for k, v in myResults.iteritems():
                            myKey = '%s_%s' % (k, self.targetField)
                            #FIXME (MB) remove next line when we get rid of
                            #shape files as internal format
                            myKey = myKey[:10]
                            myAggrFieldIndex = myAggrFieldMap[myKey]
                            myAttrs[myAggrFieldIndex] = QtCore.QVariant(v)

                    elif self.statisticsType == 'sum':
                        #by default summ attributes
                        myTotal = 0
                        for i in inside:
                            try:
                                myTotal += myRemainingValues[i][
                                    self.targetField]
                            except TypeError:
                                pass

                            #add all attributes to the impactLayerAttributes
                            self.impactLayerAttributes[myPolygonIndex].append(
                                myRemainingValues[i])
                        myAttrs = {myAggrFieldIndex: QtCore.QVariant(myTotal)}

                    # Add features inside this polygon
                    myFID = myPolygonIndex
                    myAggregationProvider.changeAttributeValues(
                        {myFID: myAttrs})

                    # make outside points the input to the next iteration
                    # this could maybe be done quicklier using directly numpy
                    # arrays like this:
                    # myRemainingPoints = myRemainingPoints[outside]
                    # myRemainingValues =
                    # [myRemainingValues[i] for i in outside]
                    myTmpPoints = []
                    myTmpValues = []
                    for i in outside:
                        myTmpPoints.append(myRemainingPoints[i])
                        myTmpValues.append(myRemainingValues[i])
                    myRemainingPoints = myTmpPoints
                    myRemainingValues = myTmpValues

                    # LOGGER.debug('Before: ' + str(len(myRemainingValues)))
                    # LOGGER.debug('After: ' + str(len(myRemainingValues)))
                    # LOGGER.debug('Inside: ' + str(len(inside)))
                    # LOGGER.debug('Outside: ' + str(len(outside)))

            elif theSafeImpactLayer.is_line_data:
                LOGGER.debug('Doing line in polygon aggregation')

            else:
                myMessage = m.Paragraph(
                    self.tr(
                        'Aggregation on vector impact layers other than points'
                        ' or polygons not implemented yet not implemented yet.'
                        ' Called on %1').arg(theQGISImpactLayer.name()))
                LOGGER.debug('Skipping postprocessing due to: %s' % myMessage)
                self.errorMessage = myMessage
                self.layer.commitChanges()
                return
        else:
            if self.statisticsType == 'class_count':
                #loop over all features in impact layer
                myResults = OrderedDict()
                for myClass in self.statisticsClasses:
                    myResults[myClass] = 0

                self.impactLayerAttributes.append([])
                for myImpactValueList in myImpactValues:
                    myKey = myImpactValueList[self.targetField]
                    try:
                        myResults[myKey] += 1
                    except KeyError:
                        myError = ('StatisticsClasses %s does not '
                                   'include the %s class which was '
                                   'found in the data. This is a '
                                   'problem in the %s '
                                   'statistics_classes definition' %
                                   (self.statisticsClasses,
                                    myKey,
                                    self.getFunctionID()))
                        raise KeyError(myError)

                    self.impactLayerAttributes[0].append(myImpactValueList)

                myAttrs = {}
                for k, v in myResults.iteritems():
                    myKey = '%s_%s' % (k, self.targetField)
                    #FIXME (MB) remove next line when we get rid of
                    #shape files as internal format
                    myKey = myKey[:10]
                    myAggrFieldIndex = myAggrFieldMap[myKey]
                    myAttrs[myAggrFieldIndex] = QtCore.QVariant(v)

            elif self.statisticsType == 'sum':
                #loop over all features in impact layer
                self.impactLayerAttributes.append([])
                for myImpactValueList in myImpactValues:
                    if myImpactValueList[self.targetField] == 'None':
                        myImpactValueList[self.targetField] = None
                    try:
                        myTotal += myImpactValueList[self.targetField]
                    except TypeError:
                        pass
                    self.impactLayerAttributes[0].append(myImpactValueList)
                myAttrs = {myAggrFieldIndex: QtCore.QVariant(myTotal)}

            #apply to all area feature
            myFID = 0
            myAggregationProvider.changeAttributeValues({myFID: myAttrs})

        self.layer.commitChanges()
        return

    def _aggregateRasterImpact(self, theQGISImpactLayer):
        """
        Performs Aggregation postprocessing step on raster impact layers by
        calling QgsZonalStatistics
        Args:
            QgsMapLayer: theQGISImpactLayer a valid QgsVectorLayer

        Returns: None
        """
        myZonalStatistics = QgsZonalStatistics(
            self.layer,
            theQGISImpactLayer.dataProvider().dataSourceUri(),
            self.prefix)
        myProgressDialog = QtGui.QProgressDialog(
            self.tr('Calculating zonal statistics'),
            self.tr('Abort...'),
            0,
            0)
        startTime = time.clock()
        myZonalStatistics.calculateStatistics(myProgressDialog)
        if myProgressDialog.wasCanceled():
            QtGui.QMessageBox.error(
                self, self.tr('ZonalStats: Error'),
                self.tr('You aborted aggregation, '
                        'so there are no data for analysis. Exiting...'))
        cppDuration = time.clock() - startTime
        print 'CPP duration: %ss' % (cppDuration)

        startTime = time.clock()
        # new way
        # myZonalStatistics = {
        # 0L: {'count': 50539,
        #      'sum': 12015061.876953125,
        #      'mean': 237.73841739949594},
        # 1L: {
        #   'count': 19492,
        #   'sum': 2945658.1220703125,
        #   'mean': 151.12138939412642},
        # 2L: {
        #   'count': 57372,
        #   'sum': 1643522.3984985352, 'mean': 28.6467684323108},
        # 3L: {
        #   'count': 0.00013265823369700314,
        #   'sum': 0.24983273179242008,
        #   'mean': 1883.2810058593748},
        # 4L: {
        #   'count': 1.8158245316933218e-05,
        #   'sum': 0.034197078505115275,
        #   'mean': 1883.281005859375},
        # 5L: {
        #   'count': 73941,
        #   'sum': 10945062.435424805,
        #   'mean': 148.024268476553},
        # 6L: {
        #   'count': 54998,
        #   'sum': 11330910.488220215,
        #   'mean': 206.02404611477172}}

        myZonalStatistics = calculateZonalStats(theQGISImpactLayer, self.layer)
        pyDuration = time.clock() - startTime
        print 'CPP duration: %ss' % (pyDuration)

        try:
            ratio = pyDuration / cppDuration
        except ZeroDivisionError:
            ratio = 1

        print 'py to CPP: %s%%' % (ratio * 100)
        # FIXME (MB) remove this once fully implemented
        oldPrefix = self.prefix

        self.prefix = 'newAggr'
        myProvider = self.layer.dataProvider()
        self.layer.startEditing()

        # add fields for stats to aggregation layer
        # { 1: {'sum': 10, 'count': 20, 'min': 1, 'max': 4, 'mean': 2},
        #             QgsField(self._minFieldName(), QtCore.QVariant.Double),
        #             QgsField(self._maxFieldName(), QtCore.QVariant.Double)]
        myFields = [QgsField(self._countFieldName(), QtCore.QVariant.Double),
                    QgsField(self._sumFieldName(), QtCore.QVariant.Double),
                    QgsField(self._meanFieldName(), QtCore.QVariant.Double)
                    ]
        myProvider.addAttributes(myFields)
        self.layer.commitChanges()

        sumIndex = myProvider.fieldNameIndex(self._sumFieldName())
        countIndex = myProvider.fieldNameIndex(self._countFieldName())
        meanIndex = myProvider.fieldNameIndex(self._meanFieldName())
        # minIndex = myProvider.fieldNameIndex(self._minFieldName())
        # maxIndex = myProvider.fieldNameIndex(self._maxFieldName())

        self.layer.startEditing()
        allPolygonAttrs = myProvider.attributeIndexes()
        myProvider.select(allPolygonAttrs)
        myFeature = QgsFeature()

        while myProvider.nextFeature(myFeature):
            myFid = myFeature.id()
            myStats = myZonalStatistics[myFid]
            #          minIndex: QtCore.QVariant(myStats['min']),
            #          maxIndex: QtCore.QVariant(myStats['max'])}
            attrs = {sumIndex: QtCore.QVariant(myStats['sum']),
                     countIndex: QtCore.QVariant(myStats['count']),
                     meanIndex: QtCore.QVariant(myStats['mean'])
                     }
            myProvider.changeAttributeValues({myFid: attrs})
        self.layer.commitChanges()

        # FIXME (MB) remove this once fully implemented
        self.prefix = oldPrefix
        return

    def _prepareLayer(self):
        """Prepare the aggregation layer to match analysis extents."""
        myMessage = m.Message(
            m.Heading(
                self.tr('Preparing aggregation layer'),
                **PROGRESS_UPDATE_STYLE),
            m.Paragraph(self.tr(
                'We are clipping the aggregation layer to match the '
                'intersection of the hazard and exposure layer extents.')))
        self._sendMessage(myMessage)

        # This is used to hold an *in memory copy* of the aggregation layer
        # or a in memory layer with the clip extents as a feature.
        if self.aoiMode:
            self.layer = self._extentsToLayer()
            # Area Of Interest (AOI) mode flag
        else:
            # we use only the exposure extent, because both exposure and hazard
            # have the same extent at this point.
            myGeoExtent = extent_to_geo_array(
                self.exposureLayer.extent(),
                self.exposureLayer.crs())

            myAggrAttribute = self.keywordIO.read_keywords(
                self.layer, self.defaults['AGGR_ATTR_KEY'])

            myClippedLayer = clip_layer(
                layer=self.layer,
                extent=myGeoExtent,
                explode_flag=True,
                explode_attribute=myAggrAttribute)

            myName = '%s %s' % (self.layer.name(), self.tr('aggregation'))
            self.layer = myClippedLayer
            self.layer.setLayerName(myName)
            if self.showIntermediateLayers:
                self.keywordIO.update_keywords(self.layer, {'title': myName})
                QgsMapLayerRegistry.instance().addMapLayer(self.layer)

    def _countFieldName(self):
        return (self.prefix + 'count')[:10]

    def _meanFieldName(self):
        return (self.prefix + 'mean')[:10]

    def _minFieldName(self):
        return (self.prefix + 'min')[:10]

    def _maxFieldName(self):
        return (self.prefix + 'max')[:10]

    def _sumFieldName(self):
        return (self.prefix + 'sum')[:10]

    # noinspection PyDictCreation
    def _setPersistantAttributes(self):
        """Mark any attributes that should remain in the self.layer table."""
        self.attributes = {}
        self.attributes[self.defaults[
            'AGGR_ATTR_KEY']] = (
                self.keywordIO.read_keywords(
                    self.layer,
                    self.defaults['AGGR_ATTR_KEY']))

        myFemaleRatioKey = self.defaults['FEM_RATIO_ATTR_KEY']
        myFemRatioAttr = self.keywordIO.read_keywords(
            self.layer,
            myFemaleRatioKey)
        if ((myFemRatioAttr != self.tr('Don\'t use')) and
                (myFemRatioAttr != self.tr('Use default'))):
            self.attributes[myFemaleRatioKey] = \
                myFemRatioAttr

    def _preparePolygonLayer(self, theQgisLayer):
        """Create a new layer with no intersecting features to self.layer.

        A helper function to align the polygons to the postprocLayer
        polygons. If one input polygon is in two or more postprocLayer polygons
        then it is divided so that each part is within only one of the
        postprocLayer polygons. this allows to aggregate in postrocessing using
        centroid in polygon.

        The function assumes EPSG:4326 but no checks are enforced

        Args:
            theQgisLayer of the file to be processed
        Returns:
            QgisLayer of the processed file

        Raises:
            Any exceptions raised by the InaSAFE library will be propagated.
        """
#        import time
#        startTime = time.clock()

        myMessage = m.Message(
            m.Heading(self.tr('Preclipping input data...')),
            m.Paragraph(self.tr(
                'Modifying %1 to avoid intersections with the aggregation '
                'layer'
            ).arg(theQgisLayer.name())))
        self._sendMessage(myMessage)

        theLayerFilename = str(theQgisLayer.source())
        myPostprocPolygons = self.safeLayer.get_geometry()
        myPolygonsLayer = safe_read_layer(theLayerFilename)
        myRemainingPolygons = numpy.array(myPolygonsLayer.get_geometry())
#        myRemainingAttributes = numpy.array(myPolygonsLayer.get_data())
        myRemainingIndexes = numpy.array(range(len(myRemainingPolygons)))

        #used for unit tests only
        self.preprocessedFeatureCount = 0

        # FIXME (MB) the intersecting array is used only for debugging and
        # could be safely removed
        myIntersectingPolygons = []
        myInsidePolygons = []

        # FIXME (MB) maybe do raw geos without qgis
        #select all postproc polygons with no attributes
        aggregationProvider = self.layer.dataProvider()
        aggregationProvider.select([])

        # copy polygons to a memory layer
        myQgisMemoryLayer = create_memory_layer(theQgisLayer)

        polygonsProvider = myQgisMemoryLayer.dataProvider()
        allPolygonAttrs = polygonsProvider.attributeIndexes()
        polygonsProvider.select(allPolygonAttrs)
        myQgisPostprocPoly = QgsFeature()
        myQgisFeat = QgsFeature()
        myInsideFeat = QgsFeature()
        fields = polygonsProvider.fields()
        myTempdir = temp_dir(sub_dir='preprocess')
        myOutFilename = unique_filename(suffix='.shp',
                                        dir=myTempdir)

        self.keywordIO.copy_keywords(theQgisLayer, myOutFilename)
        mySHPWriter = QgsVectorFileWriter(myOutFilename,
                                          'UTF-8',
                                          fields,
                                          polygonsProvider.geometryType(),
                                          polygonsProvider.crs())
        if mySHPWriter.hasError():
            raise InvalidParameterError(mySHPWriter.errorMessage())
        # end FIXME

        for (myPostprocPolygonIndex,
             myPostprocPolygon) in enumerate(myPostprocPolygons):
            LOGGER.debug('PostprocPolygon %s' % myPostprocPolygonIndex)
            myPolygonsCount = len(myRemainingPolygons)
            aggregationProvider.featureAtId(
                myPostprocPolygonIndex, myQgisPostprocPoly, True, [])
            myQgisPostprocGeom = QgsGeometry(myQgisPostprocPoly.geometry())

            # myPostprocPolygon bounding box values
            A = numpy.array(myPostprocPolygon)
            minx = miny = sys.maxint
            maxx = maxy = -minx
            myPostprocPolygonMinx = min(minx, min(A[:, 0]))
            myPostprocPolygonMaxx = max(maxx, max(A[:, 0]))
            myPostprocPolygonMiny = min(miny, min(A[:, 1]))
            myPostprocPolygonMaxy = max(maxy, max(A[:, 1]))

            # create an array full of False to store if a BB vertex is inside
            # or outside the myPostprocPolygon
            myAreVerticesInside = numpy.zeros(myPolygonsCount * 4,
                                              dtype=numpy.bool)

            # Create Nx2 vector of vertices of bounding boxes
            myBBVertices = []
            # Compute bounding box for each geometry type
            for myPoly in myRemainingPolygons:
                minx = miny = sys.maxint
                maxx = maxy = -minx
                # Do outer ring only as the BB is outside anyway
                A = numpy.array(myPoly)
                minx = min(minx, numpy.min(A[:, 0]))
                maxx = max(maxx, numpy.max(A[:, 0]))
                miny = min(miny, numpy.min(A[:, 1]))
                maxy = max(maxy, numpy.max(A[:, 1]))
                myBBVertices.extend([(minx, miny),
                                    (minx, maxy),
                                    (maxx, maxy),
                                    (maxx, miny)])

            # see if BB vertices are in myPostprocPolygon
            myBBVertices = numpy.array(myBBVertices)
            inside, _ = points_in_and_outside_polygon(myBBVertices,
                                                      myPostprocPolygon)
            # make True if the vertice was in myPostprocPolygon
            myAreVerticesInside[inside] = True

            # myNextIterPolygons has the 0:count indexes
            # myOutsidePolygons has the mapped to original indexes
            # and is overwritten at every iteration because we care only of
            # the outside polygons remaining after the last iteration
            myNextIterPolygons = []
            myOutsidePolygons = []

            for i in range(myPolygonsCount):
                k = i * 4
                myMappedIndex = myRemainingIndexes[i]
                # memory layers counting starts at 1 instead of 0 as in our
                # indexes
                myFeatId = myMappedIndex + 1
                doIntersection = False
                # summ the isInside bool for each of the boundingbox vertices
                # of each poygon. for example True + True + False + True is 3
                myPolygonLocation = numpy.sum(myAreVerticesInside[k:k + 4])

                if myPolygonLocation == 4:
                    # all vertices are inside -> polygon is inside
                    #ignore this polygon from further analysis
                    myInsidePolygons.append(myMappedIndex)
                    polygonsProvider.featureAtId(myFeatId,
                                                 myQgisFeat,
                                                 True,
                                                 allPolygonAttrs)
                    mySHPWriter.addFeature(myQgisFeat)
                    self.preprocessedFeatureCount += 1
#                    LOGGER.debug('Polygon %s is fully inside' %myMappedIndex)
#                    tmpWriter.addFeature(myQgisFeat)

                elif myPolygonLocation == 0:
                    # all vertices are outside
                    # check if the polygon BB is completely outside of the
                    # myPostprocPolygon BB.
                    myPolyMinx = numpy.min(myBBVertices[k:k + 4, 0])
                    myPolyMaxx = numpy.max(myBBVertices[k:k + 4, 0])
                    myPolyMiny = numpy.min(myBBVertices[k:k + 4, 1])
                    myPolyMaxy = numpy.max(myBBVertices[k:k + 4, 1])

                    # check if myPoly is all E,W,N,S of myPostprocPolygon
                    if ((myPolyMinx > myPostprocPolygonMaxx) or
                            (myPolyMaxx < myPostprocPolygonMinx) or
                            (myPolyMiny > myPostprocPolygonMaxy) or
                            (myPolyMaxy < myPostprocPolygonMiny)):
                        #polygon is surely outside
                        myOutsidePolygons.append(myMappedIndex)
                        # we need this polygon in the next iteration
                        myNextIterPolygons.append(i)
                    else:
                        # polygon might be outside or intersecting. consider
                        # it intersecting so it goes into further analysis
                        doIntersection = True
                else:
                    # some vertices are outside some inside -> polygon is
                    # intersecting
                    doIntersection = True

                #intersect using qgis
                if doIntersection:
#                    LOGGER.debug('Intersecting polygon %s' % myMappedIndex)
                    myIntersectingPolygons.append(myMappedIndex)

                    ok = polygonsProvider.featureAtId(myFeatId,
                                                      myQgisFeat,
                                                      True,
                                                      allPolygonAttrs)
                    if not ok:
                        LOGGER.debug('Couldn\'t fetch feature: %s' % myFeatId)
                        LOGGER.debug([str(error) for error in
                                      polygonsProvider.errors()])

                    myQgisPolyGeom = QgsGeometry(myQgisFeat.geometry())
                    myAtMap = myQgisFeat.attributeMap()
#                    for (k, attr) in myAtMap.iteritems():
#                        LOGGER.debug( "%d: %s" % (k, attr.toString()))

                    # make intersection of the myQgisFeat and the postprocPoly
                    # write the inside part to a shp file and the outside part
                    # back to the original QGIS layer
                    try:
                        myIntersec = myQgisPostprocGeom.intersection(
                            myQgisPolyGeom)
#                        if myIntersec is not None:
                        myIntersecGeom = QgsGeometry(myIntersec)

                        #from ftools
                        myUnknownGeomType = 0
                        if myIntersecGeom.wkbType() == myUnknownGeomType:
                            int_com = myQgisPostprocGeom.combine(
                                myQgisPolyGeom)
                            int_sym = myQgisPostprocGeom.symDifference(
                                myQgisPolyGeom)
                            myIntersecGeom = QgsGeometry(
                                int_com.difference(int_sym))
#                        LOGGER.debug('wkbType type of intersection: %s' %
# myIntersecGeom.wkbType())
                        polygonTypesList = [QGis.WKBPolygon,
                                            QGis.WKBMultiPolygon]
                        if myIntersecGeom.wkbType() in polygonTypesList:
                            myInsideFeat.setGeometry(myIntersecGeom)
                            myInsideFeat.setAttributeMap(myAtMap)
                            mySHPWriter.addFeature(myInsideFeat)
                            self.preprocessedFeatureCount += 1
                        else:
                            pass
#                            LOGGER.debug('Intersection not a polygon so '
#                                         'the two polygons either touch '
#                                         'only or do not intersect. Not '
#                                         'adding this to the inside list')
                        #Part of the polygon that is outside the postprocpoly
                        myOutside = myQgisPolyGeom.difference(myIntersecGeom)
#                        if myOutside is not None:
                        myOutsideGeom = QgsGeometry(myOutside)

                        if myOutsideGeom.wkbType() in polygonTypesList:
                            # modifiy the original geometry to the part
                            # outside of the postproc polygon
                            polygonsProvider.changeGeometryValues(
                                {myFeatId: myOutsideGeom})
                            # we need this polygon in the next iteration
                            myOutsidePolygons.append(myMappedIndex)
                            myNextIterPolygons.append(i)

                    except TypeError:
                        LOGGER.debug('ERROR with FID %s', myMappedIndex)

#            LOGGER.debug('Inside %s' % myInsidePolygons)
#            LOGGER.debug('Outside %s' % myOutsidePolygons)
#            LOGGER.debug('Intersec %s' % myIntersectingPolygons)
            if len(myNextIterPolygons) > 0:
                #some polygons are still completely outside of the postprocPoly
                #so go on and reiterate using only these
                nextIterPolygonsIndex = numpy.array(myNextIterPolygons)

                myRemainingPolygons = myRemainingPolygons[
                    nextIterPolygonsIndex]
#                myRemainingAttributes = myRemainingAttributes[
#                                        nextIterPolygonsIndex]
                myRemainingIndexes = myRemainingIndexes[nextIterPolygonsIndex]
                LOGGER.debug('Remaining: %s' % len(myRemainingPolygons))
            else:
                print 'no more polygons to be checked'
                break
#            del tmpWriter

        # here the full polygon set is represented by:
        # myInsidePolygons + myIntersectingPolygons + myNextIterPolygons
        # the a polygon intersecting multiple postproc polygons appears
        # multiple times in the array
        # noinspection PyUnboundLocalVariable
        LOGGER.debug('Results:\nInside: %s\nIntersect: %s\nOutside: %s' % (
            myInsidePolygons, myIntersectingPolygons, myOutsidePolygons))

        #add in- and outside polygons

        for i in myOutsidePolygons:
            myFeatId = i + 1
            polygonsProvider.featureAtId(myFeatId, myQgisFeat, True,
                                         allPolygonAttrs)
            mySHPWriter.addFeature(myQgisFeat)
            self.preprocessedFeatureCount += 1

        del mySHPWriter
#        LOGGER.debug('Created: %s' % self.preprocessedFeatureCount)

        myName = '%s %s' % (theQgisLayer.name(), self.tr('preprocessed'))
        myOutLayer = QgsVectorLayer(myOutFilename, myName, 'ogr')
        if not myOutLayer.isValid():
            #TODO (MB) use a better exception
            raise Exception('Invalid qgis Layer')

        if self.showIntermediateLayers:
            self.keywordIO.update_keywords(myOutLayer, {'title': myName})
            QgsMapLayerRegistry.instance().addMapLayer(myOutLayer)

        return myOutLayer

    def _createPolygonLayer(self, crs=None, fields=None):
        """Creates an empty shape file layer"""

        if crs is None:
            crs = QgsCoordinateReferenceSystem()
            crs.createFromEpsg(4326)

        if fields is None:
            fields = {}

        myTempdir = temp_dir(sub_dir='preprocess')
        myOutFilename = unique_filename(suffix='.shp',
                                        dir=myTempdir)
        mySHPWriter = QgsVectorFileWriter(myOutFilename,
                                          'UTF-8',
                                          fields,
                                          QGis.WKBPolygon,
                                          crs)
        #flush the writer to write to file
        del mySHPWriter
        myName = self.tr('Entire area')
        myLayer = QgsVectorLayer(myOutFilename, myName, 'ogr')
        LOGGER.debug('created' + myLayer.name())
        return myLayer

    def _extentsToLayer(self):
        """Memory layer for aggregation by using canvas extents as feature.

        We do this because the user elected to use no aggregation layer so we
        make a 'dummy' one which covers the whole study area extent.

        This layer is needed when postprocessing because we always want a
        vector layer to store aggregation information in.

        Returns:
            QgsMapLayer - a memory layer representing the extents of the clip.
        """

        # Note: this code duplicates from Dock.viewportGeoArray - make DRY. TS

        myRect = self.iface.mapCanvas().extent()
        myCrs = QgsCoordinateReferenceSystem()
        myCrs.createFromEpsg(4326)
        myGeoExtent = extent_to_geo_array(myRect, myCrs)

        if not self.layer.isValid():
            myMessage = self.tr(
                'An exception occurred when creating the entire area layer.')
            raise (Exception(myMessage))

        myProvider = self.layer.dataProvider()

        myAttrName = self.tr('Area')
        myProvider.addAttributes(
            [QgsField(myAttrName, QtCore.QVariant.String)])

        self.layer.startEditing()
        # add a feature the size of the impact layer bounding box
        myFeature = QgsFeature()
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        myFeature.setGeometry(QgsGeometry.fromRect(
            QgsRectangle(
                QgsPoint(myGeoExtent[0], myGeoExtent[1]),
                QgsPoint(myGeoExtent[2], myGeoExtent[3]))))
        myFeature.setAttributeMap({0: QtCore.QVariant(
            self.tr('Entire area'))})
        myProvider.addFeatures([myFeature])
        self.layer.commitChanges()

        try:
            self.keywordIO.update_keywords(
                self.layer,
                {self.defaults['AGGR_ATTR_KEY']: myAttrName})
        except InvalidParameterError:
            self.keywordIO.write_keywords(
                self.layer,
                {self.defaults['AGGR_ATTR_KEY']: myAttrName})
        except KeywordDbError, e:
            raise e
        return self.layer