PyQGIS Single Sided Buffers – Generating Single Sided Buffers on the Same Side of Lines

bufferpyqgis

I have a line and a point layers and trying to find points 100m to the left of each line.

The image below shows the single-sided buffer result. As can clearly be seen, one of the buffers is generated on the right side. But of course, it is the left side of the line direction.

enter image description here

How can I make sure the buffers are generated on the same side?

Here is the script I want to improve:

point_lyr = QgsProject.instance().mapLayersByName("POINTS")[0]
line_lyr = QgsProject.instance().mapLayersByName("LINES")[0]

i = 1
interval = 100 # meter

# Find all points to the left of each lines
for l in line_lyr.getFeatures():
    buffer = l.geometry().singleSidedBuffer(interval, 1, Qgis.BufferSide.Left)
    
    # find the points in the buffer
   
    # assign the lane number (i)   

    i += 1

Best Answer

You can use the fact that all lines are parallel and calculate the angle of the lines. Define a main angle (e.g. from the first line), and if a line does not fit that main angle draw the buffer on the otherside.

lines = iface.activeLayer()
interval = 250
for i,line in enumerate(lines.getFeatures()):
    if i == 0:
        myangle = line.geometry().angleAtVertex(0)
    if line.geometry().angleAtVertex(0) == myangle:
        buffer = line.geometry().singleSidedBuffer(interval, 1, Qgis.BufferSide.Left)
    else: 
        buffer = line.geometry().singleSidedBuffer(interval, 1, Qgis.BufferSide.Right)
    

You could add a tolerance if they are not perfectly parallel. E.g. by using math.isclose() with a relative or absolute tolerance:

import math
lines = iface.activeLayer()
interval = 250
tolerance = 11 # absolute, degrees for easier use
# you can also use a relative tolerance, e.g. math.isclose(a,b,rel_tol = tolerance)
for i,line in enumerate(lines.getFeatures()):
    if i == 0:
        myangle = math.degrees(line.geometry().angleAtVertex(0))
    if math.isclose(math.degrees(line.geometry().angleAtVertex(0)), myangle, abs_tol = tolerance):
        buffer = line.geometry().singleSidedBuffer(interval, 1, Qgis.BufferSide.Left)
    else: 
        buffer = line.geometry().singleSidedBuffer(interval, 1, Qgis.BufferSide.Right)

Or do some more fancy stuff like:

import math
import random
lines = iface.activeLayer()
buffers = QgsVectorLayer('Polygon?crs={}'.format(lines.crs().authid()), 'Buffers' , "memory")
buffers.dataProvider().addAttributes([QgsField("sourcefeatid", QVariant.Int), QgsField("lineangle", QVariant.Double), QgsField("mainaingle", QVariant.Double), QgsField("parallel", QVariant.String), QgsField("bufferside", QVariant.String)])
buffersize = 250 # in CRS units
tolerance = 22.5 # absolute; degrees for easier use; you can also use a relative tolerance, e.g. math.isclose(a,b,rel_tol = tolerance)
mainangle = None # define your mainangle here
bufferdirection = None # define a main-direction
# or do some fancy stuff to determine the bufferside
if not bufferdirection or bufferdirection not in ['west','east']:
    bufferdirection = random.choice(['west','east'])

for i,line in enumerate(lines.getFeatures()):
    polyline = line.geometry().asPolyline()
    startpoint, endpoint = QgsPoint(polyline[0]), QgsPoint(polyline[-1])
    
    if i == 0:
        if mainangle is None:
            mainangle = math.degrees(QgsGeometryUtils.lineAngle(startpoint.x(), startpoint.y(), endpoint.x(), endpoint.y()))
        reversemainangle = (mainangle+180)%360
        
    lineangle = math.degrees(QgsGeometryUtils.lineAngle(startpoint.x(), startpoint.y(), endpoint.x(), endpoint.y()))
    print(lineangle)
    
    if bufferdirection == 'west':
        if 0 < lineangle <= 180:
            print('Buffer left')
            bufferside = Qgis.BufferSide.Left
        else:
            print('Buffer right')
            bufferside = Qgis.BufferSide.Right
    elif bufferdirection == 'east':
        if 0 < lineangle <= 180:
            print('Buffer right')
            bufferside = Qgis.BufferSide.Right
        else:
            print('Buffer left')
            bufferside = Qgis.BufferSide.Left
    
    drawbuffer = True
    if math.isclose(mainangle,lineangle,abs_tol=tolerance):
        parallelattr = 'parallel'
        print(parallelattr)
        buffer = line.geometry().singleSidedBuffer(buffersize, 1, bufferside)
    elif math.isclose(reversemainangle,lineangle,abs_tol=tolerance):
        parallelattr = 'reverse parallel'
        print(parallelattr)
        buffer = line.geometry().singleSidedBuffer(buffersize, 1, bufferside)
    else:
        parallelattr = 'not parallel'
        print(parallelattr)
        buffer = line.geometry().singleSidedBuffer(buffersize, 1, bufferside)
        drawbuffer = False
    
    if drawbuffer:
        with edit(buffers):
            newbuffer = QgsFeature()
            newbuffer.setAttributes([line.id(),lineangle,mainangle,parallelattr,str(bufferside)])
            newbuffer.setGeometry(buffer)
            buffers.dataProvider().addFeature(newbuffer)
QgsProject.instance().addMapLayer(buffers)
Related Question