This blog post is a QGIS plugin tutorial for beginners. It was written to support a workshop we ran for the Scottish QGIS user group here in the UK and aims to be a simple step-by-step guide.
In this tutorial you will develop your first QGIS plugin - a Map Tool for selecting the closest feature within multiple loaded vector layers. Knowledge of Python is recommended but not required.
Before we get started let’s look at where we’re going.
We will develop a plugin that implements a new QGIS Map Tool. The Identify Features and Pan Map tools are both examples of QGIS Map Tools. A Map Tool is a tool which performs an action when used with the map canvas.
We will create a new Select Nearest Feature Map Tool which will sit in the plugins toolbar.
Our Select Nearest Feature Map Tool will allow the user to select the feature nearest a mouse click on the canvas. For example, clicking here:
would select the following polygon:
Before getting started:
The QGIS Plugin Builder plugin was used to create a base plugin which we’ll modify to fit our requirements.
This base plugin can be found in the zip file mentioned above under code/01__Empty Plugin/NearestFeature
code/01__Empty Plugin contains a batch file install.bat that can be used to copy the plugin into your QGIS plugins folder, making it available to QGIS.
Let’s now load and run this simple base plugin in QGIS.
A new action should now be visible in the plugins toolbar which opens the following dialog:
When activated, our plugin currently shows a simple dialog (functionality provided by the Plugin Builder plugin. We’re going to adapt it to instead activate a Map Tool.
A basic Map Tool is included within the zip file mentioned above. It can be found in nearest_feature_map_tool.py in the Additional Files folder.
gray
) below link to relevant parts of the API docs. Those links will open in a dedicated browser tab.nearest_feature_tool.py defines a new NearestFeatureMapTool
class (line 28) which inherits (is based on) QgsMapTool
, the QGIS Map Tool class. Its __init__()
method expects to be passed a reference to a QgsMapCanvas
. This canvas reference is passed to the constructor of the underlying QgsMapTool
class on line 32 and then stored on line 33 for later use. The QGIS API documentation describes the functionality made available by QgsMapTool
.
On line 34 we define a simple, different-looking cursor (a QCursor
based on Qt.CrossCursor
) later used to indicate that the Map Tool is active.
Our class definition features a method called activate()
. Notice the API documentation for QgsMapTool
already defines a method with the same name. Any methods defined as virtual methods in the API documentation can be overwritten or redefined as they have been within this file. Here we have overwritten the default implementation of activate()
.
The activate()
method is called when the tool is activated. The new cursor based on Qt.CrossCursor
defined above is set with a call to QgsMapCanvas.setCursor()
.
For the moment, when activated, our Map Tool would simply change the cursor style - that’s all.
Great - next we’ll get our plugin to use the new Map Tool.
In this section we will modify the plugin to make use of our new Map Tool.
We need to first import the NearestFeatureMapTool
class before we can use it.
os.path
is imported:from nearest_feature_map_tool import NearestFeatureMapTool
Next we will create a new instance of the NearestFeatureMapTool
class and store a reference to it in self.nearestFeatureMapTool
.
initGui()
method just before the call to self.add_action()
taking care to ensure the indentation is correct:# Create a new NearestFeatureMapTool and keep reference
self.nearestFeatureMapTool = NearestFeatureMapTool(self.iface.mapCanvas())
Notice that a reference to the map canvas has been passed when creating the new NearestFeatureMapTool instance.
The run()
method is called when our plugin is called by the user. It’s currently used to show the dialog we saw previously. Let’s overwrite its current implementation with the following:
# Simply activate our tool
self.iface.mapCanvas().setMapTool(self.nearestFeatureMapTool)
The QGIS map canvas (QgsMapCanvas
class) provides the setMapTool()
method for setting map tools. This method takes a reference to the new map tool, in this case a reference to a NearestFeatureMapTool
.
To ensure that we leave things in a clean state when the plugin is unloaded (or reloaded) we should also ensure the Map Tool is unset when the plugin’s unload()
method is called.
unload()
method:# Unset the map tool in case it's set
self.iface.mapCanvas().unsetMapTool(self.nearestFeatureMapTool)
Now let’s see the new map tool in action.
When passing the mouse over the map canvas the cursor should now be shown as a simple cursor resembling a plus sign. Congratulations - the Map Tool is being activated.
When you use the Identify Features Map Tool you’ll notice that its button remains depressed when the tool is in use. The button for our map tool does not yet act in this way. Let’s fix that.
The action (QAction
) associated with our plugin is defined in the initGui()
method with a call to self.add_action()
.
self.add_action()
actually returns a reference to the new action that’s been added. We’ll make use of this behaviour to make the action / button associated with our Map Tool toggleable (checkable).
add_action()
as follows:action = self.add_action(
icon_path,
text=self.tr(u'Select nearest feature.'),
callback=self.run,
parent=self.iface.mainWindow())
action.setCheckable(True)
We now use the reference to the new action to make it checkable.
The QgsMapTool
class has a setAction()
method which can be used to associate a QAction
with the Map Tool. This allows the Map Tool to handle making the associated button look pressed.
initGui()
:self.nearestFeatureMapTool.setAction(action)
The button should now remain pressed, indicating that the tool is in use.
The Nearest Feature button should now appear unpressed.
The QgsMapTool
class has a number of methods for handling user events such as mouse clicks and movement. We will override the canvasReleaseEvent()
method to implement the search for the closest feature. canvasReleaseEvent()
is called whenever the user clicks on the map canvas and is passed a QMouseEvent
as an argument.
We will now write some functionality which:
def canvasReleaseEvent(self, mouseEvent):
"""
Each time the mouse is clicked on the map canvas, perform
the following tasks:
Loop through all visible vector layers and for each:
Ensure no features are selected
Determine the distance of the closes feature in the layer to the mouse click
Keep track of the layer id and id of the closest feature
Select the id of the closes feature
"""
layerData = []
for layer in self.canvas.layers():
if layer.type() != QgsMapLayer.VectorLayer:
# Ignore this layer as it's not a vector
continue
if layer.featureCount() == 0:
# There are no features - skip
continue
layer.removeSelection()
The layers()
method of QgsMapCanvas
(stored earlier in self.canvas
) returns a list of QgsMapLayer
. These are references to all visible layers and could represent vector layers, raster layers or even plugin layers.
We use the type()
and featureCount()
methods to skip non-vector layers and empty vector layers.
Finally we use the layer’s removeSelection()
method to clear any existing selection. layerData
is a list that we’ll use in a moment.
Our plugin now clears the selection in all visible vector layers.
We now need access to each feature and its geometry to determine its distance from the mouse click.
canvasReleaseEvent()
within the loop over layers:# Determine the location of the click in real-world coords
layerPoint = self.toLayerCoordinates( layer, mouseEvent.pos() )
shortestDistance = float("inf")
closestFeatureId = -1
# Loop through all features in the layer
for f in layer.getFeatures():
dist = f.geometry().distance( QgsGeometry.fromPoint( layerPoint) )
if dist < shortestDistance:
shortestDistance = dist
closestFeatureId = f.id()
info = (layer, closestFeatureId, shortestDistance)
layerData.append(info)
The mouse click event (a QMouseEvent
) is stored in mouseEvent
. Its pos()
method returns a QPoint
describing the position of the mouse click relative to the map canvas (x and y pixel coordinates). To calculate its distance to each feature we’ll need to first convert the mouse click position into real world (layer) coordinates. This can be done using a call to QgsMapTool.toLayerCoordinates()
which automatically deals with on-the-fly projection and returns a QPoint
in layer coordinates.
The features of a vector layer can be accessed using the layer’s getFeatures()
method which returns (by default) a list of all QgsFeature
in the layer that we can iterate over using a simple loop.
With access to features we can easily gain access to geometry using QgsFeature.geometry()
. The QgsGeometry
class has a number of spatial relationship methods including distance()
which returns the distance to a second QgsGeometry
passed as an argument.
In the code above we loop over all features, keeping track of the feature id of the closest feature using QgsFeature.id()
. The shortest distance and closest feature id are stored in shortestDistance
and closestFeature
. When we are finished iterating through all the features in this layer, we store a note of the layer, its closest feature id and associated distance into layerData
.
Note that we convert layerPoint
(a QgsPoint
) into a QgsGeometry
so we can use it directly in spatial relationship operations such as QgsGeometry.distance()
.
We’re almost done. At this point layerData
is a list of tuples, one for each vector layer containing:
Now we can simply sort layerData
by distance (its 3rd column) and make a selection based on the layer and feature in the first row of layerData
.
canvasReleaseEvent()
outside the outer for loop:if not len(layerData) > 0:
# Looks like no vector layers were found - do nothing
return
# Sort the layer information by shortest distance
layerData.sort( key=lambda element: element[2] )
# Select the closest feature
layerWithClosestFeature, closestFeatureId, shortestDistance = layerData[0]
layerWithClosestFeature.select( closestFeatureId )
The code above returns early if no workable vector layers were found. It sorts layerData
(the list of tuples) by the 3rd element (the distance).
The code then calls QgsVectorLayer.select()
to select the closest feature by its feature id.
The plugin should now be finished.
Within this tutorial we’ve worked briefly with the following parts of the QGIS API:
Hopefully this has been a useful tutorial. Please feel free to contact us with any specific questions.