A frequent use case in spatial analysis is routing (or navigation) – finding the optimal route between two (or more) locations.

As the use case is a common one there are a number of REST APIs providing standardized solutions.

In this blog post I share my personal experience with three different routing engines, implemented via three R packages:

  • Project OSRM, implemented via the {osrm} package, available on CRAN

  • Mapbox, implemented via the {mapboxapi} package, available on GitHub via remotes::install_github("walkerke/mapboxapi")

  • HERE, implemented via the {hereR} package, available on CRAN

For my demonstration I will be using two semi-random locations in central Prague. While all the three APIs are pretty global in coverage I have found that I can more easily pick my favorite when looking at results from a familiar setting (and as a consequence your mileage may vary).

The first step is attaching all the packages and creating a spatial data frame of my locations; to achieve this I am using the {tidygeocoder} package (other approaches are possible, but I have found the OSM geocoder very reliable).


# two semi-random Prague addresses
adresy <- c("Gogolova 212/1, Praha 1",
            "Soudní 988/1, Praha 4")

# geocode the two addresses & transform to {sf} data structure
data <- tidygeocoder::geo(adresy, method = "osm") %>% 
  st_as_sf(coords = c("long", "lat"), crs = 4326)

The first approach is via the completely open source OSRM engine; this approach is unique in that it does not require any API authorization.

It will run by default against the OSRM demo server, with some limitations (in order to manage the resource costs), but you are free to run it against your own implementation of the routing engine; docker images are available.

osroute <- osrm::osrmRoute(loc = data,
                           returnclass = "sf")

##      src                dst               duration        distance    
##  Length:1           Length:1           Min.   :13.85   Min.   :8.558  
##  Class :character   Class :character   1st Qu.:13.85   1st Qu.:8.558  
##  Mode  :character   Mode  :character   Median :13.85   Median :8.558  
##                                        Mean   :13.85   Mean   :8.558  
##                                        3rd Qu.:13.85   3rd Qu.:8.558  
##                                        Max.   :13.85   Max.   :8.558  
##           geometry
##  LINESTRING   :1  
##  epsg:4326    :0  
##  +proj=long...:0  

The osrm::osrmRoute() accepts as argument either {sf} data frame of points (as used in this example) or source and destination as points. It returns a {sf} dataframe with LINESTRING geometry type and data fields duration (in minutes) and distance (in kilometers).

The first commercial alternative is the Mapbox engine. While it requires a registration a very generous free tier is available. An API key is necessary; {mapboxapi} provides a convenient way of setting it once for ever in your R profile.

mroute <- mapboxapi::mb_directions(input_data = data,
                                   profile = "driving")

##           geometry    distance        duration    
##  LINESTRING   :1   Min.   :10.25   Min.   :20.92  
##  epsg:4326    :0   1st Qu.:10.25   1st Qu.:20.92  
##  +proj=long...:0   Median :10.25   Median :20.92  
##                    Mean   :10.25   Mean   :20.92  
##                    3rd Qu.:10.25   3rd Qu.:20.92  
##                    Max.   :10.25   Max.   :20.92

The mapboxapi::mb_directions() again accepts as argument {sf} data frame of points; setting origin and destination separately is possible, but not quite straightforward as with the other two APIs / packages.

It will again return a {sf} dataframe with LINESTRING geometry type and data fields duration (in minutes) and distance (in kilometers), in a structure very similar to {osrm} results.

The second commercial alternative is the HERE engine (provided by a company known as Nokia Maps in the times before truly smart phones). It again requires registration, and provides a very generous free tier.

The key has to be set once per R session via a hereR::set_key() function call; for practical reasons this call was omitted from the code chunk quoted below.

hroute <- hereR::route(origin = data[1, ],
                       destination =  data[2, ],
                       transport_mode = "car") %>% 
  st_zm() # the Z (height) dimension is troublesome...

##        id         rank      section    departure                  
##  Min.   :1   Min.   :1   Min.   :1   Min.   :2021-02-19 18:00:03  
##  1st Qu.:1   1st Qu.:1   1st Qu.:1   1st Qu.:2021-02-19 18:00:03  
##  Median :1   Median :1   Median :1   Median :2021-02-19 18:00:03  
##  Mean   :1   Mean   :1   Mean   :1   Mean   :2021-02-19 18:00:03  
##  3rd Qu.:1   3rd Qu.:1   3rd Qu.:1   3rd Qu.:2021-02-19 18:00:03  
##  Max.   :1   Max.   :1   Max.   :1   Max.   :2021-02-19 18:00:03  
##     arrival                        type               mode          
##  Min.   :2021-02-19 18:17:57   Length:1           Length:1          
##  1st Qu.:2021-02-19 18:17:57   Class :character   Class :character  
##  Median :2021-02-19 18:17:57   Mode  :character   Mode  :character  
##  Mean   :2021-02-19 18:17:57                                        
##  3rd Qu.:2021-02-19 18:17:57                                        
##  Max.   :2021-02-19 18:17:57                                        
##     distance       duration    duration_base  consumption            geometry
##  Min.   :8779   Min.   :1074   Min.   :824   Min.   :4.48   LINESTRING   :1  
##  1st Qu.:8779   1st Qu.:1074   1st Qu.:824   1st Qu.:4.48   epsg:4326    :0  
##  Median :8779   Median :1074   Median :824   Median :4.48   +proj=long...:0  
##  Mean   :8779   Mean   :1074   Mean   :824   Mean   :4.48                    
##  3rd Qu.:8779   3rd Qu.:1074   3rd Qu.:824   3rd Qu.:4.48                    
##  Max.   :8779   Max.   :1074   Max.   :824   Max.   :4.48

The API requires setting of origin and destination separately; in my case provided in {sf} format as the first and second row of the data data frame.

The results of the hereR::route() call are the most detailed from the three. Apart form the distance (in meters) and travelTime (in seconds), so not quite like the OSRM and Mapbox results, it returns departure and arrival times and allows consideration for traffic. It again returns a {sf} LINESTRING geometry.

In order to make an evaluation of the three routes I draw a simple leaflet overview:


leaflet(data = data) %>% 
  addProviderTiles("CartoDB.Positron") %>% 
  addMarkers(label = ~address) %>% 
  addPolylines(data = osroute,
               label = "OSRM engine",
               color = "red") %>% 
  addPolylines(data = mroute,
               label = "Mapbox engine",
               color = "blue") %>% 
  addPolylines(data = hroute,
               label = "HERE engine",
               color = "green")

As someone familiar with both Prague road network and traffic situation I find all three routes accurate and relevant.

Having said that I consider, based on a sample of one, the green route generated by the HERE API preferable.

It correctly finds that the shortest route (the one identified by OSRM in red) passes through the very historical center of Prague, and is likely to be plagued by heavy traffic.

The routes chosen by the two commercial implementations are quite close, but the one from HERE makes better use of the Letná tunnel and is likely to avoid some of the traffic commonly encountered on the Vltava river embankments.