QGIS Labeling – Callout Labels with More Lines

labelingqgis

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.

Best Answer

@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

Related Question