How to create multiline string sf object to create a time slider in Leaflet map

Viewed 162

I'm trying to create a leaflet map which shows different movement paths for different months of the year. I.e. I've got a dataset showing multiple journeys per month and I want to display the movement paths separately for each month using the addTimeslider feature of the leaflet.extras2 package.

To do so I have been trying to adapt the code posted by SymbolixAU I found here: leaflet add multiple polylines

This code uses sf functions including st_linestring to create an object that can be supplied to a addPolylines leaflet function to show all movement paths at once.

I'm pretty sure for my purposes (showing data separately for each month) I have to use st_multilinestring, which takes a list of matrices containing the coordinates for multiple polylines per row (with one row per month) rather than a single polyline per row.

Once I have that I think I could supply that object to the addTimeslider function of leaflet.extras2 to achieve what I need. I'm quite sure of this because when I used the sf object created using sf_linestring inthe AddTimeslider feature I was able to use the time slider on the map to individual movement paths at a time.

However, I have been trying for hours and haven't been successful. Would be hugely grateful for any pointers, please and thank you.

Some example data:

#load packages

library(dplyr)
library(leaflet)
library(leaflet.extras2)
library(sf)
library(data.table)

# create the example dataset

data <- structure(list(arrival_month = structure(c(3L, 3L, 4L, 4L, 4L, 
5L, 5L, 6L, 6L, 6L), .Label = c("January", "February", "March", 
"April", "May", "June", "July", "August", "September", "October", 
"November", "December"), class = c("ordered", "factor")), start_lat = c(33.40693, 
33.64672, 33.57127, 33.42848, 33.54936, 33.53418, 33.60399, 33.49554, 
33.5056, 33.61696), start_long = c(-112.0298, -111.9255, -112.049, 
-112.0998, -112.0912, -112.0911, -111.9273, -111.9687, -112.0563, 
-111.9866), finish_lat...4 = c(33.40687, 33.64776, 33.57125, 
33.42853, 33.54893, 33.53488, 33.60401, 33.49647, 33.5056, 33.61654
), finish_lat...5 = c(-112.0343, -111.9303, -112.0481, -112.0993, 
-112.0912, -112.0911, -111.931, -111.9711, -112.0541, -111.986
)), row.names = c(NA, -10L), class = c("data.table", "data.frame"
), .internal.selfref = <pointer: 0x0000026e5df41ef0>)

My attempt at the code:


# Convert the data into a list of matrices for each month

mnths <- c("May","March","April","June")


mat_list <- list()



for (i in mnths) {
  
  month <- as.matrix(data %>% filter(arrival_month == i) %>% select(-1)) 
  mat_list[[i]] <- month
  
}

# convert to an sf object


data_DT <- setDT(data)

sf <- data_DT[
  , {
    geometry <- sf::st_multilinestring(x = mat_list)
    geometry <- sf::st_sfc(geometry)
    geometry <- sf::st_sf(geometry = geometry)
  }
  , by = arrival_month
  ]

sf <- sf::st_as_sf(sf)

This yields the following result:

sf

It's not correct because each row contains the coordinates for all the months, rather than just for the month in the respective row. I'm at a loss as to where to go from here - any help would be hugely appreciated.

Thanks

2 Answers

I would do it slightly differently today, making use of {sfheaders} to build the linestrings.

library(sfheaders)
library(sf)
library(data.table)


setDT( data )

data[, line_id := .I ]  ## Assuming each row is a line

## create a long-form of the data
dt_line <- rbindlist(
  list(
    data[, .(arrival_month, line_id, lon = start_long, lat = start_lat, sequence = 1)]
    , data[, .(arrival_month, line_id, lon = finish_lat...5, lat = finish_lat...4, sequence = 2)] ## I think 'finish_lat...5' is actually the 'long' 
  )
)

setorder(dt_line, line_id, sequence)

sf <- sfheaders::sf_multilinestring(
  obj = dt_line
  , x = "lon"
  , y = "lat"
  , multilinestring_id = "arrival_month"
  , linestring_id = "line_id"
  , keep = T
)

sf::st_crs( sf ) <- 4326 ## Assuming it's in Web Mercator 

# Simple feature collection with 4 features and 2 fields
# Geometry type: MULTILINESTRING
# Dimension:     XY
# Bounding box:  xmin: -112.0998 ymin: 33.40687 xmax: -111.9255 ymax: 33.64776
# Geodetic CRS:  WGS 84
#.  arrival_month sequence                       geometry
# 1             3        1 MULTILINESTRING ((-112.0298...
# 2             4        1 MULTILINESTRING ((-112.049 ...
# 3             5        1 MULTILINESTRING ((-112.0911...
# 4             6        1 MULTILINESTRING ((-111.9687...

Note the arrival_month has been re-coded to it's factor levels.

I managed to adapt the code provided by SymbolixAU to suit my purposes, by using sf_multilinestring instead of sf_line string:

## Generate data

data <- structure(list(arrival_month = structure(c(3L, 3L, 4L, 4L, 4L, 
5L, 5L, 6L, 6L, 6L), .Label = c("January", "February", "March", 
"April", "May", "June", "July", "August", "September", "October", 
"November", "December"), class = c("ordered", "factor")), start_lat = c(33.40693, 
33.64672, 33.57127, 33.42848, 33.54936, 33.53418, 33.60399, 33.49554, 
33.5056, 33.61696), start_long = c(-112.0298, -111.9255, -112.049, 
-112.0998, -112.0912, -112.0911, -111.9273, -111.9687, -112.0563, 
-111.9866), finish_lat...4 = c(33.40687, 33.64776, 33.57125, 
33.42853, 33.54893, 33.53488, 33.60401, 33.49647, 33.5056, 33.61654
), finish_lat...5 = c(-112.0343, -111.9303, -112.0481, -112.0993, 
-112.0912, -112.0911, -111.931, -111.9711, -112.0541, -111.986
)), row.names = c(NA, -10L), class = c("data.table", "data.frame"
))

## Add id column and convert to data table

data[, line_id := .I ]

setDT(data)

## create a long-form of the data

dt_line <- rbindlist(
  list(
    data[, .(arrival_month, line_id, lon = start_long, lat = start_lat, sequence = 1)]
    , data[, .(arrival_month, line_id, lon = finish_lat...5, lat = finish_lat...4, sequence = 2)] ## I think 'finish_lat...5' is actually the 'long' 
  )
)

setorder(dt_line, line_id, sequence)

## Create multistring sf object

sf <- sfheaders::sf_multilinestring(
  obj = dt_line
  , x = "lon"
  , y = "lat"
  , linestring_id = "line_id"
  , multilinestring_id = "arrival_month"
  , keep = TRUE
)

sf::st_as_sf(sf)

## Convert arrival_month back to factor

sf$arrival_month <- as.factor(sf$arrival_month)

Might not be the most elegant but this does the job.

The key was to use sf_multilinestring and include/specify both linestring_id and multilinestring_id to distinguish between separate sets of polylines.

This is the result:

resulting sf object

Now when this sf object is used in AddTimeslider it behaves as desired.

Credit to SymbolixAU for most of the code and bringing my attention to the sfheaders package

Related