10

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.

  • 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 4 '17 at 7: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 4 '17 at 7: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 4 '17 at 7: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 4 '17 at 7: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 4 '17 at 7:54
12

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
  • 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 9 '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 9 '17 at 12:05
  • The number is 5 (what does it mean?) – GuiOm Clair May 9 '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 9 '17 at 12:25
  • 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 9 '17 at 12:42

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.