7

I have a layer in QGIS with thousands of lines and I would need to sort out the curved lines and lines that are straight. Can I do this easily in QGIS?

4
  • 6
    Can you add a screenshot? What is your definition of straight and curved?
    – BERA
    May 27, 2021 at 6:51
  • Basically every line, that has two nodes, must be straight, while all the other most likely are not... so, you could simply look for the number of nodes/vertices. Or you could add a screenshot of your data.
    – Erik
    May 27, 2021 at 7:15
  • 1
    GIS vector data is not "curved" in a strict sense (like circle), its always vertex-based. So any complex line consists of small, straight segments. "Curved" in this context can only mean a certain definition of how two or more connected segments relate to each other - like angle/length. So you must provide a definition of what you consider to be "curved" for your purpose.
    – Babel
    May 27, 2021 at 7:32
  • 1
    @Erik There can be several points on a straight line, so the presence of three vertices is not always a sign of a curved line. May 27, 2021 at 7:37

5 Answers 5

8
  1. Use the simplify tool to remove unnecessary vertices that lie on a straight line as described in this answer.

  2. Using the field calculator, create a new field of type text with the following expression:

if( num_points( nodes_to_points($geometry, True) )> 2, 'Curved', 'Straight' )

Results: enter image description here

8

You can use pyqgis and compare each line's length to the distance between start and end points:

enter image description here

lyr = QgsProject.instance().mapLayersByName('Single parts')[0]

straight = [] #A list to hold feature ids of straight lines
curved = []

for f in lyr.getFeatures():
    line = f.geometry()
    linelength = line.length()
    vertices = [v for v in line.vertices()]
    shortest_line = QgsLineString(vertices[0], vertices[-1])
    
    if linelength > 1.03 * shortest_line.length(): #If the line length is longer than the shortest line + 3 % then it's curved. (I just chose 3 % subjectively)
        curved.append(f.id()) #Append id to list
    else:
        straight.append(f.id())

lyr.select(straight)

enter image description here

0
5

Given a curved line, you can apply the following expression to identify and visualize those sub-sections of the line that are (either absolute or more or less) straight; or: which ones are, to the contrary, the curved ones. Both "straight" or "curved" can be defined freely based on two parameters: dist and tolerance:

  • dist sets a distance. The line is than segmented in segments of this length
  • tolerance is based on the "straigtness"/"curvyness" of each of these segments.

Identify straight segements (red), generated with a length of 100 [meters] and tolerance of <5. The line is segmented in 100 m-segments. Only those segments are drawn in red if their accumulated difference of angles for 10 evenly distributed points in this segment is less than 5:

enter image description here

Highlight strongly curved segments with settings dist: 70, tolerance: >5: enter image description here

Identify spots where the line turns to a susbtantially new direction with settings dist: 150, tolerance: >60: enter image description here

Using this method to identify curves (red) of a road on an OpenStreetMap basemap with dist: 90, tolerance: >100 enter image description here

"Counting" u-turn like curves with dist: 20 and tolerance: >100: enter image description here

On the line layer, use this expression with geometry generator and adapt values for dist in line 3 (change the value of 150) and tolerance on line 6 (change >30 and keep the single quotes '). If satisfied with the result, you can create actual geometries form it using Geometry by expression (see below for a slightly simplified variant):

with_variable(
    'dist',
    150,
    with_variable (
        'tolerance',
        '>30',
        collect_geometries (
            array_foreach (
                generate_series (0, length($geometry)/@dist),
                if (
                    eval (
                        (abs((line_interpolate_angle( $geometry, @element*@dist + 0)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.1)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.1)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.2)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.2)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.3)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.3)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.4)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.4)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.5)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.5)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.6)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.6)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.7)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.7)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.8)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.8)-line_interpolate_angle( $geometry, @element*@dist +@dist*0.9)))+
                        abs((line_interpolate_angle( $geometry, @element*@dist +@dist*0.9)-line_interpolate_angle( $geometry, @element*@dist +@dist)))) 
                        || @tolerance
                    ),
                    line_substring( $geometry, @element*@dist, @element*@dist+@dist),
                    make_line (make_point (0,0),make_point (0,0))
                )
            )
        )
    )
)

How it works:

  1. Segment the line into segments of a freely selectable length with array_foreach () and generate_series()

  2. Each segment is sub-divede again in 10 equal parts (lines 12 to 21).

  3. At each of these points, the angle (tangent, using line_interpolate_angle()) is measured.

  4. The angle is compared with the one of the next part: subtract angle from the following part from the angle of the current part. If the line here is completely straight, it will be the same, thus the difference is 0. The larger the difference, the more the line deviates from a straight line.

  5. Use abs() to avoid negative values.

  6. Sum up all differences for the 10 parts of each segment.

  7. Compare the resulting value with a condition. To be able to use the condition on top of the expression as variable, I had to introduce it afterwards using an eval() statement: concatenate the resulting number with the string of the condition.

  8. If this condition is true, a geometry will be drawn for the current segment, based on the line_substring() function. Otherwise, an empty line is created.

  9. With collect_geometries(), the array of geometries is transfered to a geometry (line).


Variant

In the expression above, there is the repeating part (lines 12-21) starting with abs((line_interpolate_angle(.... To avoid repeating the same string again and again, you can create it as a variable and call it inside the eval() function. Create the repeating parts of the these repeating part as a string and concatenate it with the varying @element from an array_foreach() function that creates an individual line for each of the 10 sub-segments. The expression looks like this with the additional variable expression (lines 7-13) that is called in line 19:

with_variable(
    'dist',
    100,
    with_variable (
        'tolerance',
        '>30',
        with_variable(
            'expression',
            replace (
                array_to_string (
                    array_foreach (
                        generate_series (0,9),
                        'abs((line_interpolate_angle( $geometry, @element *@dist + @dist*'  || (@element/10)  || ' )-line_interpolate_angle( $geometry,  @element  *@dist +@dist*'  ||  (@element/10+0.1)  || ')))'  || if (@element <9, '+', ''))), '+,','+'),
            collect_geometries (
                array_foreach (
                    generate_series (0, length($geometry)/@dist),
                    if (
                        eval (
                            (@expression) || @tolerance
                        ),
                        line_substring( $geometry, @element*@dist, @element*@dist+@dist),
                        make_line (make_point (0,0),make_point (0,0))
                    )
                )
            )
        )
    )
)

Remark about the very conpets of "curved" and "straight":

What means "curved" and "straight" depends very much on the definition. Sould a zig-zag line witch angles of 90 degrees (like a stair) be considered curved or straight? Should the **whole line" be considered? Than only a mathematical line would fulfill this condition. Or should we go to identify straight segments that are part of the line? How long should these be?

As this solution and the real-world example (street extracted from OpenStreetMap: Nuova strada del Passo del San Gottardo) show, changing the input values (and thus the definiton), the result changes dramatically.

As the question did not provide any idea about how "curved" or "straight" should be defined in the given context, there are several great solutions here, each with a different approach. So in fact each solution is somehow an answer to a different definition.

This solution here tries to make definition flexible as you can define two parameters to adapt the definition to different needs and visualize the result.

0
4

If the curvature is only a true/false statement per feature, it could be resolved into a field value using an expression like:

if(round(area(oriented_bbox($geometry))) <> 0,'curved','straight')

The oriented bounding box for a curved polyline will have a nonzero area, contrary to straight lines with two or more aligned vertices, as illustrated below (oriented_bbox as symbology and the area of that polygon as label).

enter image description here

2

Create a segment from the first to last vertex and then for each interior point, determine its distance from this segment. Set a threshold. Below the threshold, the line is considered straight. Above the threshold it's curved.

There are probably simpler collinearity measures.

Not the answer you're looking for? Browse other questions tagged or ask your own question.