3

I am currently trying to develop a QGIS plugin which takes in data to plot points onto a PNG file. One of the objectives of this plugin is to be able to shift points on a layer by dragging and dropping the selected point.

So far I have been able to take a pointerTool to identify the feature that is going to be shifted, but I am stuck on how to use the canvasPressEvent, canvasMoveEvent, and canvasReleasedEvent to do this.

I've tried looking at the API on the PyQGIS repo, but it has not been too helpful.

Here is the section of the code that selects the features and prints out some information using the QgsMapToolIdentifyFeature class:

def selectPoints(self):
    canvas = self.iface.mapCanvas()
    #self.setCursor(Qt.ArrowCursor)
    vl = self.layer#selecting the vector layer with points only 
    self.selectTool = QgsMapToolIdentifyFeature(canvas, vl)
    canvas.setMapTool(self.selectTool)
       
    self.selectTool.featureIdentified.connect(self.select)

def select(self,feature):
    id1 = feature.id()
    feats =self.layer.select(id1)
    attributes = feature.attributes()
    #attributes0 x, attributes1 y
    x=attributes[0]
    y = -1*attributes[1]
        
    attributes[0] = x+200
    attributes[1] = y - 200
    self.layer.updateFeature(feature)
    self.layer.commitChanges()
    print(x,y)
1
  • I read your question many times, but I cannot understand what exactly you ask. Commented Sep 13, 2021 at 23:09

1 Answer 1

3

The first and perhaps simplest solution which comes to mind is that you can use the native move features tool which is available in the gui via the Advanced Digitizing Toolbar. You could activate it with PyQgis inside your main plugin class like this:

self.iface.activeLayer().startEditing()
self.iface.actionMoveFeature().trigger()

If you want to override the methods of QgsMapTool classes such as canvasReleaseEvent(), canvasMoveEvent() etc. you need to subclass (create your own class definition which inherits from the appropriate parent class) and reimplement those methods.

I have written an example below which you can simply run in the Python console to test.

class customMoveTool(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.msg = QMessageBox()
        self.msg.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
        self.msg.setDefaultButton(QMessageBox.Ok)
        
        self.vm = None #Vertex Marker
        self.rb = None #Rubber Band
        self.first_click = True
        self.fid = 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 QgsProject.instance().crs() != self.layer.crs():
            click_point = self.transform_point(QgsProject.instance().crs(),
                                                self.layer.crs(),
                                                QgsPointXY(event.mapPoint()))
        else:
            click_point = QgsPointXY(event.mapPoint())
        if self.layer.geometryType() != QgsWkbTypes.PointGeometry:
            self.iface.messageBar().pushMessage('Please select a point layer', Qgis.Info, 2)
        if self.first_click:
            found_features = self.identify(event.x(), event.y(), [self.layer], QgsMapToolIdentify.ActiveLayer)
            #Create a vertex marker around the identified point
            if found_features:
                if not self.layer.isEditable():
                    self.msg.setText('Layer is not editable\nToggle edit mode?')
                    res = self.msg.exec_()
                    if res == QMessageBox.Ok:
                        self.layer.startEditing()
                    elif res == QMessageBox.Cancel:
                        return
                feat = found_features[0].mFeature
                self.fid = feat.id()
                ft_geom = feat.geometry()
                if QgsProject.instance().crs() != self.layer.crs():
                    f_point = self.transform_point(self.layer.crs(),
                                                    QgsProject.instance().crs(),
                                                    ft_geom.asPoint())
                else:
                    f_point = ft_geom.asPoint()
                self.create_vertex_marker(f_point)
                self.first_click = False
                return
        elif not self.first_click:
            self.clean_up()
            new_geom = QgsGeometry().fromPointXY(click_point)
            success = self.layer.changeGeometry(self.fid, new_geom)
#            print(success)
            self.layer.triggerRepaint()
            self.first_click = True
            return
    
    def canvasMoveEvent(self, event):
        if self.rb:
            self.rb.reset()
        if not self.first_click:
            self.rb = QgsRubberBand(self.canvas, QgsWkbTypes.PointGeometry)
            self.rb.setColor(QColor('green'))
            self.rb.setIcon(QgsRubberBand.ICON_CIRCLE)
            self.rb.setIconSize(10)
            geom = QgsGeometry().fromPointXY(QgsPointXY(event.mapPoint()))
            self.rb.setToGeometry(geom)
            self.rb.show()
    
    def create_vertex_marker(self, pnt):
        self.vm = QgsVertexMarker(self.canvas)
        self.vm.setCenter(pnt) # pnt is QgsPointXY
        self.vm.setColor(QColor('Black'))
        self.vm.setIconType(QgsVertexMarker.ICON_BOX)
        self.vm.setIconSize(15)
        self.vm.setPenWidth(1)
        
    def transform_point(self, src, dest, point):
        # point is QgsPointXY
        xform = QgsCoordinateTransform(src, dest, QgsProject.instance())
        transformed_point = xform.transform(point)
        return transformed_point
    
        
    def deactivate(self):
        self.clean_up()
        self.iface.currentLayerChanged.disconnect(self.active_changed)
            
    def clean_up(self):
        if self.vm:
            self.canvas.scene().removeItem(self.vm)
        if self.rb:
            self.rb.reset()
        
t = customMoveTool(iface)
iface.mapCanvas().setMapTool(t)

An example of it working is shown in the screencast below:

enter image description here

Now, if you wanted to use something like this in your plugin, you could add this class definition in your main plugin.py file below your plugin class, minus the last two lines.

Then inside your plugin class you could create an instance of the custom map tool class (e.g in the __init__() method) like this:

self.myMapTool = customMoveTool(self.iface)

Then when you want to activate it in your plugin, just call:

self.iface.mapCanvas().setMapTool(self.myMapTool)

Naturally, you will also need to add any additional classes/modules used here to your plugin's import statements.

Edit:

In comments section you have asked how to update the attribute columns with new geometry values after moving points with the map tool.

Again, the simple/manual answer is to use the field calculator and update those fields with the expressions '$x' and '$y'.

However, I guess you mean programmatically from within the script. In that case, please see the updated code below. You will see I have added a method called update_geometry_attributes(). We can connect this to the editingStopped() signal of the active layer.

This update_geometry_attributes() method looks up field indexes for columns called 'x' and 'y'. If your geometry columns are named differently make sure you edit the code to match your actual field names.

Please note also, a couple of additions in the active_changed() method. We must ensure that when the active layer is changed, we disconnect the signal, set the self.layer object to the new active layer, set the new layer to the map tool and then reconnect the slot to the editingStopped() signal of the new layer.

Updated script:

class customMoveTool(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.msg = QMessageBox()
        self.msg.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
        self.msg.setDefaultButton(QMessageBox.Ok)
        
        self.vm = None #Vertex Marker
        self.rb = None #Rubber Band
        self.first_click = True
        self.fid = None
        
        self.layer.editingStopped.connect(self.update_geometry_attributes)
        
    def active_changed(self, layer):
        if layer.type() == QgsMapLayerType.VectorLayer and layer.isSpatial():
            self.layer.editingStopped.disconnect(self.update_geometry_attributes)
            self.layer = layer
            self.setLayer(self.layer)
            self.layer.editingStopped.connect(self.update_geometry_attributes)
            
    def canvasReleaseEvent(self, event):
        if QgsProject.instance().crs() != self.layer.crs():
            click_point = self.transform_point(QgsProject.instance().crs(),
                                                self.layer.crs(),
                                                QgsPointXY(event.mapPoint()))
        else:
            click_point = QgsPointXY(event.mapPoint())
        if self.layer.geometryType() != QgsWkbTypes.PointGeometry:
            self.iface.messageBar().pushMessage('Please select a point layer', Qgis.Info, 2)
            return
        if self.first_click:
            found_features = self.identify(event.x(), event.y(), [self.layer], QgsMapToolIdentify.ActiveLayer)
            #Create a vertex marker around the identified point
            if found_features:
                if not self.layer.isEditable():
                    self.msg.setText('Layer is not editable\nToggle edit mode?')
                    res = self.msg.exec_()
                    if res == QMessageBox.Ok:
                        self.layer.startEditing()
                    elif res == QMessageBox.Cancel:
                        return
                feat = found_features[0].mFeature
                self.fid = feat.id()
                ft_geom = feat.geometry()
                if QgsProject.instance().crs() != self.layer.crs():
                    f_point = self.transform_point(self.layer.crs(),
                                                    QgsProject.instance().crs(),
                                                    ft_geom.asPoint())
                else:
                    f_point = ft_geom.asPoint()
                self.create_vertex_marker(f_point)
                self.first_click = False
                return
        elif not self.first_click:
            self.clean_up()
            new_geom = QgsGeometry().fromPointXY(click_point)
            success = self.layer.changeGeometry(self.fid, new_geom)
#            print(success)
            self.layer.triggerRepaint()
            self.first_click = True
            return
    
    def canvasMoveEvent(self, event):
        if self.rb:
            self.rb.reset()
        if not self.first_click:
            self.rb = QgsRubberBand(self.canvas, QgsWkbTypes.PointGeometry)
            self.rb.setColor(QColor('green'))
            self.rb.setIcon(QgsRubberBand.ICON_CIRCLE)
            self.rb.setIconSize(10)
            geom = QgsGeometry().fromPointXY(QgsPointXY(event.mapPoint()))
            self.rb.setToGeometry(geom)
            self.rb.show()
    
    def create_vertex_marker(self, pnt):
        self.vm = QgsVertexMarker(self.canvas)
        self.vm.setCenter(pnt) # pnt is QgsPointXY
        self.vm.setColor(QColor('Black'))
        self.vm.setIconType(QgsVertexMarker.ICON_BOX)
        self.vm.setIconSize(15)
        self.vm.setPenWidth(1)
        
    def transform_point(self, src, dest, point):
        # point is QgsPointXY
        xform = QgsCoordinateTransform(src, dest, QgsProject.instance())
        transformed_point = xform.transform(point)
        return transformed_point
    
    def update_geometry_attributes(self):
        '''Note: geometry field names are x & y
        You can change these accordingly'''
#        print('geom_updated!')
        x_fld_idx = self.layer.fields().lookupField('x')
        y_fld_idx = self.layer.fields().lookupField('y')
        if x_fld_idx != -1 and y_fld_idx != -1:
            for f in self.layer.getFeatures():
                self.layer.dataProvider().changeAttributeValues({f.id(): {x_fld_idx: f.geometry().asPoint().x(),
                                                            y_fld_idx: f.geometry().asPoint().y()}})
        
    def deactivate(self):
        self.clean_up()
        self.iface.currentLayerChanged.disconnect(self.active_changed)
            
    def clean_up(self):
        if self.vm:
            self.canvas.scene().removeItem(self.vm)
        if self.rb:
            self.rb.reset()
        
t = customMoveTool(iface)
iface.mapCanvas().setMapTool(t)
2
  • Thank you so much ben, this code turned out out to be very successful , i do have a question , how would i update the attribute table for this point layer to reflect the changes made by the cursor?
    – prashant
    Commented Sep 14, 2021 at 20:42
  • @prashant, I have updated my answer to achieve your request. You are welcome for the help, but I would really appreciate it if you would read the following information about upvoting and accepting answers: gis.stackexchange.com/help/someone-answers as I have put a lot of time and effort into answering your questions (a couple of them!)
    – Ben W
    Commented Sep 16, 2021 at 4:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.