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)
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)
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)
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