PyQGIS – Modifying Attributes with Map Selection Using PyQGIS

pyqgisselect

I am trying to write a basic script or tool that will allow me to select a feature from the map, store those features initial attributes, then click additional features from the map and assign the incremented initial attribute values to those fields for each new feature selected. Right clicking the mouse on the map would close the tool.

For example: Suppose I have a layer named "Points" consisting of 5 point features. There are two attributes in the layer called "A" and "B". Only one of the points has A and B values: they are 20 and 30. The other points have A and B values that are NULL and I would like to automatically fill them out.

I would like to be able to select an initial reference point and then additional points on the map and have the script automatically assign A and B values by incrementing from the 1st point's values so that the next point I select will have A = 21 and B = 31. The next point I click will have A = 22, B = 32, etc. until I click the last point which will assign A = 25, B = 35. Then I would right click the mouse to close out the procedure.

enter image description here

I am sure this is possible, but I am not quite sure how to get started. Based on this answer, I believe I may need to subclass QgsMapToolIdentifyFeature since that would allow me to have access to the feature where I can reference the attributes.

From another example, it looks like I will need a callback function that will connect to featureIdentified. The problem is I do not know how to pass an argument to the callback – I assume I will need to set a counter outside of this callback function with an initial value of 0 so I can then increment the attributes for subsequent selected features each time the user selects another feature.

Can you help me do it?

def callback(feature):
  """Code called when the feature is selected by the user"""
  print("You clicked on feature {}".format(feature.id()))

canvas = iface.mapCanvas()
feature_identifier = QgsMapToolIdentifyFeature(canvas)

# indicates the layer on which the selection will be done
feature_identifier.setLayer(vlayer)

# use the callback as a slot triggered when the user identifies a feature
feature_identifier.featureIdentified.connect(callback)

# activation of the map tool
canvas.setMapTool(feature_identifier)

Best Answer

Try the script below. Note that the 'A' and 'B' fields must be of numeric type (not string).

class customMapTool(QgsMapToolIdentifyFeature):
    
    def __init__(self, iface):
        self.iface = iface
        self.canvas = self.iface.mapCanvas()
        self.layer = self.iface.activeLayer()
        QgsMapToolIdentifyFeature.__init__(self, self.canvas, self.layer)
        self.iface.currentLayerChanged.connect(self.active_changed)
        
        self.first_click = True
        
        self.A_Val = None
        self.B_Val = None
        
        
    def active_changed(self, layer):
        if layer.type() == QgsMapLayerType.VectorLayer and layer.isSpatial():
            self.layer = layer
            self.setLayer(self.layer)
            
    def canvasReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if self.layer.geometryType() != QgsWkbTypes.PointGeometry:
                self.iface.messageBar().pushMessage('Please select a point layer', Qgis.Info, 2)
                return
            found_features = self.identify(event.x(), event.y(), [self.layer], QgsMapToolIdentify.ActiveLayer)
            if found_features:
                feat = found_features[0].mFeature
                if self.first_click:
                    if feat['A'] is not NULL and feat['B'] is not NULL:
                        self.A_Val = feat['A']
                        self.B_Val = feat['B']
                        self.first_click = False
                    return
                elif not self.first_click:
                    if self.A_Val and self.B_Val:
                        self.A_Val += 1
                        self.B_Val += 1
                        self.update_attributes(feat)
                    return
        elif event.button() == Qt.RightButton:
            self.iface.actionPan().trigger()
    
    def update_attributes(self, feat):
        a_idx = self.layer.fields().lookupField('A')
        b_idx = self.layer.fields().lookupField('B')
        if a_idx != -1 and b_idx != -1:
            if self.A_Val and self.B_Val:
                self.layer.dataProvider().changeAttributeValues({feat.id(): {a_idx: self.A_Val,
                                                                        b_idx: self.B_Val}})
            self.layer.triggerRepaint()
        
    def deactivate(self):
        self.iface.currentLayerChanged.disconnect(self.active_changed)
            
        
t = customMapTool(iface)
iface.mapCanvas().setMapTool(t)

See screencast below for expected results. I labelled the points with the expression: "A" ||', '|| "B" and added a call to self.layer.triggerRepaint() to illustrate the feature attributes being updated on each mouse click.

enter image description here

Related Question