Exemple #1
0
    def set_layer_style_from_qml(self,
                                 db,
                                 layer,
                                 is_error_layer=False,
                                 emit=False,
                                 layer_modifiers=dict(),
                                 models=list()):  # TODO: Add tests
        if db is None:
            self.logger.critical(__name__,
                                 "DB connection is none. Style not set.")
            return

        qml_name = None
        if db.is_ladm_layer(layer):
            layer_name = db.get_ladm_layer_name(layer)
        else:
            layer_name = layer.name(
            )  # we identify some error layer styles using the error table names

        if not is_error_layer:
            # Check if we should use modifier style group
            if LayerConfig.STYLE_GROUP_LAYER_MODIFIERS in layer_modifiers:
                style_group_modifiers = layer_modifiers.get(
                    LayerConfig.STYLE_GROUP_LAYER_MODIFIERS)

                if style_group_modifiers:
                    qml_name = style_group_modifiers.get(layer_name)

            if not qml_name:  # If None or empty string, we use default styles
                qml_name = Symbology().get_default_style_group(
                    db.names, models).get(layer_name)

        else:
            style_custom_error_layers = Symbology().get_custom_error_layers()
            if layer_name in style_custom_error_layers:
                qml_name = style_custom_error_layers.get(layer_name)
            else:
                qml_name = Symbology().get_default_error_style_layer().get(
                    layer.geometryType())

        if qml_name:
            renderer, labeling = self.get_style_from_qml(qml_name)
            if renderer:
                layer.setRenderer(renderer)
                if emit:
                    self.layer_symbology_changed.emit(layer.id())
            if labeling:
                layer.setLabeling(labeling)
                layer.setLabelsEnabled(True)
    def __init__(self, iface, db, supplies_db, qgis_utils, ladm_data):
        QObject.__init__(self)
        self.iface = iface
        self.canvas = iface.mapCanvas()
        self._db = db
        self._supplies_db = supplies_db
        self.qgis_utils = qgis_utils
        self.ladm_data = ladm_data
        self.symbology = Symbology()

        self._layers = dict()
        self._supplies_layers = dict()
        self.initialize_layers()

        self._compared_parcels_data = dict()
        self._compared_parcels_data_inverse = dict()
    def set_layer_style_from_qml(self,
                                 db,
                                 layer,
                                 emit=False,
                                 layer_modifiers=dict(),
                                 models=list()):  # TODO: Add tests
        if db is None:
            self.logger.critical(__name__,
                                 "DB connection is none. Style not set.")
            return

        qml_name = None
        if db.is_ladm_layer(layer):
            layer_name = db.get_ladm_layer_name(layer)
        else:
            layer_name = layer.name(
            )  # we identify some error layer styles using the error table names

        # Check if we should use modifier style group
        if LayerConfig.STYLE_GROUP_LAYER_MODIFIERS in layer_modifiers:
            style_group_modifiers = layer_modifiers.get(
                LayerConfig.STYLE_GROUP_LAYER_MODIFIERS)

            if style_group_modifiers:
                qml_name = style_group_modifiers.get(layer_name)

        if not qml_name:  # If None or empty string, we use default styles
            qml_name = Symbology().get_default_style_group(
                db.names, models).get(layer_name)

        if qml_name:
            self.set_style_from_qml_name(layer, qml_name)
Exemple #4
0
    def __init__(self, parent, utils, parcel_number=None, collected_parcel_t_id=None):
        QgsPanelWidget.__init__(self, None)
        self.setupUi(self)
        self.parent = parent
        self.utils = utils
        self.logger = Logger()
        self.symbology = Symbology()

        self.setDockMode(True)
        self.setPanelTitle(QCoreApplication.translate("ChangesPerParcelPanelWidget", "Change detection per parcel"))

        self._current_supplies_substring = ""
        self._current_substring = ""

        self.utils.add_layers()
        self.fill_combos()

        # Remove selection in plot layers
        self.utils._layers[self.utils._db.names.OP_PLOT_T][LAYER].removeSelection()
        self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER].removeSelection()

        # Map tool before activate map swipe tool
        self.init_map_tool = self.utils.canvas.mapTool()

        self.active_map_tool_before_custom = None
        self.btn_identify_plot.setIcon(QIcon(":/Asistente-LADM_COL/resources/images/spatial_unit.png"))
        self.btn_identify_plot.clicked.connect(self.btn_plot_toggled)

        # Create maptool
        self.maptool_identify = QgsMapToolIdentifyFeature(self.utils.canvas)

        # Set connections
        self.btn_alphanumeric_query.clicked.connect(self.alphanumeric_query)
        self.chk_show_all_plots.toggled.connect(self.show_all_plots)
        self.cbo_parcel_fields.currentIndexChanged.connect(self.search_field_updated)
        self.panelAccepted.connect(self.initialize_tools_and_layers)
        self.tbl_changes_per_parcel.itemDoubleClicked.connect(self.call_party_panel)

        self.initialize_field_values_line_edit()
        self.initialize_tools_and_layers()

        if parcel_number is not None:  # Do a search!
            self.txt_alphanumeric_query.setValue(parcel_number)
            if collected_parcel_t_id is not None:  # Search data for a duplicated parcel_number, so, take the t_id into account!
                self.search_data(parcel_number=parcel_number, collected_parcel_t_id=collected_parcel_t_id)
            else:
                self.search_data(parcel_number=parcel_number)
    def add_layers(self):
        # We can pick any required layer, if it is None, no prior load has been done, otherwise skip...
        if self._layers[self._db.names.LC_PLOT_T] is None:
            self.app.gui.freeze_map(True)

            self.app.core.get_layers(self._db, self._layers, load=True, emit_map_freeze=False)
            if not self._layers:
                return None

            # Now load supplies layers
            # Set layer modifiers
            layer_modifiers = {
                LayerConfig.PREFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_PREFIX,
                LayerConfig.SUFFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_SUFFIX,
                LayerConfig.STYLE_GROUP_LAYER_MODIFIERS: Symbology().get_style_group_layer_modifiers(self._supplies_db.names)
            }
            self.app.core.get_layers(self._supplies_db,
                                     self._supplies_layers,
                                     load=True,
                                     emit_map_freeze=False,
                                     layer_modifiers=layer_modifiers)
            if not self._supplies_layers:
                return None
            else:
                # In some occasions the supplies and collected plots might not overlap and have different extents
                self.iface.setActiveLayer(self._supplies_layers[self._supplies_db.names.GC_PLOT_T])
                self.iface.zoomToActiveLayer()

            self.app.gui.freeze_map(False)

            for layer_name in self._layers:
                if self._layers[layer_name]: # Layer was found, listen to its removal so that we can react properly
                    try:
                        self._layers[layer_name].willBeDeleted.disconnect(self.change_detection_layer_removed)
                    except:
                        pass
                    self._layers[layer_name].willBeDeleted.connect(self.change_detection_layer_removed)

            for layer_name in self._supplies_layers:
                if self._supplies_layers[layer_name]:  # Layer was found, listen to its removal so that we can react properly
                    try:
                        self._supplies_layers[layer_name].willBeDeleted.disconnect(self.change_detection_layer_removed)
                    except:
                        pass
                    self._supplies_layers[layer_name].willBeDeleted.connect(self.change_detection_layer_removed)
 def __init__(self):
     QObject.__init__(self)
     self.logger = Logger()
     self.symbology = Symbology()
class SymbologyUtils(QObject):

    layer_symbology_changed = pyqtSignal(str) # layer id

    def __init__(self):
        QObject.__init__(self)
        self.logger = Logger()
        self.symbology = Symbology()

    def set_layer_style_from_qml(self, db, layer, is_error_layer=False, emit=False, layer_modifiers=dict()):
        style_group = self.symbology.get_default_style_group(db.names)
        style_group_const = self.symbology.get_default_style_group(db.names)
        if LayerConfig.STYLE_GROUP_LAYER_MODIFIERS in layer_modifiers:
            if layer_modifiers[LayerConfig.STYLE_GROUP_LAYER_MODIFIERS]:
                style_group = layer_modifiers[LayerConfig.STYLE_GROUP_LAYER_MODIFIERS]

        qml_name = None
        if is_error_layer:
            if layer.name() in self.symbology.get_custom_error_layers():
                # Symbology is selected according to the language
                if QGIS_LANG in self.symbology.get_custom_error_layers()[layer.name()]:
                    qml_name = self.symbology.get_custom_error_layers()[layer.name()][QGIS_LANG]
                else:
                    qml_name = self.symbology.get_custom_error_layers()[layer.name()][DEFAULT_LANGUAGE]
            else:
                qml_name = style_group_const[self.symbology.get_error_layer_name()][layer.geometryType()]
        else:
            if db is None:
                return

            layer_name = db.get_ladm_layer_name(layer)

            if layer_name in style_group:
                if layer.geometryType() in style_group[layer_name]:
                    qml_name = style_group[layer_name][layer.geometryType()]

            # If style not in style group then we use default simbology
            if qml_name is None:
                if layer_name in style_group_const:
                    if layer.geometryType() in style_group_const[layer_name]:
                        qml_name = style_group_const[layer_name][layer.geometryType()]

        if qml_name is not None:
            renderer, labeling = self.get_style_from_qml(qml_name)
            if renderer:
                layer.setRenderer(renderer)
                if emit:
                    self.layer_symbology_changed.emit(layer.id())
            if labeling:
                layer.setLabeling(labeling)
                layer.setLabelsEnabled(True)

    def get_style_from_qml(self, qml_name):
        renderer = None
        labeling = None

        style_path = os.path.join(STYLES_DIR, qml_name + '.qml')
        file = QFile(style_path)
        if not file.open(QIODevice.ReadOnly | QIODevice.Text):
            self.logger.warning(__name__, "Unable to read style file from {}".format(style_path))

        doc = QDomDocument()
        doc.setContent(file)
        file.close()
        doc_elem = doc.documentElement()

        nodes = doc_elem.elementsByTagName("renderer-v2")
        if nodes.count():
            renderer_elem = nodes.at(0).toElement()
            renderer = QgsFeatureRenderer.load(renderer_elem, QgsReadWriteContext())

        nodes = doc_elem.elementsByTagName("labeling")
        if nodes.count():
            labeling_elem = nodes.at(0).toElement()
            labeling = QgsAbstractVectorLayerLabeling.create(labeling_elem, QgsReadWriteContext())

        return (renderer, labeling)
Exemple #8
0
    def fill_table(self, search_criterion_supplies,
                   search_criterion_collected):
        """
        Shouldn't handle 'inverse' mode as we won't switch table columns at runtime.

        :param search_criterion_supplies: key-value pair to build an expression to search data in the supplies source
        :param search_criterion_collected: key-value pair to build an expression to search data in the collected source
        :return:
        """
        plural = LayerConfig.get_dict_plural(self.utils._db.names)
        dict_collected_parcels = self.utils.ladm_data.get_parcel_data_to_compare_changes(
            self.utils._db, search_criterion_collected)

        # Custom layer modifiers
        layer_modifiers = {
            LayerConfig.PREFIX_LAYER_MODIFIERS:
            LayerConfig.SUPPLIES_DB_PREFIX,
            LayerConfig.SUFFIX_LAYER_MODIFIERS:
            LayerConfig.SUPPLIES_DB_SUFFIX,
            LayerConfig.STYLE_GROUP_LAYER_MODIFIERS:
            Symbology().get_style_group_layer_modifiers(
                self.utils._supplies_db.names)
        }
        dict_supplies_parcels = self.utils.ladm_data.get_parcel_data_to_compare_changes_supplies(
            self.utils._supplies_db,
            search_criterion_supplies,
            layer_modifiers=layer_modifiers)

        # Before filling the table we make sure we get one and only one parcel attrs dict
        collected_attrs = dict()
        if dict_collected_parcels:
            collected_parcel_number = list(dict_collected_parcels.keys())[0]
            collected_attrs = dict_collected_parcels[collected_parcel_number][
                0]
            del collected_attrs[
                self.utils._db.names.
                T_ID_F]  # Remove this line if self.utils._db.names.T_ID_F is somehow needed

        supplies_attrs = dict()
        if dict_supplies_parcels:
            supplies_parcel_number = list(dict_supplies_parcels.keys())[0]
            supplies_attrs = dict_supplies_parcels[supplies_parcel_number][0]
            del supplies_attrs[
                self.utils._supplies_db.names.
                T_ID_F]  # Remove this line if self.utils._supplies_db.names,T_ID_F is somehow needed

        number_of_rows = len(collected_attrs) or len(supplies_attrs)
        self.tbl_changes_per_parcel.setRowCount(
            number_of_rows)  # t_id shouldn't be counted
        self.tbl_changes_per_parcel.setSortingEnabled(False)

        field_names = list(
            collected_attrs.keys()) if collected_attrs else list(
                supplies_attrs.keys())
        if PLOT_GEOMETRY_KEY in field_names:
            field_names.remove(
                PLOT_GEOMETRY_KEY)  # We'll handle plot geometry separately

        for row, field_name in enumerate(field_names):
            supplies_value = supplies_attrs[
                field_name] if field_name in supplies_attrs else NULL
            collected_value = collected_attrs[
                field_name] if field_name in collected_attrs else NULL
            field_alias = DICT_ALIAS_KEYS_CHANGE_DETECTION[
                field_name] if field_name in DICT_ALIAS_KEYS_CHANGE_DETECTION else field_name
            self.fill_row(field_alias, supplies_value, collected_value, row,
                          plural)

        if number_of_rows:  # At least one row in the table?
            self.fill_geometry_row(
                PLOT_GEOMETRY_KEY, supplies_attrs[PLOT_GEOMETRY_KEY]
                if PLOT_GEOMETRY_KEY in supplies_attrs else QgsGeometry(),
                collected_attrs[PLOT_GEOMETRY_KEY] if PLOT_GEOMETRY_KEY
                in collected_attrs else QgsGeometry(), number_of_rows - 1)

        self.tbl_changes_per_parcel.setSortingEnabled(True)
Exemple #9
0
class ChangesPerParcelPanelWidget(QgsPanelWidget, WIDGET_UI):
    def __init__(self, parent, utils, parcel_number=None, collected_parcel_t_id=None):
        QgsPanelWidget.__init__(self, None)
        self.setupUi(self)
        self.parent = parent
        self.utils = utils
        self.logger = Logger()
        self.symbology = Symbology()

        self.setDockMode(True)
        self.setPanelTitle(QCoreApplication.translate("ChangesPerParcelPanelWidget", "Change detection per parcel"))

        self._current_supplies_substring = ""
        self._current_substring = ""

        self.utils.add_layers()
        self.fill_combos()

        # Remove selection in plot layers
        self.utils._layers[self.utils._db.names.OP_PLOT_T][LAYER].removeSelection()
        self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER].removeSelection()

        # Map tool before activate map swipe tool
        self.init_map_tool = self.utils.canvas.mapTool()

        self.active_map_tool_before_custom = None
        self.btn_identify_plot.setIcon(QIcon(":/Asistente-LADM_COL/resources/images/spatial_unit.png"))
        self.btn_identify_plot.clicked.connect(self.btn_plot_toggled)

        # Create maptool
        self.maptool_identify = QgsMapToolIdentifyFeature(self.utils.canvas)

        # Set connections
        self.btn_alphanumeric_query.clicked.connect(self.alphanumeric_query)
        self.chk_show_all_plots.toggled.connect(self.show_all_plots)
        self.cbo_parcel_fields.currentIndexChanged.connect(self.search_field_updated)
        self.panelAccepted.connect(self.initialize_tools_and_layers)
        self.tbl_changes_per_parcel.itemDoubleClicked.connect(self.call_party_panel)

        self.initialize_field_values_line_edit()
        self.initialize_tools_and_layers()

        if parcel_number is not None:  # Do a search!
            self.txt_alphanumeric_query.setValue(parcel_number)
            if collected_parcel_t_id is not None:  # Search data for a duplicated parcel_number, so, take the t_id into account!
                self.search_data(parcel_number=parcel_number, collected_parcel_t_id=collected_parcel_t_id)
            else:
                self.search_data(parcel_number=parcel_number)

    def btn_plot_toggled(self):
        self.clear_result_table()

        if self.btn_identify_plot.isChecked():
            self.prepare_identify_plot()
        else:
            # The button was toggled and deactivated, go back to the previous tool
            self.utils.canvas.setMapTool(self.active_map_tool_before_custom)

    def clear_result_table(self):
        self.tbl_changes_per_parcel.clearContents()
        self.tbl_changes_per_parcel.setRowCount(0)

    def prepare_identify_plot(self):
        """
            Custom Identify tool was activated, prepare everything for identifying plots
        """
        self.active_map_tool_before_custom = self.utils.canvas.mapTool()

        self.btn_identify_plot.setChecked(True)

        self.utils.canvas.mapToolSet.connect(self.initialize_maptool)

        if self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER] is None:
            self.utils.add_layers()

        self.maptool_identify.setLayer(self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER])
        cursor = QCursor()
        cursor.setShape(Qt.PointingHandCursor)
        self.maptool_identify.setCursor(cursor)
        self.utils.canvas.setMapTool(self.maptool_identify)

        try:
            self.maptool_identify.featureIdentified.disconnect()
        except TypeError as e:
            pass
        self.maptool_identify.featureIdentified.connect(self.get_info_by_plot)

    def get_info_by_plot(self, plot_feature):
        """
        :param plot_feature: from supplies db
        """
        plot_t_id = plot_feature[self.utils._supplies_db.names.T_ID_F]

        self.utils.canvas.flashFeatureIds(self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER],
                                    [plot_feature.id()],
                                    QColor(255, 0, 0, 255),
                                    QColor(255, 0, 0, 0),
                                    flashes=1,
                                    duration=500)

        if not self.isVisible():
            self.show()

        self.spatial_query(plot_t_id)
        self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER].selectByIds([plot_feature.id()])

    def spatial_query(self, plot_id):
        if plot_id:
            parcel_number = self.utils.ladm_data.get_parcels_related_to_plots_supplies(self.utils._supplies_db, [plot_id], self.utils._supplies_db.names.GC_PARCEL_T_PARCEL_NUMBER_F)
            if parcel_number:  # Delegate handling of duplicates to search_data() method
                self.search_data(parcel_number=parcel_number[0])

    def call_party_panel(self, item):
        row = item.row()
        if self.tbl_changes_per_parcel.item(row, 0).text() == DICT_ALIAS_KEYS_CHANGE_DETECTION[DICT_KEY_PARTIES]:
            data = {SUPPLIES_DB_SOURCE: self.tbl_changes_per_parcel.item(row, 1).data(Qt.UserRole),
                    COLLECTED_DB_SOURCE: self.tbl_changes_per_parcel.item(row, 2).data(Qt.UserRole)}
            self.parent.show_party_panel(data)

    def search_field_updated(self, index=None):
        self.initialize_field_values_line_edit()

    def initialize_field_values_line_edit(self):
        # We search for alphanumeric data in supplies data source
        self.txt_alphanumeric_query.setLayer(self.utils._supplies_layers[self.utils._supplies_db.names.GC_PARCEL_T][LAYER])
        search_option = self.cbo_parcel_fields.currentData()
        search_field_supplies = get_supplies_search_options(self.utils._supplies_db.names)[search_option]
        idx = self.utils._supplies_layers[self.utils._supplies_db.names.GC_PARCEL_T][LAYER].fields().indexOf(search_field_supplies)
        self.txt_alphanumeric_query.setAttributeIndex(idx)

    def fill_combos(self):
        self.cbo_parcel_fields.clear()
        self.cbo_parcel_fields.addItem(QCoreApplication.translate("DockWidgetChanges", "Parcel Number"), PARCEL_NUMBER_SEARCH_KEY)
        self.cbo_parcel_fields.addItem(QCoreApplication.translate("DockWidgetChanges", "Previous Parcel Number"), PREVIOUS_PARCEL_NUMBER_SEARCH_KEY)
        self.cbo_parcel_fields.addItem(QCoreApplication.translate("DockWidgetChanges", "Folio de Matrícula Inmobiliaria"), FMI_PARCEL_SEARCH_KEY)

    @_with_override_cursor
    def search_data(self, **kwargs):
        """
        Get plot geometries associated with parcels, both collected and supplies, zoom to them, fill comparison table
        and activate map swipe tool.

        To fill the comparison table we build two search dicts, one for supplies (already given because the alphanumeric
        search is on supplies db source), and another one for collected. For the latter, we have 3 cases. We specify
        them below (inline).

        :param kwargs: key-value (field name-field value) to search in parcel tables, both collected and supplies
                       Normally, keys are parcel_number, old_parcel_number or FMI, but if duplicates are found, an
                       additional t_id disambiguates only for the collected source. In the supplies source we assume
                       we will not find duplicates, if there are, we will choose the first record found an will not deal
                       with letting the user choose one of the duplicates by hand (as we do for the collected source).
        """
        self.chk_show_all_plots.setEnabled(False)
        self.chk_show_all_plots.setChecked(True)
        self.initialize_tools_and_layers()  # Reset any filter on layers

        plots_supplies = list()
        plots_collected = list()
        self.clear_result_table()

        search_option = self.cbo_parcel_fields.currentData()
        search_field_supplies = get_supplies_search_options(self.utils._supplies_db.names)[search_option]
        search_field_collected = get_collected_search_options(self.utils._db.names)[search_option]
        search_value = list(kwargs.values())[0]


        # Build search criterion for both supplies and collected
        search_criterion_supplies = {search_field_supplies: search_value}

        # Get supplies parcel's t_id and get related plot(s)
        expression_supplies = QgsExpression("{}='{}'".format(search_field_supplies, search_value))
        request = QgsFeatureRequest(expression_supplies)
        field_idx = self.utils._supplies_layers[self.utils._supplies_db.names.GC_PARCEL_T][LAYER].fields().indexFromName(self.utils._supplies_db.names.T_ID_F)
        request.setFlags(QgsFeatureRequest.NoGeometry)
        request.setSubsetOfAttributes([field_idx])  # Note: this adds a new flag
        supplies_parcels = [feature for feature in self.utils._supplies_layers[self.utils._supplies_db.names.GC_PARCEL_T][LAYER].getFeatures(request)]

        if len(supplies_parcels) > 1:
            # We do not expect duplicates in the supplies source!
            pass  # We'll choose the first one anyways
        elif len(supplies_parcels) == 0:
            self.logger.info(__name__, "No supplies parcel found! Search: {}={}".format(search_field_supplies, search_value))

        supplies_plot_t_ids = []
        if supplies_parcels:
            supplies_plot_t_ids = self.utils.ladm_data.get_plots_related_to_parcels_supplies(self.utils._supplies_db,
                                              [supplies_parcels[0][self.utils._supplies_db.names.T_ID_F]],
                                              self.utils._supplies_db.names.T_ID_F,
                                              self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER])

            if supplies_plot_t_ids:
                self._current_supplies_substring = "\"{}\" IN ('{}')".format(self.utils._supplies_db.names.T_ID_F, "','".join([str(t_id) for t_id in supplies_plot_t_ids]))
                plots_supplies = self.utils.ladm_data.get_features_from_t_ids(
                    self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER],
                    self.utils._supplies_db.names.T_ID_F, supplies_plot_t_ids, True)


        # Now get COLLECTED parcel's t_id to build the search dict for collected
        collected_parcel_t_id = None
        if 'collected_parcel_t_id' in kwargs:
            # This is the case when this panel is called and we already know the parcel number is duplicated
            collected_parcel_t_id = kwargs['collected_parcel_t_id']
            search_criterion_collected = {self.utils._db.names.T_ID_F: collected_parcel_t_id}  # As there are duplicates, we need to use t_ids
        else:
            # This is the case when:
            #   + Either this panel was called and we know the parcel number is not duplicated, or
            #   + This panel was shown without knowing about duplicates (e.g., individual parcel search) and we still
            #     need to discover whether we have duplicates for this search criterion
            search_criterion_collected = {search_field_collected: search_value}

            expression_collected = QgsExpression("{}='{}'".format(search_field_collected, search_value))
            request = QgsFeatureRequest(expression_collected)
            request.setFlags(QgsFeatureRequest.NoGeometry)
            request.setSubsetOfAttributes([self.utils._db.names.T_ID_F],
                                          self.utils._layers[self.utils._db.names.OP_PARCEL_T][LAYER].fields())  # Note this adds a new flag
            collected_parcels = self.utils._layers[self.utils._db.names.OP_PARCEL_T][LAYER].getFeatures(request)
            collected_parcels_t_ids = [feature[self.utils._db.names.T_ID_F] for feature in collected_parcels]

            if collected_parcels_t_ids:
                collected_parcel_t_id = collected_parcels_t_ids[0]
                if len(collected_parcels_t_ids) > 1:  # Duplicates in collected source after a search
                    QApplication.restoreOverrideCursor()  # Make sure cursor is not waiting (it is if on an identify)
                    QCoreApplication.processEvents()
                    dlg_select_parcel = SelectDuplicateParcelDialog(self.utils, collected_parcels_t_ids, self.parent)
                    dlg_select_parcel.exec_()

                    if dlg_select_parcel.parcel_t_id:  # User selected one of the duplicated parcels
                        collected_parcel_t_id = dlg_select_parcel.parcel_t_id
                        search_criterion_collected = {self.utils._db.names.T_ID_F: collected_parcel_t_id}
                    else:
                        return  # User just cancelled the dialog, there is nothing more to do


        self.fill_table(search_criterion_supplies, search_criterion_collected)

        # Now get related plot(s) for both collected and supplies,
        if collected_parcel_t_id is not None:
            plot_t_ids = self.utils.ladm_data.get_plots_related_to_parcels(self.utils._db,
                                                                           [collected_parcel_t_id],
                                                                           self.utils._db.names.T_ID_F,
                                                                           plot_layer=self.utils._layers[self.utils._db.names.OP_PLOT_T][LAYER],
                                                                           uebaunit_table=self.utils._layers[self.utils._db.names.COL_UE_BAUNIT_T][LAYER])

            if plot_t_ids:
                self._current_substring = "{} IN ('{}')".format(self.utils._db.names.T_ID_F, "','".join([str(t_id) for t_id in plot_t_ids]))
                plots_collected = self.utils.ladm_data.get_features_from_t_ids(self.utils._layers[self.utils._db.names.OP_PLOT_T][LAYER],
                                                                               self.utils._db.names.T_ID_F,
                                                                               plot_t_ids,
                                                                               True)

        # Zoom to combined extent
        plot_features = plots_supplies + plots_collected  # Feature list
        plots_extent = QgsRectangle()
        for plot in plot_features:
            plots_extent.combineExtentWith(plot.geometry().boundingBox())

        if not plots_extent.isEmpty():
            self.utils.iface.mapCanvas().zoomToFeatureExtent(plots_extent)

            if plots_supplies and plots_collected:  # Otherwise the map swipe tool doesn't add any value :)
                # Activate Swipe Tool
                self.utils.qgis_utils.activate_layer_requested.emit(self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER])
                self.parent.activate_map_swipe_tool()

                # Send a custom mouse move on the map to make the map swipe tool's limit appear on the canvas
                coord_x = plots_extent.xMaximum() - (plots_extent.xMaximum() - plots_extent.xMinimum()) / 9  # 90%
                coord_y = plots_extent.yMaximum() - (plots_extent.yMaximum() - plots_extent.yMinimum()) / 2  # 50%

                coord_transform = self.utils.iface.mapCanvas().getCoordinateTransform()
                map_point = coord_transform.transform(coord_x, coord_y)
                widget_point = map_point.toQPointF().toPoint()
                global_point = self.utils.canvas.mapToGlobal(widget_point)

                self.utils.canvas.mousePressEvent(QMouseEvent(QEvent.MouseButtonPress, global_point, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier))
                self.utils.canvas.mouseMoveEvent(QMouseEvent(QEvent.MouseMove, widget_point + QPoint(1,0), Qt.NoButton, Qt.LeftButton, Qt.NoModifier))
                self.utils.canvas.mouseReleaseEvent(QMouseEvent(QEvent.MouseButtonRelease, widget_point + QPoint(1,0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier))

        # Once the query is done, activate the checkbox to alternate all plots/only selected plot
        self.chk_show_all_plots.setEnabled(True)

    def fill_table(self, search_criterion_supplies, search_criterion_collected):
        """
        Shouldn't handle 'inverse' mode as we won't switch table columns at runtime.

        :param search_criterion_supplies: key-value pair to build an expression to search data in the supplies source
        :param search_criterion_collected: key-value pair to build an expression to search data in the collected source
        :return:
        """
        plural = LayerConfig.get_dict_plural(self.utils._db.names)
        dict_collected_parcels = self.utils.ladm_data.get_parcel_data_to_compare_changes(self.utils._db, search_criterion_collected)

        # Custom layer modifiers
        layer_modifiers = {
            LayerConfig.PREFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_PREFIX,
            LayerConfig.SUFFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_SUFFIX,
            LayerConfig.STYLE_GROUP_LAYER_MODIFIERS: self.symbology.get_supplies_style_group(self.utils._supplies_db.names)
        }
        dict_supplies_parcels = self.utils.ladm_data.get_parcel_data_to_compare_changes_supplies(self.utils._supplies_db, search_criterion_supplies, layer_modifiers=layer_modifiers)

        # Before filling the table we make sure we get one and only one parcel attrs dict
        collected_attrs = dict()
        if dict_collected_parcels:
            collected_parcel_number = list(dict_collected_parcels.keys())[0]
            collected_attrs = dict_collected_parcels[collected_parcel_number][0]
            del collected_attrs[self.utils._db.names.T_ID_F]  # Remove this line if self.utils._db.names.T_ID_F is somehow needed

        supplies_attrs = dict()
        if dict_supplies_parcels:
            supplies_parcel_number = list(dict_supplies_parcels.keys())[0]
            supplies_attrs = dict_supplies_parcels[supplies_parcel_number][0]
            del supplies_attrs[self.utils._supplies_db.names.T_ID_F]  # Remove this line if self.utils._supplies_db.names,T_ID_F is somehow needed

        number_of_rows = len(collected_attrs) or len(supplies_attrs)
        self.tbl_changes_per_parcel.setRowCount(number_of_rows)  # t_id shouldn't be counted
        self.tbl_changes_per_parcel.setSortingEnabled(False)

        field_names = list(collected_attrs.keys()) if collected_attrs else list(supplies_attrs.keys())
        if PLOT_GEOMETRY_KEY in field_names:
            field_names.remove(PLOT_GEOMETRY_KEY)  # We'll handle plot geometry separately

        for row, field_name in enumerate(field_names):
            supplies_value = supplies_attrs[field_name] if field_name in supplies_attrs else NULL
            collected_value = collected_attrs[field_name] if field_name in collected_attrs else NULL
            field_alias = DICT_ALIAS_KEYS_CHANGE_DETECTION[field_name] if field_name in DICT_ALIAS_KEYS_CHANGE_DETECTION else field_name
            self.fill_row(field_alias, supplies_value, collected_value, row, plural)

        if number_of_rows:  # At least one row in the table?
            self.fill_geometry_row(PLOT_GEOMETRY_KEY,
                               supplies_attrs[PLOT_GEOMETRY_KEY] if PLOT_GEOMETRY_KEY in supplies_attrs else QgsGeometry(),
                               collected_attrs[PLOT_GEOMETRY_KEY] if PLOT_GEOMETRY_KEY in collected_attrs else QgsGeometry(),
                               number_of_rows - 1)

        self.tbl_changes_per_parcel.setSortingEnabled(True)

    def fill_row(self, field_name, supplies_value, collected_value, row, plural):
        item = QTableWidgetItem(field_name)
        # item.setData(Qt.UserRole, parcel_attrs[self.names.T_ID_F])
        self.tbl_changes_per_parcel.setItem(row, 0, item)

        if field_name == DICT_ALIAS_KEYS_CHANGE_DETECTION[DICT_KEY_PARTIES]:
            item = self.fill_party_item(supplies_value)
            self.tbl_changes_per_parcel.setItem(row, 1, item)

            item = self.fill_party_item(collected_value)
            self.tbl_changes_per_parcel.setItem(row, 2, item)

            self.tbl_changes_per_parcel.setItem(row, 3, QTableWidgetItem())
            self.tbl_changes_per_parcel.item(row, 3).setBackground(Qt.green if supplies_value == collected_value else Qt.red)
        else:
            item = QTableWidgetItem(str(supplies_value) if supplies_value != NULL else '')
            #item.setData(Qt.UserRole, parcel_attrs[self.names.T_ID_F])
            self.tbl_changes_per_parcel.setItem(row, 1, item)

            item = QTableWidgetItem(str(collected_value) if collected_value != NULL else '')
            # item.setData(Qt.UserRole, parcel_attrs[self.names.T_ID_F])
            self.tbl_changes_per_parcel.setItem(row, 2, item)

            self.tbl_changes_per_parcel.setItem(row, 3, QTableWidgetItem())
            self.tbl_changes_per_parcel.item(row, 3).setBackground(Qt.green if supplies_value == collected_value else Qt.red)

    def fill_party_item(self, value):
        # Party's info comes in a list or a list of lists if it's a group party
        display_value = ''

        if value != NULL:
            if type(value) is list and value:
                display_value = "{} {}".format(len(value),
                                               QCoreApplication.translate("DockWidgetChanges", "parties") if len(value)>1 else QCoreApplication.translate("DockWidgetChanges", "party"))
        #else:
        #    display_value = QCoreApplication.translate("DockWidgetChanges", "0 parties")

        item = QTableWidgetItem(display_value)
        item.setData(Qt.UserRole, value)
        return item

    def fill_geometry_row(self, field_name, supplies_geom, collected_geom, row):
        self.tbl_changes_per_parcel.setItem(row, 0, QTableWidgetItem(QCoreApplication.translate("DockWidgetChanges", "Geometry")))
        self.tbl_changes_per_parcel.setItem(row, 1, QTableWidgetItem(self.get_geometry_type_name(supplies_geom)))
        self.tbl_changes_per_parcel.setItem(row, 2, QTableWidgetItem(self.get_geometry_type_name(collected_geom)))

        self.tbl_changes_per_parcel.setItem(row, 3, QTableWidgetItem())
        self.tbl_changes_per_parcel.item(row, 3).setBackground(
            Qt.green if self.utils.compare_features_geometries(collected_geom, supplies_geom) else Qt.red)

    @staticmethod
    def get_geometry_type_name(geometry):
        if geometry is None:
            return QCoreApplication.translate("DockWidgetChanges", "No associated plot")
        elif geometry.type() == QgsWkbTypes.UnknownGeometry:
            return ''
        elif geometry.type() == QgsWkbTypes.PolygonGeometry:
            return QCoreApplication.translate("DockWidgetChanges", "Polygon")
        else:
            return "Type: {}".format(geometry.type())

    def alphanumeric_query(self):
        """
        Alphanumeric query (On supplies db)
        """
        option = self.cbo_parcel_fields.currentData()
        query = self.txt_alphanumeric_query.value()
        if query:
            if option == FMI_PARCEL_SEARCH_KEY:
                self.search_data(parcel_fmi=query)
            elif option == PARCEL_NUMBER_SEARCH_KEY:
                self.search_data(parcel_number=query)
            else: # previous_parcel_number
                self.search_data(previous_parcel_number=query)

        else:
            self.utils.iface.messageBar().pushMessage("Asistente LADM_COL",
                QCoreApplication.translate("DockWidgetChanges", "First enter a query"))

    def show_all_plots(self, state):
        try:
            self.utils._supplies_layers[self.utils._supplies_db.names.GC_PLOT_T][LAYER].setSubsetString(self._current_supplies_substring if not state else "")
        except RuntimeError:  # If the layer was previously removed
            pass

        try:
            self.utils._layers[self.utils._db.names.OP_PLOT_T][LAYER].setSubsetString(self._current_substring if not state else "")
        except RuntimeError:  # If the layer was previously removed
            pass

    def initialize_tools_and_layers(self, panel=None):
        self.parent.deactivate_map_swipe_tool()
        self.show_all_plots(True)

    def initialize_maptool(self, new_tool, old_tool):
        if self.maptool_identify == old_tool:
            # custom identify was deactivated
            try:
                self.utils.canvas.mapToolSet.disconnect(self.initialize_maptool)
            except TypeError as e:
                pass

            self.btn_identify_plot.setChecked(False)
        else:
            # custom identify was activated
            pass

    def close_panel(self):
        self.show_all_plots(True)  # Remove filter in plots layers if it was activate and panel is closed
        # custom identify was deactivated
        try:
            self.utils.canvas.mapToolSet.disconnect(self.initialize_maptool)
        except TypeError as e:
            pass

        self.utils.canvas.setMapTool(self.init_map_tool)
    def _get_compared_parcels_data(self, inverse=False):
        """
        inverse: By default False, which takes the collected db as base_db and the supplies_db as compare_db
                 Inverse True is useful to find missing parcels (from the supplies authority's perspective)

        :return: dict() --> {PARCEL_NUMBER: X,
                             PARCEL_ATTRIBUTES: {PARCEL_ID: [self._db.names.T_ID_F], PARCEL_STATUS: '', PARCEL_STATUS_DISPLAY: ''}]
        """
        base_db = self._supplies_db if inverse else self._db
        compare_db = self._db if inverse else self._supplies_db

        layer_modifiers = {
            LayerConfig.PREFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_PREFIX,
            LayerConfig.SUFFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_SUFFIX,
            LayerConfig.STYLE_GROUP_LAYER_MODIFIERS: Symbology().get_style_group_layer_modifiers(self._supplies_db.names)
        }

        if inverse:
            dict_collected_parcels = self.ladm_data.get_parcel_data_to_compare_changes_supplies(self._supplies_db, None)
            dict_supplies_parcels = self.ladm_data.get_parcel_data_to_compare_changes(self._db, None, layer_modifiers=layer_modifiers)
        else:
            dict_collected_parcels = self.ladm_data.get_parcel_data_to_compare_changes(self._db, None)
            dict_supplies_parcels = self.ladm_data.get_parcel_data_to_compare_changes_supplies(self._supplies_db, None, layer_modifiers=layer_modifiers)

        dict_compared_parcel_data = dict()
        for collected_parcel_number, collected_features in dict_collected_parcels.items():
            dict_attrs_comparison = dict()

            if not collected_parcel_number: # NULL parcel numbers
                dict_attrs_comparison[DICT_KEY_PARCEL_T_PARCEL_NUMBER_F] = NULL
                dict_attrs_comparison[base_db.names.T_ID_F] = [feature[base_db.names.T_ID_F] for feature in collected_features]
                dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_NULL_PARCEL
                dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = "({})".format(len(collected_features))
            else:
                # A parcel number has at least one dict of attributes (i.e., one feature)
                dict_attrs_comparison[DICT_KEY_PARCEL_T_PARCEL_NUMBER_F] = collected_parcel_number
                dict_attrs_comparison[base_db.names.T_ID_F] = [feature[base_db.names.T_ID_F] for feature in collected_features]

                if len(collected_features) > 1:
                    dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_SEVERAL_PARCELS
                    dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = "({})".format(len(collected_features))
                else:  # Only one feature, at this point is safe to call the first element ([0]) of the array
                    if not collected_parcel_number in dict_supplies_parcels:
                        dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_NEW_PARCEL
                        dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_NEW_PARCEL
                    else:
                        supplies_features = dict_supplies_parcels[collected_parcel_number]

                        del collected_features[0][base_db.names.T_ID_F]  # We won't compare ID_FIELDS
                        del supplies_features[0][compare_db.names.T_ID_F]  # We won't compare ID_FIELDS

                        # Compare all attributes except geometry: a change in feature attrs is enough to mark it as
                        #   changed in the summary panel
                        if not self.compare_features_attrs(collected_features[0], supplies_features[0]):
                            dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_PARCEL_CHANGED
                            dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_PARCEL_CHANGED
                        else:  # Attrs are equal, what about geometries?
                            collected_geometry = QgsGeometry()
                            supplies_geometry = QgsGeometry()
                            if PLOT_GEOMETRY_KEY in collected_features[0]:
                                collected_geometry = collected_features[0][PLOT_GEOMETRY_KEY]
                            if PLOT_GEOMETRY_KEY in supplies_features[0]:
                                supplies_geometry = supplies_features[0][PLOT_GEOMETRY_KEY]

                            if not self.compare_features_geometries(collected_geometry, supplies_geometry):
                                dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_PARCEL_ONLY_GEOMETRY_CHANGED
                                dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_PARCEL_ONLY_GEOMETRY_CHANGED
                            else:  # Attrs and geometry are the same!
                                dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_PARCEL_REMAINS
                                dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_PARCEL_REMAINS

            dict_compared_parcel_data[collected_parcel_number or NULL] = dict_attrs_comparison

        return dict_compared_parcel_data
class ChangeDetectionUtils(QObject):

    change_detection_layer_removed = pyqtSignal()

    def __init__(self, iface, db, supplies_db, qgis_utils, ladm_data):
        QObject.__init__(self)
        self.iface = iface
        self.canvas = iface.mapCanvas()
        self._db = db
        self._supplies_db = supplies_db
        self.qgis_utils = qgis_utils
        self.ladm_data = ladm_data
        self.symbology = Symbology()

        self._layers = dict()
        self._supplies_layers = dict()
        self.initialize_layers()

        self._compared_parcels_data = dict()
        self._compared_parcels_data_inverse = dict()

    def initialize_layers(self):
        self._layers = {
            self._db.names.OP_PLOT_T: {'name': self._db.names.OP_PLOT_T, 'geometry': QgsWkbTypes.PolygonGeometry, LAYER: None},
            self._db.names.OP_PARCEL_T: {'name': self._db.names.OP_PARCEL_T, 'geometry': None, LAYER: None},
            self._db.names.COL_UE_BAUNIT_T: {'name': self._db.names.COL_UE_BAUNIT_T, 'geometry': None, LAYER: None}
        }

        self._supplies_layers = {
            self._supplies_db.names.GC_PLOT_T: {'name': self._supplies_db.names.GC_PLOT_T, 'geometry': QgsWkbTypes.PolygonGeometry, LAYER: None},
            self._supplies_db.names.GC_PARCEL_T: {'name': self._supplies_db.names.GC_PARCEL_T, 'geometry': None, LAYER: None}
        }

    def initialize_data(self):
        self._compared_parcels_data = dict()
        self._compared_parcels_data_inverse = dict()

    def add_layers(self):
        # We can pick any required layer, if it is None, no prior load has been done, otherwise skip...
        if self._layers[self._db.names.OP_PLOT_T][LAYER] is None:
            self.qgis_utils.map_freeze_requested.emit(True)

            self.qgis_utils.get_layers(self._db, self._layers, load=True, emit_map_freeze=False)
            if not self._layers:
                return None

            # Now load supplies layers
            # Set layer modifiers
            layer_modifiers = {
                LayerConfig.PREFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_PREFIX,
                LayerConfig.SUFFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_SUFFIX,
                LayerConfig.STYLE_GROUP_LAYER_MODIFIERS: self.symbology.get_supplies_style_group(self._supplies_db.names)
            }
            self.qgis_utils.get_layers(self._supplies_db,
                                       self._supplies_layers,
                                       load=True,
                                       emit_map_freeze=False,
                                       layer_modifiers=layer_modifiers)
            if not self._supplies_layers:
                return None
            else:
                # In some occasions the supplies and collected plots might not overlap and have different extents
                self.iface.setActiveLayer(self._supplies_layers[self._supplies_db.names.GC_PLOT_T][LAYER])
                self.iface.zoomToActiveLayer()

            self.qgis_utils.map_freeze_requested.emit(False)

            for layer_name in self._layers:
                if self._layers[layer_name][LAYER]: # Layer was found, listen to its removal so that we can react properly
                    try:
                        self._layers[layer_name][LAYER].willBeDeleted.disconnect(self.change_detection_layer_removed)
                    except:
                        pass
                    self._layers[layer_name][LAYER].willBeDeleted.connect(self.change_detection_layer_removed)

            for layer_name in self._supplies_layers:
                if self._supplies_layers[layer_name][LAYER]: # Layer was found, listen to its removal so that we can react properly
                    try:
                        self._supplies_layers[layer_name][LAYER].willBeDeleted.disconnect(self.change_detection_layer_removed)
                    except:
                        pass
                    self._supplies_layers[layer_name][LAYER].willBeDeleted.connect(self.change_detection_layer_removed)

    def get_compared_parcels_data(self, inverse=False):
        # If it's the first call, get from the DB, else get from a cache
        if inverse:
            if not self._compared_parcels_data_inverse:
                self._compared_parcels_data_inverse = self._get_compared_parcels_data(inverse)

            return self._compared_parcels_data_inverse
        else:
            if not self._compared_parcels_data:
                self._compared_parcels_data = self._get_compared_parcels_data()

            return self._compared_parcels_data

    def _get_compared_parcels_data(self, inverse=False):
        """
        inverse: By default False, which takes the collected db as base_db and the supplies_db as compare_db
                 Inverse True is useful to find missing parcels (from the supplies authority's perspective)

        :return: dict() --> {PARCEL_NUMBER: X,
                             PARCEL_ATTRIBUTES: {PARCEL_ID: [self._db.names.T_ID_F], PARCEL_STATUS: '', PARCEL_STATUS_DISPLAY: ''}]
        """
        base_db = self._supplies_db if inverse else self._db
        compare_db = self._db if inverse else self._supplies_db

        layer_modifiers = {
            LayerConfig.PREFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_PREFIX,
            LayerConfig.SUFFIX_LAYER_MODIFIERS: LayerConfig.SUPPLIES_DB_SUFFIX,
            LayerConfig.STYLE_GROUP_LAYER_MODIFIERS: self.symbology.get_supplies_style_group(self._supplies_db.names)
        }

        if inverse:
            dict_collected_parcels = self.ladm_data.get_parcel_data_to_compare_changes_supplies(self._supplies_db, None)
            dict_supplies_parcels = self.ladm_data.get_parcel_data_to_compare_changes(self._db, None, layer_modifiers=layer_modifiers)
        else:
            dict_collected_parcels = self.ladm_data.get_parcel_data_to_compare_changes(self._db, None)
            dict_supplies_parcels = self.ladm_data.get_parcel_data_to_compare_changes_supplies(self._supplies_db, None, layer_modifiers=layer_modifiers)

        dict_compared_parcel_data = dict()
        for collected_parcel_number, collected_features in dict_collected_parcels.items():
            dict_attrs_comparison = dict()

            if not collected_parcel_number: # NULL parcel numbers
                dict_attrs_comparison[DICT_KEY_PARCEL_T_PARCEL_NUMBER_F] = NULL
                dict_attrs_comparison[base_db.names.T_ID_F] = [feature[base_db.names.T_ID_F] for feature in collected_features]
                dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_NULL_PARCEL
                dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = "({})".format(len(collected_features))
            else:
                # A parcel number has at least one dict of attributes (i.e., one feature)
                dict_attrs_comparison[DICT_KEY_PARCEL_T_PARCEL_NUMBER_F] = collected_parcel_number
                dict_attrs_comparison[base_db.names.T_ID_F] = [feature[base_db.names.T_ID_F] for feature in collected_features]

                if len(collected_features) > 1:
                    dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_SEVERAL_PARCELS
                    dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = "({})".format(len(collected_features))
                else:  # Only one feature, at this point is safe to call the first element ([0]) of the array
                    if not collected_parcel_number in dict_supplies_parcels:
                        dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_NEW_PARCEL
                        dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_NEW_PARCEL
                    else:
                        supplies_features = dict_supplies_parcels[collected_parcel_number]

                        del collected_features[0][base_db.names.T_ID_F]  # We won't compare ID_FIELDS
                        del supplies_features[0][compare_db.names.T_ID_F]  # We won't compare ID_FIELDS

                        # Compare all attributes except geometry: a change in feature attrs is enough to mark it as
                        #   changed in the summary panel
                        if not self.compare_features_attrs(collected_features[0], supplies_features[0]):
                            dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_PARCEL_CHANGED
                            dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_PARCEL_CHANGED
                        else:  # Attrs are equal, what about geometries?
                            collected_geometry = QgsGeometry()
                            supplies_geometry = QgsGeometry()
                            if PLOT_GEOMETRY_KEY in collected_features[0]:
                                collected_geometry = collected_features[0][PLOT_GEOMETRY_KEY]
                            if PLOT_GEOMETRY_KEY in supplies_features[0]:
                                supplies_geometry = supplies_features[0][PLOT_GEOMETRY_KEY]

                            if not self.compare_features_geometries(collected_geometry, supplies_geometry):
                                dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_PARCEL_ONLY_GEOMETRY_CHANGED
                                dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_PARCEL_ONLY_GEOMETRY_CHANGED
                            else:  # Attrs and geometry are the same!
                                dict_attrs_comparison[PARCEL_STATUS] = CHANGE_DETECTION_PARCEL_REMAINS
                                dict_attrs_comparison[PARCEL_STATUS_DISPLAY] = CHANGE_DETECTION_PARCEL_REMAINS

            dict_compared_parcel_data[collected_parcel_number or NULL] = dict_attrs_comparison

        return dict_compared_parcel_data

    def compare_features_attrs(self, collected, supplies):
        """
        Compare all alphanumeric attibutes for two custom feature dicts

        :param collected: Dict with parcel info defined in parcel_fields_to_compare, party_fields_to_compare,
                          plot_field_to_compare, PROPERTY_RECORD_CARD_FIELDS_TO_COMPARE
        :param supplies: Dict with parcel info defined in parcel_fields_to_compare, party_fields_to_compare,
                          plot_field_to_compare, PROPERTY_RECORD_CARD_FIELDS_TO_COMPARE
        :return: True means equal, False unequal
        """
        if len(collected) != len(supplies):
            return False

        for k,v in collected.items():
            if k != PLOT_GEOMETRY_KEY:
                if v != supplies[k]:
                    return False

        return True

    def compare_features_geometries(self, geometry_a, geometry_b):
        """
        Function to compare two plot geometries:
            First compare bboxes, if equal compare centroids, if equal use QGIS equals() function.

        :param geometry_a: QgsGeometry
        :param geometry_b: QgsGeometry
        :return: True means equal, False unequal
        """
        if geometry_a is None:  # None for parcels that don't have any associated plot
            return geometry_b is None or geometry_b.isNull()

        if geometry_b is None:  # None for parcels that don't have any associated plot
            return geometry_a is None or geometry_a.isNull()

        if not geometry_a.isGeosValid() and not geometry_b.isGeosValid():
            return True

        if geometry_a.boundingBox() != geometry_b.boundingBox():
            return False

        if not geometry_a.centroid().equals(geometry_b.centroid()):
            return False

        if not geometry_a.equals(geometry_b):
            return False

        return True