Great circles with sf and leaflet

Great circles are the shortest journeys between two points on a map, which can be easily computed and manipulated through the use of the excellent sf library, and visualised interactively with leaflet.
R
dataviz
GIS
Published

February 28, 2018

Surprisingly often I’m asked to help folks build maps which visualise the journeys between geographic locations. The shortest distance between two points on a map is a great circle, and you can see these on flight information screens — their exact curvature depends on the projection of the map.

I’m now an sf convert and try to do everything within that package. In my original version of this blogpost I introduced my painfully inefficient way to compute these great circles with sf and asked folks in the R4DS Slack group if they had any suggested improvements, which I’ve now baked in below.

Typically my datasets are collections of journeys, here’s an example (some journeys I took during the 3 years I worked for Wolfram Research):

library("tidyverse")
library("sf")
raw_journey_data <- tribble(
  ~start.long, ~start.lat, ~end.long, ~end.lat, ~name,
  -0.118092, 51.509865, -119.698189, 34.420830, "London to Santa Barbara",
  31.496773, 30.026300, 24.753574, 59.436962, "New Cairo to Tallinn",
  126.633333, 45.75, 46.738586, 24.774265, "Harbin to Riyadh"
)
raw_journey_data
# A tibble: 3 × 5
  start.long start.lat end.long end.lat name                   
       <dbl>     <dbl>    <dbl>   <dbl> <chr>                  
1     -0.118      51.5   -120.     34.4 London to Santa Barbara
2     31.5        30.0     24.8    59.4 New Cairo to Tallinn   
3    127.         45.8     46.7    24.8 Harbin to Riyadh       

journeys_to_sf

Here’s my function for converting a data.frame containing journeys to an "sf" "data.frame" using tidyeval:

journeys_to_sf <- function(journeys_data,
                           start_long = start.long,
                           start_lat = start.lat,
                           end_long = end.long,
                           end_lat = end.lat) {
  quo_start_long <- enquo(start_long)
  quo_start_lat <- enquo(start_lat)
  quo_end_long <- enquo(end_long)
  quo_end_lat <- enquo(end_lat)

  journeys_data |>
    select(!!quo_start_long, !!quo_start_lat, !!quo_end_long, !!quo_end_lat) |>
    transpose() |>
    map(~ matrix(flatten_dbl(.), nrow = 2, byrow = TRUE)) |>
    map(st_linestring) |>
    st_sfc(crs = 4326) |>
    st_sf(geometry = _) |>
    bind_cols(journeys_data) |>
    select(everything(), geometry)
}

This can very easily be inserted into a pipe chain for visualising with leaflet:

library("leaflet")
raw_journey_data |>
  journeys_to_sf() |>
  st_segmentize(units::set_units(100, km)) |>
  leaflet() |>
  addTiles() |>
  addPolylines() |>
  addCircleMarkers(lng = ~start.long, lat = ~start.lat, color = "green", opacity = 1, radius = 2) |>
  addCircleMarkers(lng = ~end.long,   lat = ~end.lat,   color = "red",   opacity = 1, radius = 2) |>
  addLegend(colors = c("green", "red"), labels = c("Journey start", "Journey end"))

Creating sf features from data.frame

Originally my blogpost used a quite inefficient method for converting data.frame to sf. Here’s my old method:

start_locs <- raw_journey_data |>
  select(start.long, start.lat) |>
  setNames(c("long", "lat")) |>
  mutate(journey_id = row_number())

end_locs <- raw_journey_data |>
  select(end.long, end.lat) |>
  setNames(c("long", "lat")) |>
  mutate(journey_id = row_number())

journey_data_linestrings <- start_locs |>
  bind_rows(end_locs) |>
  st_as_sf(coords = c("long", "lat")) |>
  group_by(journey_id) |>
  arrange(journey_id) |>
  summarise() |>
  st_cast("LINESTRING")

journey_data_linestrings <- st_set_crs(journey_data_linestrings, st_crs(4326))

Let’s go through the improved stages. First, create LINESTRINGs using purrr:

list_of_linestrings <- raw_journey_data |>
  select(-name) |>
  transpose() |>
  map(~ matrix(flatten_dbl(.), nrow = 2, byrow = TRUE)) |>
  map(st_linestring)

Then collect into an sf object and join with original data:

collection_sf <- list_of_linestrings |>
  st_sfc(crs = 4326) |>
  st_sf(geometry = _)

collection_sf |>
  bind_cols(raw_journey_data) |>
  st_segmentize(units::set_units(100, km)) |>
  leaflet() |>
  addTiles() |>
  addPolylines(label = ~name) |>
  addCircleMarkers(lng = ~start.long, lat = ~start.lat, color = "green", opacity = 1, radius = 2) |>
  addCircleMarkers(lng = ~end.long,   lat = ~end.lat,   color = "red",   opacity = 1, radius = 2) |>
  addLegend(colors = c("green", "red"), labels = c("Journey start", "Journey end"))