2

I made a custom map tool that can both drag and drop objects and select them.

It works well, the only problem is that the selection by rectangle gets features that are hidden by the layer style.

Selecting feature by rect:

enter image description here

But two features are selected:

enter image description here

How to select only those features that are visible on the canvas?

The full code of the tool that can be run in the Qgis python editor:

from qgis.gui import QgsMapTool, QgsMapMouseEvent
from qgis.core import QgsFeature, QgsGeometry, QgsPointXY
from qgis.PyQt import QtCore, QtWidgets


class DragAndDropFeatureTool(QgsMapTool):
    
     def __init__(self, canvas):
         super().__init__(canvas)
         self.canvas = canvas
         self.start_point = None
         self.end_point = None
         self.feature = None
         self.layer = None
        
         # Geometry projection transformation objects
         map_crs = iface.mapCanvas().mapSettings().destinationCrs() # project coordinate system
         lyr_crs = QgsCoordinateReferenceSystem(4326) # layer coordinate system (EPSG:4326)
         # geometry to map projection
         self.to_map_crs = QgsCoordinateTransform(lyr_crs, map_crs, QgsProject.instance())
         # geometry to layer projection
         self.to_lyr_crs = QgsCoordinateTransform(map_crs, lyr_crs, QgsProject.instance())
        
         # flag to check if the left mouse button was pressed
         self.is_left_button_pressed = False
         # create a rubber line object
         # to display the geometry of the dragged object on the canvas
         self.rubber_band = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry)
         self.rubber_band.setColor(QColor(255, 0, 0, 50))
         self.rubber_band.setWidth(2)
        
         # object geometry moved
         self.new_geometry = None

     # Mouse button pressed.
     def canvasPressEvent(self, event: QgsMapMouseEvent):
         self.layer = iface.activeLayer()
         if (self.layer.isEditable() and
                 self.is_layer_visible(self.layer) and
                 event.button() == Qt.LeftButton):
             point = self.to_lyr_crs.transform(event.mapPoint())
             self.feature = None
             result = QgsMapToolIdentifyFeature(self.canvas).identify(event.x(),
                 event.y(), [self.layer], QgsMapToolIdentify.TopDownStopAtFirst)
             # if the mouse button is pressed over the object
             if result:
                 feature = result[0].mFeature
                 self.feature = feature
                 self.start_point = point
                 self.is_left_button_pressed = True
                 # the geometry of the object on which the left mouse button is pressed
                 self.new_geometry = feature.geometry()
                 self.rubber_band.setToGeometry(feature.geometry(), self.layer)
                 # draw object geometry on canvas
                 self.rubber_band.show()
             # if the mouse button is pressed over an empty space
             else:
                 self.start_point = self.toMapCoordinates(event.pos())
                 self.end_point = self.start_point
                 self.is_left_button_pressed = True
    
     # The cursor moves across the screen.
     def canvasMoveEvent(self, event: QgsMapMouseEvent):
         # if the left mouse button is pressed
         # and the user drags the object (self.feature)
         if self.is_left_button_pressed and self.feature:
             point = self.to_lyr_crs.transform(event.mapPoint())
             # update the dragged object on the canvas
             delta_x = point.x() - self.start_point.x()
             delta_y = point.y() - self.start_point.y()
             geometry = self.feature.geometry()
             geometry.translate(delta_x, delta_y)
             self.rubber_band.setToGeometry(geometry, self.layer)
             self.new_geometry = geometry
         # if the left mouse button is pressed
         elif self.is_left_button_pressed:
             self.end_point = self.toMapCoordinates(event.pos())
             # draw a rectangle on the canvas
             self.showRect(self.start_point, self.end_point)
    
     # The mouse button is released.
     def canvasReleaseEvent(self, event: QgsMapMouseEvent):
         # if the left mouse button was released
         if event.button() == Qt.LeftButton:
             # if the user dragged the object (self.feature)
             # (or just clicked on the object)
             if self.feature:
                 # drag endpoint
                 end_point = self.to_lyr_crs.transform(event.mapPoint())
                 # set the geometry (self.new_geometry),
                 # created by the canvasMoveEvent function
                 self.layer.changeGeometry(self.feature.id(), self.new_geometry)
                 # select the moved object
                 self.layer.selectByIds([self.feature.id()])
             # if the user didn't drag the feature (self.feature = None)
             else:
                 # get the rectangle created from the click point and
                 # left mouse button release points
                 mouse_rect = self.rectangle()
                 # if the rectangle turned out (the user pressed and released
                 # mouse button at different points on the screen)
                 if mouse_rect:
                     modifiers = QtWidgets.QApplication.keyboardModifiers()
                     # if the Shift button was pressed when releasing
                     if modifiers == QtCore.Qt.ShiftModifier:
                         # add to the selection objects intersecting with the rectangle
                         self.layer.selectByRect(mouse_rect, QgsVectorLayer.AddToSelection)
                     # if the Control button was pressed when releasing
                     elif modifiers == QtCore.Qt.ControlModifier:
                         # subtract from the selection the objects intersecting with the rectangle
                         self.layer.selectByRect(mouse_rect, QgsVectorLayer.RemoveFromSelection)
                     else:
                         # select objects that intersect with the rectangle
                         self.layer.selectByRect(mouse_rect)
                 # if the rectangle is not received
                 # (user just clicked on the map in an empty space)
                 else:
                     # reset the old selection
                     self.layer.removeSelection()
             # toggle the flag, the left mouse button is no longer held down
             self.is_left_button_pressed = False
             self.rubber_band.reset() # remove rubber band from canvas
    
     def showRect(self, startPoint, endPoint):
         self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)
         if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
             return

         point1 = QgsPointXY(startPoint.x(), startPoint.y())
         point2 = QgsPointXY(startPoint.x(), endPoint.y())
         point3 = QgsPointXY(endPoint.x(), endPoint.y())
         point4 = QgsPointXY(endPoint.x(), startPoint.y())

         self.rubber_band.addPoint(point1, False)
         self.rubber_band.addPoint(point2, False)
         self.rubber_band.addPoint(point3, False)
         self.rubber_band.addPoint(point4, True) # true to update canvas
         self.rubber_band.show()

     def rectangle(self):
         if self.start_point is None or self.end_point is None:
             return None
         elif (self.start_point.x() == self.end_point.x() or \
               self.start_point.y() == self.end_point.y()):
             return None
         # in layer coordinates
         start_point = self.to_lyr_crs.transform(self.start_point)
         end_point = self.to_lyr_crs.transform(self.end_point)
         return QgsRectangle(start_point, end_point)
        
     def is_layer_visible(self, layer: QgsMapLayer):
         layer_tree_root = QgsProject.instance().layerTreeRoot()
         layer_tree_layer = layer_tree_root.findLayer(layer)
         return layer_tree_layer.isVisible()
    
     def deactivate(self):
         QgsMapTool.deactivate(self)
         self.deactivated.emit()

    
canvas = iface.mapCanvas()
move_tool = DragAndDropFeatureTool(canvas)
iface.mapCanvas().setMapTool(move_tool) 

Features selection happens in def canvasReleaseEvent (the layer must be editable).

1 Answer 1

4

Here is a solution based on this answer by Yoann Quenach de Quivillic.

You can add this function:

 def get_visible_selected_features(self):
     renderer = self.layer.renderer().clone()
     ctx = QgsRenderContext()
     renderer.startRender(ctx, QgsFields())
     for feature in self.layer.selectedFeatures():
         ctx.expressionContext().setFeature(feature)
         if renderer.willRenderFeature(feature, ctx):
             yield feature.id()
     renderer.stopRender(ctx)

I modified this line :

yield feature

To get feature id instead of features:

yield feature.id()

It creates a generator based on your selected features. It will only keep the visible features. It can be transformed to a list of id to make a new selection.

In your code you can add this line :

                     self.layer.selectByRect(mouse_rect, QgsVectorLayer.RemoveFromSelection)
                 else:
                     # select objects that intersect with the rectangle
                     self.layer.selectByRect(mouse_rect)

# LINE TO ADD
                  self.layer.selectByIds(list(self.get_visible_selected_features()))


             # if the rectangle is not received
             # (user just clicked on the map in an empty space)
             else:
                 # reset the old selection
                 self.layer.removeSelection()
0

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.