5

I am working with vector polygons that have derived from hand drawn areas from when mapping was carried out with tracing paper over paper maps. Therefore many of the polygons don't exactly line up to the boundaries on the ground.

Another dataset of vector lines I have is accurate and so I have been manually snapping nodes of the polygons to the nodes of the lines. But this is a lengthy process.

Here is a screenshot...

enter image description here

The green line is the accurate dataset. You can see how the polygon boundaries (orange) are roughly correct. Bottom left I have started to edit the nodes, with the snapping options set up, and this works to manually move the nodes of the polygons to snap to the nodes of the line. (Yes, I could have enabled topographical editing to move both the polygon boundaries together, and I do do that).

Is there a method for snapping multiple nodes of the polygon to the nodes of the lines, or is there perhaps a better method of doing this? Using Python perhaps? I am looking for something akin to the 'Enable tracing' function (which is great) but for moving polygon boundaries.

I am aware that this may in places be a duplicate as there are many similar questions, but I haven't quite found anything that answers my particular situation? In particular I have looked at...

QGIS how to align existing polygon borders

How to snap polygons to lines?

How to snap lines to nearest point?

https://gis.stackexchange.com/search?q=how+to+snap+many+polygons+to+lines

8
  • 1
    Very interesting question! Are you restricted to snap the existing polygon features or is it possible to create a new polygon layer?
    – mgri
    Commented Jun 22, 2017 at 17:56
  • @mgri It is a shared dataset, so others also make edits. There is also a workflow that allows you to add polygons, but not delete them, this adds a layer of protection to the data so we don't inadvertently lose data. However, I do have admin rights onto the data so it would be possible to delete a polygon and then copy and paste a new one in from a new polygon layer. But I guess I would rather find a solution that doesn't involve extra work...if it exists? Commented Jun 22, 2017 at 18:11
  • 1
    Ok, I get it. So, you would prefer to directly edit the geometries and apply the changes directly to the original layer. If I find a solution, I will share it.
    – mgri
    Commented Jun 22, 2017 at 18:26
  • Use the split features tool to cut the polygons along the green line, then merge the splinter polygons to the correct polygon.
    – csk
    Commented Jun 23, 2017 at 21:09
  • @csk Yes of course it works, such a simple solution. Can you post your comment as an answer so I can accept it? Commented Jun 24, 2017 at 17:07

2 Answers 2

5

Preamble

I propose a solution using PyQGIS. This is a complex issue and I'm sure that there are (many) specific cases that I didn't consider in my approach (so, take this answer as a starting point rather than a complete solution).

I didn't deeply test the code since I was not sure about the specific issue (see my unanswered comment where I requested some explanations), so please do some tests for validating it: if you are going to use the first attached code, create a backup copy of your layers because the edits are immediately committed to the geometry; instead, if you want to do some test without affecting the original layer, try to use the last attached code (both codes are the same, the unique difference is that the first one commits the edits on the layer itself, while the second one commits them to a new memory layer).

Finally, let's assume to start from this situation, where the red line is the guideline and the two polygons are two separate layers (each one having one feature):

enter image description here


Workflow explanation

The solution requires a line layer as a guideline and a set of polygon layers that need to be snapped. My approach iterates over the nodes of every polygon feature and tries to make a collage with the line geometry by deleting the unnecessary vertices and adding new ones.

This approach is quite simple to understand, but there are some things to consider:

  • while the polygon vertices are automatically ordered clockwise when digitizing (i.e. there is only one possible direction), a line layer can be digitized in two different directions according to which side you start from (the final direction is obviously unique, but you don't know if it follows the same order of the polygon vertices);
  • as a consequence of the previous point, it is very easy creating features that are geometrically wrong and it basically depends on how you are able to consider all the different situations that may occur (this is the part where my solution needs to be improved).

Having said that, my workflow could be summarized in this way:

  1. read the current line feature geometry as a sequence of coordinates (they are returned in a list);
  2. read the current polygon feature geometry as a sequence of coordinates (they are returned in a list);
  3. from the current polygon feature, find the closest vertex to both line start and line end;
  4. edit the list from point 2 by assigning the coordinates within the list from point 1 in the proper position.

As you can understand, the main issues are in the last step because it didn't seem too easy enumerating all the possible cases (it would be easier if you provide a sample dataset).


Solution with edits applied to the original layer

You may run the following code as a new script from the Processing Toolbox:

##input_guideline=vector line
##input_polygons=multiple vector

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

polygons = input_polygons.split(';')
line = QgsVectorLayer(input_guideline, input_guideline, 'ogr')

for linefeat in line.getFeatures():
    line_geom = linefeat.geometry()
    geom_to_add = line_geom.asPolyline()
    (first_line_point, end_line_point) = (line_geom.interpolate(0), line_geom.interpolate(-1))

    for polygon in polygons:
        poly = QgsVectorLayer(polygon, polygon, 'ogr')
        for feat in poly.getFeatures():
            attrs = feat.attributes()
            geom = feat.geometry()
            coords = geom.asPolygon()[0]

            first_vertex=geom.closestVertex(first_line_point.asPoint())[0]
            last_vertex=geom.closestVertex(end_line_point.asPoint())[0]
            first_indices = [i for i, x in enumerate(coords) if x == first_vertex]
            first_index = first_indices[0]
            second_index = coords.index(last_vertex)
            if first_index < second_index:
                coords[first_index:second_index] = geom_to_add
                new_geom = QgsGeometry.fromPolygon([coords])
                if not new_geom.isGeosValid():
                    first_index = first_indices[-1]
                    if first_index < second_index:
                        coords = geom.asPolygon()[0]
                        coords[second_index:first_index] = (geom_to_add)
                        new_geom = QgsGeometry.fromPolygon([coords])
                    else:
                        coords = geom.asPolygon()[0]
                        coords[second_index:first_index] = reversed(geom_to_add)
                        new_geom = QgsGeometry.fromPolygon([coords])  
            else:
                coords[first_index:second_index] = reversed(geom_to_add)
                new_geom = QgsGeometry.fromPolygon([coords])
                if not new_geom.isGeosValid():
                    first_index = first_indices[-1]
                    coords = geom.asPolygon()[0]
                    coords[second_index:first_index] = reversed(geom_to_add)
                    new_geom = QgsGeometry.fromPolygon([coords])

            poly.dataProvider().changeGeometryValues({feat.id(): new_geom})
iface.mapCanvas().refreshAllLayers()

and you will get this result:

enter image description here

If everything works, the edited polygons will perfectly match the guideline and have both the edges and the vertices perfectly aligned to it.


Solution with edits applied to a new layer

This solution differs from the previous one because saves the edits to a new memory layer instead of committing the changes to the original layer (it could be useful for testing):

##input_guideline=vector line
##input_polygons=multiple vector

from qgis.core import *

polygons = input_polygons.split(';')
line = QgsVectorLayer(input_guideline, input_guideline, 'ogr')

for linefeat in line.getFeatures():
    line_geom = linefeat.geometry()
    geom_to_add = line_geom.asPolyline()
    (first_line_point, end_line_point) = (line_geom.interpolate(0), line_geom.interpolate(-1))

    for polygon in polygons:
        poly = QgsVectorLayer(polygon, polygon, 'ogr')

        # Create the output layer
        crs = poly.crs().toWkt()
        outLayer = QgsVectorLayer('Polygon?crs='+ crs, 'snapped' , 'memory')
        prov = outLayer.dataProvider()
        fields = poly.pendingFields() # Fields from the input layer
        prov.addAttributes(fields) # Add input layer fields to the outLayer
        outLayer.updateFields()


        for feat in poly.getFeatures():
            attrs = feat.attributes()
            geom = feat.geometry()
            coords = geom.asPolygon()[0]

            first_vertex=geom.closestVertex(first_line_point.asPoint())[0]
            last_vertex=geom.closestVertex(end_line_point.asPoint())[0]
            first_indices = [i for i, x in enumerate(coords) if x == first_vertex]
            first_index = first_indices[0]
            second_index = coords.index(last_vertex)
            if first_index < second_index:
                coords[first_index:second_index] = geom_to_add
                new_geom = QgsGeometry.fromPolygon([coords])
                if not new_geom.isGeosValid():
                    first_index = first_indices[-1]
                    if first_index < second_index:
                        coords = geom.asPolygon()[0]
                        coords[second_index:first_index] = (geom_to_add)
                        new_geom = QgsGeometry.fromPolygon([coords])
                    else:
                        coords = geom.asPolygon()[0]
                        coords[second_index:first_index] = reversed(geom_to_add)
                        new_geom = QgsGeometry.fromPolygon([coords])  
            else:
                coords[first_index:second_index] = reversed(geom_to_add)
                new_geom = QgsGeometry.fromPolygon([coords])
                if not new_geom.isGeosValid():
                    first_index = first_indices[-1]
                    coords = geom.asPolygon()[0]
                    coords[second_index:first_index] = reversed(geom_to_add)
                    new_geom = QgsGeometry.fromPolygon([coords])

            outGeom = QgsFeature()
            outGeom.setAttributes(attrs)
            outGeom.setGeometry(new_geom)
            prov.addFeatures([outGeom])

        # Add the layer to the Layers Panel
        QgsMapLayerRegistry.instance().addMapLayer(outLayer)
5
  • 1
    Thanks mgri. I'll have a try of what you have posted. It look like you have put a lot of work in, which is much appreciated. Sorry for unanswered question, also was busy, end of quarter deadlines. The polygons are separate polygons. Commented Jul 2, 2017 at 16:56
  • @MartinHügi the real challenge was thinking the best method for doing the job (this is only the last one and the most convincing).This solution could be a good path to follow, but I'm quite sure it won't work for all the polygons you have. I'm curious to read your feedback.
    – mgri
    Commented Jul 2, 2017 at 18:09
  • Wow! That's amazing. It works fantastically when I create a basic test version. With real datasets I haven't managed to get it to work just yet, the line dataset is complex. How's the best method to share data. Contact you through your blog? Commented Jul 4, 2017 at 18:21
  • @MartinHügi I expected some wrong results... As I said, this is not an easy task. If you want, feel free to contact me through the blog and I will give a look to your files.
    – mgri
    Commented Jul 4, 2017 at 18:40
  • @MartinHügi I assumed the input layers were linestring or polygon vectors, while yours maybe are multilinestring or multipoligon objects.
    – mgri
    Commented Jul 4, 2017 at 18:56
1

As well as using csk's method of splitting polygons and then merging I have found that one method is to select multiple nodes, and with the snapping options set appropriately, move them all towards the vector line.

This works best when there is a gap between the polygon and the line, rather than an adjacent polygon that can be split.

enter image description here

You need to hold down 'Ctrl' at the same time as selecting each node.

It can be easier to use the 'Vertex Editor' to select multiple nodes, again using the 'Ctrl'key, rather than mouse clicks as a slight move of the mouse when clicking moves them and then deselects them and you have to start all over again.

It is a quick solution, but not perfect as it does not add, or subtract, nodes to exactly match the vector line. But with thousands of polygons like this, it is relatively fast to get something that will do in most cases for what I need.

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.