avoid obstacles with openrouteservice

In this Jupyter example, we’d like to showchase how to use the direction API and to avoid a number of construction sites while routing.

Avoiding construction sites dynamically

Note: All notebooks need the environment dependencies as well as an openrouteservice API key to run

In this example, we'd like to showcase how to use the directions API and to avoid a number of construction sites while routing.

The challenge here is to prepare the data appropriately and construct a reasonable GET request.

In [1]:
import folium
import pyproj
import requests
from openrouteservice import client
from shapely import geometry
from shapely.geometry import Point, LineString, Polygon, MultiPolygon

Rostock is beautiful, but, as in most other pan-European cities, there are a lot of construction sites. Wouldn't it be great if we could plan our trip avoiding these sites and consequently save lots of time!?

Construction sites in Rostock

We take the open data from the Rostock authorities. It's hard (to impossible) to find construction site polygons, so these are points, and we need to buffer them to a polygon to be able to avoid them when they cross a street.

For the investigatory in you: yes, no CRS is specified on the link (shame on you, Rostock!). It's fair enough to assume it comes in WGS84 lat/long though (my reasoning: they show Leaflet maps plus GeoJSON is generally a web exchange format, and many web clients (Google Maps, Leaflet) won't take CRS other than WGS84). Since degrees are not the most convenient unit to work with, let's first define a function which does the buffering job with UTM32N projected coordinates:

In [13]:
url = 'https://geo.sv.rostock.de/download/opendata/baustellen/baustellen.json'


def create_buffer_polygon(point_in, resolution=10, radius=10):
    convert = pyproj.Transformer.from_crs("epsg:4326", 'epsg:32632')  # WGS84 to UTM32N
    convert_back = pyproj.Transformer.from_crs('epsg:32632', "epsg:4326")  # UTM32N to WGS84
    point_in_proj = convert.transform(*point_in)
    point_buffer_proj = Point(point_in_proj).buffer(radius, resolution=resolution)  # 10 m buffer

    # Iterate over all points in buffer and build polygon
    poly_wgs = []
    for point in point_buffer_proj.exterior.coords:
        poly_wgs.append(convert_back.transform(*point))  # Transform back to WGS84

    return poly_wgs
In [14]:
# Set up the fundamentals
api_key = 'your_key'  # Individual api key
ors = client.Client(key=api_key)  # Create client with api key
rostock_json = requests.get(url).json()  # Get data as JSON

map_params = {'tiles': 'Stamen Toner',
              'location': ([54.13207, 12.101612]),
              'zoom_start': 12}
map1 = folium.Map(**map_params)

# Populate a construction site buffer polygon list
sites_poly = []
for site_data in rostock_json['features']:
    site_coords = site_data['geometry']['coordinates']
    folium.features.Marker(list(reversed(site_coords)),
                           popup='Construction point<br>{0}'.format(site_coords)).add_to(map1)

    # Create buffer polygons around construction sites with 10 m radius and low resolution
    site_poly_coords = create_buffer_polygon(site_coords,
                                             resolution=2,  # low resolution to keep polygons lean
                                             radius=10)
    sites_poly.append(site_poly_coords)

    site_poly_coords = [(y, x) for x, y in site_poly_coords]  # Reverse coords for folium/Leaflet
    folium.vector_layers.Polygon(locations=site_poly_coords,
                                 color='#ffd699',
                                 fill_color='#ffd699',
                                 fill_opacity=0.2,
                                 weight=3).add_to(map1)

map1
Out[14]:
Make this Notebook Trusted to load map: File -> Trust Notebook

That's a lot of construction sites in Rostock! If you dig into the properties of the JSON, you'll see that those are kept up-to-date though. Seems like an annoying place to ride a car...

Anyways, as you might know, a GET request can only contain so many characters. Unfortunately, > 80 polygons are more than a GET can take (that's why we set resolution = 2). Because there's no POST endpoint available currently, we'll have to work around it:

One sensible thing one could do, is to eliminate construction zones which are not in the immediate surrounding of the route of interest. Hence, we can request a route without construction sites, take a reasonable buffer, filter construction sites within the buffer and try again.

Let's try this:

In [15]:
# GeoJSON style function
def style_function(color):
    return lambda feature: dict(color=color,
                                weight=3,
                                opacity=0.5)


# Create new map to start from scratch
map_params.update({'location': ([54.091389, 12.096686]),
                   'zoom_start': 13})
map2 = folium.Map(**map_params)

# Request normal route between appropriate locations without construction sites
request_params = {'coordinates': [[12.108259, 54.081919],
                                  [12.072063, 54.103684]],
                  'format_out': 'geojson',
                  'profile': 'driving-car',
                  'preference': 'shortest',
                  'instructions': 'false', }
route_normal = ors.directions(**request_params)
folium.features.GeoJson(data=route_normal,
                        name='Route without construction sites',
                        style_function=style_function('#FF0000'),
                        overlay=True).add_to(map2)

# Buffer route with 0.009 degrees (really, just too lazy to project again...)
route_buffer = LineString(route_normal['features'][0]['geometry']['coordinates']).buffer(0.009)
folium.features.GeoJson(data=geometry.mapping(route_buffer),
                        name='Route Buffer',
                        style_function=style_function('#FFFF00'),
                        overlay=True).add_to(map2)

# Plot which construction sites fall into the buffer Polygon
sites_buffer_poly = []
for site_poly in sites_poly:
    poly = Polygon(site_poly)
    if route_buffer.intersects(poly):
        folium.features.Marker(list(reversed(poly.centroid.coords[0]))).add_to(map2)
        sites_buffer_poly.append(poly)

map2
Out[15]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Finally, we can try to request a route using avoid_polygons, which conveniently takes a GeoJSON as input.

In [16]:
# Add the site polygons to the request parameters
request_params['options'] = {'avoid_polygons': geometry.mapping(MultiPolygon(sites_buffer_poly))}
route_detour = ors.directions(**request_params)

folium.features.GeoJson(data=route_detour,
                        name='Route with construction sites',
                        style_function=style_function('#00FF00'),
                        overlay=True).add_to(map2)

map2.add_child(folium.map.LayerControl())
map2
Out[16]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Note: This request might fail sometime in the future, as the JSON is loaded dynamically and changes a few times a week. Thus the amount of sites within the buffer can exceed the GET limit (which is between 15-20 site polygons approx).