27

Is there a QGIS function or plugin to draw wiggly line?

I have used "Spline Tool" to manually draw some waves, but it is time consuming.

If possible, I would like to draw something like:

enter image description here

Inkscape Function Plotter (sin(x) curve).

3
  • Really interesting question! Are you thinking about a tool for instantly drawing the lines (i.e. like when you draw a simple line feature using the mouse), or maybe a way to get this result starting from coordinates as input (eventually from points, lines or polygons)? – mgri May 26 '17 at 12:39
  • 1
    @mgri Thanks for your comment. I expect to use this line to mark a boundary with some uncertainties (like a fluctuating shoreline), so primary idea was to convert a polyline (through pre-defined coordinates) into wiggles. But the idea of instantly drawing this type of line does sound attractive, too. – Kazuhito May 26 '17 at 13:04
  • Please, see if my solution helps. I didn't test it widely, so please let me know if something goes wrong (I can't do it right now, but I will probably edit my answer with more information). – mgri May 26 '17 at 18:35
19

I propose a solution using PyQGIS. It should work both for Linestring and MultiLineString layers.

This solution is based on the creation of semicircular rings, so you need to set a value for the diameter (i.e. the step variable in the code below). The step you choose won't be the real step used because it is adjusted on the basis of the line length (but it would be really similar to the value initially set). You need to do some attempts before finding the best value for the step variable.

The code also requires a second (optional) parameter (called crv_angle), which helps for decreasing or increasing the curvature for the rings (I performed a few tests for it, so I suggest leaving 45 degrees as default angle since it would lead to real circular rings).

You only need to run this code from the Python Console:

from math import sin, cos, radians

step = 3 # choose the proper value (e.g. meters or degrees) with reference to the CRS used
crv_angle = 45 # degrees

def segment(polyline):
    for x in range(0, len(polyline) - 1):
        first_point = polyline[x]
        second_point = polyline[x +1]
        seg = QgsGeometry.fromPolyline([first_point, second_point])
        tmp_azim = first_point.azimuth(second_point)
        len_feat = seg.length()
        parts = int(len_feat/step)
        real_step = len_feat/parts # this is the real step applied

        points = []
        current = 0
        up = True

        while current < len_feat:
            if up:
                round_angle = radians(90 - (tmp_azim - crv_angle))
                up = False
            else:
                round_angle = radians(90 - (tmp_azim + crv_angle))
                up = True
            first = seg.interpolate(current)
            coord_x, coord_y = (first.asPoint().x(), first.asPoint().y())
            p1=QgsPointV2(coord_x, coord_y)
            dist_x, dist_y = ((real_step*sin(rad_crv_angle))* cos(round_angle), (real_step*sin(rad_crv_angle)) * sin(round_angle))
            p2 = QgsPointV2(coord_x + dist_x, coord_y + dist_y)
            points.extend([p1, p2])
            current += real_step

        second = seg.interpolate(current + real_step)
        p3=QgsPointV2(second.asPoint().x(), second.asPoint().y())
        points.append(p3)

        circularRing = QgsCircularStringV2()
        circularRing.setPoints(points) # set points for circular rings
        fet = QgsFeature()
        fet.setGeometry(QgsGeometry(circularRing))
        prov.addFeatures([fet])

layer = iface.activeLayer() # load the input layer as you want
crs = layer.crs().toWkt()
rad_crv_angle = radians(crv_angle)

# Create the output layer
outLayer = QgsVectorLayer('Linestring?crs='+ crs, 'wiggly_line' , 'memory')
prov = outLayer.dataProvider()
fields = layer.pendingFields()
prov.addAttributes(fields)
outLayer.updateFields()

for feat in layer.getFeatures():
    geom = feat.geometry()
    polyline = geom.asPolyline()
    segment(polyline)

# Add the layer to the Layers panel
QgsMapLayerRegistry.instance().addMapLayer(outLayer)

and it will create a new line memory layer with the expected result:

enter image description here

5
  • Wow! I cannot stop playing with this, so gorgeous. On top of it the output itself can be used for further operations like buffer. Thank you @mgri. One last thing, I sometimes see small circles appear at or near vertices. Is this avoidable? I can remove them by erasing the center node of such circle by Node Tool (so it is not a big deal). – Kazuhito May 27 '17 at 5:20
  • 1
    @Kazuhito please, see my edited code. It seems there was something wrong with the discretization and I hope it is fixed by now. I performed several tests and it seems working well (the code is also more readable). – mgri May 27 '17 at 14:16
  • 1
    Thanks so much @mgri. There are very minor circles, for which I am sorry I cannot clearly explain how these appear. Most vertices are now "circle-free". Only distinction I noticed was such node (with circle) had shown a smaller "r-value" on Vertex Editor Table of Node Tool. This is easily manageable and far better than I had anticipated. Thank you again. Let me accept your answer as solution. – Kazuhito May 27 '17 at 17:53
  • 1
    Thanks, @Kazuhito. I'm sorry for having not created a perfect solution. However, I hope you will enjoy it (otherwise, you may also send me a sample shapefile and I will try to fix the issue). If I find a more efficient way, I will post it! – mgri May 27 '17 at 22:36
  • 1
    Many thanks @mgri. If I find any distinctive pattern in appearance of circles I will update you with reproducible example. This is so much enjoyable (and its output is beautiful)! – Kazuhito May 28 '17 at 1:59
24

Short answer: you can get it using a custom SVG. See bottom of this post for one.

Long answer:

I believe it is better to represent it than to modify the line geometry. Should you want to move an edge or do other actions on the geometry, it would be a nightmare to manage if the wiggles are part of the geometry instead of just a representation of a straight line.

You can play with the style marker line. There is a way to easily get close to what you need, and with a bit more effort it is likely possible to get it exactly. enter image description here

To get this, you would style the line with two Marker lines. Each Marker line is made of a Simple Marker, the half-circle. The 1st one is rotated by 180. Both are set to transparent.

On the Marker line, you instruct one of them to be offset so the two symbols are not drawn in front of each other, but side by side. If you use offest = 1/2 * interval size, the output will be a sinusoidal curve. I suggest you play with the interval size, offset and symbol sizes.

The main limitation with this approach is the diameter line of the half circles, which sums to the original line. If your background is white (or any plain color), you could add a 3rd simple line using the background color.

enter image description here

enter image description here

enter image description here

enter image description here

** EDIT **

Another option to get rid of the center line is to create a new SVG symbol. I modified the half-curve, only living the rounded part. It works, though a 1/2 ellipse might be more appealing. The screenshot was done using symbol size 10, interval 4, offset 2.

enter image description here

save the code below in a file half_circle_line.svg and make sure the path to the svg is set in QGIS // Settings / Options / System / SVG Paths

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="11.2889mm" height="11.2889mm"
 viewBox="0 0 32 32"
 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"  version="1.2" baseProfile="tiny">
<title>Qt Svg Document</title>
<desc>Generated with Qt</desc>
<defs>
</defs>
<g fill="none" stroke="black" stroke-width="1" fill-rule="evenodd" stroke-linecap="square" stroke-linejoin="bevel" >

<g fill="#ffffff" fill-opacity="0" stroke="#000000" stroke-opacity="1" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" transform="matrix(1,0,0,1,0,0)"
font-family="MS Shell Dlg 2" font-size="8.25" font-weight="400" font-style="normal" 
>
<path vector-effect="non-scaling-stroke" fill-rule="evenodd" d="M19.1181,16 C19.1181,16 19.1181,14.2779 17.7221,12.8819 16,12.8819 C14.2779,12.8819 12.8819,14.2779 12.8819,16"/>
</g>
</g>
</svg>
10
  • Nice idea. Currently struggling with persistent "center line"... :\ – Kazuhito May 26 '17 at 14:57
  • +1 from me too. In the meanwhile, I'm trying to thinking to a PyQGIS solution. @Kazuhito, please let me know if this would be enough for you or if you prefer a physical solution. – mgri May 26 '17 at 14:58
  • @mgri With this answer I have a "chain" not "wave" now (trying to modify it). Would really appreciate to have a physical solution. – Kazuhito May 26 '17 at 15:05
  • JGH Do you potentially have an idea to remove the "center line", other than masking it by a white line (i.e.your bottom figure)? It looks as if it is fragmented. – Kazuhito May 26 '17 at 15:22
  • 1
    @Kazuhito - You can change the Pen style to No Pen :) – Joseph May 26 '17 at 15:27
11

Attention!

This answer is outdated. Please follow the link here: Custom QGIS linestyle expression function won't work with MultiLineStrings

In QGIS 3.10 it's possible to dynamically create zig-zag lines and wavelines with the help of the indispensable "geometry generator" and a custom Python expression function.

enter image description here

from qgis.core import qgsfunction,QgsExpressionContextUtils,QgsExpression,QgsProject,QgsPoint,QgsGeometry
@qgsfunction(args='auto', group='Custom', usesGeometry=False, referencedColumns=[])
def make_zigzagline(geom,dist,offset,feature,parent):
    """
    <style>
    span { color: red }

    </style>
    <h2>converts a linestring to a zig-zag line</h2>
    
    make_zigzagline(<span>geometry</span>,<span>distance(s)</span>,<span>offset</span>)<br/>

    <table>
        <tr><td><span>geometry</span></td><td>linestring geometry</td></tr>
        <tr><td><span>distance(s)</span></td><td>linear point distances (single number or a string of comma separated numbers)</td></tr>
        <tr><td><span>offset</span></td><td>perpendicular offset</td></tr>
    </table>
    <br/><br/>
    Examples:
    <ul>
        <li>make_zigzagline($geometry,'15,30',15) -> zig-zag line</li>
        <li>make_zigzagline($geometry,15,15) -> zig-zag line</li>
    </ul>

    Use smooth function to create wavelines:<br/><br/>
    Example:
    <ul><li>smooth(make_zigzagline($geometry,'15,30',15),3)</li></ul>
    """

    if not type(dist) is str:
        dist = str(dist)

    dist = [float(n) for n in dist.split(',')]
    l = geom.length()
    dist_sum = 0
    distances = []
    while dist_sum + round(sum(dist),2) < l:
        for d in dist:
            dist_sum += d
            distances.append(dist_sum)

    # interpolate points on linestring
    points2d = [(lambda g: (g.x(), g.y()))(geom.interpolate(d).asPoint()) for d in distances]
    vertices = geom.asPolyline()
    start = (vertices[0].x(),vertices[0].y())
    end = (vertices[-1].x(),vertices[-1].y())
    
    points2d.insert(0,start) # prepend start point
    points = [QgsPoint(start[0],start[1])]
    i = 0
    n = 0
    b = -90
    for point in points2d[1:]:
        pt1 = QgsPoint(points2d[i][0],points2d[i][1])
        pt2 = QgsPoint(point[0],point[1])
        a = pt1.azimuth(pt2) + b
        pt = pt2.project(offset, a)
        points.append(pt)
        i += 1
        n += 1
        if n == len(dist):
            n = 0
            b = -b
        
    points.append(QgsPoint(end[0],end[1])) # append end point
    return QgsGeometry.fromPolyline(points)

Load the code in the Python console and style your Linestring layer with a Geometry generator:

enter image description here

9
  • Fantastic! Thanks @christoph – Kazuhito Mar 24 '20 at 8:37
  • @christoph Thanks for this great solution. I am trying to get it to work with polygons as well and created a new question here gis.stackexchange.com/q/367422/94350 – Baswein Jul 10 '20 at 22:11
  • @christoph, thanks for this very nice solution. However, QGIS 3.14 started to crash every time I tried to open the field calculator. After removing the custom function, the crash stopped. Do you think it is related to QGIS version? – Maxime Riou Jul 28 '20 at 10:50
  • 1
    @Nico, did you try my latest version? gis.stackexchange.com/questions/376411/… – christoph Nov 30 '20 at 12:26
  • 1
    @Nico, to create a new layer, you can use the Processing Tool Geometry by expression – christoph Nov 30 '20 at 12:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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