# Wavy Edge on polygon in QGIS using geometry generators

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:

• 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 :-) 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)) Commented Jul 11, 2020 at 17:03
• Hmm, „exterior_ring“ ... good point! ... how could I oversee this excellent function ;-) Commented Jul 13, 2020 at 16:54
• 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. Commented Jul 13, 2020 at 17:16
• Marvelous... unfortunately I can‘t test it on my iPhone:-) Commented Jul 13, 2020 at 17:20

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)
``````

``````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...

• 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 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. Commented Jul 31, 2020 at 14:30
• Great- so it goes (step offset,step offset,step offset) ? Commented Jul 31, 2020 at 14:34
• Yes, that’s how it goes. To take interior rings or multipolygons into account, needs some refactoring. Commented Jul 31, 2020 at 14:37

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. 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.