Find and plot your optimal path using OSM, Plotly and NetworkX in Python

A guide to finding the optimal path using OpenStreetMap, OSMnx & NetworkX and plot the detailed path with bends and curves using Plotly in Python.

Apurv Priyam
Towards Data Science

--

Source: Pexels, via pixabay

Many libraries can be used to plot a path using Google Maps API, but this leads to reduced flexibility. Also, if you use a set of lines to draw a path then, in a lack of better words, it doesn’t look good. Let me give you an example:

Generated using Plotly

Also, on many occasions, you may want some flexibility to not only change the desired path between two points (for example, instead of the shortest or fastest path given by google maps you want the path which satisfies your criteria) but also how you want to plot.

In this blog, I took an attempt to solve such a problem where not only we will find the optimal path but also plot them using Plotly. Let’s first start with importing some libraries.

import osmnx as ox
import networkx as nx
import plotly.graph_objects as go
import numpy as np

The OSMnx library helps to retrieve, analyze, and visualize street networks from OpenStreetMap. You can read more about OSMnx here. To demonstrate I will download the state boundary of Georgia, US and plot it using the OSMnx library.

state = ox.gdf_from_place('Georgia, US') ox.plot_shape(ox.project_gdf(state))
Georgia state map generated using OSMnx library

Now let’s download a map. This time we will download a more detailed map of the area surrounding Georgia Tech by providing an argument network_type = 'drive'. OSMnx downloads the map as a graph object which can easily be used by the NetworkX library.

# Defining the map boundaries 
north, east, south, west = 33.798, -84.378, 33.763, -84.422
# Downloading the map as a graph object
G = ox.graph_from_bbox(north, south, east, west, network_type = 'drive')
# Plotting the map graph
ox.plot_graph(G)
A road network graph showing the edges (roads) and nodes generated using OSMnx library

In the above graph, we can see all the nodes (blue) and edges (gray) representing the roads with exact shapes. Let’s take a deep-dive into the graph object we downloaded and see what the edges and nodes look like:

# Displaying the 3rd node 
list(G.nodes(data=True))[2]

(69128194,
{‘y’: 33.7692046,
‘x’: -84.390567,
‘osmid’: 69128194,
‘ref’: ‘249C’,
‘highway’: ‘motorway_junction’})

Above output shows the information of node id 69128194 with its x and y coordinates, OSM id, and type of node (a junction). Similarly, we can see the details of the edges:

# Displaying the 1st edge
list(G.edges(data=True))[1]

(69259268,
69508853,
{‘osmid’: 9270483,
‘name’: ‘Lindsay Street Northwest’,
‘highway’: ‘residential’,
‘oneway’: False,
‘length’: 145.352,
‘geometry’: })

This shows the details of the edge connecting node 69259264 to 69290452 along with its OSM id, name, type, oneway/twoway, length and one interesting element of type geometry.linestring. We can see how this geometry looks like.

# Displaying the shape of edge using the geometry list(G.edges(data=True))[1][2]['geometry']
Geometry of the edge

This shows that the edge is not a straight line but a curved path. We will use this information later while plotting this type of curved edges.

Now we will find the optimal path. For now, lets set our objective as to find the path with the smallest length. This can be changed to satisfy any criteria and will be covered in a separate blog. After deciding an origin and destination, first, we will need to get the closest nodes from these locations. This can be done using the function get_nearest_node from OSMnx.

# define origin and desination locations 
origin_point = (33.787201, -84.405076)
destination_point = (33.764135, -84.394980)
# get the nearest nodes to the locations
origin_node = ox.get_nearest_node(G, origin_point)
destination_node = ox.get_nearest_node(G, destination_point)
# printing the closest node id to origin and destination points origin_node, destination_node

(69425048, 2919090915)

Now, we can use the shortest_path function from the NetworkX library to get the optimal path which minimizes the total length using Dijkstra's algorithm. Notice that we have provided weight='length'. This function returns a list of ordered nodes in the path.

# Finding the optimal path 
route = nx.shortest_path(G, origin_node, destination_node, weight = 'length') route

[69425048,
69425021,
69466983,
69466977,
.
.

Let’s plot these nodes on a map. We will use the Plotly library for this. We will plot all these nodes and connect them with lines to represent a path. Let’s see how it looks. But first, let’s get the co-ordinates of these nodes

# getting coordinates of the nodes# we will store the longitudes and latitudes in following list 
long = []
lat = []
for i in route:
point = G.nodes[i]
long.append(point['x'])
lat.append(point['y'])

We will plot the path on a map multiple times so let’s define a function for this:

def plot_path(lat, long, origin_point, destination_point):

"""
Given a list of latitudes and longitudes, origin
and destination point, plots a path on a map

Parameters
----------
lat, long: list of latitudes and longitudes
origin_point, destination_point: co-ordinates of origin
and destination
Returns
-------
Nothing. Only shows the map.
"""
# adding the lines joining the nodes
fig = go.Figure(go.Scattermapbox(
name = "Path",
mode = "lines",
lon = long,
lat = lat,
marker = {'size': 10},
line = dict(width = 4.5, color = 'blue')))
# adding source marker
fig.add_trace(go.Scattermapbox(
name = "Source",
mode = "markers",
lon = [origin_point[1]],
lat = [origin_point[0]],
marker = {'size': 12, 'color':"red"}))

# adding destination marker
fig.add_trace(go.Scattermapbox(
name = "Destination",
mode = "markers",
lon = [destination_point[1]],
lat = [destination_point[0]],
marker = {'size': 12, 'color':'green'}))

# getting center for plots:
lat_center = np.mean(lat)
long_center = np.mean(long)
# defining the layout using mapbox_style
fig.update_layout(mapbox_style="stamen-terrain",
mapbox_center_lat = 30, mapbox_center_lon=-80)
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0},
mapbox = {
'center': {'lat': lat_center,
'lon': long_center},
'zoom': 13})
fig.show()

Let’s call the function and plot the path on the map:

plot_path(lat, long, origin_point, destination_point)
A map showing the shortest path using straight lines joining the nodes. Orange boxes show deviation of plotted lines from the actual roads. Generated using Plotly library.

This looks good. We have plotted a path from the source node to the destination node, and these nodes are very close to the actual co-ordinates we provided as the source and destination (shown using red and green markers respectively). But something is missing. Yes, the curves on road are replaced by straight lines joining the two nodes in the path. This can be observed in the orange boxes. If you remember the geometry element of the edges from above, we will use that to solve this issue.

To illustrate we will see the edge shape of this portion of the path. This path is between the 6th and 7th nodes from the end in out route variable. Let’s get the edge connecting these two nodes and we will see its shape.

# Getting the start and end node of this part 
start_node=route[-7]
end_node=route[-6]
# Getting the edge connecting these nodes and storing it as a list in z to maintain the data structure of G.edges
z = []
for i in list(G.edges(data=True)):
if (i[0]==start_node) & (i[1]==end_node):
z.append(i)

z[0][2]['geometry']
Geometry of one of the roads marked in the orange box in the previous figure

This looks exactly like the street inside the second orange box. So now we have a way to get these curved shapes to plot. We just have to get these shapes and replace them with our original route data. For this, we will define a function that does exactly this. It loops over all the edges and if a curved shape is present for that edge, it stores that curve.

def node_list_to_path(G, node_list):
"""
Given a list of nodes, return a list of lines that together
follow the path
defined by the list of nodes.
Parameters
----------
G : networkx multidigraph
route : list
the route as a list of nodes
Returns
-------
lines : list of lines given as pairs ( (x_start, y_start),
(x_stop, y_stop) )
"""
edge_nodes = list(zip(node_list[:-1], node_list[1:]))
lines = []
for u, v in edge_nodes:
# if there are parallel edges, select the shortest in length
data = min(G.get_edge_data(u, v).values(),
key=lambda x: x['length'])
# if it has a geometry attribute
if 'geometry' in data:
# add them to the list of lines to plot
xs, ys = data['geometry'].xy
lines.append(list(zip(xs, ys)))
else:
# if it doesn't have a geometry attribute,
# then the edge is a straight line from node to node
x1 = G.nodes[u]['x']
y1 = G.nodes[u]['y']
x2 = G.nodes[v]['x']
y2 = G.nodes[v]['y']
line = [(x1, y1), (x2, y2)]
lines.append(line)
return lines# getting the list of coordinates from the path
# (which is a list of nodes)
lines = node_list_to_path(G, route)long2 = []
lat2 = []
for i in range(len(lines)):
z = list(lines[i])
l1 = list(list(zip(*z))[0])
l2 = list(list(zip(*z))[1])
for j in range(len(l1)):
long2.append(l1[j])
lat2.append(l2[j])

If we compare the length of lat and lat2 we will notice that lat2 has more than 6 times the co-ordinates as compared to the lat.

print("Length of lat: ", len(lat))
print("Length of lat2: ", len(lat2))

Length of lat: 23
Length of lat2: 141

The last thing to do now is to plot the path and see how it looks.

plot_path(lat2, long2, origin_point, destination_point)
Final path generated using Plotly library

This looks much better. In this blog, we used libraries like OSMnx and Plotly to create our direction map. We used NetworkX to get the most optimal path based on our objective. Here we have used the most common objective – length, but this can be easily replaced.

Find the original post on my website apurv.page/plotthepath.html

--

--