First of, this question is related to this one: Snapping lines' start_point/end_points to the closest point features to them. I am trying to modify my answer to make it work with MultiLineStrings.
Basically I have two inputs:
- MultiLineString Layer
- MultiPoint Layer
I am now trying to snap the start and end points of each part of the MultiLines to the closest Point. Therefore the MultiPoints are converted to SinglePoints and the nearest points geometry is beeing read. Now the issue is, that it seems like I am not able to modify the Parts of the MultiLineString by using
part.setXAt()
part.setYAt()
part.moveVertex()
Link to the QGIS-Docs. Debugging all this using print()
statements actually return the correct results. Start and endpoints seem to be modified and the correctly modified LineString is printed.
However, there are no changes made in the actual geometries. The input lines just stay as they were before running the script, no error, no nothing. So my question is: why doesnt my script commit the changes? And in case setXAt()
and setYAt()
are not meant to change things, what are they for and what could I use instead?
Here is the entire code:
points = QgsProject.instance().mapLayersByName('points')[0]
lines = QgsProject.instance().mapLayersByName('lines')[0]
tolerance = 10000 # snapping tolerance in CRS units
if QgsWkbTypes.isMultiType(points.wkbType()): # if point-input is of type multigeometry
res = processing.run("native:multiparttosingleparts",{'INPUT':points,'OUTPUT':'TEMPORARY_OUTPUT'}) # convert to singlepoint
singlepoints = res['OUTPUT']
else:
singlepoints = points
# creat the spatial index of the pointlayer
points_idx = QgsSpatialIndex(singlepoints.getFeatures())
with edit(lines):
for i, feat in enumerate(lines.getFeatures()):
for j, part in enumerate(geom.parts()):
# get start and end point of line
line_startgeom = QgsPointXY(part.startPoint())
line_endgeom = QgsPointXY(part.endPoint())
print('\n')
print('Feat: ' + str(i) + ' Part: ' + str(j) + ' OriginalLine: ' + str(part))
# find nearest point of point layer to start and endpoint of line
nearestneighbor_start = points_idx.nearestNeighbor(line_startgeom, neighbors=1, maxDistance=tolerance)
nearestneighbor_end = points_idx.nearestNeighbor(line_endgeom, neighbors=1, maxDistance=tolerance)
if len(nearestneighbor_start) > 0:# only adjust the startpoint if there is a nearest point within the maxdistance
nearpoint_startgeom = singlepoints.getFeature(nearestneighbor_start[0]).geometry() # get the geometry of the nearest point in pointlayer
part.setXAt(0, nearpoint_startgeom.asPoint().x())
part.setYAt(0, nearpoint_startgeom.asPoint().y())
if len(nearestneighbor_end) > 0:
nearpoint_endgeom = singlepoints.getFeature(nearestneighbor_end[0]).geometry()
part.setXAt(-1, nearpoint_endgeom.asPoint().x())
part.setYAt(-1, nearpoint_endgeom.asPoint().y())
#part.moveVertex(QgsVertexId(-1,-1,-1),QgsPoint(nearpoint_endgeom.asPoint())) # honestly, QgsVertexId is not well documented, so I have no idea how this is supposed to work...
#print(str(part.xAt(-1))+ '\n' + str(nearpoint_endgeom.asPoint())+'\n')
print('Feat: ' + str(i) + ' Part: ' + str(j) + ' ModifiedLine: ' + str(part))
lines.updateFeature(feat)
lines.commitChanges()
And an example output from the print statement:
Feat: 2 Part: 3 OriginalLine: <QgsLineString: LineString (689753.69139794236980379 5338632.41991709917783737, 697525.0708597571356222 5339841.7065342990681529, 695706.26743028790224344 5337553.34959724545478821)>
Feat: 2 Part: 3 ModifiedLine: <QgsLineString: LineString (688633.31491136807017028 5338762.2247987687587738, 697525.0708597571356222 5339841.7065342990681529, 695385.03714443475473672 5337152.7669952092692256)>
As you can see it prints different line strings, but does not change geometries.
Best Answer
About why it does not modify the actual vertices, here a quote from the docs --> its a read only copy:
I guess, even thoug I do not use
vertices()
,part.setXAt()
,part.setYAt()
andpart.moveVertex()
apply to it.What you can do instead:
I guess there is a more intuitive way to do this, but for now I am stepping back and make use of
QgsVectorLayerEditUtils(layer).moveVertex()
again. Luckily Python has dictionarys which can be used for a lot of stuff, just like this:So basically I am iterating over each vertex of each part of each feature and check wheter the current vertex is a start- or an endpoint of the current part. If so, I'll search for the nearest point, read its geometry and store this geometry as value in the dictionary and the index of the current vertex as key. So I get a dictionary of vertexindizes I want to move (keys) and points where these vertices should be moved to (values). When iterating over this dictionary, I can go back using
QgsVectorLayerEditUtils(layer).moveVertex(value.x(),value.y(),feat.id(),key)
instead of the not workingpart.moveVertex()
,part.setXAt()
orpart.setYAt()
methods. Simple as that; here is the complete code for reference with comments as further explanations: