Set up a route direction API with Open Street Map

Openrouteservice.org provides a free API for calculating directions from one point to another on a map. This service uses OpenStreetMap data to calculate this directions. OpenRouteService provides a cloud API solution that can be used for free, out of the box. This API is limited a number of requests per day so if you exceed this limit it is an option to install it on your own servers.

The documentation on the openrouteservice.org website is OK. But, with the docker-compose instructions of the website, the service will not run out of the box (at time of writing). This blog post will help you to start the service quickly without problems with the correct configuration and the map you like to choose (your country or city).

Quick overview

OpenRouteService docker image contains an API and the engine that generates direction data for the API to use. It makes use of an osm.pbf file that can be downloaded from (for example) Geofabrik.

If you start the OpenRouteService docker for the first time, it starts generating the direction data for the given osm.pbf file. This takes sometime, in the meantime the API is not available for usage. You simply have to wait.

After the direction data is generated you can use the API.

Thinks to keep in mind:

  • Generation of direction data is resource intensive. The larger the map (osm.pbf), the more resource intensive. Especially memory. For generating directions of the whole world, you need no less than 128Gb memory on your server.
  • Generating directions can take some time. For a city it could take around 1 minute. For the whole world it could take some days. This also depends on the hardware used.
  • Default, with the documentation from the website, the service starts generating data for the city of Heidelberg where the makers of this software are located.
  • If you want to create directions for a region you select yourself, you need to tweak the configurations.
  • It is possible to generate directions on one server and move it to another server. So it is possible to generate the directions on a heavy machine at home or some server your rent for a short period of time. Afterwards, you can move the generated content to a more lightweight server.
  • The service can generate directions for:
    • Cars
    • Hiking
    • Walking
    • WheelChair
    • Bikes (regular/electric/mountain/road)
    • Heavy Goods Vehicles

Set up the docker

I am a fan of using a docker-compose.yml for setting up my docker containers. Therefore, this tutorial will focus on an OpenRouteService setup that uses docker-compose.yml. If you never used it, no problem, everything is explained down here.

I suppose you are working in Linux or Mac environment. Running this solution on Windows gives me trouble. On my Windows machine it was running endlessly without ever finishing, I did not investigate what the source of this problem was. But I know there are solutions available. Take a look at the support section of the OpenRouteService website.

Configuration

  1. Create a directory on your server. Open the directory and create a set of subdirectories:
1mkdir osr
2cd osr
3mkdir -p {elevation_cache,graphs,conf,logs/ors,logs/tomcat}
  1. elevation_cache and graphs will contain the generated direction's data.
  2. Create a docker-compose.yml file in the root directory with the following contents:
 1version: '2.4'
 2services:
 3  ors-app:
 4    container_name: ors-app
 5    ports:
 6      - "8080:8080"
 7      - "9001:9001"
 8    image: openrouteservice/openrouteservice:latest
 9    user: "${ORS_UID:-0}:${ORS_GID:-0}"
10    volumes:
11      - ./graphs:/ors-core/data/graphs
12      - ./elevation_cache:/ors-core/data/elevation_cache
13      - ./logs/ors:/var/log/ors/
14      - ./logs/tomcat:/usr/local/tomcat/logs
15      - ./conf:/ors-conf
16      - ./#MY_OSM_PBF#:/ors-core/data/osm_file.pbf
17    environment:
18      - BUILD_GRAPHS=True  # Forces the container to rebuild the graphs, e.g. when PBF is changed
19      - "JAVA_OPTS=-Djava.awt.headless=true -server -XX:TargetSurvivorRatio=75 -XX:SurvivorRatio=64 -XX:MaxTenuringThreshold=3 -XX:+UseG1GC -XX:+ScavengeBeforeFullGC -XX:ParallelGCThreads=4 -Xms1g -Xmx2g"
20      - "CATALINA_OPTS=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9001 -Dcom.sun.management.jmxremote.rmi.port=9001 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost"
  1. Replace #MY_OSM_PBF# with your own osm.pbf that can be downloaded here. Put the file within the root, in the same place as the docker-compose.yml file. Be sure to select a small file, like a city. Starting with a large area, will result in memory issues if you doesn't tweak your system correctly. Later in this post we will focus on memory tweaking.
  2. Create a new file with the name ors-config.json. Put this file in the conf directory. This file contains the full OpenRoutingMap configuration. Add the following content:
  1{
  2    "ors": {
  3      "info": {
  4        "base_url": "https://openrouteservice.org/",
  5        "swagger_documentation_url": "https://api.openrouteservice.org/",
  6        "support_mail": "support@openrouteservice.org",
  7        "author_tag": "openrouteservice",
  8        "content_licence": "LGPL 3.0"
  9      },
 10      "services": {
 11        "matrix": {
 12          "enabled": true,
 13          "maximum_routes": 100,
 14          "maximum_routes_flexible": 25,
 15          "maximum_search_radius": 5000,
 16          "maximum_visited_nodes": 100000,
 17          "allow_resolve_locations": true,
 18          "attribution": "openrouteservice.org, OpenStreetMap contributors"
 19        },
 20        "isochrones": {
 21          "enabled": true,
 22          "maximum_range_distance": [
 23            {
 24              "profiles": "any",
 25              "value": 50000
 26            },
 27            {
 28              "profiles": "driving-car, driving-hgv",
 29              "value": 100000
 30            }
 31          ],
 32          "maximum_range_time": [
 33            {
 34              "profiles": "any",
 35              "value": 18000
 36            },
 37            {
 38              "profiles": "driving-car, driving-hgv",
 39              "value": 3600
 40            }
 41          ],
 42          "fastisochrones": {
 43            "maximum_range_distance": [
 44              {
 45                "profiles": "any",
 46                "value": 50000
 47              },
 48              {
 49                "profiles": "driving-car, driving-hgv",
 50                "value": 500000
 51              }
 52            ],
 53            "maximum_range_time": [
 54              {
 55                "profiles": "any",
 56                "value": 18000
 57              },
 58              {
 59                "profiles": "driving-car, driving-hgv",
 60                "value": 10800
 61              }
 62            ],
 63            "profiles": {
 64              "default_params": {
 65                "enabled": false,
 66                "threads": 1,
 67                "weightings": "recommended",
 68                "maxcellnodes": 5000
 69              },
 70              "profile-hgv": {
 71                "enabled": false,
 72                "threads": 1,
 73                "weightings": "recommended, shortest",
 74                "maxcellnodes": 5000
 75              }
 76            }
 77          },
 78          "maximum_intervals": 10,
 79          "maximum_locations": 2,
 80          "allow_compute_area": true
 81        },
 82        "routing": {
 83          "enabled": true,
 84          "_mode": "preparation",
 85          "mode": "normal",
 86          "routing_description": "This is a routing file from openrouteservice",
 87          "routing_name": "openrouteservice routing",
 88          "sources": [
 89            "/ors-core/data/osm_file.pbf"
 90          ],
 91          "init_threads": 1,
 92          "attribution": "openrouteservice.org, OpenStreetMap contributors",
 93          "elevation_preprocessed": false,
 94          "profiles": {
 95            "active": [
 96              "car",
 97              "hgv",
 98              "bike-regular",
 99              "bike-mountain",
100              "bike-road",
101              "bike-electric",
102              "walking",
103              "hiking",
104              "wheelchair"
105            ],
106            "default_params": {
107              "encoder_flags_size": 8,
108              "graphs_root_path": "/ors-core/data/graphs",
109              "elevation_provider": "multi",
110              "elevation_cache_path": "/ors-core/data/elevation_cache",
111              "elevation_cache_clear": false,
112              "instructions": true,
113              "maximum_distance": 100000,
114              "maximum_distance_dynamic_weights": 100000,
115              "maximum_distance_avoid_areas": 100000,
116              "maximum_waypoints": 50,
117              "maximum_snapping_radius": 400,
118              "maximum_avoid_polygon_area": 200000000,
119              "maximum_avoid_polygon_extent": 20000,
120              "maximum_distance_alternative_routes": 100000,
121              "maximum_alternative_routes": 3,
122              "maximum_distance_round_trip_routes": 100000,
123              "maximum_speed_lower_bound": 80,
124              "preparation": {
125                "min_network_size": 200,
126                "min_one_way_network_size": 200,
127                "methods": {
128                  "lm": {
129                    "enabled": true,
130                    "threads": 1,
131                    "weightings": "recommended,shortest",
132                    "landmarks": 16
133                  }
134                }
135              },
136              "execution": {
137                "methods": {
138                  "lm": {
139                    "disabling_allowed": true,
140                    "active_landmarks": 8
141                  }
142                }
143              }
144            },
145            "profile-car": {
146              "profiles": "driving-car",
147              "parameters": {
148                "encoder_flags_size": 8,
149                "encoder_options": "turn_costs=true|block_fords=false|use_acceleration=true",
150                "maximum_distance": 100000,
151                "elevation": true,
152                "maximum_snapping_radius": 350,
153                "preparation": {
154                  "min_network_size": 200,
155                  "min_one_way_network_size": 200,
156                  "methods": {
157                    "ch": {
158                      "enabled": true,
159                      "threads": 1,
160                      "weightings": "fastest"
161                    },
162                    "lm": {
163                      "enabled": false,
164                      "threads": 1,
165                      "weightings": "fastest,shortest",
166                      "landmarks": 16
167                    },
168                    "core": {
169                      "enabled": true,
170                      "threads": 1,
171                      "weightings": "fastest,shortest",
172                      "landmarks": 64,
173                      "lmsets": "highways;allow_all"
174                    }
175                  }
176                },
177                "execution": {
178                  "methods": {
179                    "ch": {
180                      "disabling_allowed": true
181                    },
182                    "lm": {
183                      "disabling_allowed": true,
184                      "active_landmarks": 6
185                    },
186                    "core": {
187                      "disabling_allowed": true,
188                      "active_landmarks": 6
189                    }
190                  }
191                },
192                "ext_storages": {
193                  "WayCategory": {},
194                  "HeavyVehicle": {},
195                  "WaySurfaceType": {},
196                  "RoadAccessRestrictions": {
197                    "use_for_warnings": true
198                  }
199                }
200              }
201            },
202            "profile-hgv": {
203              "profiles": "driving-hgv",
204              "parameters": {
205                "encoder_flags_size": 8,
206                "encoder_options": "turn_costs=true|block_fords=false|use_acceleration=true",
207                "maximum_distance": 100000,
208                "elevation": true,
209                "preparation": {
210                  "min_network_size": 200,
211                  "min_one_way_network_size": 200,
212                  "methods": {
213                    "ch": {
214                      "enabled": true,
215                      "threads": 1,
216                      "weightings": "recommended"
217                    },
218                    "core": {
219                      "enabled": true,
220                      "threads": 1,
221                      "weightings": "recommended,shortest",
222                      "landmarks": 64,
223                      "lmsets": "highways;allow_all"
224                    }
225                  }
226                },
227                "execution": {
228                  "methods": {
229                    "ch": {
230                      "disabling_allowed": true
231                    },
232                    "core": {
233                      "disabling_allowed": true,
234                      "active_landmarks": 6
235                    }
236                  }
237                },
238                "ext_storages": {
239                  "WayCategory": {},
240                  "HeavyVehicle": {
241                    "restrictions": true
242                  },
243                  "WaySurfaceType": {}
244                }
245              }
246            },
247            "profile-bike-regular": {
248              "profiles": "cycling-regular",
249              "parameters": {
250                "encoder_options": "consider_elevation=true|turn_costs=true|block_fords=false",
251                "elevation": true,
252                "ext_storages": {
253                  "WayCategory": {},
254                  "WaySurfaceType": {},
255                  "HillIndex": {},
256                  "TrailDifficulty": {}
257                }
258              }
259            },
260            "profile-bike-mountain": {
261              "profiles": "cycling-mountain",
262              "parameters": {
263                "encoder_options": "consider_elevation=true|turn_costs=true|block_fords=false",
264                "elevation": true,
265                "ext_storages": {
266                  "WayCategory": {},
267                  "WaySurfaceType": {},
268                  "HillIndex": {},
269                  "TrailDifficulty": {}
270                }
271              }
272            },
273            "profile-bike-road": {
274              "profiles": "cycling-road",
275              "parameters": {
276                "encoder_options": "consider_elevation=true|turn_costs=true|block_fords=false",
277                "elevation": true,
278                "ext_storages": {
279                  "WayCategory": {},
280                  "WaySurfaceType": {},
281                  "HillIndex": {},
282                  "TrailDifficulty": {}
283                }
284              }
285            },
286            "profile-bike-electric": {
287              "profiles": "cycling-electric",
288              "parameters": {
289                "encoder_options": "consider_elevation=true|turn_costs=true|block_fords=false",
290                "elevation": true,
291                "ext_storages": {
292                  "WayCategory": {},
293                  "WaySurfaceType": {},
294                  "HillIndex": {},
295                  "TrailDifficulty": {}
296                }
297              }
298            },
299            "profile-walking": {
300              "profiles": "foot-walking",
301              "parameters": {
302                "encoder_options": "block_fords=false",
303                "elevation": true,
304                "ext_storages": {
305                  "WayCategory": {},
306                  "WaySurfaceType": {},
307                  "HillIndex": {},
308                  "TrailDifficulty": {}
309                }
310              }
311            },
312            "profile-hiking": {
313              "profiles": "foot-hiking",
314              "parameters": {
315                "encoder_options": "block_fords=false",
316                "elevation": true,
317                "ext_storages": {
318                  "WayCategory": {},
319                  "WaySurfaceType": {},
320                  "HillIndex": {},
321                  "TrailDifficulty": {}
322                }
323              }
324            },
325            "profile-wheelchair": {
326              "profiles": "wheelchair",
327              "parameters": {
328                "encoder_options": "block_fords=true",
329                "elevation": true,
330                "maximum_snapping_radius": 50,
331                "ext_storages": {
332                  "WayCategory": {},
333                  "WaySurfaceType": {},
334                  "Wheelchair": {
335                    "KerbsOnCrossings": "true"
336                  },
337                  "OsmId": {}
338                }
339              }
340            }
341          }
342        }
343      },
344      "logging": {
345        "enabled": true,
346        "level_file": "DEBUG_LOGGING.json",
347        "location": "/var/log/ors",
348        "stdout": true
349      },
350      "system_message": [
351        {
352          "active": false,
353          "text": "This message would be sent with every routing bike fastest request",
354          "condition": {
355            "request_service": "routing",
356            "request_profile": "cycling-regular,cycling-mountain,cycling-road,cycling-electric",
357            "request_preference": "fastest"
358          }
359        },
360        {
361          "active": false,
362          "text": "This message would be sent with every request for geojson response",
363          "condition": {
364            "api_format": "geojson"
365          }
366        },
367        {
368          "active": false,
369          "text": "This message would be sent with every request on API v1 from January 2020 until June 2050",
370          "condition": {
371            "api_version": 1,
372            "time_after": "2020-01-01T00:00:00Z",
373            "time_before": "2050-06-01T00:00:00Z"
374          }
375        },
376        {
377          "active": false,
378          "text": "This message would be sent with every request"
379        }
380      ]
381    }
382  }

Your directory will look like this:

1- conf
2  + ors-config.json
3- elevation_cache
4- graphs  
5- logs
6  - ors
7  - tomcat
8+ docker-compose.yml
9+ netherlands-latest.osm.pdf  

Run the project

We will start running the docker container. Be sure you select a small region from the Geofabrik website to start with. If you directly want to go large,m go to the next section.

  1. Start with a small file. Like a city or a small state or province.
  2. Start the docker container: docker-compose up -d.
  3. The docker container now starts in daemon mode. You will see nothing, but the docker container is started.
  4. To get a look on what is happening enter the following command: docker-compose logs -f. Now you see the container doing its first direction generation work. To exit the logging, press CTRL+C. The container keeps running, but you can use the terminal for other tasks.
  5. The API is already available on port 8080. You can access it here: http://localhost:8080
  6. You have to be patient now. When the process is finished, you will see something like this:
 1ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfile] - [9] Finished at: 2023-02-21 12:58:08.
 2ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfile] -                               
 3ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - Total time: 131.0s.
 4ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - ========================================================================
 5ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - ====> Recycling garbage...
 6ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - Before:  Total - 1.74 GB, Free - 746.34 MB, Max: 10 GB, Used - 1.01 GB
 7ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - After:  Total - 1.74 GB, Free - 1.19 GB, Max: 10 GB, Used - 562.85 MB
 8ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - ========================================================================
 9ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - ====> Memory usage by profiles:
10ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [1] 25.03 MB (4.4%)
11ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [2] 23.03 MB (4.1%)
12ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [3] 20.03 MB (3.6%)
13ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [4] 20.03 MB (3.6%)
14ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [5] 20.03 MB (3.6%)
15ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [6] 20.03 MB (3.6%)
16ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [7] 18.03 MB (3.2%)
17ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [8] 18.03 MB (3.2%)
18ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - [9] 18.03 MB (3.2%)
19ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - Total: 182.28 MB (32.4%)
20ors-app  | 21 Feb 12:58:08 INFO [routing.RoutingProfileManager] - ========================================================================
  1. To be sure that the process of generation is finished, you can perform the following API call: http://localhost:8080/ors/v2/health. It will return "ready" or "not ready".
  2. When the API is ready, you can test it by entering a direction request. Be sure to select a location that is covered by the map you have selected from GeoFabrik. An example: http://localhost:8080/ors/v2/directions/driving-car?start=5.47,52.511603&end=5.461901,52.51005.

Most API's use the order: [latitude],[longitude]. OpenRouteService uses it the other way around: [longitude],[latitude]. If your do it the wrong way you will get the following result:

 1{
 2    "error": {
 3        "code": 2010,
 4        "message": "Could not find routable point within a radius of 350.0 meters of specified coordinate 0: 52.0253728 4.6921885."
 5    },
 6    "info": {
 7        "engine": {
 8            "version": "6.7.0",
 9            "build_date": "2023-02-21T19:24:37Z"
10        },
11        "timestamp": 1677048897683
12    }
13}
  1. Now that you are done, change the following line in the docker-compose.yml file to prevent the docker-container from re-generating the same map after the container is restarted:
1environment:
2      - BUILD_GRAPHS=True  # Forces the container to rebuild the graphs, e.g. when PBF is changed
3      
4to
5
6environment:
7      - BUILD_GRAPHS=False  # Forces the container to rebuild the graphs, e.g. when PBF is changed
  1. To stop the container, type: docker-compose down

Generating large maps and manage memory

If you want to use a larger map than just a city or small province you need to be sure to provide enough memory to the docker container. For example to generate the full map of the Netherlands (netherlands-latest.osm.pbf) , I found out that at least 11Gb of RAM is necessary. To provide this memory you need to change 2 things:

  1. The memory allocation setting of Java in the docker-compose.yml.
  2. The available memory within your docker host configuration.

If you have not enough memory, generating the directions will stop suddenly with strange errors, like:

  • HEAP overflow
  • /ors-core/docker-entrypoint.sh: line 39: 128 Killed /usr/local/tomcat/bin/catalina.sh run

You can monitor the memory usage and the limit of the docker container with the following command:

1docker stats

The result will look like this:

1CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT     MEM %     NET I/O          BLOCK I/O       PIDS
2658b31de2d5f   ors-app   193.45%   8.554GiB / 17.58GiB   48.65%    222MB / 2.15MB   44MB / 1.18GB   49

Changing the Java memory limit

Modify the following line from docker-compose.yml

1"JAVA_OPTS=-Djava.awt.headless=true -server -XX:TargetSurvivorRatio=75 -XX:SurvivorRatio=64 -XX:MaxTenuringThreshold=3 -XX:+UseG1GC -XX:+ScavengeBeforeFullGC -XX:ParallelGCThreads=4 -Xms1g -Xmx2g"

The parameters -Xms1g and -Xmx2g tells Java how much memory may be used. The Xms1g parameter tells Java to start with 1Gb of RAM initially. This parameter can stay this way. The Xmx2g parameter tells Java what the maximum amount of memory is to use. 2Gb of RAM in this example. Change this to a higher value when you experience problems with generating. Never give a higher value than that is available on your system.

Changing the available docker memory

If you run Docker using a UI, like on a Mac. You have to modify the available memory under the resources section of the Docker dashboard. You can change it according to the Java setting. Before you change it. Shutdown all docker containers.

img.png

Read more about changing the resources with the terminal here.

Running it in production

To run the API in a production environment, you don't want the service running on a plain HTTP port. The service must be run using HTTPS and probably basic auth to prevent unauthorized use. Therefore, use a reverse proxy. A great one is Traefik. This is also a docker container and can manage Let's Encrypt certificates for HTTPS, rate limiting, load balancing, message compression and of course basic auth. An excellent tutorial about Traefik and docker can be found here.

Errors

/ors-core/docker-entrypoint.sh: line 39: 128 Killed /usr/local/tomcat/bin/catalina.sh run

This happens when your docker runs out of memory. Read the blog post above to find out how to tune your system's memory.

Translations: