Search
Search Menu

routingpy – Compare Providers

Estimated reading time:

Compare output of routing providers

In this example, we'll show you how easy it is to compare different routing providers with routingpy. We'll request directions, isochrones and matrices for all currently implemented routing engines and plot the output on folium maps.

Note, you'll have to have the appropriate API keys or comment out the providers you're not interested in.

In [1]:
import sys
from itertools import chain
import pandas as pd
import folium
import random
from shapely.geometry import box, Point
from colour import Color

from routingpy.routers import get_router_by_name

Create random routes

Just a quick function to create randomly chosen coordinate pairs within a bounding box (in this case Berlin). You can specify the amount of coordinate pairs (i.e. routes) and their distance ranges from start to destination.

In [2]:
# We'll need the bounding box throughout the notebook
bbox = [13.280066,52.459562,13.507532,52.576611]  # bbox Berlin
minx, miny, maxx, maxy = bbox
poly_berlin = box(*bbox)

def random_coordinates(n, min_dist, max_dist):
    assert min_dist < max_dist # make sure parameters are valid
    
    coordinates = []
    for _ in range(n):
        counter = 0
        in_poly = False
        while not in_poly:
            counter += 1
            x = random.uniform(minx, maxx)
            y = random.uniform(miny, maxy)
            p = Point(x, y)
            if poly_berlin.contains(p):
                # Make sure all route segments are within limits
                if coordinates:
                    if not min_dist < p.distance(Point(coordinates[-1])) < max_dist:
                        continue
                coordinates.append([x, y])
                in_poly = True
            if counter > 1000:
                raise ValueError("Distance settings are too restrictive. Try a wider range and remember it's in degrees.")

    return coordinates

Define all router clients

First we need to setup our router clients, i.e. provide the API keys, and specify which profile you want to route from. In this case, we chose car.

If you don't want to compare that many providers, simply comment out the ones you don't want to calculate.

In [3]:
routers = {
    'ors': {
        'api_key': '', 
        'profile': 'driving-car',
        'color': '#b5152b',
        'isochrones': True
    },
    'mapbox_osrm': {
        'api_key': '', 
        'profile': 'driving', 
        'color': '#ff9900',
        'isochrones_profile': '@mapbox/driving',
        'isochrones': True
    },
    'mapbox_valhalla': {
       'api_key': '',
       'profile': 'auto',
       'color': '#000000',
       'isochrones': True
    },
    'google': {
        'api_key': '',
        'profile': 'driving', 
        'color': '#ff33cc',
        'isochrones': False
    },
    'graphhopper': {
        'api_key': '', 
        'profile': 'car', 
        'color': '#417900',
        'isochrones': True
    },
    'heremaps': {
        'app_id': '', 
        'app_code': '',
        'profile': 'car',
        'color': '#8A2BE2',
        'isochrones': True
    }
}

Calculate Directions

First, let's create 2 random coordinate pairs in Berlin, which will serve as the source and destination points of the routes.

In [4]:
m = folium.Map(location=list(poly_berlin.centroid.coords[0]).reverse(), tiles='cartodbpositron')
m.fit_bounds([[miny, minx], [maxy, maxx]])

route_amount = 2
# distance for 1 degree in Berlin: ~ 110 km latitude, ~68 km longitude, 
# i.e. 3.4-7 km < distance < 6.8-11 km
input_pairs = [random_coordinates(n=2, min_dist=0.05, max_dist=0.1) for i in range(route_amount)]
for idx, pair in enumerate(input_pairs):
    for widx, waypoint in enumerate(pair):
        folium.Marker(list(reversed(waypoint)), 
                      popup='Start' if widx == 0 else 'Destination', 
                      icon=folium.Icon(color='green', icon="flag" if widx == 0 else "ok") if idx==0 else folium.Icon(color='blue', icon="flag" if widx==0 else "ok")).add_to(m)
m
Out[4]:

Now we can call the directions endpoints for all routing engines. Note, that we use get_router_by_name() here. Alternatively, you can also import each routing class individually from routingpy.

We'll include popups for the plotted routes with duration and distance information.

In [5]:
for router in routers:    
    group = folium.FeatureGroup(name=router, show=True)
    if router == 'heremaps':
        api = get_router_by_name(router)(app_id=routers[router]['app_id'], app_code=routers[router]['app_code'])
    else:
        api = get_router_by_name(router)(api_key=routers[router]['api_key'])
    
    for coords_pair in input_pairs:

        # just from A to B without intermediate points
        route = api.directions(
            profile=routers[router]['profile'],
            locations=coords_pair
        )
        
        # Access the route properties with .geometry, .duration, .distance
        distance, duration = route.distance / 1000, int(route.duration / 60)
        folium.PolyLine(
            locations=[list(reversed(coords)) for coords in route.geometry],
            color='#ffffff',
            weight=10,
        ).add_to(group)

        popup_html = """
            <h4>Router: {0}</h4><br><br>
            Distance: {1:.3f} km,<br>
            Duration: {2} minutes
        """.format(router, distance, duration)
        folium.PolyLine(
            locations=[list(reversed(coords)) for coords in route.geometry],
            color=routers[router]['color'],
            weight=3,
            popup=popup_html
        ).add_to(group)
    group.add_to(m)
    
    print("Calulated {}".format(router))

folium.LayerControl().add_to(m)
m
Calulated ors
Calulated mapbox_osrm
Calulated mapbox_valhalla
Calulated google
Calulated graphhopper
Calulated heremaps
Out[5]:

Calculate Isochrones

First, let's create 5 random points in Berlin, which will serve as the center coordinate for our isochrones.

In [6]:
m = folium.Map(location=list(poly_berlin.centroid.coords[0]).reverse(), tiles='cartodbpositron')
m.fit_bounds([[miny, minx], [maxy, maxx]])

# coordinates for 2 locations
# distance for 1 degree in Berlin: ~ 110 km latitude, ~68 km longitude, 
# i.e. 6.8 km < distance < 13.4 km
input_isochrones = random_coordinates(n=2, min_dist=0.1, max_dist=0.2)
for idx, location in enumerate(input_isochrones):
    folium.Marker(list(reversed(location)), 
                  popup='Center: ' + str(idx),
                  icon=folium.Icon(color="red", icon="star")).add_to(m)

m
Out[6]:

Unfortunately, the isochrones methods are not as consistent as the other endpoints. Graphhopper does not allow for arbitrary ranges, while all others do. And Mapbox did the glorious decision to name their isochrones profiles different than their directions profiles. Here, it's interesting to note though, that their OSRM isochrone extension is not supported anymore and instead their isochrone endpoint runs on Valhalla (as you will see when you compare the Mapbox OSRM and Valhalla isochrones in the map below).

In [7]:
for router in routers:
    if routers[router]["isochrones"]:
        isochrones_group = folium.FeatureGroup(name=router, show=True)
        for location in input_isochrones:
            
            # HERE works with different credential system than the rest
            if router == 'heremaps':
                api = get_router_by_name(router)(app_id=routers[router]['app_id'],
                                                 app_code=routers[router]['app_code'])
            else:
                api = get_router_by_name(router)(api_key=routers[router]['api_key'])
                
            # Mapbox decided to call their isochrones profiles different from their directions profiles
            # Why, exactly?
            profile = routers[router].get('isochrones_profile') or routers[router]['profile']
            
            if router == 'graphhopper':
                isochrones = api.isochrones(
                    profile=profile,
                    # note: graphhopper just takes one interval which 
                    # can be split into equal buckets with the below parameter
                    intervals=[600],
                    buckets=3,
                    locations=location
                )
            else:
                isochrones = api.isochrones(
                    profile=profile,
                    intervals=[120,300,420,600],
                    locations=location,
                )
            green = Color("green")
            red = Color("red")
            colors = list(green.range_to(red, len(isochrones)))
            for idx, isochrone in reversed(list(enumerate(isochrones))):
                popup_html = """
                    <h4>Router: {0}</h4><br><br>
                    Interval: {1} seconds,<br>
                    Center: {2}
                """.format(router, isochrone.interval, isochrone.center)
                folium.Polygon(
                    locations=[list(reversed(coords)) for coords in isochrone.geometry],
                    fill_color=colors[idx].hex_l,
                    color=colors[idx].hex_l,
                    fill=True,
                    popup=popup_html
                ).add_to(isochrones_group)
            isochrones_group.add_to(m)
        
        print("Calulated {}".format(router))

folium.LayerControl().add_to(m)
m
Calulated ors
Calulated mapbox_osrm
Calulated mapbox_valhalla
Calulated graphhopper
Calulated heremaps
Out[7]: