4

I have an issue with the Labeling in QGIS. I want to label lines, that are connected to each other, but have different attributes and so also different labels. The lines QGIS draws from the label to the line only marks one place of the line. In my case, this not very helpful because I can't see, where the start and the end of the line is.

I would like to have like two separate lines from the Label, which go to the start- and endpoint of the Line, so the visibility of the line segment, which is labeled gets improved.

enter image description here

The Labels should look like in the Picture above. This from AutoCAD.

3
  • Easiest probably to create start- and end point of the lines.
    – Babel
    Commented May 30, 2023 at 21:42
  • 1
    Including a screenshot of the current situation and a mock-up of what you want to achieve would clarify your question immensely.
    – Matt
    Commented May 31, 2023 at 6:07
  • I Added a Picture to clarify.
    – Toby
    Commented May 31, 2023 at 14:52

3 Answers 3

9

UPDATE:

This answer is outdated!

Please look at my newer improved solution, which works in any Layout map window, regardless of map canvas visibility.

@Toby your question inspired me!

As @Tom already mentioned, we can solve the task with the Geometry Generator. Only in this case we need an additional Python expression function to solve the main part of the task: determining the label boundary.

I will explain the whole solution, but first let me show you the final result:

enter image description here

And here are the necessary steps:

1.) Add the Python code with the Function Editor of the Expression Dialog (tested with QGIS 3.30.1):

from qgis.core import *
from qgis.gui import *
from qgis.utils import iface
import math
    
def midpt(pt1, pt2):
    x = (pt1.x() + pt2.x())/2
    y = (pt1.y() + pt2.y())/2
    return QgsPointXY(x,y)
    
def createRelPoint(point, dist, azim):
    point_x, point_y = point.x(), point.y()
    azim_rad = math.radians(azim)
    x = point_x + dist * math.cos(azim_rad)
    y = point_y + dist * math.sin(azim_rad)
    return QgsPointXY(x, y)

@qgsfunction(args='auto', group='Custom', usesGeometry=True, referenced_columns=[])
def getLabelLeaders(x, y, handle_width, feature, parent):
    center=QgsPointXY(x,y)
    geom = feature.geometry().asPolyline()
    startPt = geom[0]
    endPt = geom[-1]
    buffer=1
    rec=QgsRectangle.fromCenterAndSize(center,buffer,buffer)
    labels=iface.mapCanvas().labelingResults ().labelsWithinRect(rec)
    #labels=iface.mapCanvas().labelingResults().labelsAtPosition(center)
    if len(labels)>0:
        polygon=labels[0].labelGeometry.asPolygon()[0]
        p1 = midpt(polygon[0],polygon[3])
        p2 = midpt(polygon[1],polygon[2])
        if p1.distance(startPt) > p2.distance(startPt):
            p1,p2 = p2,p1
  
        azim = p1.azimuth(p2)
        p1 = createRelPoint(p1,-handle_width,90-azim)
        p2 = createRelPoint(p2,handle_width,90-azim)
            
        l1 = QgsGeometry.fromPolylineXY([startPt,p1,center])
        l2 = QgsGeometry.fromPolylineXY([endPt,p2,center])
        mline = QgsGeometry.collectGeometry([l1,l2])
        return mline

2.) Create and configure your labels:

My example uses a Single Label value: "strasse" ||'\n'||"str_name"||'\nLänge: '|| round("measurelength",0)||'km' with a rectangular background (all dimension values are in Map Units to prevent label resizing).

Move one label to activate the Auxiliary Storage mechanism. You can rotate your labels as well. After that open the label Placement tab and configure the label Alignment: horizontal='Center', vertical='Half'. Set Priority to High. On the Rendering tab I'm setting Overlapping Mode to Allow Overlaps without Penalty.

3.) Add the Geometry Generator symbol to your layer symbology: enter image description here

getLabelLeaders("auxiliary_storage_labeling_positionx","auxiliary_storage_labeling_positiony",300)

The third parameter of the getLabelLeaders function defines the length of the short leader segments (I call it label handles).

Only labels that are moved will show the custom leaders.

The solution is not perfect, because sometimes label boundaries are not exact, but in most cases it should work as expected. If you don't see the leaders in your Print Layout, you have to zoom to the labels in Map Canvas and refresh the Layout afterwards (the expression needs Map Canvas to fetch the label boundaries).

BTW: it took me some hours to solve this task, but it was a lot of fun!

Enjoy, Christoph

7
  • I even tried to solve the task with Callout Lines, but had an issue with the Geometry Generator (see github.com/qgis/QGIS/issues/53377).
    – christoph
    Commented Jun 6, 2023 at 12:52
  • Hi Christoph, sorry for my late response, but i was on holidays. I tried your solution today, and followed all the steps, but the lines dont show up in my project. Can it be an issue with the map canvas you mention at the end? I'm pretty new to qgis, and dont know anything about it.
    – Toby
    Commented Jun 12, 2023 at 9:17
  • Hi Toby, never mind. You need a projected coordinate system like Gauss Krüger.
    – christoph
    Commented Jun 12, 2023 at 12:57
  • Hi Christoph. I Just found the mistake. I Didn't delete the standart part in the Geometry generator. Now it works perfectly fine. Thanks for that nice work. If can appreciate your work in some way let me know.
    – Toby
    Commented Jun 12, 2023 at 13:04
  • 2
    @Toby maybe it's easier to create an AutoLisp Routine for labeling in AutoCAD/BricsCAD and export the generated labels to QGIS.
    – christoph
    Commented Jun 20, 2023 at 15:35
2

You could use Rule-based Labeling, with Callouts and Geometry Generator to create the lines to the start and end points. I'm not convinced that the visibility is improved, but maybe it will for your data set. You may need to play around with the position of the labels.

  1. Turn on Rule-based Labeling and create two rules

enter image description here

  1. Enable Callouts

enter image description here

  1. Use Geometry Generator to create the Start Point (start_point($geometry)). Similar for the other rule (End Point -> end_point($geometry)). Set the distance to some way away from the point eg 20 mm

enter image description here

Result on a small sample data set - a callout line to each end of the actual lines:

enter image description here

1
  • Thanks for that quick answer, but this isn't what I was looking for. I need Something where the label is in the middle an the lines go from one label to start an endpoint. But Maybe I could play alittle with what you gave me. I'll try.
    – Toby
    Commented May 31, 2023 at 14:44
2

A year has passed and I've learned a lot, which has helped to massively improve the code for solving this issue.

The new Python expression function getLabelRect now also works in every layout map item, regardless of the map canvas. It uses the context parameter to retrieve the bounding boxes of all visible labels.

from qgis.core import *
from qgis.gui import *
from qgis.utils import iface

@qgsfunction(args='auto', group='Custom', usesGeometry=False, referenced_columns=[])
def getLabelRect(feature, parent, context):
    mapItemId = context.variable('map_id')
    if mapItemId == 'canvas':
        labelResults=iface.mapCanvas().labelingResults()
    else:
        layoutName = context.variable('layout_name')
        map = QgsProject.instance().layoutManager().layoutByName(layoutName).itemById(mapItemId)
        labelResults = map.previewLabelingResults()
    
    for label in labelResults.allLabels():
        if label.featureId == feature.id():
            return label.labelGeometry
        
    return

For showing the labels with leaders and a box around the labels, I had to style the layer labels with 3 geometry generators.

enter image description here enter image description here

The topmost geometry generator - with type "Polygon" - draws a polygon around the labels:

buffer(getLabelRect(),100,join:='miter')

The geometry generator below - with type "LineString" - draws the left leader. The layer column label_leader_pos is used to define the anchor position of the left and right leader ('ll','lr','rr','tt','tb','bb','lb',...). If the column is NULL the default is 'lr'.

with_variable('box', buffer(getLabelRect(),100,join:='miter'),
with_variable('cent', centroid(@box),
with_variable('anchor', left(coalesce("label_leader_pos" ,'l'),1),
with_variable('midpt', 
CASE 
    WHEN @anchor = 'l' THEN centroid( make_line(start_point(@box),point_n(@box,2)))
    WHEN @anchor = 'r' THEN centroid( make_line(point_n(@box,3),point_n(@box,4)))
    WHEN @anchor = 't' THEN centroid( make_line(point_n(@box,2),point_n(@box,3)))
    WHEN @anchor = 'b' THEN centroid( make_line(point_n(@box,1),point_n(@box,4)))
END,
 make_line(start_point(@geometry),end_point(extend(make_line(@cent,@midpt),0,100)),@midpt)))))

And the lowest geometry generator draws the right leader:

with_variable('box', buffer(getLabelRect(),100,join:='miter'),
with_variable('cent',centroid(@box),
with_variable('anchor', right(coalesce("label_leader_pos" ,'r'),1),
with_variable('midpt', 
CASE 
    WHEN @anchor = 'l' THEN centroid( make_line(start_point(@box),point_n(@box,2)))
    WHEN @anchor = 'r' THEN centroid( make_line(point_n(@box,3),point_n(@box,4)))
    WHEN @anchor = 't' THEN centroid( make_line(point_n(@box,2),point_n(@box,3)))
    WHEN @anchor = 'b' THEN centroid( make_line(point_n(@box,1),point_n(@box,4)))
END,
 make_line(@midpt,end_point(extend(make_line(centroid(@box),@midpt),0,100)),end_point(@geometry))))))

To manually move and rotate the labels, we need to store the label coordinates and the rotation angle in additional columns (i.e. label_x,label_y and label_rot). If we want to limit the leaders to labels that are correctly placed, we have to add expressions to enable/disable all geometry generators:

enter image description here

To correctly align the labels, we must set the horizontal Alignment to ' Center' and the vertical Alignment to 'Half'.

To get best results for DXF exports, I set text size to Map Units and varied letter spacing and line heights accordingly.

To show the Linestrings in a separate layer linestrings, I duplicated the labels layer and styled the Linestrings without labels. Doing this with layers coming from the same GeoPackage data source crashed QGIS 3.36 Windows. So, in my working example the linestring layer comes from a different data source (i.e. PostGIS).

And here is the result shown in BricsCAD: enter image description here

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.