15  OpenRoute Service (ORS)

15.1 Introducción

OpenRoute Service (ORS) es un servicio que ofrece diversas herramientas relacionadas con la planificación de rutas y la geolocalización. Este servicio es mantenido por HeiGIT (Heidelberg Institute for Geoinformation Technology) y está construido sobre datos de OpenStreetMap, lo que permite ofrecer funcionalidades como el cálculo de rutas para diferentes modos de transporte (automóvil, bicicleta, a pie, etc.), la optimización de rutas, geocodificación, y más.

Se pueden testear las diferentes funcionalidades de ORS desde la propia plataforma web, por ejemplo medir distancias entre puntos u obtener rutas usando diferentes medios de transporte.

ORS es utilizado en una amplia gama de aplicaciones, desde logística y transporte hasta el turismo y la planificación urbana, permitiendo a los desarrolladores integrar capacidades de mapeo y navegación en sus aplicaciones y sistemas utilizando una API.

El paquete openrouteservice(Oleś, 2024), desarrollado por Andrzej Oleś, facilita la interacción entre la API de OpenRoute Service y el entorno de programación R. Este paquete simplifica el proceso de enviar consultas a OpenRoute Service y de procesar las respuestas, permitiendo a los usuarios de R acceder a las capacidades del servicio de manera más directa y eficiente. Al integrar OpenRoute Service con R, se pueden realizar desde tareas simples de geocodificación hasta análisis espaciales complejos y optimización de rutas.

En esta práctica nos basaremos en la viñeta (vignette) del paquete openrouteservice en su versión 0.4.2.

15.2 Instalación

Actualmente, el paquete no está disponible en CRAN, por lo que es necesario instalar la versión en desarrollo de GitHub:

install.packages("remotes")
remotes::install_github("GIScience/openrouteservice-r")

Cargamos la librería (y el resto de paquetes que usaremos en esta práctica):

library(openrouteservice)
library(tidyverse)
library(sf)
library(leaflet)
library(mapview)
library(ggforce)
library(units)

Además de lo anterior, es necesario configurar la API key de ORS, que se puede obtener de forma gratuita aquí.

Una vez realizado el registro, se solicita la creación del TOKEN desde el Dev dashboard (en la parte inferior de la página). El nombre puede ser cualquiera.

Configuramos la API key en R:

openrouteservice::ors_api_key("<your-api-key>")

15.3 Direcciones

La función ors_directions() conecta con el servicio de direcciones de ORS para calcular rutas entre coordenadas dadas.

coordinates <- list(c(-0.37705734472373525, 39.469768641850955),
                    c(-0.3418411018964883, 39.478389143521134))
x <- openrouteservice::ors_directions(coordinates)

Los puntos de paso se pueden proporcionar como una lista de pares de coordenadas c(lon, lat) o un objeto similar a una matriz de 2 columnas, como un marco de datos.

coordinates <- data.frame(lon = c(-0.37705734472373525, -0.3418411018964883),
                          lat = c(39.469768641850955, 39.478389143521134))

El formato de respuesta por defecto es geoJSON, lo que permite visualizarlo fácilmente con, por ejemplo, leaflet.

leaflet() %>%
  addTiles() %>%
  addGeoJSON(x, fill=FALSE) %>%
  fitBBox(x$bbox)
Info

Por defecto, el método (profile) que usa ORS es driving-car. Para saber las alternativas posibles podemos usar la función ors_profile.

openrouteservice::ors_profile()
               car                hgv               bike 
     "driving-car"      "driving-hgv"  "cycling-regular" 
          roadbike                mtb             e-bike 
    "cycling-road" "cycling-mountain" "cycling-electric" 
           walking             hiking         wheelchair 
    "foot-walking"      "foot-hiking"       "wheelchair" 

Se pueden configurar otros parámetros adicionales y así obtener información adicional, como la pendiente (steepness):

coords <- list(c(-0.37705734472373525, 39.469768641850955),
               c(-0.4682523843758004, 39.69920728116197),
               c(-0.5891612217642153, 39.637068860632944))
x <- openrouteservice::ors_directions(coords,
                                      profile = "cycling-mountain",
                                      elevation = TRUE,
                                      extra_info = "steepness",
                                      output = "sf")
height <- st_geometry(x)[[1]][, 3]

class(x)
[1] "sf"         "data.frame"
sf::st_geometry_type(x)
[1] LINESTRING
18 Levels: GEOMETRY POINT LINESTRING POLYGON ... TRIANGLE

Como vemos, en esta ocasión, el objeto obtenido es de clase sf, facilitando la aplicación de los métodos vistos hasta ahora, como el cálculo de la distancia entre el origen y cada point que conforma la linestring.

points <- st_cast(st_geometry(x), "POINT")
n <- length(points)
segments <- cumsum(st_distance(points[-n], points[-1], by_element = TRUE))
st_as_s2(): dropping Z and/or M coordinate
st_as_s2(): dropping Z and/or M coordinate

La pendiente, por otro lado, se puede extraer directamente de la información extra que hemos solicitado:

steepness <- x$extras$steepness$values
steepness <- rep(steepness[,3], steepness[,2]-steepness[,1])
steepness <- factor(steepness, -5:5)

palette <- setNames(rev(RColorBrewer::brewer.pal(11, "RdYlBu")), levels(steepness))

Con esta información, podemos generar un gráfico de desnivel:

units(height) <- as_units("m")

df <- data.frame(x1 = c(set_units(0, "m"), segments[-(n-1)]),
                 x2 = segments,
                 y1 = height[-n],
                 y2 = height[-1],
                 steepness)

y_ran <- range(height) * c(0.9, 1.1)

n = n-1

df2 = data.frame(x = c(df$x1, df$x2, df$x2, df$x1),
                 y = c(rep(y_ran[1], 2*n), df$y2, df$y1),
                 steepness,
                 id = 1:n)

ggplot() + theme_bw() +
  geom_segment(data = df, aes(x1, y1, xend = x2, yend = y2), linewidth = 1) +
  geom_polygon(data = df2, aes(x, y, group = id), fill = "white") +
  geom_polygon(data = df2, aes(x, y , group = id, fill = steepness)) +
  scale_fill_manual(values = alpha(palette, 0.8), drop = FALSE) +
  scale_x_unit(unit = "km", expand = c(0,0)) +
  scale_y_unit(expand = c(0,0), limits = y_ran) +
  labs(x = "Distancia", y = "Altitud", fill = "Intensidad de\nla pendiente")

15.4 Isocronas

Otra posibilidad que brinda ORS es conocer la accesibilidad de un lugar/servicio. La función ors_isochrones() ayuda a determinar qué áreas se pueden alcanzar desde cierta(s) ubicación(es) en un tiempo dado o distancia de viaje. Las áreas de accesibilidad se devuelven como contornos de polígonos. Junto al rango proporcionado en segundos o metros, también se puede especificar los intervalos correspondientes. La lista de argumentos opcionales para ors_isochrones() es similar a la de ors_directions().

coordinates <- data.frame(lon = -0.37705734472373525,
                          lat = 39.469768641850955)

# rango de 30 minutos (1800 segundos) en intervalos de 10 minutos (600 segundos)
res <- openrouteservice::ors_isochrones(coordinates,
                                        range = 1800,
                                        interval = 600,
                                        output = "sf")
# Obtener los valores y reordenarlos
values <- levels(factor(res$value))
ranges <- split(res, values)
ranges <- ranges[rev(values)]

# Asignar nombres como "10 min", "20 min", "30 min"
names(ranges) <- sprintf("%s min", as.numeric(names(ranges))/60)

for (nm in names(ranges)) {
  rn <- rownames(ranges[[nm]])
  rn[1] <- nm
  rownames(ranges[[nm]]) <- rn
}

# Visualizamos las isocronas
mapview(ranges, alpha.regions = 0.2, homebutton = FALSE, legend = F, use.layer.names = F)

15.5 Puntos de Interés

El servicio Puntos de Interés (POIs) permite encontrar lugares de interés alrededor o dentro de unas coordenadas. Se puede buscar características específicas alrededor de un punto, un camino o dentro de un polígono. La función ors_pois('list') permite listar todas las categorías de puntos de interés disponibles (también están disponibles aquí).

geometry <- list(buffer = 1000,
                 geojson = list(type = "Point",
                                coordinates = c(-0.34148097142768424,
                                                39.47829770128991)))

dfPois <- openrouteservice::ors_pois(request = 'pois',
                                     geometry = geometry,
                                     limit = 2000,
                                     sortby = "distance",
                                     filters = list(wheelchair = "yes"),
                                     output = 'sf')
head(dfPois)
Simple feature collection with 6 features and 5 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -0.346147 ymin: 39.47507 xmax: -0.336884 ymax: 39.48135
Geodetic CRS:  WGS 84
      osm_id osm_type distance              category_ids osm_tags
1 3373439357        1 299.6144       platform, transport      yes
2 1958814137        1 368.1017           bank, financial      yes
3  411418789        2 384.8024         school, education      yes
4  149495204        2 411.2633 library, arts_and_culture      yes
5   23389318        2 455.6849    restaurant, sustenance      yes
6 1569469283        1 501.8109        parking, transport      yes
                    geometry
1  POINT (-0.34452 39.47962)
2 POINT (-0.343472 39.48123)
3 POINT (-0.343124 39.47507)
4  POINT (-0.346147 39.4791)
5 POINT (-0.336884 39.48034)
6 POINT (-0.337176 39.48135)
dfPois %>%
  leaflet() %>%
  addTiles() %>%
  addCircleMarkers(label = ~category_ids)

15.6 Optimización

La optimización consiste en resolver el problema de enrutamiento de vehículos (VRP) encontrando un conjunto óptimo de rutas para que una flota de vehículos recorra un trayecto en el menor tiempo posible (o con el menor coste posible), pasando por un conjunto dado de ubicaciones.

El siguiente ejemplo representa una flota de 2 vehículos que realizan entregas en 6 ubicaciones (3 cada uno).

En primer lugar, se definen las coordenadas del punto de inicio (base):

home_base <- data.frame(lon = 2.370658, lat = 48.721666)

Se definen los elementos que se van a desplazar (en este caso vehículos), especificando:

  • id: identificador de los elementos (1 y 2).
  • profile: tipo de ruta, en este caso “conducción en coche”.
  • start y end: puntos de inicio y finalización de cada elemento (vehículo).
  • capacity: capacidad de cada elemento (por ejemplo, número de personas o peso que pueden transportar).
  • skills: habilidades específicas para cada vehículo (por ejemplo, tipo de carga que pueden transportar o servicios que pueden realizar).
  • time_window: ventana de tiempo (en segundos) en la que los vehículos están disponibles para realizar entregas.
vehicles <- openrouteservice::vehicles(id = 1:2,
                                       profile = "driving-car",
                                       start = home_base,
                                       end = home_base,
                                       capacity = 4,
                                       skills = list(c(1, 14), c(2, 14)),
                                       time_window = c(28800, 43200)) # de 8 a 12

Se define una lista de coordenadas geográficas que representan las ubicaciones donde se realizarán los trabajos:

locations <- list(
  c(1.98806, 48.705),
  c(2.03655, 48.61128),
  c(2.39719, 49.07611),
  c(2.41808, 49.22619),
  c(2.28325, 48.5958),
  c(2.89357, 48.90736)
)

Se definen los trabajos a realizar, especificando, por ejemplo:

  • id: identificador de los trabajos.
  • service: tiempo (en segundos) requerido para completar cada trabajo.
  • amount: cantidad de recursos necesarios para cada trabajo.
  • location: ubicaciones de cada trabajo.
  • skills: habilidades requeridas para cada trabajo.
jobs <- openrouteservice::jobs(id = 1:6,
                               service = 300,
                               amount = 1,
                               location = locations,
                               skills = list(1, 1, 2, 2, 14, 14))

Se realiza la optimización de las rutas usando la función ors_optimization():

res <- ors_optimization(jobs, vehicles, options = list(g = TRUE))
class(res)
[1] "ors_optimization" "ors_api"          "list"            
str(res$routes[[1]]$geometry)
 chr "k}jhHg_nMvGQpQY~EKN?T?~J]rA?BAx@Ar@CPCbCGnCEnCG~BG`CGhAC|ACbCGtAEXBb@?tCCtCIL?vCIrCKbAMTAlACvCGvFMz@?TAb@A`ACz@"| __truncated__

Se procesan las rutas para extraer la geometría y las ubicaciones de cada paso en las rutas de los vehículos, decodificando la geometría con la función decode() del paquete googlePolylines(Cooley, 2025):

install.packages("googlePolylines")
lapply(res$routes, with, {
  list(
    geometry = googlePolylines::decode(geometry)[[1L]],
    locations = lapply(steps, with, if (type=="job") location) %>%
      do.call(rbind, .) %>% data.frame %>% setNames(c("lon", "lat"))
  )
  }) -> routes

# función que crea una lista de rutas en el orden correcto
addRoutes <- function(map, routes, colors) {
  routes <- mapply(c, routes, color = colors, SIMPLIFY = FALSE)
  f <- function (map, route) {
    with(route, {
      labels <- sprintf("<b>%s</b>", 1:nrow(locations))
      markers <- awesomeIcons(markerColor = color, text = labels, fontFamily = "arial")
      map %>%
        addPolylines(data = geometry, lng = ~lon, lat = ~lat, col = ~color) %>%
        addAwesomeMarkers(data = locations, lng = ~lon, lat = ~lat, icon = markers)
    })
  }
  Reduce(f, routes, map)
}

Representamos gráficamente:

leaflet() %>%
  addTiles() %>%
  addAwesomeMarkers(data = home_base, icon = awesomeIcons("home")) %>%
  addRoutes(routes, c("purple", "green"))