Introduction

An isochrone is a geometrical representation of the area that can be reached for a starting on a network within a given distance (in time or space units).

There are already several libraries to build isochrones with R (opentripplanner, osrm, rmapzen, etc.), but they are all based on OpenStreetMap data or on external API. We propose here a simple implementation that can be used on a geographical network read as SpatialLinesDataFrame.

We give here a simple example with the Montreal network dataset and calculate several isochrones from the centre of the dataset.

calculating the isochrones

The first step is to load (or create) a geographical network and to have some starting points.

# first load data and packages
library(sf)
library(spNetwork)
library(tmap)

data(mtl_network)

# calculating the length of each segment
mtl_network$length <- as.numeric(st_length(mtl_network))

# extracting the coordinates in one matrix
coords <- st_coordinates(mtl_network)
center <- colMeans(coords)
df_center <- data.frame("OID" = 1,
                        "x" = center[1],
                        "y" = center[2])
df_center <- st_as_sf(df_center, coords = c("x","y"), crs = st_crs(mtl_network))

# then plotting the data
tm_shape(mtl_network) + 
  tm_lines("black") + 
  tm_shape(df_center) + 
  tm_dots("red", size = 0.2)

The second step is to calculate the isochrones with the function calc_isochrones. In this example, we use the length of a segment as the weight but other values could be used. We also consider that all the roads can be used in both directions (see parameter direction)

iso_results <- calc_isochrones(lines = mtl_network,
                               start_points = df_center,
                               dists = c(500,1000,2000),
                               weight = "length"
                               )

The results are reported as lines, which can be mapped.

# creating a factor and changing order for better vizualisation
iso_results$fac_dist <- as.factor(iso_results$distance)
iso_results <- iso_results[order(-1*iso_results$distance),]

tm_shape(mtl_network) + 
  tm_lines(col = "grey") +
  tm_shape(iso_results) + 
  tm_lines(col = "fac_dist",title.col = "distance (m)",
           palette = c("500"="#005f73", "1000"="#ca6702", "2000"="#9b2226"))+
  tm_layout(legend.outside = TRUE) + 
  tm_shape(df_center) + 
  tm_dots(col = "black", size = 0.1)

From lines to polygons

The lines are the most exact form of an isochrone because they depict exactly what parts of the network can be reached. However, isochrones are represented as polygons most of the time. We propose here some approaches to build some meaningful polygons for isochrones.

The best starting point is probably to calculate for each set of points a concave hull. This is different from the traditional convex hull and is more adapted to the irregular geometry of an isochrone. To do so, we use the library concaveman

library(concaveman)

# identifying each isochrone
iso_results$iso_oid <- paste(iso_results$point_id,
                             iso_results$distance,
                             sep = "_")

# creating the polygons for each isochrone
polygons <- lapply(unique(iso_results$iso_oid), function(oid){
  
  # subseting the required lines
  lines <- subset(iso_results, iso_results$iso_oid == oid)
  
  # extracting the coordinates of the lines
  coords <- st_coordinates(lines)
  poly_coords <- concaveman(points = coords, concavity = 3)
  poly <- st_polygon(list(poly_coords[,1:2]))
  return(poly)
})

# creating a SpatialPolygonsDataFrame
iso_sp <- st_sf(
  iso_oid = unique(iso_results$iso_oid),
  distance = unique(iso_results$distance),
  geometry = polygons,
  crs = st_crs(iso_results)
)

We can now map the obtained polygons.

# creating a factor and changing order for better vizualisation
iso_sp$fac_dist <- as.factor(iso_sp$distance)
iso_sp <- iso_sp[order(-1*iso_sp$distance),]

tm_shape(iso_results) + 
  tm_lines(col = "black")+
  tm_shape(iso_sp) + 
  tm_polygons(col = "fac_dist",title.col = "distance (m)",
           palette = c("500"="#005f73", "1000"="#ca6702", "2000"="#9b2226"),
           alpha = 0.6, border.col = "white") +
  tm_shape(df_center) + 
  tm_dots(col = "black", size = 0.1)

This is more similar to classical isochrone maps. Another possible enhancement would be to simplify and smooth the limits of the polygons. To do so, we will use the functions st_simplify from sf and smooth from smoothr.

library(smoothr)

simple_polygons <- st_simplify(iso_sp, dTolerance = 50)
smooth_iso <- smooth(simple_polygons, method = "chaikin", 
                            refinements = 5)

tm_shape(iso_results) + 
  tm_lines(col = "black")+
  tm_shape(smooth_iso) +
  tm_polygons(col = "fac_dist",
           title.col = "distance (m)",
           palette = c("500"="#005f73", "1000"="#ca6702", "2000"="#9b2226"),
           border.col = "white",
           alpha = 0.6) +
  tm_shape(df_center) + 
  tm_dots(col = "black", size = 0.1) + 
  tm_layout(legend.outside = TRUE)

The shapes are cleaner for mapping but the results are not exact anymore.