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 }
&lt;/style&gt;
&lt;h2&gt;converts a linestring to a zig-zag line&lt;/h2&gt;

make_zigzagline(&lt;span&gt;geometry&lt;/span&gt;,&lt;span&gt;distance(s)&lt;/span&gt;,&lt;span&gt;offset&lt;/span&gt;)&lt;br/&gt;

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

Use smooth function to create wavelines:&lt;br/&gt;&lt;br/&gt;
Example:
&lt;ul&gt;&lt;li&gt;smooth(make_zigzagline($geometry,'15,30',15),3)&lt;/li&gt;&lt;/ul&gt;
&quot;&quot;&quot;

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) &lt; 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

Baswein
  • 2,294
  • 13
  • 21
  • 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 Jul 11 '20 at 07: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 Jul 11 '20 at 17:03
  • Hmm, „exterior_ring“ ... good point! ... how could I oversee this excellent function ;-) – christoph Jul 13 '20 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: https://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 Jul 13 '20 at 17:16
  • Marvelous... unfortunately I can‘t test it on my iPhone:-) – christoph Jul 13 '20 at 17:20

2 Answers2

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

christoph
  • 5,605
  • 1
  • 20
  • 34
  • 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. https://gis.stackexchange.com/q/369196/94350 – Baswein Jul 31 '20 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 Jul 31 '20 at 14:30
  • Great- so it goes (step offset,step offset,step offset) ? – Baswein Jul 31 '20 at 14:34
  • Yes, that’s how it goes. To take interior rings or multipolygons into account, needs some refactoring. – christoph Jul 31 '20 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.

Baswein
  • 2,294
  • 13
  • 21