Using Isodistance to Find EV Charging Stations Within Range

R
geospatial
mapping
EV
leaflet
Author
Published

July 19, 2024

Introduction

An important transportation analysis problem is determining the maximum possible geographic area that can be reached from a given point with a limited vehicle range. This applies to all types of vehicles, but is especially relevant currently for electric vehicles (EVs). Drivers are interested in knowing what charging stations or locations they can reach, and this analysis can also inform where to build charging station or other infrastructure and businesses.

  • The problem statement: Given a starting location and a vehicle range, what are the possible locations that can be reached by driving?

The simplest solution would probably be to draw a circle with the given range and find the area within it. For example there is an API that can be used to find charging stations within a certain distance of a location. However, this is probably an overestimate of the actual geographic range that cna be reached, since you can rarely drive in a straight line from a given location.

A more realistic way to estimate the range is to compute a isodistance map, the maximum area that can be reached by driving a fixed distance from a starting point. This is similar to a isochrone map that shows the area that can be reached in a certain amount of time. Check out this blog post by Kyle Walkerfor a cool example of using isochrones in R.

In this analysis I will compare the areas from the two methods, and how the method affects the number of EV charging stations located within a certain range.

Methods

Results

Code
library(mapboxapi) # compute isocrhone/isodistance 
library(leaflet) # mapping
library(sf) # working with shapefiles
library(altfuelr) # get EV charging station info from NREL AFDC API
library(glue)
library(dplyr)
Note

You will need to sign up for API keys for both the mapboxapi and altfuelr packages. I have done this already and saved the API keys to my .Renviron file.

Tip

I was getting an error when using the sf package : Warning in CPL_crs_from_input(x) : GDAL Error 1: PROJ: proj_create_from_database: Cannot find proj.db. I solved this issue by re-installing sf : install.packages(‘sf’, repos = c(‘https://r-spatial.r-universe.dev’))

Compute the circle

First I’ll define the starting point and create a simple circle around the given point (Figure 1)

Compute circle
# starting point
starting_point <-  "Denver, CO"

# get long/lat of starting_point
home <- mb_geocode(starting_point)

# convert to sf point object
home_df <- data.frame(long = home[1], lat = home[2])
dat_sf <- st_as_sf(home_df, coords = c("long", "lat"), crs = 4326) 

# set radius
radius_miles <- 20
radius_km    <- radius_miles * 1.609344
radius_m     <- radius_km * 1e3

# create circle around starting point
my_circle <- st_buffer(dat_sf, dist = radius_m)

leaflet() |>
  addTiles() |>
  addPolygons(data = my_circle, fill = FALSE) |>
  addMarkers(dat = dat_sf, label = "Starting Point")
Figure 1: Map of starting point and circle with specified radius.

Compute isodistance map

Next I’ll compute the isodistance shape and plot it on the map along with the simple circle (Figure 2). You can see that the isodistance boundary is within the circle and varies by direction. It extends further along major roads/highways.

Compute isodistance shape
isos_dist <- mapboxapi::mb_isochrone(
  location = starting_point,
  profile = "driving",
  distance = round(radius_m)
) |> st_transform(4326)#
Plot map with isodistance
leaflet() |>
  addTiles() |>
  addPolygons(data = my_circle, fill = FALSE, color = "red", label = "Simple Circle") |>
  addPolygons(data = isos_dist, fill = FALSE, label = "Drive Time Isochrone") |>
  addMarkers(dat = dat_sf, label = "Starting Point")
Figure 2: Map of starting point, circle with specified radius, and driving isodistance polygon computed for same radius.

Find EV charging stations within specified radius

Next I get a list of the EV charging stations within the 20 mile radius of the starting point and add these to the map (Figure 3).

Finally, I calculate how many of the charging stations within the 20 mile radius are actually outside of the driving distance isochrone. This can be done using the st_within() or st_join() functions from the sf package.

We can see that some of the charging stations are actually outside of the isodistance boundary.

Find EV charging stations
my_api_key <- Sys.getenv("AFDC_KEY")

params <- altfuelr::nrel_params( fuel_type = "ELEC", limit = "all")

stations <- altfuelr::nearest_stations(api_key = my_api_key,
                                       longitude = home[1], 
                                       latitude = home[2],
                                       params = params,
                                       radius = radius_miles
)

df <- stations$content$fuel_stations

#glue('Found {stations$content$total_results} EV charging stations within {radius_miles} miles of location')
Calculate stations outside of isodistance
# convert the dataframe of ev charging stations to a sf point object
ev_points <- df |> select(station_name,longitude,latitude) |>  st_as_sf(coords = c("longitude", "latitude"), crs = 4326)#4269

# method 1 - use st_within() to find the points (stations) in each polygon
# this returns an array of true/false
ev_in_circle <- st_within(ev_points, my_circle,sparse = FALSE)
ev_in_iso    <- st_within(ev_points, isos_dist,sparse = FALSE)

#not_in_iso <- which(ev_in_iso == FALSE)
#ev_points[not_in_iso,]

# method 2 - use st_join
# this returns a dataframe, where the dummy variable is na if there was no match (ie point not in polygon)
# gives same results, but working with a data frame is a little nicer
df2 <- st_join(ev_points, my_circle |> mutate(dummy = 1), join = st_within,)
df3 <- st_join(ev_points, isos_dist |> mutate(dummy = 1), join = st_within)

ev_points_in <- df3  |> filter(!is.na(dummy))
ev_points_out <- df3 |> filter(is.na(dummy))

#glue('{nrow(ev_points_out)} out of {stations$content$total_results} EV charging stations were within circle radius but outside of isodistance line')
Plot Map with isodistance and charging stations
leaflet() |>
  addTiles() |>
  addCircleMarkers(data = ev_points_in,
                   fillOpacity = 0.25, 
                   color = "black",
                   weight = 1, 
                   fillColor = "gray",
                   radius = 8, 
                   label = "EV Charger in isodistance") |>
  addCircleMarkers(data = ev_points_out,
                   fillOpacity = 0.25, 
                   color = "black",
                   weight = 1, 
                   fillColor = "red",
                   radius = 8, 
                   label = "EV Charger beyond isodistance") |>
  addPolygons(data = my_circle, fill = FALSE, color = "red", label = "Simple Circle") |>
  addPolygons(data = isos_dist, fill = FALSE, label = "Drive Time Isochrone") |>
  addMarkers(dat = dat_sf, label = "Starting Point")
Figure 3: Map showing circle and isodistance, with EV charging station locations within radius. Charging station points outside of isodistance are colored red.

Summary

  • The (driving) isodistance shape for a given range was computed and compared to a circle with same radius.
  • The AFDC API was used to find EV charging stations within a 20 mile radius from the starting location.
  • Starting from the center of Denver with a radius of 20 miles, 142 out of 1022 charging stations were within the radius but outside of the isodistance boundary.
  • Using the isodistance gives a more accurate and realistic picture of vehicle range.

SessionInfo

R version 4.4.1 (2024-06-14)
Platform: x86_64-apple-darwin20
Running under: macOS Sonoma 14.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Denver
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
[1] dplyr_1.1.4    glue_1.7.0     altfuelr_0.1.0 sf_1.0-17      leaflet_2.2.2 
[6] mapboxapi_0.6 

loaded via a namespace (and not attached):
 [1] s2_1.1.6            tidyr_1.3.1         utf8_1.2.4         
 [4] generics_0.1.3      renv_1.0.4          class_7.3-22       
 [7] xml2_1.3.6          KernSmooth_2.23-24  stringi_1.8.4      
[10] lattice_0.22-6      jpeg_0.1-10         digest_0.6.36      
[13] magrittr_2.0.3      evaluate_0.24.0     grid_4.4.1         
[16] aws.s3_0.3.21       aws.signature_0.6.0 fastmap_1.2.0      
[19] jsonlite_1.8.8      e1071_1.7-14        DBI_1.2.2          
[22] httr_1.4.7          purrr_1.0.2         fansi_1.0.6        
[25] crosstalk_1.2.1     slippymath_0.3.1    jquerylib_0.1.4    
[28] codetools_0.2-20    cli_3.6.3           rlang_1.1.4        
[31] units_0.8-5         withr_3.0.1         base64enc_0.1-3    
[34] yaml_2.3.10         raster_3.6-26       tools_4.4.1        
[37] geojsonsf_2.0.3     curl_5.2.1          vctrs_0.6.5        
[40] R6_2.5.1            png_0.1-8           proxy_0.4-27       
[43] lifecycle_1.0.4     protolite_2.3.0     classInt_0.4-10    
[46] magick_2.8.4        htmlwidgets_1.6.4   pkgconfig_2.0.3    
[49] terra_1.7-71        pillar_1.9.0        Rcpp_1.0.13        
[52] xfun_0.46           tibble_3.2.1        tidyselect_1.2.1   
[55] rstudioapi_0.16.0   knitr_1.48          htmltools_0.5.8.1  
[58] rmarkdown_2.27      wk_0.9.1            compiler_4.4.1     
[61] sp_2.1-4           

References

Burch, Christopher. 2020. “Altfuelr: Provides an Interface to the NREL Alternate Fuels Locator.” https://CRAN.R-project.org/package=altfuelr.
Cheng, Joe, Barret Schloerke, Bhaskar Karambelkar, and Yihui Xie. 2024. “Leaflet: Create Interactive Web Maps with the JavaScript ’Leaflet’ Library.” https://rstudio.github.io/leaflet/.
Walker, Kyle. 2024. “Mapboxapi: R Interface to ’Mapbox’ Web Services.” https://CRAN.R-project.org/package=mapboxapi.