Additional PyQGIS functions¶
Below are two functions you can use for challenge, inspiration, or fun when further diving into PyQGIS. Simply copy the code to QGIS’s code editor and you’re good to go. Both of the functions have additional coding challenges below them.
Buffer Party¶
This function takes the currently active QgsVectorLayer and creates buffers of random size around each individual feature. The user can define the min and max size of the buffers when calling the function.
import random
def bufferParty(min_length=1, max_length=100):
"""This function takes the currently active (vector) layer, loops through its
features and for each geometry creates a buffer of random size. The size
falls within the parameters given. A new vector layer is created from these
and added to the current project."""
original_layer = iface.activeLayer()
buffered_feat_list = []
# looping through features of the active layer
for id, feat in enumerate(original_layer.getFeatures()):
# original geometry
geometry = feat.geometry()
# random integer within the defined values
buffer_radius = random.randint(min_length, max_length)
# create the buffered geometry. The other integer is the number
# of segments
buffer = geometry.buffer(buffer_radius, 16)
# creating a new feature that shares the same id number but new geometry
feat = QgsFeature(id)
feat.setGeometry(buffer)
# adding all the features to a list
buffered_feat_list.append(feat)
# making sure the CRS is the same in both layers
layer_crs = original_layer.sourceCrs().toWkt()
# creating the new vector layer as a temporary (memory) layer
buff_layer = QgsVectorLayer('Polygon?crs='+layer_crs, "Buffered "+ original_layer.sourceName(), "memory")
# adding all the features to it
buff_layer.dataProvider().addFeatures(buffered_feat_list)
#check the layer is valid
if buff_layer.isValid():
# inserting the new layer to the project
QgsProject.instance().addMapLayer(buff_layer)
else:
print("faulty layer")
# call the function
bufferParty(min_length=10, max_length=1000)
Tasks
Can you think of a way to check that the current layer is indeed a vector layer and not raster or empty?
Can you think of a way keep the original attributes in the new layer as well? Check out the documentation for the relevant classes, e.g. QgsFeature.
Polygon drawing window¶
This neat function throws a new window in which the user may freely draw by clicking on the canvas. When user closes the window, the map points are printed in console. Consists of two classes with multiple methods: the window itself and the polygon drawing tool.
class drawWindow(QMainWindow):
def __init__(self):
"""Initializing the necessary resources."""
QMainWindow.__init__(self)
# creating map canvas, which draws the maplayers
# setting up features like canvas color
self.canvas = QgsMapCanvas()
self.canvas.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.canvas.setCanvasColor(Qt.white)
self.canvas.enableAntiAliasing(True)
# Qmainwindow requires a central widget. Canvas is placed
self.setCentralWidget(self.canvas)
# creating each desired action
self.actionPan = QAction("Pan tool", self)
self.actionDraw = QAction("Polygon tool", self)
self.actionConnect = QAction("Connect polygon", self)
self.actionClear = QAction("Clear", self)
self.actionClose = QAction("Close", self)
# these two function as on/off. the rest are clickable
self.actionPan.setCheckable(True)
self.actionDraw.setCheckable(True)
# when actions are clicked, do corresponding function
self.actionPan.triggered.connect(self.pan)
self.actionDraw.triggered.connect(self.draw)
self.actionClear.triggered.connect(self.clear)
self.actionConnect.triggered.connect(self.connect)
self.actionClose.triggered.connect(self.close)
# toolbar at the top of the screen: houses actions as buttons
self.toolbar = self.addToolBar("Canvas actions")
# ensure user can't close the toolbar
self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
self.toolbar.setMovable(False)
# change order here to change their placement on toolbar
self.toolbar.addAction(self.actionPan)
self.toolbar.addAction(self.actionDraw)
self.toolbar.addAction(self.actionConnect)
self.toolbar.addAction(self.actionClear)
self.toolbar.addAction(self.actionClose)
# link action to premade map tool
self.toolPan = QgsMapToolPan(self.canvas)
self.toolPan.setAction(self.actionPan)
# And the draw tool created below
self.toolDraw = PolygonMapTool(self.canvas)
self.toolDraw.setAction(self.actionDraw)
# set draw tool by default
self.draw()
def pan(self):
"""Simply activates pan tool"""
self.canvas.setMapTool(self.toolPan)
# make sure the other button isn't checked to avoid confusion
self.actionDraw.setChecked(False)
def draw(self):
"""Activates draw tool"""
self.canvas.setMapTool(self.toolDraw)
self.actionPan.setChecked(False)
def clear(self):
self.toolDraw.reset()
def connect(self):
"""Calls the polygon tool to connect an unconnected polygon"""
self.toolDraw.finishPolygon()
def showWindow(self):
"""Shows the map canvas: currently the canvas is empty,
but a reference layer can be added to it """
"""
Add code here if you want to add a layer to the window
self.canvas.setExtent(self.layer.extent())
self.canvas.setLayers([self.layer])
"""
self.show()
def closeEvent(self, event):
"""Activated anytime Mapwindow is closed either programmatically or
if the user finds some other way to close the window. Automatically
finishes the polygon if it's unconnected.
"""
self.toolDraw.finishPolygon()
points = self.getPolygon()
if points:
for point in points:
print(point)
QMainWindow.closeEvent(self, event)
def getPolygon(self):
return self.toolDraw.getPoints()
def getPolygonBbox(self):
return self.toolDraw.getPolyBbox()
class PolygonMapTool(QgsMapToolEmitPoint):
"""This class holds a map tool to create a polygon from points got by clicking
on the map window. Points are stored in a list of point geometries, which is when finishing the polygon"""
def __init__(self, canvas):
self.canvas = canvas
QgsMapToolEmitPoint.__init__(self, self.canvas)
# rubberband class gives the user visual feedback of the drawing
self.rubberBand = QgsRubberBand(self.canvas, True)
# setting up outline and fill color: both red
self.rubberBand.setColor(QColor(235,36,21))
# RGB color values, last value indicates transparency (0-255)
self.rubberBand.setFillColor(QColor(255,79,66,140))
self.rubberBand.setWidth(3)
self.points = []
# a flag indicating when a single polygon is finished
self.finished = False
self.poly_bbox = False
self.double_click_flag = False
self.reset()
def reset(self):
"""Empties the canvas and the points gathered thus far"""
self.rubberBand.reset(True)
self.poly_bbox = False
self.points.clear()
def keyPressEvent(self, e):
"""Pressing ESC resets the canvas. Pressing enter connects the polygon"""
if (e.key() == 16777216):
self.reset()
if (e.key() == 16777220):
self.finishPolygon()
def canvasDoubleClickEvent(self, e):
"""Finishes the polygon on double click"""
self.double_click_flag = True
self.finishPolygon()
def canvasReleaseEvent(self, e):
"""Activated when user clicks on the canvas. Gets coordinates, draws
them on the map and adds to the list of points."""
if self.double_click_flag:
self.double_click_flag = False
return
# if the finished flag is activated, the canvas will be reset
# for a new polygon
if self.finished:
self.reset()
self.finished = False
self.click_point = self.toMapCoordinates(e.pos())
self.rubberBand.addPoint(self.click_point, True)
self.points.append(self.click_point)
self.rubberBand.show()
def finishPolygon(self):
"""Activated by user or when the map window is closed without connecting
the polygon. Makes the polygon valid by making first and last point
the same. This is reflected visually. Up until now the user has been
drawing a line: a polygon is created and shown on the map."""
# nothing will happen if the code below has already been ran
if self.finished:
return
# connecting the polygon is valid if there's already at least 3 points
elif len(self.points)>2:
first_point = self.points[0]
self.points.append(first_point)
self.rubberBand.closePoints()
self.rubberBand.addPoint(first_point, True)
self.finished = True
# a polygon is created and added to the map for visual purposes
map_polygon = QgsGeometry.fromPolygonXY([self.points])
self.rubberBand.setToGeometry(map_polygon)
# get the bounding box of this new polygon
self.poly_bbox = self.rubberBand.asGeometry().boundingBox()
else:
self.finished = True
def getPoints(self):
"""Returns list of PointXY geometries, i.e. the polygon in list form"""
self.rubberBand.reset(True)
return self.points
myDrawWindow = drawWindow()
myDrawWindow.showWindow()
Tasks
The polygon is drawn in an eye catching red. Find where the color is defined, figure out how it works and change it to something else
Check out method showWindow in class drawWindow. There’s some code for adding a layer to the canvas. Find out how to add the active layer to the map canvas to use for drawing reference.