Draw line to next feature with distance as label (dynamic)

You can do this with geometry generator in QGIS 3.x using the collect aggregate within the aggregate() function to extract the collected geometry of another layer and use it in an expression.

Here are the expressions I used to generate this example - the line layer was called Line; you just need to replace that with the name of your line layer.

enter image description here

Geometry generator (on point layer):

shortest_line($geometry,aggregate('Line','collect',$geometry))

Label:

round(length(shortest_line($geometry,aggregate('Line','collect',$geometry))),2)

Label placement (Label -> Placement -> Data defined):

Coordinate X - x(line_interpolate_point(shortest_line($geometry,aggregate('Line','collect',$geometry)),round(length(shortest_line($geometry,aggregate('Line','collect',$geometry))),2)/2))

Coordinate Y - y(line_interpolate_point(shortest_line($geometry,aggregate('Line','collect',$geometry)),round(length(shortest_line($geometry,aggregate('Line','collect',$geometry))),2)/2))

Rotation - line_interpolate_angle(shortest_line($geometry,aggregate('Line','collect',$geometry)),round(length(shortest_line($geometry,aggregate('Line','collect',$geometry))),2)/2)+90

Label alignment horizontal - 'Center'


UPDATE QGIS 3.8

Label placement

From QGIS 3.8 onwards you can use Geometry Generator to place labels. So instead of using 3 expressions above for X/Y/Rotation, go to the Geometry Generator under the Placement tab in Label settings, change the geometry type to Linestring, and paste the same expression used to generate the lines as above in the text box.

Now you can configure your label placement as if it were actually on a line and set distance from the line, label repetition, etc. without using an expression to set X and Y coordinates.

enter image description here


Here are two Python solution:

Rubber Band approach:

  1. searches for the nearest point on the road
  2. creates a rubber band
  3. labels the rubber band

    from qgis.gui import *
    from qgis.utils import *
    from qgis.core import *
    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
    
    p_lyr = QgsMapLayerRegistry.instance().mapLayersByName('points')[0]
    l_lyr = QgsMapLayerRegistry.instance().mapLayersByName('roads')[0]
    lines = [feature for feature in l_lyr.getFeatures()]
    
    def removeCanvasItems():
        canvas_items = [ i for i in iface.mapCanvas().scene().items() if issubclass(type(i), QgsRubberBand) or issubclass(type(i), QgsTextAnnotationItem) or issubclass(type(i), QgsVertexMarker)]
        if canvas_items:
            for item in canvas_items:
                if item in iface.mapCanvas().scene().items():
                    iface.mapCanvas().scene().removeItem(item)
    
    def nearest_road():
        #removeCanvasItems()           #removes the old rubberbands, calling this causes a mini dump
        for point in p_lyr.getFeatures():
            minDistPoint = min([l.geometry().closestSegmentWithContext(QgsPoint(point.geometry().asPoint())) for l in lines])[1]
            points = [QgsPoint(point.geometry().asPoint()), QgsPoint(minDistPoint[0], minDistPoint[1])]
            r_polyline = QgsRubberBand(iface.mapCanvas(), False)
            r_polyline.setToGeometry(QgsGeometry.fromPolyline(points), None)
            r_polyline.setWidth(2)
            r_polyline.setColor(QColor(0,255,0,255))
            seg = QgsFeature()
            seg.setGeometry(QgsGeometry.fromPolyline(points))
            geom = seg.geometry()
            length = seg.geometry().length()
            symbol = QgsMarkerSymbolV2()
            symbol.setSize(0)
            lbltext = QTextDocument(str(round(length,2)) + 'm')
            label = QgsTextAnnotationItem(iface.mapCanvas())
            label.setMapPosition(seg.geometry().interpolate(length/2.0).asPoint())
            label.setDocument(lbltext)
            label.setFrameSize(QSizeF(lbltext.size().width(),lbltext.size().height()))
            label.setFrameBorderWidth(0)
            label.setFrameColor(QColor("#ff4b00"))
            label.setFrameBackgroundColor(QColor("#ff4b00"))
            label.setMarkerSymbol(symbol)
    
    nearest_road() #calling the function the first time
    p_lyr.geometryChanged.connect(nearest_road) # creates new rubberband when point is moved
    p_lyr.featureAdded.connect(nearest_road) # creates new rubberband when a new point is created
    

enter image description here

When I want to remove the old rubber bands while adding a new feature or move an existing feature, I get a mini dump and QGIS crashes.

Vector Layer approach:

Instead of rubber bands you could use a layer, which stores the lines and the distance. In my case I have a shapefile with two attributes: id (int), and distance(double). With this layer you can better label and style your features. The function removeFeatures(), removes all features from the distance layer. Better way is to get the ID of the feature that is moved and deletes only this line from the provider. I think I will update this soon.

from qgis.gui import *
from qgis.utils import *
from qgis.core import *
from PyQt4.QtGui import *
from PyQt4.QtCore import *

p_lyr = QgsMapLayerRegistry.instance().mapLayersByName('points')[0]
l_lyr = QgsMapLayerRegistry.instance().mapLayersByName('roads')[0]
lines = [feature for feature in l_lyr.getFeatures()]
d_lyr = QgsMapLayerRegistry.instance().mapLayersByName('distance')[0]
prov = d_lyr.dataProvider()

def removeFeatures():
    with edit(d_lyr):
        listOfIds = [feat.id() for feat in d_lyr.getFeatures()]
        d_lyr.deleteFeatures( listOfIds )

def nearest_road():
    removeFeatures()
    for point in p_lyr.getFeatures():
        minDistPoint = min([l.geometry().closestSegmentWithContext(QgsPoint(point.geometry().asPoint())) for l in lines])[1]
        points = [QgsPoint(point.geometry().asPoint()), QgsPoint(minDistPoint[0], minDistPoint[1])]
        seg = QgsFeature()
        seg.setGeometry(QgsGeometry.fromPolyline(points))
        geom = seg.geometry()
        length = seg.geometry().length()
        seg.setAttributes([1,seg.geometry().length()])
        prov.addFeatures([seg])
    d_lyr.updateExtents()
    d_lyr.triggerRepaint()
    d_lyr.updateFields()

nearest_road()
p_lyr.geometryChanged.connect(nearest_road)
p_lyr.featureAdded.connect(nearest_road)

enter image description here


Thanks to "she_weeds" for the QGIS 3.x.x Geometry Generator solution.

For the users of QGIS 2.18.x here is modified version. You have to install the plugin refFunctions. The function geomnearest searches for the nearest line feature of the points. In my case I have a LineString layer called 'roads'.

  1. Create the linestring with the Geometry generator (Geometry Type "Linestring/MultiLineString"). shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry')))

  2. Label Placement:

    X: x(line_interpolate_point(shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry'))),round(length(shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry')))),2)/2))

    Y:y(line_interpolate_point(shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry'))),round(length(shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry')))),2)/2))

  3. Label Rotation:

    180-degrees(azimuth(start_point(shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry')))), end_point(shortest_line( $geometry, geom_from_wkt( geomnearest( 'roads', '$geometry'))))))+90

From my point of view there is a big disadvantage using this: time is used for rendering. For about 25 features it takes about 7 seconds to load the lines and labels(!).

Maybe there is better way doing this in the QGIS 2.18.x Geometry Generator?