5

I have a number of existing points which are snapped to a line, however they are unevenly spaced along the line, like this:

enter image description here

How do I align them so they are evenly spaced out along the line like this:

enter image description here

I know how to create new, evenly spaced out points along a line, however these are existing points which have attribute info that I want to keep.

I am using QGIS 3.14.16.

  • Is it only for this instance or are there many more cases? You could give the original points a unique ID and then make a new data set of points that are alligned as you want with the same ID's. Then it is a simple join – MarcM Oct 16 at 14:13
  • 2
    That's just an example of what I want to do, it's for over 300 points but it is only in two locations. The points already have a unique ID but it is not in order along the line. – TeddyTedTed Oct 16 at 14:17
7
+50

I've put together a Python script which should do what you want here.

Open the Python console, and use the tool bar icon outlined in red below to open a new editor.

enter image description here

Copy the code below and paste into the editor (not the Python prompt) and use the icon outlined in red below (green arrow) to run the code.

enter image description here

*A couple of notes:

  • Both your line and point layers should be in a projected CRS (the resulting memory layer containing the evenly spaced points will be created with the same CRS as the original point layer.
  • You will need to edit the first 2 (executed) lines of code to match the names of your line and point layers.
'''This script creates evenly spaced points along lines from points which
lie along the line's geometry but are randomly spaced. Order of point features
from the start point of the line are preserved. Fields and attributes are also
copied from the original point layer and added to the resulting memory layer'''

# Edit the 2 lines below to match the names or your line & point layers
line_layer_name = 'Line_layer_A'
point_layer_name = 'Random points on lines'

project = QgsProject().instance()
lines = project.mapLayersByName(line_layer_name)[0]
points = project.mapLayersByName(point_layer_name)[0]
line_fts = [f for f in lines.getFeatures()]
point_fts = [f for f in points.getFeatures()]
type_crs = 'Point?crs={}'.format(points.crs().authid())
temp_layer = QgsVectorLayer(type_crs, 'Spaced_points', 'memory')
temp_layer.dataProvider().addAttributes([f for f in points.fields()])
temp_layer.updateFields()

def distance_to_point(line_feature, point_xy):
    '''Returns the distance from the start point of a line feature to a point
    which lies along the line's geometry. In this script, we use this value to
    preserve the order of the unevenly spaced points along each line feature'''
    geom = line_feature.geometry()
    verts = [v for v in geom.constGet().vertices()]
    point, minDistPoint, nextVertex, leftOf = geom.closestSegmentWithContext(point_xy)
    start_to_vert_after = geom.distanceToVertex(nextVertex)
    point_to_vert_after = minDistPoint.distance(QgsPointXY(verts[nextVertex]))
    distance_of_point_along_line = start_to_vert_after - point_to_vert_after
    return distance_of_point_along_line

with edit(temp_layer):
    for l in line_fts:
        line_geom = l.geometry()
        snapped_points = [p for p in point_fts if line_geom.buffer(0.01, 4).intersects(p.geometry())]
        if snapped_points:
            dist = {}
            for p in snapped_points:
                dist[p] = distance_to_point(l, p.geometry().asPoint())
            # guard against division by zero error
            if len(snapped_points) == 1:
                feat = QgsFeature()
                feat.setAttributes(snapped_points[0].attributes())
                new_geom = line_geom.interpolate(line_geom.length()/2)
                feat.setGeometry(new_geom)
                temp_layer.addFeature(feat)
            else:
                interp_dist = line_geom.length()/(len(snapped_points)-1)
                if len(dist) > 0:
                    n = 0
                    for k, v in {k: v for k, v in sorted(dist.items(), key=lambda item: item[1])}.items():
                        feat = QgsFeature()
                        feat.setAttributes(k.attributes())
                        new_geom = line_geom.interpolate(n)
                        feat.setGeometry(new_geom)
                        temp_layer.addFeature(feat)
                        n = n + interp_dist-0.001
            
project.addMapLayer(temp_layer)

Results:

Original, randomly spaced points along line features

enter image description here

After running script

enter image description here

| improve this answer | |
  • 1
    Wow thanks so much. I just tried it with my own layer names but I got an invalid syntax error: imgur.com/0lT0ycy – TeddyTedTed Oct 19 at 8:29
  • 2
    @TeddyTedTed, Oddly enough I get the same error if I simply paste the entire code after the >>> Python prompt and hit enter- I'm buggered if I know why. You should actually open a Python editor, paste the code and run it with the 'run' icon. I have edited my answer to show you the process. When running from an editor I had no issues. That's what I did originally (since I wrote the script in an editor) and I just tested it again. The editor will also allow you to save the script as a .py file to reuse whenever you want. Hope it works now! – Ben W Oct 19 at 10:19
  • 1
    Perfect, that worked when I ran it the way you suggested. Thank you. Bounty incoming once it becomes available. – TeddyTedTed Oct 19 at 10:52
  • 2
    @TeddyTedTed, that's awesome, thank you! I'm very glad it's working for you. – Ben W Oct 19 at 11:01
3

Here is a slighlty modified version, that transforms the layer in-place. It also works if the line layer and point layer have different CRS :

def space_evenly_along_line(point_layer, line_layer):

    points_by_line = {line.geometry(): [] for line in line_layer.getFeatures()}

    # Reproject geometries if neeeded
    if point_layer.crs() != line_layer.crs():
        transform = QgsCoordinateTransform(
            line_layer.crs(), point_layer.crs(), QgsProject.instance()
        )
        for k in points_by_line:
            k.transform(transform)

    # Sort point features by line feature
    for f in point_layer.getFeatures():
        min_dist = None
        line_feature = None
        for k in points_by_line:
            dist = k.distance(f.geometry())
            if min_dist is None or dist < min_dist:
                min_dist = dist
                line_geometry = k
        points_by_line[line_geometry].append(f)

    # Sort by distance along line
    for line_geometry, point_features in points_by_line.items():
        point_features.sort(key=lambda x: line_geometry.lineLocatePoint(x.geometry()))

    with edit(point_layer):
        point_layer.beginEditCommand("Even spacing")
        for line_geometry, point_features in points_by_line.items():
            total_dist = line_geometry.length()

            # No Feature, nothhing to do
            if len(point_features) == 0:
                continue

            # Only one feature place it in the middle
            if len(point_features) == 1:
                point_layer.changeGeometry(
                    point_features[0].id(), line_geometry.interpolate(total_dist / 2)
                )
                continue

            # Move each feature along the line
            step = total_dist / (len(point_features) - 1)
            for i, point in enumerate(point_features):
                point_layer.changeGeometry(
                    point.id(), line_geometry.interpolate(step * i)
                )

        point_layer.endEditCommand()

Exemple Usage

>>> project = QgsProject().instance()
>>> points = project.mapLayersByName("Points")[0]
>>> lines_3857 = project.mapLayersByName("Lines EPSG:3857")[0]
>>> space_evenly_along_line(points, lines_3857)

space_evenly_along_line

| improve this answer | |

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.