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