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¶
In this example, we'd like to showchase how to use the direction 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.
import json
import requests
import folium
import pyproj
from shapely import geometry
from shapely.geometry import Point, LineString, Polygon, MultiPolygon
from openrouteservice import client
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!?
Rostock construction sites¶
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:
url = 'https://geo.sv.rostock.de/download/opendata/baustellen/baustellen.json'
def CreateBufferPolygon(point_in, resolution=10, radius=10):
sr_wgs = pyproj.Proj(init='epsg:4326')
sr_utm = pyproj.Proj(init='epsg:32632') # WGS84 UTM32N
point_in_proj = pyproj.transform(sr_wgs, sr_utm, *point_in) # unpack list to arguments
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(pyproj.transform(sr_utm, sr_wgs, *point)) # Transform back to WGS84
return poly_wgs
# Set up the fundamentals
api_key = 'your_key' # Individual api key
clnt = 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 = CreateBufferPolygon(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
Hui, that's a lot 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...
Aaanyways, as you might know, a GET request can only contain so many characters. Unfortunately, > 80 polygons are more than a GET can take (HINT: that's why we set resolution = 2
). And 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:
# 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 = clnt.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
Finally we can try to request a route using avoid_polygons
, which conveniently takes a GeoJSON as input.
# Add the site polygons to the request parameters
request_params['options'] = {'avoid_polygons': geometry.mapping(MultiPolygon(sites_buffer_poly))}
route_detour = clnt.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
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).