3

With QGIS I need to build the shortest route between two points inside a polygon. I have a vector polygon layer and I need to build a path inside it from the starting point to the ending point. How can I do this?

I found that the vector layer needs to be rasterized, but I don’t understand what to do next. Existing tools are based on vector line layers. I studied so much information and nothing helps. I would write a Python module or use ready modules.

enter image description here

Taras
  • 32,823
  • 4
  • 66
  • 137
redhat
  • 347
  • 6
  • Yes, I have one separate polygon layer and one separate point layer. The point layer contains two points. I need to calculate "yellow line" - the shortest path between starting and ending points inside the polygon – redhat Jan 04 '24 at 18:33
  • Yes, it can go on polygon edges – redhat Jan 05 '24 at 11:05
  • Thanks all for answer! They are all very helpful and it is a pitty that I cannot choose all of them as the best – redhat Jan 14 '24 at 13:04

3 Answers3

6

There are probably better approaches available, however I am suggesting the following intuitive workaround for Vector Data.

Let's assume there is a point layer (two features), and a polygon layer (one feature), see the image below.

input

Step 1. Apply the "Polygon Divider" plugin to break the original polygon feature into pieces of equal areas. Alternatively, one can use the "Create grid" geoalgorithm and overlap it with the initial polygon feature

step1

Step 2. Select all objects from the previous output, without overlap with original points. Afterward, get "Centroids" for the selected pieces only

step21

step22

Step 3. "Extract vertices" of the original polygon

step3

Step 4. Merge original points with outcomes of Steps 2 and 3 with the "Merge vector layers" tool

step4

On this step it is vital to mention, that the next approach should be chosen wisely, depending on precision/tolerance/complexity etc.


Approach #1

Step 5. Triangulate points with the "Voronyi/Delaunay triangulation"

step5

Step 6. "Extract by location" triangles, that are within the original polygon

step6

Step 7. Convert these triangles to polylines, with "Polygons to lines"

step7

Step 8. "Explode lines" from the previous step, and also "Delete duplicate geometries"

step8

Step 9. Apply the "Shortest path (point to point)" and get the output like this:

result1

Simplify and/or smooth if needed.


Approach #2

Step 5. Use the "Geometry by expression" together with this expression:

collect_geometries(
    array_filter(
        array_foreach(
            overlay_nearest(
                layer:='points_to_test', -- change accordingly
                expression:=@geometry,
                limit:=10 -- adjust properly
                ),
            make_line($geometry, @element)
            ),
        within(@element, aggregate(
                                layer:='polygon', -- change accordingly
                                aggregate:='collect',
                                expression:=$geometry
                                )
            )
        )
    )

step5

Step 6. "Multipart to singleparts", and if required "Delete duplicate geometries"

step6

Step7. Apply the "Shortest path (point to point)" and get the output like this:

result2

Simplify and/or smooth if needed.


Approach #3

Step 5. Create all possible line connections between merged points like in this tread Creating all possible line segments between all points using QGIS. And after extract only hub lines that are within the original polygon. Do not forget about the spatial index. And also be careful with polygon edges, do not skip them. Apply the "Multipart to singleparts" if needed

step5

Step 6. Apply the "Shortest path (point to point)" and get the output like this:

result3


Once can also compare the results in the end. With some pros&cons one will find a suitable approach.

comparison


Notes:

  • for the Step 1 the area value has to be chosen wisely
  • this workflow was only performed on a single polygon and two points, not multiple inputs of each layer
  • this workflow can be converted into a QGIS-model or pure PyQGIS code for automized routine work
  • mind some issues, that could happen. This one evoked after diving the polygon issue1
  • In the first approach, triangulation result may not always correspond to the initial polygon issue2
  • In the second approach, some polygon sides can be skipped' issue3

References:

Taras
  • 32,823
  • 4
  • 66
  • 137
  • Thank you a lot! Could you explain what I'm doing wrong? I've created a new line layer with all possible connections between points through geomentry expression. And then I used "explode lines" function to split the connections into separate lines. But it didn't work. I have more than 1 line in an object when I selected it. – redhat Jan 07 '24 at 08:40
  • Yes. For a simple polygon is looks like https://disk.yandex.ru/i/WzMPo_TGwpLp2w (yellow lined, I tried to select only one line). And for more complicated like this https://disk.yandex.ru/i/l3lgnWjfBOZTmQ – redhat Jan 07 '24 at 10:02
  • I think I can use Edit Geometry => Split features tool instead of Explode Nodes for it – redhat Jan 07 '24 at 10:06
  • By any means, I will not open (hu)yandex. Слава Україні! Слава Нації! – Taras Jan 07 '24 at 10:12
  • @Taras great answer, +1! Could this workflow not not easier by creating less lines: in step 5, only connect each point to it's neighbours (if polygons of the centroid border each other) then to create all possible connections, even between far away points? – Babel Jan 12 '24 at 22:05
  • 2
    Would be nice to be able to upvote it again for the added optimisation! – Matt Jan 17 '24 at 18:14
  • Thank you, @Matt. You can upvote Babel's answer, that was his idea with the optimisation ^_^ – Taras Jan 17 '24 at 18:16
  • 1
    @Matt second upvote is not possible, but you could reward a bounty to Taras ;-) – Babel Jan 17 '24 at 18:55
  • A further simplification - for Approach #3, you can actually start at Step 5, using only the Start and Finish points, and the vertices of the original polygon (OK, you need Step 3!) to create the network. You don't actually need to create hundreds of new vertices, as they won't be part of the shortest path (or if they are, it is only by accident). The main thing to be careful of is that the lines all need to be single part, otherwise the Shortest Path algorithm doesn't work. Also, Approach #3 if carried out correctly should guarantee you the Shortest Path, unlike Approaches #1 & #2 – Tom Brennan Jan 18 '24 at 02:21
3

Basic solution: shortest path/least cost

A very easy way is a raster least cost analysis. The only cost is distance in this case what makes it very simple:

  1. Rasterize the polygon. Use a resolution small enogh that the smallest "bottleneck" of the polygon is still at least 3 to 5 times larger than the pixel size. For A fixed value to burn use 1.

  2. Install Least Cost Path plugin and run it (from Menu Processing > Toolbox > Cost Distance analysis) with the raster created before and the start- and end-point.

Variant: If the path should not run along the polygon's boundary, before step 1 apply a (small) negative buffer to the polygon.

Black shape: rasterized shape of the initial polygon; red line: shortest path: enter image description here

Variant: prefer path in the middle of the polygon

A bit more sophisticated, you can create a path that prefers a path in the middle of the polygon - e.g. to model a ship on a lake/river that always wants to stay in the middle where the water is deeper, not near the shore.

  1. Create a polygon grid with the same resolution as the raster. Select by location the cells that are within the polygon, inverse selection, delete selected to keep only grid cells completely inside the polygon.

  2. Use Field calculator to create a new attribute cost that measures the inverse distance from each grid cell to the shore with this expression:

     1/distance(
         centroid ($geometry),
         closest_point (
             overlay_within(
                 'polygon',
                 boundary($geometry)
             )[0],
             centroid ($geometry)
         )
     )
    
  3. Rasterize (as in step 1 above), but this time use Field to use for a burn-in value and select the attribute cost.

  4. Run Least Cost Path plugin as in step 2 above.

Yellow line shows the result of the refined approach; in the background the raster with values from white (high costs) to blue (low costs), so the algorithm tries to create the shortest distance path, but also tries to avoid the white cell and prefers the darker blue cells; see for comparision the red line created above: enter image description here

The line simplified and smoothed:

enter image description here

"Bottleneck" of the polygon (SSE of the red point in the picture above): the path prefers the darker pixels; pixel size is chosen in a way that even the bottleneck is covered with at least 2 to 3 neighboring pixels: enter image description here

Babel
  • 71,072
  • 14
  • 78
  • 208
  • I could repeat it, it's amazing! Thank you so much for help! May be you know if it is possible to configure smoothing so that the smoothed line does not intersect the boundaries of the original polygon? My polygon is long and quite winding, I apparently smoothed the line too much and went beyond the boundaries of the polygon. Maybe it is possible to set a condition - smooth as much as possible, but not go beyond the polygon? – redhat Jan 13 '24 at 19:29
  • You're welcome. This site tries to avoid "thank you" massages but rather incourages you to upvote the question or accept it if it helped to solve your problem: https://gis.stackexchange.com/help/someone-answers For the smoothing: no easy/short answer here in the comments, you should better ask this as a new question and provide sample data for testing. Consider the option with negative buffer to the initial polygon, then areas near the polygon's border are avoided at all. For simplifying and smoothing, consider using Geometry Generator as you can change settings without changing the geometry. – Babel Jan 13 '24 at 21:48
1

I don't have a correct answer, but the idea is like this

  1. Create a visibility graph
  2. Explore all the paths or use A* algo to find the shortest path.
  3. Create a line from that in QGIS.

Although the current solution doesn't give correct answer you can get the idea.

enter image description here

import networkx as nx
from qgis.core import *
import processing

from shapely.geometry import Polygon, Point, LineString from shapely.ops import unary_union

def visibility_graph_2(polygon, start, end): # Create graph G = nx.Graph()

# Add start and end points
G.add_node((start.x, start.y))
G.add_node((end.x, end.y))

# Add polygon vertices to graph
for vertex in polygon.exterior.coords:
    G.add_node(vertex)

# Connect visible nodes
for node1 in G.nodes:
    for node2 in G.nodes:
        if node1 != node2 and LineString([node1, node2]).intersects(polygon) and not LineString([node1, node2]).crosses(polygon):
            G.add_edge(node1, node2, weight=Point(node1).distance(Point(node2)))

return G

def visibility_graph(polygon, start, end, num_steiner_points=10): # Create graph G = nx.Graph()

# Convert start and end points to tuples
start = (start.x, start.y)
end = (end.x, end.y)

# Add start and end points
G.add_node(start)
G.add_node(end)

# Add polygon vertices to graph
for vertex in polygon.exterior.coords:
    G.add_node(vertex)

# Add Steiner points to graph
coords = list(polygon.exterior.coords[:-1])
for i in range(len(coords) - 1):
    for j in range(num_steiner_points):
        point = ((1 - j/num_steiner_points) * coords[i][0] + (j/num_steiner_points) * coords[i+1][0],
                 (1 - j/num_steiner_points) * coords[i][1] + (j/num_steiner_points) * coords[i+1][1])
        G.add_node(point)

# Connect visible nodes
for node1 in G.nodes:
    for node2 in G.nodes:
        if node1 != node2:
            line = LineString([node1, node2])
            # Only add the edge if the line is entirely inside the polygon
            if polygon.contains(line):
                G.add_edge(node1, node2, weight=Point(node1).distance(Point(node2)))

return G

def shortest_path(polygon, start, end, path_shortest_path): # Create visibility graph print('creating visibility graph') G = visibility_graph(polygon, start, end)

# Find shortest path
print('Find shortest path')
path = nx.dijkstra_path(G, source=(start.x, start.y), target=(end.x, end.y), weight='weight')

# Create a new memory layer to store the shortest path
print('Create a new memory layer to store the shortest path')
vl = QgsVectorLayer("LineString", "shortest_path", "memory")
pr = vl.dataProvider()

# Create a new feature
seg = QgsFeature()

# Add the shortest path to the feature as a polyline
seg.setGeometry(QgsGeometry.fromPolyline([QgsPoint(pt[0], pt[1]) for pt in path]))

# Add the feature to the layer
pr.addFeatures([seg])

# Update extent of the layer
vl.updateExtents()

# Write layer to a new shapefile
QgsVectorFileWriter.writeAsVectorFormat(vl, path_shortest_path, "utf-8", vl.crs(), "ESRI Shapefile")

return path

path_polygon = '/Users/manish.sahu/Downloads/polygon.shp' path_point = '/Users/manish.sahu/Downloads/point.shp' path_shortest_path = '/Users/manish.sahu/Downloads/path_shortest.shp'

polygon_layer = QgsVectorLayer(path_polygon, 'polygon', 'ogr') points_layer = QgsVectorLayer(path_point, 'point', 'ogr')

if not layer.isValid(): print("Layer failed to load!") else: QgsProject.instance().addMapLayer(layer)

Assuming 'polygon_layer' and 'points_layer' are your loaded layers

###################################

Get the first (and only) feature from the polygon layer

polygon_feature = polygon_layer.getFeature(0)

Convert the feature to a Shapely Polygon

polygon = Polygon(polygon_feature.geometry().asMultiPolygon()[0])

polygons = [p for p in polygon_feature.geometry().asMultiPolygon()]

Filter out polygons with less than 3 points

selected_polygon = Polygon(polygons[0][0])

Get the features from the points layer

points_features = points_layer.getFeatures()

Convert the features to Shapely Points

points = [Point(feature.geometry().asPoint()) for feature in points_features]

The start and end points are the first and second points

start, end = points[0], points[1]

Calculate the shortest path and write it to a shapefile

path = shortest_path(selected_polygon, start, end, path_shortest_path) ###################################

Create a new layer from the shapefile

output_layer = QgsVectorLayer(path_shortest_path, "Shortest Path", "ogr")

Check if the layer is valid

if not output_layer.isValid(): print("Layer failed to load!") else: # Add the layer to the project QgsProject.instance().addMapLayer(output_layer)

  • Thanks! It works with my example. But do you have any idea why it doesn't work with more complicated polygon? I had a very complicated and big poligon and I got the exception when single_source_dijkstra return multi_source_dijkstra and raise nx.NetworkXNoPath (no path to target). May be it find multiple routes? – redhat Jan 05 '24 at 09:06
  • It is because the algorithm is NOT considering the polygon intersection. Could be possible. Can you share your polygon here too? Can try with that – Manish Sahu Jan 05 '24 at 09:40
  • Yes, you can get it by link https://disk.yandex.ru/d/XMRgLqD4mxQPvw (Sorry if links are not permitted. I'm new here too). I pack it in an archive – redhat Jan 05 '24 at 11:03
  • Can upload zip file instead of rar? – Manish Sahu Jan 05 '24 at 18:33
  • Yes, I uploaded the new one https://disk.yandex.ru/d/ZX94mbxpK21JUA – redhat Jan 05 '24 at 20:19
  • Could you check the folder task2 in zip too, please? It can't calculate the shortest path for such polygon even it is not a negative buffer. I tried it with the first script – redhat Jan 05 '24 at 20:40
  • Could you check this error, please? I have an error when I try to convert my polygon into multipolygon and I cannot decide it – redhat Jan 13 '24 at 12:36