4

Update: November 2021 - It looks like an expression that solves this was added to QGIS https://github.com/qgis/QGIS/pull/45583

I would like to have wavy edges on a polygon similar to what @christoph shows in this answer for wavy lines. https://gis.stackexchange.com/a/354873/94350 I have gotten his great python expression function working well on lines but have been unsuccessful adapting it to polygons.

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)

As a bonus I would like to be able to vary the size of each wave to create something like this: wavy edge

5
  • 1
    Thanks for giving me credit! For a quick workaround you could convert your polygons to linestrings using expression functions in your geometry generator: smooth(make_zigzagline( geom_from_wkt(regexp_replace( geom_to_wkt( $geometry ),'^Polygon\\s*\(([^\)]+)\)','Linestring\\1')),'15,30',15)) ... a longterm solution needs a complete revision, which I will post after my vacation :-)
    – christoph
    Commented Jul 11, 2020 at 7:39
  • Thanks @christoph please ignore this and enjoy your vacation- You're suggestion worked but took forever but got me thinking and I worked out this that works well except at the corners. make_polygon(smooth(make_zigzagline(exterior_ring( $geometry),5,5),3))
    – Baswein
    Commented Jul 11, 2020 at 17:03
  • Hmm, „exterior_ring“ ... good point! ... how could I oversee this excellent function ;-)
    – christoph
    Commented Jul 13, 2020 at 16:54
  • 1
    I also got the corners working using a smooth on the initial geometry make_polygon(smooth(make_zigzagline(exterior_ring(smooth($geometry,1,.1)),3,2),3)) and with a buffer I can get a tree line or revision cloud style: gis.stackexchange.com/a/367489/94350. I am sure it would be better just using arcs but I don't know enough to do that.
    – Baswein
    Commented Jul 13, 2020 at 17:16
  • Marvelous... unfortunately I can‘t test it on my iPhone:-)
    – christoph
    Commented Jul 13, 2020 at 17:20

2 Answers 2

2

And here's another quick and dirty solution for custom line styles with even more styling options (COVID-19 aware!):

test1: smooth(make_polygon(customLineStyle( exterior_ring($geometry), '20 80,60 -40,40 40,40 -8,40 30,40 -40,20 -40')),5)
test2,test3: smooth(make_polygon(customLineStyle( exterior_ring($geometry), '200 0,0.1 150,-50 150,0.1 200,150 200,0.1 150,-50 150,0.1 0,100 0')),5)

enter image description here

import itertools, math
from qgis.core import qgsfunction,QgsPoint,QgsGeometry
@qgsfunction(args='auto', group='Custom', usesGeometry=False, referencedColumns=[])
def customLineStyle(geom,param,feature,parent):
    dst = geom.length()
    vertices = geom.asPolyline()
    start = (vertices[0].x(),vertices[0].y())
    end = (vertices[-1].x(),vertices[-1].y())
    l = [list(map(float,x.split(' '))) for x in param.split(',')]
    steps,offsets = zip(*l)
    d = sum(steps)
    newdst = dst / int(dst / d)
    newsteps = [x / d  * newdst for x in steps]
    d = sum(newsteps)
    cnt = int(dst / d)
    distances = list(itertools.chain.from_iterable(itertools.repeat(newsteps,cnt)))[0:-1]
    alloffsets = list(itertools.chain.from_iterable(itertools.repeat(offsets,cnt)))
    alloffsets.insert(0,0)
    distances = list(itertools.accumulate(distances))
    points2d = [(lambda g,d: (g.x(), g.y(),d))(geom.interpolate(d).asPoint(),d) for d in distances]
    distances.insert(0,0)
    points2d.insert(0,start) # prepend start point
    points = [QgsPoint(start[0],start[1])]
    for i,pt in enumerate(points2d[1:]):
        if distances[i+1] > distances[i]:
            corrAngle = -90
        else:
            corrAngle = 90
        qgsPt = QgsPoint(pt[0],pt[1])
        points.append(qgsPt.project(alloffsets[i+1],QgsPoint(points2d[i][0],points2d[i][1]).azimuth(qgsPt) + corrAngle))

    points.append(QgsPoint(end[0],end[1])) # append end point
    return QgsGeometry.fromPolyline(points)

And YES, there's still much room for improvements! ... i.e. storing the calculated geometries in a global dictionary for faster retrievement, support for interior rings, etc...

4
  • Thanks! That works. And is less jumpier than my use of random numbers. Although I like the look of the random numbers yours solution appears more versatile. I don't understand exactly what each set of parameters does. Could you clarify that for me? Also I created a question here to try to figure out the interior rings question. gis.stackexchange.com/q/369196/94350
    – Baswein
    Commented Jul 31, 2020 at 14:19
  • The string parameter „param“ defines a list of step distances and offsets along the line, which will be repeated as often as possible. Both, steps and offsets, can be negative as well.
    – christoph
    Commented Jul 31, 2020 at 14:30
  • Great- so it goes (step offset,step offset,step offset) ?
    – Baswein
    Commented Jul 31, 2020 at 14:34
  • Yes, that’s how it goes. To take interior rings or multipolygons into account, needs some refactoring.
    – christoph
    Commented Jul 31, 2020 at 14:37
0

I think I figured it out. In the geometry generator for your polygon layer set the type to Polygon/Multiploygon and use this expression.

make_polygon(smooth(make_zigzagline_rand( boundary( $geometry),1.5,.1,3),6))

In order for this function to work you need to define a custom function called make_zigzagline_rand using this code that I modified from @christoph answer for wavy lines https://gis.stackexchange.com/a/354873/94350 to make the wave offset varied instead of static.

import random
from qgis.core import qgsfunction,QgsExpressionContextUtils,QgsExpression,QgsProject,QgsPoint,QgsGeometry

@qgsfunction(args='auto', group='Custom', usesGeometry=False, referencedColumns=[])
def make_zigzagline_rand(geom,dist,min_offset,max_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>min_offset</span>,<span>max_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>min_offset</span></td><td>minimun perpendicular offset</td></tr>
    <tr><td><span>max_offset</span></td><td>maximum perpendicular offset</td></tr>
</table>
<br/><br/>
Examples:
<ul>
    <li>make_zigzagline_rand($geometry,'15,30',5,15) -> zig-zag line</li>
    <li>make_zigzagline_rand($geometry,15,5,15) -> zig-zag line</li>
</ul>

Use smooth function to create wavelines:<br/><br/>
Example:
<ul><li>smooth(make_zigzagline_rand($geometry,'15,30',5,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(random.uniform(min_offset,max_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)

This will give you this output. QGIS random wavy line edge function One issue I have found is that if your geometry has interior rings (holes) the expression will not work. Using exterior_ring instead of boundary in the expression will fill in the interior ring instead of making the entire feature disappear.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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