15

When points are overlapping, there is this property which allow to automatically display the lot of them separately around where they are, called 'Point displacement'. But it doesn't work for lines, even so it seems to me quite conceptually feasable in order to achieve something like that :

enter image description here

I absolutely need to see the differents lines which in reality are all in the same place (I am working in telecomunication networking). The only way I see for now is to really create different lines like in the picture above, thus creating spatial mistakes.

I'm using QGIS 2.14.

mgri
  • 16,159
  • 6
  • 47
  • 80
GuiOm Clair
  • 1,191
  • 9
  • 25
  • I think something could be done recurring to styling. Is the line in the middle the starting line? Then, I see that you created each one of the other lines by using three different geometries, so my question is if there are some specific additional rules from rendering them? – mgri May 04 '17 at 07:05
  • @mgri I am not sure to understand your question. The picture provided is an example in which I drew five different lines for the sake of demonstration. In reality it would be more that these 5 lines are in fact indeed on the spot of the middle one (they are wires, so all stuck in the same sheath). – GuiOm Clair May 04 '17 at 07:08
  • 1
    You can render lines with a displacement ("offset") as well, but they would not meet at the start and end points. – AndreJ May 04 '17 at 07:37
  • @AndreJ Yes, and another problem would be that it would be quite manual operation where I would need something more automatic since it would be used by many users. – GuiOm Clair May 04 '17 at 07:52
  • 1
    @GuiOmClair Following the attached image, I assumed that you start from one line which overlaps (for example) four other lines and that you need to find a way for displaying them separately, even if they overlap. I just said that it could be possible to reproduce what is displayed in the attached image without the needing of creating new geometries (but only recurring to style properties of the starting layer). Another way would be the one proposed by AndreJ, but it seems it doesn't fit to your needs. – mgri May 04 '17 at 07:54
  • @GuiOmClair Is it clearer by now? Am I right about your initial situation? If my solution would be of interest, I can try to help you, but I need to know if you start from one line geometry. – mgri May 04 '17 at 08:37
  • 1
    @mgri I am still not totally sure we understand each other. I have let's say 5 lines (so 5 different items, in the same layer since they would be wires) that are perfectly overlapping, and I would like to display them automatically in a way such as the picture above. I must say that I don't properly understand the solution you propose (I am not native english speaker so I have trouble figuring out what you mean by "recurring to style properties"), but your help is most welcome. :) – GuiOm Clair May 04 '17 at 09:12
  • @GuiOmClair don't worry, I will surely try to help you in the next hours! (however, follow this link to understand the technique that I would try to apply for solving the issue). – mgri May 04 '17 at 10:07
  • @mgri Ok. I am looking on the thread you linked, and in order to precise, I must add that in my case it can be 2, 3, 4, 5 or more wires (although I think rarely more than 10) overlapping each other. So any solution would need to be flexible enough to adapt automatically to the variations in number. ;) – GuiOm Clair May 04 '17 at 11:13
  • @GuiOmClair yes and I know it, but it should be a bit tricky thinking about the workflow. You will have my 2 cents as soon as I can if I find a solution! – mgri May 04 '17 at 11:53
  • As promised, here my 2 cents. Please, let me know if you encounter any issue. – mgri May 05 '17 at 17:39

2 Answers2

20

I propose an approach that only recurs to a geometry generator and a custom function.

Before starting, I want to underline that I will focus the attention on the explanation of the minimal things to do for reproducing the desired result: this means that some other minor parameters (like sizes, widths and so on) should be easily adjusted by you for better fitting your needs.

Therefore, this solution works both for Geographic and Projected Reference Systems: in the following, I assumed to use a projected CRS (i.e. units of measurements are meters), but you can change them according to your CRS.


Context

Let's assume to start from this linestring vector layer representing the wires (the labels represent the number of overlapping (coincident) wires):

enter image description here


Solution

Firstly, go to Layer Properties | Style and then choose the Single symbol renderer.

From the Symbol selector dialog, choose a Geometry generator as symbol layer type and Linestring / MultiLinestring as geometry type. Then, click on the Function Editor tab:

enter image description here

Then, click on New file and type draw_wires as the name of the new function:

enter image description here

You will see that a new function has been created and it is listed on the left side of the dialog. Now, click on the name of the function and replace the default @qgsfunction with the following code (don't forget to add all the libraries attached here):

from qgis.core import *
from qgis.gui import *
from math import sin, cos, radians

@qgsfunction(args='auto', group='Custom')
def draw_wires(angle, percentage, curr_feat, layer_name, feature, parent):

    def wires(polyline, new_angle, percentage):
        for x in range(0, len(polyline)-1):
            vertices = []
            first_point = polyline[x]
            second_point = polyline[x +1]
            seg = QgsGeometry.fromPolyline([first_point, second_point])
            len_feat = seg.length()
            frac_len = percentage * len_feat
            limb = frac_len/cos(radians(new_angle))
            tmp_azim = first_point.azimuth(second_point)
            angle_1 = radians(90 - (tmp_azim+new_angle))
            dist_x, dist_y = (limb * cos(angle_1), limb * sin(angle_1))
            point_1 = QgsPoint(first_point[0] + dist_x, first_point[1] + dist_y)
            angle_2 = radians(90 - (tmp_azim-new_angle))
            dist_x, dist_y = (limb * cos(angle_2), limb * sin(angle_2))
            point_2 = QgsPoint(second_point[0] - dist_x, second_point[1] - dist_y)
            tmp_azim = second_point.azimuth(first_point)
            angle_3 = radians(90 - (tmp_azim+new_angle))
            dist_x, dist_y = (limb * cos(angle_3), limb * sin(angle_3))
            point_3 = QgsPoint(second_point[0] + dist_x, second_point[1] + dist_y)
            angle_4 = radians(90 - (tmp_azim-new_angle))
            dist_x, dist_y = (limb * cos(angle_4), limb * sin(angle_4))
            point_4 = QgsPoint(first_point[0] - dist_x, first_point[1] - dist_y)
            vertices.extend([first_point, point_1, point_2, second_point, point_3, point_4, first_point])
            tempGeom = QgsGeometry.fromPolyline(vertices)
            num.append(tempGeom)
        return num


    layer = QgsMapLayerRegistry.instance().mapLayersByName(layer_name)[0]

    all_feats = {}
    index = QgsSpatialIndex()
    for ft in layer.getFeatures():
        index.insertFeature(ft)
        all_feats[ft.id()] = ft

    first = True

    tmp_geom = curr_feat.geometry()
    polyline = tmp_geom.asPolyline()
    idsList = index.intersects(tmp_geom.boundingBox())
    occurrences = 0
    for id in idsList:
        test_feat = all_feats[id]
        test_geom = test_feat.geometry()
        if tmp_geom.equals(test_geom):
            occurrences += 1
    if occurrences & 0x1:
        num = [tmp_geom]
    else:
        num = []

    rapp = occurrences/2
    i=2
    new_angle = angle

    while i <= occurrences:
        draw=wires(polyline, new_angle, percentage)
        i += 2
        new_angle -= new_angle/rapp
    first = True
    for h in num:
        if first:
            geom = QgsGeometry(h)
            first = False
        else:
            geom = geom.combine(h)
    return geom

Once you have done this, click on the Load button and you will be able to see the function from the Custom Menu of the Expression dialog.

Now, type this expression (see the image below as a reference):

draw_wires(40, 0.3, $currentfeature, @layer_name)

enter image description here

You have just run a function which is saying, in an imaginary way:

"For the current layer (@layer_name) and the current feature ($currentfeature), display the wires together using an initial maximum opening of 40 degrees and with a change in direction at a distance of 0.3 times the length of the current segment."

The only thing you need to change is the value of the first two parameters as you want, but obviously in a reasonable way (leave the other function parameters as provided).

Finally, click on the Apply button for applying the changes.

You will see something like this:

enter image description here

as expected.


EDIT

According to a specific request raised by the OP in a comment:

"Would it be possible to create this pattern only between the beginning and the end of each polyline instead of between each vertex?"

I slightly edited the code. The following function should return the expected result:

from qgis.core import *
from qgis.gui import *
from math import sin, cos, radians

@qgsfunction(args='auto', group='Custom')
def draw_wires(angle, percentage, curr_feat, layer_name, feature, parent):

    def wires(polyline, new_angle, percentage):
        vertices = []
        len_feat = polyline.length()
        frac_len = percentage * len_feat
        limb = frac_len/cos(radians(new_angle))
        tmp_azim = first_point.azimuth(second_point)
        angle_1 = radians(90 - (tmp_azim+new_angle))
        dist_x, dist_y = (limb * cos(angle_1), limb * sin(angle_1))
        point_1 = QgsPoint(first_point[0] + dist_x, first_point[1] + dist_y)
        angle_2 = radians(90 - (tmp_azim-new_angle))
        dist_x, dist_y = (limb * cos(angle_2), limb * sin(angle_2))
        point_2 = QgsPoint(second_point[0] - dist_x, second_point[1] - dist_y)
        tmp_azim = second_point.azimuth(first_point)
        angle_3 = radians(90 - (tmp_azim+new_angle))
        dist_x, dist_y = (limb * cos(angle_3), limb * sin(angle_3))
        point_3 = QgsPoint(second_point[0] + dist_x, second_point[1] + dist_y)
        angle_4 = radians(90 - (tmp_azim-new_angle))
        dist_x, dist_y = (limb * cos(angle_4), limb * sin(angle_4))
        point_4 = QgsPoint(first_point[0] - dist_x, first_point[1] - dist_y)
        vertices.extend([first_point, point_1, point_2, second_point, point_3, point_4, first_point])
        tempGeom = QgsGeometry.fromPolyline(vertices)
        num.append(tempGeom)

    layer = QgsMapLayerRegistry.instance().mapLayersByName(layer_name)[0]

    all_feats = {}
    index = QgsSpatialIndex()
    for ft in layer.getFeatures():
        index.insertFeature(ft)
        all_feats[ft.id()] = ft
    first = True
    tmp_geom = curr_feat.geometry()
    coords = tmp_geom.asMultiPolyline()
    if coords:
        new_coords = [QgsPoint(x, y) for x, y in z for z in coords]
    else:
        coords = tmp_geom.asPolyline()
        new_coords = [QgsPoint(x, y) for x, y in coords]
    first_point = new_coords[0]
    second_point = new_coords[-1]
    polyline=QgsGeometry.fromPolyline([first_point, second_point])
    idsList = index.intersects(tmp_geom.boundingBox())
    occurrences = 0
    for id in idsList:
        test_feat = all_feats[id]
        test_geom = test_feat.geometry()
        if tmp_geom.equals(test_geom):
            occurrences += 1
    if occurrences & 0x1:
        num = [polyline]
    else:
        num = []

    rapp = occurrences/2
    i=2
    new_angle = angle

    while i <= occurrences:
        draw=wires(polyline, new_angle, percentage)
        i += 2
        new_angle -= new_angle/rapp
    first = True
    for h in num:
        if first:
            geom = QgsGeometry(h)
            first = False
        else:
            geom = geom.combine(h)
    return geom
mgri
  • 16,159
  • 6
  • 47
  • 80
  • Wow! That's an impressive answer! Thank you very much for taking this time to find and share it. However : 1. I am having trouble applying it to my datas (when I apply the function, the lines disappear), but I guess the problem comes from my datas since it works on a temporary layer and 2. would it be possible to create this pattern only between the beginning and the end of each polyline instead of between each vertice? – GuiOm Clair May 09 '17 at 11:51
  • @GuiOmClair the lines disappear because something goes wrong with the function. The problem doesn't come from the using of temporary layer, but it could be related to the using of MultiLine geometries instead of Line geometries. Please, load the layer in QGIS and then type these two lines in the Python Console: layer=iface.activeLayer() and then print layer.wkbType(). ClickRun: which is the value of the printed number? – mgri May 09 '17 at 12:05
  • The number is 5 (what does it mean?) – GuiOm Clair May 09 '17 at 12:13
  • @GuiOmClair It means that your layer is a MultiLineString layer, while I assumed it was a LineString layer (since you didn't specify it). This wouldn't be a problem and I will properly edit the code as soon as I can (maybe tomorrow). Furthermore, I should be able to render the wires only between the first and the last point of each (multi)line feature. – mgri May 09 '17 at 12:25
  • @GuiOmClair Since I need to set the percentage of the length from which changing the direction of wires (0.3 in my example), could it be ok for you if I measure the length as the distance between the first and the last point of the feature? Or you want to still measure the percentage as a fraction of the real length of the feature? The latter would change the result if you are working with arcs, but I imagine your features are nearly straight lines... – mgri May 09 '17 at 12:37
  • 1
    Yes, the features are straight lines (since they are generally easier to manage and export), so it would be better considering the real length of the wires. – GuiOm Clair May 09 '17 at 12:42
  • @GuiOmClair, see my edited answer. If it still doesn't work, please join the chat. – mgri May 10 '17 at 17:48
  • @GuiOmClair any news? – mgri May 18 '17 at 21:05
  • Yes, I sent some rather long messages on the chat, and see now the chat is not available. So I tried, it worked however still not like I would need. The last piece of code you added create the pattern between the first and last point, but without following the path of the original polyline. I guess in other words I would say that it needs to be indeed joining at the first and last point, but have some offset along the intermediary vertices. – GuiOm Clair May 22 '17 at 08:45
  • That being said, and even if I think this is a feature that should exists in QGIS, the client said that finally it is better to actually draw the polylines separately since their GIS do not handle the situation the same way I thought to do (which was the spatially right way to do). So I won't be using this feature in my current project. I am sorry to say that now, after the time you spent working on the question. – GuiOm Clair May 22 '17 at 08:49
  • @GuiOmClair sorry, I didn't notice it because I never received any notification from the chat. I thought my first solution was the best approach (in fact, it clearly reproduces the image you attached); then, I tried to follow your suggestion and it is still not enough for you; right now, I still don't understand what you are looking for. Probably, it would have been better if you reported a clear graphical example of the desired result because, as you said, I spent much time for nothing: take this as a suggestion for the next time. – mgri May 22 '17 at 09:02
  • @mgri Imagine 5 line features (bus routes) from the same layer. Each line travels the same route around a city, following the same roads. These 5 lines all overlap each other obviously. He wants to know how we can spread out the line symbols of each layer so that they don't overlap. And to do this without editing the data itself to shift the vertices apart. – Theo F Jul 14 '21 at 16:38
  • This answer is somehow misleading form many, it only works for very simple overlapping lines. Since most real life data will have lines overlapping only on certain segments, this method is simply useless. since the geometry equality clause that determines the number of occurrences will always come up with a single occurrence at a time, and the shape will not be shifted. It is simply not doable on GIS software. Better results may be obtained with CAD software. – Beetroot Nov 25 '21 at 14:42
4

Very nice work!

On the QGIS-users mailing list somebody asked for the QGIS3 version of the functions.

I tried to rewrite them both (draw_wires for all vertices and draw_wires2 for only start/end). They work for me (QGIS 3.20 here):

both versions

from qgis.core import *
from qgis.gui import *
from math import sin, cos, radians

@qgsfunction(args='auto', group='Custom') def draw_wires(angle, percentage, curr_feat, layer_name, feature, parent):

def wires(polyline, new_angle, percentage, referenced_columns=[]):
    for x in range(0, len(polyline)-1):
        vertices = []
        first_point = polyline[x]
        second_point = polyline[x +1]
        #seg = QgsGeometry.fromPolyline([first_point, second_point])
        seg = QgsGeometry.fromPolylineXY([first_point, second_point])            
        len_feat = seg.length()
        frac_len = percentage * len_feat
        limb = frac_len/cos(radians(new_angle))
        tmp_azim = first_point.azimuth(second_point)
        angle_1 = radians(90 - (tmp_azim+new_angle))
        dist_x, dist_y = (limb * cos(angle_1), limb * sin(angle_1))
        #point_1 = QgsPoint(first_point[0] + dist_x, first_point[1] + dist_y)
        point_1 = QgsPointXY(first_point[0] + dist_x, first_point[1] + dist_y)
        angle_2 = radians(90 - (tmp_azim-new_angle))
        dist_x, dist_y = (limb * cos(angle_2), limb * sin(angle_2))
        #point_2 = QgsPoint(second_point[0] - dist_x, second_point[1] - dist_y)
        point_2 = QgsPointXY(second_point[0] - dist_x, second_point[1] - dist_y)
        tmp_azim = second_point.azimuth(first_point)
        angle_3 = radians(90 - (tmp_azim+new_angle))
        dist_x, dist_y = (limb * cos(angle_3), limb * sin(angle_3))
        #point_3 = QgsPoint(second_point[0] + dist_x, second_point[1] + dist_y)
        point_3 = QgsPointXY(second_point[0] + dist_x, second_point[1] + dist_y)
        angle_4 = radians(90 - (tmp_azim-new_angle))
        dist_x, dist_y = (limb * cos(angle_4), limb * sin(angle_4))
        #point_4 = QgsPoint(first_point[0] - dist_x, first_point[1] - dist_y)
        point_4 = QgsPointXY(first_point[0] - dist_x, first_point[1] - dist_y)
        vertices.extend([first_point, point_1, point_2, second_point, point_3, point_4, first_point])
        #tempGeom = QgsGeometry.fromPolyline(vertices)
        tempGeom = QgsGeometry.fromPolylineXY(vertices)
        num.append(tempGeom)
    return num

#layer = QgsMapLayerRegistry.instance().mapLayersByName(layer_name)[0]
layer = QgsProject().instance().mapLayersByName(layer_name)[0]

all_feats = {}
index = QgsSpatialIndex()
for ft in layer.getFeatures():
    index.insertFeature(ft)
    all_feats[ft.id()] = ft

first = True

tmp_geom = curr_feat.geometry()
polyline = tmp_geom.asPolyline()
idsList = index.intersects(tmp_geom.boundingBox())
occurrences = 0
for id in idsList:
    test_feat = all_feats[id]
    test_geom = test_feat.geometry()
    if tmp_geom.equals(test_geom):
        occurrences += 1
if occurrences &amp; 0x1:
    num = [tmp_geom]
else:
    num = []

rapp = occurrences/2
i=2
new_angle = angle

while i &lt;= occurrences:
    draw=wires(polyline, new_angle, percentage)
    i += 2
    new_angle -= new_angle/rapp
first = True
for h in num:
    if first:
        geom = QgsGeometry(h)
        first = False
    else:
        geom = geom.combine(h)
return geom

and

from qgis.core import *
from qgis.gui import *
from math import sin, cos, radians

@qgsfunction(args='auto', group='Custom') def draw_wires2(angle, percentage, curr_feat, layer_name, feature, parent):

def wires2(polyline, new_angle, percentage):
    vertices = []
    len_feat = polyline.length()
    frac_len = percentage * len_feat
    limb = frac_len/cos(radians(new_angle))
    tmp_azim = first_point.azimuth(second_point)
    angle_1 = radians(90 - (tmp_azim+new_angle))
    dist_x, dist_y = (limb * cos(angle_1), limb * sin(angle_1))
    #point_1 = QgsPoint(first_point[0] + dist_x, first_point[1] + dist_y)
    point_1 = QgsPointXY(first_point[0] + dist_x, first_point[1] + dist_y)
    angle_2 = radians(90 - (tmp_azim-new_angle))
    dist_x, dist_y = (limb * cos(angle_2), limb * sin(angle_2))
    #point_2 = QgsPoint(second_point[0] - dist_x, second_point[1] - dist_y)
    point_2 = QgsPointXY(second_point[0] - dist_x, second_point[1] - dist_y)
    tmp_azim = second_point.azimuth(first_point)
    angle_3 = radians(90 - (tmp_azim+new_angle))
    dist_x, dist_y = (limb * cos(angle_3), limb * sin(angle_3))
    #point_3 = QgsPoint(second_point[0] + dist_x, second_point[1] + dist_y)
    point_3 = QgsPointXY(second_point[0] + dist_x, second_point[1] + dist_y)        
    angle_4 = radians(90 - (tmp_azim-new_angle))
    dist_x, dist_y = (limb * cos(angle_4), limb * sin(angle_4))
    #point_4 = QgsPoint(first_point[0] - dist_x, first_point[1] - dist_y)
    point_4 = QgsPointXY(first_point[0] - dist_x, first_point[1] - dist_y)
    vertices.extend([first_point, point_1, point_2, second_point, point_3, point_4, first_point])
    #tempGeom = QgsGeometry.fromPolyline(vertices)
    tempGeom = QgsGeometry.fromPolylineXY(vertices)
    num.append(tempGeom)

#layer = QgsMapLayerRegistry.instance().mapLayersByName(layer_name)[0]
layer = QgsProject.instance().mapLayersByName(layer_name)[0]

all_feats = {}
index = QgsSpatialIndex()
for ft in layer.getFeatures():
    index.insertFeature(ft)
    all_feats[ft.id()] = ft
first = True
tmp_geom = curr_feat.geometry()
#coords = tmp_geom.asMultiPolyline()
#if coords: 
if tmp_geom.isMultipart():
    coords = tmp_geom.asMultiPolyline()
    new_coords = [QgsPointXY(x, y) for x, y in z for z in coords]
else:
    coords = tmp_geom.asPolyline()
    new_coords = [QgsPointXY(x, y) for x, y in coords]
first_point = new_coords[0]
second_point = new_coords[-1]
#polyline=QgsGeometry.fromPolyline([first_point, second_point])
polyline=QgsGeometry.fromPolylineXY([first_point, second_point])
idsList = index.intersects(tmp_geom.boundingBox())
occurrences = 0
for id in idsList:
    test_feat = all_feats[id]
    test_geom = test_feat.geometry()
    if tmp_geom.equals(test_geom):
        occurrences += 1
if occurrences &amp; 0x1:
    num = [polyline]
else:
    num = []

rapp = occurrences/2
i=2
new_angle = angle

while i &lt;= occurrences:
    draw=wires2(polyline, new_angle, percentage)
    i += 2
    new_angle -= new_angle/rapp
first = True
for h in num:
    if first:
        geom = QgsGeometry(h)
        first = False
    else:
        geom = geom.combine(h)
return geom