R/estimate_travel_demand.R

Defines functions line_to_bearing linestring_to_line estimate_travel_demand

Documented in estimate_travel_demand linestring_to_line line_to_bearing

#' Estimate Travel Demand
#'
#' @description Estimate the travel deamnd from desire lines
#'
#' @param infra a sf dataframe of infrastucutre
#' @param desire as sf dataframe of desire lines
#' @param stop_buff_dist distance in metres to buffer stops
#' @param point_buff_dist distance in metres to buffer points
#' @param infra_buff_dist_ratio distance in metres to buffer infrastructure
#' @return a dataframe of
#' @export

estimate_travel_demand <- function(infra, 
                                   desire, 
                                   stop_buff_dist = 3000,
                                   point_buff_dist = 200,
                                   infra_buff_dist_ratio = 7){
  
  desire = desire[desire$from != desire$to, ]
  
  # Bug in R 3.6?
  desire = as.data.frame(desire)
  desire = sf::st_as_sf(desire, crs = 4326)
  
  # Measure length
  desire$length_km <- as.numeric(sf::st_length(desire)) / 1000
  desire$id <- seq_len(nrow(desire))
  
  #If active travel only consider short desire lines
  if(all(infra$mode_class %in% c("Active travel","onward_travel"))){
    desire <- desire[desire$length_km < 5,]
  }
  
  # Split out Lines and non-lines
  infra_lines = infra[sf::st_geometry_type(infra$geometry) == "LINESTRING",]
  infra_point = infra[sf::st_geometry_type(infra$geometry) == "POINT",]
  infra_stops = infra_point[grepl("station",
                                  infra_point$intervention, 
                                  ignore.case = TRUE),]
  infra_point = infra_point[!grepl("station",
                                   infra_point$intervention, 
                                  ignore.case = TRUE),]
  
  # Options
  # 1) - Just lines - work out along lines
  # 2) - Lines and points where points are stations - Station mode doe demand between stations
  # 3) - lines and non-station points - work out along lines and points add small bonus
  
  
  if(nrow(infra_point) > 0){
    # Point infrastructure but not a station - e.g. cycle parking
    # Demand is to/from the point
    point_buff = sf::st_combine(sf::st_buffer(infra_point, point_buff_dist))
    desire_point = desire[point_buff,] #SOme just pass
    desire_point$point = TRUE
    suppressWarnings(desire_point_ends <- sf::st_cast(desire_point, "POINT"))
    desire_point_ends = desire_point_ends[point_buff,]
    desire_point = desire_point[desire_point$id %in% desire_point_ends$id,]
    
    
  }
  
  if(nrow(infra_stops) > 0){
    # Station Mode - Demand is between stations
    onward_lines = infra_lines[infra_lines$intervention == "onward_travel",]
    if(nrow(onward_lines) > 0){
      onward_lines = sf::st_cast(onward_lines$geometry, "POINT")
      buff_stops = sf::st_buffer(c(infra_stops$geometry,onward_lines), stop_buff_dist)
    } else {
      buff_stops = sf::st_buffer(infra_stops, stop_buff_dist)
    }
    
    desire_points = suppressWarnings(sf::st_cast(desire[,c("from","to")], "POINT"))
    desire_points$msoa = ifelse(rep(c(TRUE,FALSE), nrow(desire)), 
                                desire_points$from, desire_points$to)
    desire_points = desire_points[!duplicated(desire_points$geometry),]
    desire_points = desire_points[buff_stops, op = sf::st_intersects]
    
    #plot(desire$geometry)
    desire = desire[desire$from %in% desire_points$msoa &
                      desire$to %in% desire_points$msoa, ]
    
    #plot(foo$geometry, col = "red", add = TRUE)
    #plot(buff_stops, add = TRUE)
    desire$point = FALSE
    
  } else if(nrow(infra_lines) > 0) {
    # Road Mode - Demand is along infrastructure
    
    
    #Get straight line from infra
    infra_straight = linestring_to_line(infra_lines)
    
    # infra_buff_dist is ratio of length
    infra_buff_dist = sum(as.numeric(sf::st_length(infra))) / infra_buff_dist_ratio
    if(infra_buff_dist < 3000){
      infra_buff_dist = 3000
    }
    
    
    buff_straight = sf::st_buffer(infra_straight, 
                                  infra_buff_dist,
                                  endCapStyle = "SQUARE")
    
    # plot(desire$geometry)
    desire = desire[buff_straight, op = sf::st_within,]
    
    if(nrow(desire) == 0){
      return(make_empty_demand())
    } 
    
    # Get Bearings
    infra_straight$bearing = line_to_bearing(infra_straight)
    desire$bearing = line_to_bearing(desire)
    
    # Select those +/- 30 degrees in radians
    desire$parallel = (desire$bearing <= infra_straight$bearing + 0.523599 &
                         desire$bearing >= infra_straight$bearing - 0.523599)
    
    # plot(desire["parallel"])
    # plot(infra_lines$geometry, add = TRUE, col = "red", lwd = 3)
    desire <- desire[desire$parallel,]
    desire$point = FALSE
    
  }
  
  # COmbine point and line desires
  if(exists("desire_point")){
    #if(nrow(desire_point) > 0){
      if(nrow(infra_stops) == 0 & nrow(infra_lines) == 0){
        desire = desire_point
      } else {
        desire$point = FALSE
        desire = rbind(desire, desire_point)
        desire = desire[order(desire$point),]
        desire = desire[!duplicated(desire$id),]
      }
    #} else {
    #}
  }
  
  # Check again for empty results
  if(nrow(desire) == 0){
    return(make_empty_demand())
  }
  
  desire <- sf::st_drop_geometry(desire)
  
  desire$from <- NULL
  desire$to <- NULL
  desire$bearing <- NULL
  desire$parallel <- NULL
  desire$all <- NULL
  desire$id <- NULL
  
  names(desire)[1:8] <- paste0(names(desire)[1:8],"_before")
  
  #TODO: Proper fix
  infra$mode[infra$mode == "Local"] <- "Road - Minor"
  infra$mode[infra$mode == "Local road"] <- "Road - Minor"
  infra$mode[infra$mode == "Foot"] <- "Walking"
  
  # Estimate New mode splits
  mode_shifts <- NULL
  utils::data("mode_shifts", envir=environment())
  mode_shifts <- mode_shifts[mode_shifts$mode %in% infra$mode, ]
  mode_shifts <- mode_shifts[mode_shifts$intervention_class %in% infra$intervention_class, ]
  
  #Loop over each mode
  for(md in c("walk","cycle","drive","passenger","rail","bus")){
    induceddemand_low = desire[,paste0(md,"_before")] * dplyr::if_else(desire$point, 0.0001, mode_shifts$induceddemand_low[mode_shifts$travel_mode == md])
    induceddemand_average = desire[,paste0(md,"_before")] * dplyr::if_else(desire$point, 0.0001,mode_shifts$induceddemand_average[mode_shifts$travel_mode == md])
    induceddemand_high = desire[,paste0(md,"_before")] * dplyr::if_else(desire$point, 0.0001,mode_shifts$induceddemand_high[mode_shifts$travel_mode == md])
    
    modeshift_low = -desire[,paste0(md,"_before")] * dplyr::if_else(desire$point, 0.0001,mode_shifts$modeshift_low[mode_shifts$travel_mode == md])
    modeshift_average = -desire[,paste0(md,"_before")] * dplyr::if_else(desire$point, 0.0001,mode_shifts$modeshift_average[mode_shifts$travel_mode == md])
    modeshift_high = -desire[,paste0(md,"_before")] * dplyr::if_else(desire$point, 0.0001,mode_shifts$modeshift_high[mode_shifts$travel_mode == md])
    
    desire[,paste0(md,"_after-low")] = desire[,paste0(md,"_before")] + modeshift_low
    desire[,paste0(md,"_after-average")] = desire[,paste0(md,"_before")] + modeshift_average
    desire[,paste0(md,"_after-high")] = desire[,paste0(md,"_before")] + modeshift_high
    
    desire[,paste0(md,"_induceddemand-low")] = induceddemand_low
    desire[,paste0(md,"_induceddemand-average")] = induceddemand_average
    desire[,paste0(md,"_induceddemand-high")] = induceddemand_high
    
    #Change in number of travellers
    desire[,paste0(md,"_change-low")] = desire[,paste0(md,"_after-low")] - desire[,paste0(md,"_before")]
    desire[,paste0(md,"_change-average")] = desire[,paste0(md,"_after-average")] - desire[,paste0(md,"_before")]
    desire[,paste0(md,"_change-high")] = desire[,paste0(md,"_after-high")] - desire[,paste0(md,"_before")]
  }
  
  # Get the total change in trips and add to the infrastructure mode type
  desire$total_shifted_travellers_low = rowSums(desire[,grepl("_change-low",names(desire))])
  desire$total_shifted_travellers_average = rowSums(desire[,grepl("_change-average",names(desire))])
  desire$total_shifted_travellers_high = rowSums(desire[,grepl("_change-high",names(desire))])
  
  # Add the new travellers to the correct mode
  md = unique(mode_shifts$mode)
  if(md %in% c("High speed rail","Rail","Light Rail")){
    md = "rail"
  } else if(md %in% c("Walking")){
    md = "walk"
  } else if (md %in% c("Bus")){
    md = "bus"
  } else if (md %in% c("Road - Minor","Road - Major")){
    md = "drive"
  } else if (md %in% c("Bicycle")){
    md = "cycle"
  } else {
    stop("Unknown mode for demand modelling")
  }
  
  # Best case is lots of mode shift and little induced demand
  # For cars special case as mode shift is bad if high
  # Worst case is the opposite
  if(md == "drive"){
    desire[,paste0(md,"_after-low")] = desire[,paste0(md,"_after-low")] - 
      desire$total_shifted_travellers_high + desire[,paste0(md,"_induceddemand-high")]
    desire[,paste0(md,"_after-average")] = desire[,paste0(md,"_after-average")] - 
      desire$total_shifted_travellers_average + desire[,paste0(md,"_induceddemand-average")]
    desire[,paste0(md,"_after-high")] = desire[,paste0(md,"_after-high")] - 
      desire$total_shifted_travellers_low + desire[,paste0(md,"_induceddemand-low")]
  } else {
    desire[,paste0(md,"_after-low")] = desire[,paste0(md,"_after-low")] - 
      desire$total_shifted_travellers_low + desire[,paste0(md,"_induceddemand-high")]
    desire[,paste0(md,"_after-average")] = desire[,paste0(md,"_after-average")] - 
      desire$total_shifted_travellers_average + desire[,paste0(md,"_induceddemand-average")]
    desire[,paste0(md,"_after-high")] = desire[,paste0(md,"_after-high")] - 
      desire$total_shifted_travellers_high + desire[,paste0(md,"_induceddemand-low")]
  }
  
  # Update the change in travellers
  desire[,paste0(md,"_change-low")] = desire[,paste0(md,"_after-low")] - desire[,paste0(md,"_before")]
  desire[,paste0(md,"_change-average")] = desire[,paste0(md,"_after-average")] - desire[,paste0(md,"_before")]
  desire[,paste0(md,"_change-high")] = desire[,paste0(md,"_after-high")] - desire[,paste0(md,"_before")]
  
  # Estimate change in km and emissions
  # 1.4 circuity factor
  for(md in c("walk","cycle","drive","passenger","rail","bus")){
    desire[,paste0(md,"_changekm-low")] = desire[,paste0(md,"_change-low")] * desire$length_km * 1.4
    desire[,paste0(md,"_changekm-average")] = desire[,paste0(md,"_change-average")] * desire$length_km * 1.4 
    desire[,paste0(md,"_changekm-high")] = desire[,paste0(md,"_change-high")] * desire$length_km * 1.4
  }
  
  
  # Emission factors kg/km DEFRA 2020
  utils::data("emissions_factors", envir=environment())
  
  # Also switch to years of emissions
  for(md in c("walk","cycle","drive","passenger","rail","bus")){
    desire[,paste0(md,"_changeemissions-low")] = desire[,paste0(md,"_changekm-low")] * emissions_factors[,md] * 365
    desire[,paste0(md,"_changeemissions-average")] = desire[,paste0(md,"_changekm-average")] * emissions_factors[,md] * 365
    desire[,paste0(md,"_changeemissions-high")] = desire[,paste0(md,"_changekm-high")] * emissions_factors[,md] * 365
  }

  desire$length_km <- NULL
  desire$point <- NULL
  
  # Total Emissions in kgco2e / year
  emissions_total <- dplyr::summarise_all(desire, .funs = sum, na.rm = TRUE)
  emissions_total <- tidyr::pivot_longer(emissions_total, 
                          cols = tidyr::everything(),
                          names_to = c("mode",".value"),
                          names_pattern = "(.*)_(.*)")
  
  emissions_total <- emissions_total[emissions_total$mode != "total_shifted_travellers",]
  emissions_total <- emissions_total[,c("mode","before","after-low","after-average","after-high",
                                        "changeemissions-low","changeemissions-average","changeemissions-high")]
  names(emissions_total) <- c("mode","before","after_low","after_average","after_high",
                              "changeemissions_low","changeemissions_average","changeemissions_high")
  
  
  emissions_increase <- sum(emissions_total$`changeemissions_average`[emissions_total$`changeemissions_average` > 0], na.rm = TRUE)
  emissions_decrease <- sum(emissions_total$`changeemissions_average`[emissions_total$`changeemissions_average` < 0], na.rm = TRUE)
  emissions_net <- emissions_increase + emissions_decrease
  
  emissions_increase_low <- sum(emissions_total$`changeemissions_low`[emissions_total$`changeemissions_low` > 0], na.rm = TRUE)
  emissions_decrease_low <- sum(emissions_total$`changeemissions_low`[emissions_total$`changeemissions_low` < 0], na.rm = TRUE)
  emissions_net_low <- emissions_increase_low + emissions_decrease_low
  
  emissions_increase_high <- sum(emissions_total$changeemissions_high[emissions_total$changeemissions_high > 0], na.rm = TRUE)
  emissions_decrease_high <- sum(emissions_total$changeemissions_high[emissions_total$changeemissions_high < 0], na.rm = TRUE)
  emissions_net_high <- emissions_increase_high + emissions_decrease_high
  
  emissions_total[2:ncol(emissions_total)] <- lapply(emissions_total[2:ncol(emissions_total)], round)
  
  res <- list(emissions_increase, emissions_decrease, emissions_net,
              emissions_increase_low, emissions_decrease_low, emissions_net_low,
              emissions_increase_high, emissions_decrease_high, emissions_net_high,
              emissions_total)
  names(res) <- c("emissions_increase", "emissions_decrease", "emissions_net",
                  "emissions_increase_low", "emissions_decrease_low", "emissions_net_low",
                  "emissions_increase_high", "emissions_decrease_high", "emissions_net_high",
                  "emissions_total")
  
  return(res)
}

#' Convert linestring into straight line
#'
#'
#' @param l linestring
#' @return a sf dataframe
linestring_to_line <- function(l){
  crs <- sf::st_crs(l)
  l <- sf::st_coordinates(l)
  l <- l[,c("X","Y")]
  l <- l[c(1,nrow(l)),]
  l <- sf::st_linestring(l)
  l <- sf::st_as_sfc(list(l), crs = crs)
  l <- sf::st_as_sf(data.frame(geometry = l))
}


#' Convert lines to bearings
#'
#' Only works for straight lines
#'
#' @param l linestring
#' @param simplify logical, make all positive and rotate 90 degrees
#' @return numeric between -pi and +pi
line_to_bearing <- function(l, simplify = TRUE){
  p <- sf::st_cast(sf::st_geometry(l), "POINT")
  bearing <- lwgeom::st_geod_azimuth(p)
  bearing <- bearing[seq(1,length(bearing), by = 2)]
  bearing <- as.numeric(bearing)
  if(simplify){
    bearing <- ifelse(bearing >= 0, 
          bearing,
          bearing +3.141593) + 1.570796
  }
  return(bearing)
}
SDCA-tool/sdca-package documentation built on Aug. 13, 2022, 5:41 p.m.