3

It would be very useful to have a tool like this when measuring and even more useful to generate lines at the end of the operation.

I use this plugin which is part of a small GIS program (Map Maker 3.5 old version) Map Maker for my needs, but I think it should be an integral part of any GIS program. There are many tools there that are better solved than in QGIS. Video

enter image description here

4
  • 3
    The correct answer is a "yes" but surely that is not what you really want to know, right? ;) Mar 12 at 17:01
  • You are right :-) 😂. It is important that it is feasible! Someone will find way to make this plugin for the next version of QGIS
    – nagib
    Mar 12 at 17:55
  • 1
    Literally anything is possible with QGIS plugins ;) Mar 12 at 20:59
  • You're really right.
    – nagib
    Mar 13 at 18:28

2 Answers 2

11

Coming close to what you want, simply using geometry generator, you can use this expression to create the lines from each point to the cursor position where you click:

make_line (
    $geometry, 
    @canvas_cursor_point 
)

Use the same expression, enclosed in the function length() to create the label.

enter image description here


Be aware: projection

Not every projection is good for measurements, due to distortion. You should either use a local projection (like UTM zones or national grid) or calculate ellipsoidal distances.

To automatically convert measurents to a local CRS (even if project and layers are in EPSG:3857 or any other CRS), change the expression accordingly:

length(
    transform(
        make_line ($geometry, @canvas_cursor_point), 
        'EPSG:3857',  -- CRS of your layer
        'EPSG:2056'  -- your local CRS, here: Swiss national grid
    )
)
2
  • This is close to what is required but it behaves somehow strangely when you try to select the measurement location. If you have multiple points, this code will offer you the distances from each point and not from the ones you choose. This is the simplest solution so far, and judge whether it is the best.
    – nagib
    Mar 13 at 18:01
  • 1
    Yes, that's how it works: it creates the distance/line from all points. Not possible to use only for selected points - this would maybe possible creating a virtual layer
    – Babel
    Mar 13 at 18:29
6

You would probably need to create a custom map tool. For starters read about using the map canvas and creating map tools at the PyQGIS developer cookbook at https://docs.qgis.org/3.28/en/docs/pyqgis_developer_cookbook/canvas.html# .

The actual line drawing is done using rubber bands ( https://qgis.org/pyqgis/master/gui/QgsRubberBand.html ).

I have implemented a static version of your requirements. Is has hardcoded points for the points the user clicked and the mouse position. It assumes the current map layer is a projected map layer.

The points are in central Germany and the coordinates are in EPSG:3857.

I am not sure of the distance calculation, so please take it only as a demonstration and a hint on how it can be done, not as a final correct calculation, but it corresponds quite well with the QGIS interface length measurement.

My QGIS version is 3.28.3

def createDistanceAnnotation(p1, p2):
    """create an annotation of the distance between two points.
    
    Parameters p1 and p2 are of type QgsPoint 
    
    Adapted from https://gis.stackexchange.com/a/421040/15183 by CodeBard"""
    line = QgsLineString(p1, p2)
    line_geom = QgsGeometry(line)

    # calculate length of line
    d = QgsDistanceArea()
    d.setEllipsoid('WGS84')
    d.setSourceCrs(qgis.utils.iface.activeLayer().crs(), QgsProject.instance().transformContext())
    length = d.measureLength(line_geom)
    # calculate gradient of line to place label at an angle
    gradient = QgsGeometryUtils.gradient(p1, p2)
    # some math to convert gradient to an angle in degrees
    angle = math.degrees(math.atan(gradient))
    
    (x, y) = QgsGeometryUtils.perpendicularOffsetPointAlongSegment( p1.x(), p1.y(), p2.x(), p2.y(), 0.5, 400 )
    # distance Unit enum has changed in 3.30. See https://qgis.org/pyqgis/master/core/Qgis.html#qgis.core.Qgis.DistanceUnit .
    label = QgsAnnotationPointTextItem(d.formatDistance(length, 2,QgsUnitTypes.DistanceUnit.DistanceMeters), QgsPointXY(x, y))
    label.setAngle(angle)
    return label

def resetRB():
    """resets all created rubber bands and the annotation layer"""
    for rb in createdRubberBands:
        rb.reset()
    QgsProject.instance().mainAnnotationLayer().reset()
        

# this stores the points the user has already clicked on
clickedPoints = [
QgsPoint(1069980,6958263), # walsrode
QgsPoint(1053886,6890568), # neustadt
QgsPoint(922838,6936805) # vechta
]

# this stores the current mouse position
mousePosition = QgsPoint(978272,6929652) # middle

# this stores the rubber bands that are created between the clicked points and the mouse position
createdRubberBands = []

# the layer where the lengths will be shown on
annotation_layer = QgsProject.instance().mainAnnotationLayer()

# draw line between each clicked point and the mouse position and draw annotation
for p in clickedPoints:
    rb = QgsRubberBand(iface.mapCanvas() , QgsWkbTypes.LineGeometry)
    rb.setToGeometry(QgsGeometry.fromPolyline([p, mousePosition]), QgsCoordinateReferenceSystem("EPSG:3857"))
    rb.setColor(QColor(255, 0, 0))
    rb.setWidth(2)
    createdRubberBands.append(rb)
    annotation_layer.addItem(createDistanceAnnotation(p, mousePosition))
6
  • I'm not a programmer and I'm not very good with python. I mostly use ready-made tools. When I copy your code into the python console, I get a series of errors and I don't know what they are. If I understood correctly, I have to make a point layer with crs 3857 and then copy your code and execute it. When I do all that, nothing happens. My question was in the form of a suggestion to the developers. It would certainly be useful if someone made it in this discussion :-)
    – nagib
    Mar 13 at 6:51
  • No. You need a layer with EPSG:3857, select that and then run the code. The points are hardcoded in the code. It's a demonstration on what can be done, nothing you could use as-is.
    – til_b
    Mar 13 at 12:11
  • 1
    Be aware when measuring: measurements in EPSG:3857 are always wrong due to heavy distortion. You should either use a local CRS, apt for measuremnts in your area of interest or alternatively calculate elipsoidal distances. But this is another question that has been asked many times here, see e.g.: gis.stackexchange.com/a/438809/88814
    – Babel
    Mar 13 at 13:30
  • I thought the calculation using the QgsDistanceArea and using setEllipsoid('WGS84') would return ellipsoidal measurements and not cartesian ones, but, as I said, this needs to be checked again. Maybe I will flesh this out into a plugin and have that "peer reviewed".
    – til_b
    Mar 13 at 14:38
  • @til_b :I'm probably doing something wrong, this is part of error message: Python Console exec(Path('/var/folders/pb/lzwwd1_j1d57yt7_nvbcbgbc0000gn/T/tmpno4tqhwp.py').read_text()) Traceback (most recent call last): File "/Applications/QGIS 2.app/Contents/MacOS/../Resources/python/code.py", line 63, in runsource code = self.compile(source, filename, symbol) File "/Applications/QGIS 2.app/Contents/MacOS/../Resources/python/codeop.py", line 178, in call return _maybe_compile(self.compiler, source, filename, symbol) invalid syntax... ect. This code I tried on windows and macOS
    – nagib
    Mar 13 at 18:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.