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.
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
$length <- as.numeric(st_length(mtl_network))
mtl_network
# extracting the coordinates in one matrix
<- st_coordinates(mtl_network)
coords <- colMeans(coords)
center <- data.frame("OID" = 1,
df_center "x" = center[1],
"y" = center[2])
<- st_as_sf(df_center, coords = c("x","y"), crs = st_crs(mtl_network))
df_center
# 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
)
<- calc_isochrones(lines = mtl_network,
iso_results 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
$fac_dist <- as.factor(iso_results$distance)
iso_results<- iso_results[order(-1*iso_results$distance),]
iso_results
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)
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_oid <- paste(iso_results$point_id,
iso_results$distance,
iso_resultssep = "_")
# creating the polygons for each isochrone
<- lapply(unique(iso_results$iso_oid), function(oid){
polygons
# subseting the required lines
<- subset(iso_results, iso_results$iso_oid == oid)
lines
# extracting the coordinates of the lines
<- st_coordinates(lines)
coords <- concaveman(points = coords, concavity = 3)
poly_coords <- st_polygon(list(poly_coords[,1:2]))
poly return(poly)
})
# creating a SpatialPolygonsDataFrame
<- st_sf(
iso_sp 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
$fac_dist <- as.factor(iso_sp$distance)
iso_sp<- iso_sp[order(-1*iso_sp$distance),]
iso_sp
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)
<- st_simplify(iso_sp, dTolerance = 50)
simple_polygons <- smooth(simple_polygons, method = "chaikin",
smooth_iso 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.