Introduction

This tutorial demonstrates how to use the Bing Maps API to calculate drive time (in minutes) and driving distance (in miles) between two locations. In order to use the Bing Maps API, you must first request create a Bing Maps Developer account and then generate an API Key.

Install and load libraries

This step uses the pacman package to install and load all of the packages for this tutorial.

# Use pacman package to load packages used for exercise
library(pacman)

p_load(dplyr, kableExtra, RCurl, rjson, FNN, lubridate, leaflet)

Part I: Load and Prepare Data

Step 1: Load list of ZIP Codes

The Bing Maps API can be used to calculate drive time and distance between two locations. For this tutorial, 10 randomly selected ZIP codes in Allegheny County, PA will be used as starting locations and the nearest hospital from a large regional healthcare provider will be used as the destination location.The list of ZIP codes is from the 2021 Q4 ZIP code to county crosswalk provided by the US Department of Housing and Urban Development.

# Load List of Allegheny County, PA ZIP Codes from HUD
allegheny_zips <- read.csv("./data/ZIP_COUNTY_122021.csv",
                           colClasses = (zip = "character")) %>%
  filter(county == 42003) %>%
  group_by(zip) %>%
  top_n(1, res_ratio) %>%
  mutate(City = gsub("(?<=\\b)([a-z])", "\\U\\1",
                     tolower(usps_zip_pref_city), perl=TRUE)) %>%
  rename(ZIP = zip,
         County = county,
         State = usps_zip_pref_state) %>%
  select(ZIP, City, County, State) %>%
  arrange(ZIP)

# Select 10 at random
random_zips <- as.data.frame(allegheny_zips) %>%
  sample_n(10)

# Display data
random_zips %>%
  kbl(caption = "10 Randomly Selected ZIP Codes in Allegheny County", 
      align = "r", row.names = FALSE) %>%
  kable_minimal("hover")
10 Randomly Selected ZIP Codes in Allegheny County
ZIP City County State
15236 Pittsburgh 42003 PA
15005 Baden 42003 PA
15065 Natrona Heights 42003 PA
15143 Sewickley 42003 PA
15229 Pittsburgh 42003 PA
15020 Bunola 42003 PA
15126 Imperial 42003 PA
15226 Pittsburgh 42003 PA
15134 Mckeesport 42003 PA
16059 Valencia 42003 PA

Step 2: Geocode ZIP Codes

The code below finds coordinates for a street address in each of the ZIP codes loaded in step 1 above, using just the 5-digit ZIP code and the user’s Bing Maps API key. If no match is found, then “NA” will be assigned for the two coordinate fields for that ZIP code.

# Store Bing Maps API key
BingMapsKey <- "[insert your API key here]"

# Loop through each of the randomly selected Allegehny County ZIP codes to find geospatial coordinates for a street address near the center of that ZIP code
for(i in 1:nrow(random_zips)){
  
  url <- URLencode(paste0("http://dev.virtualearth.net/REST/v1/Locations?q=",
                          random_zips$ZIP[i],"&maxResults=1&key=",BingMapsKey))
  json <- fromJSON(getURL(url), simplify = FALSE)
  
  if (json$resourceSets[[1]]$estimatedTotal > 0) {
    lat <- json$resourceSets[[1]]$resources[[1]]$point$coordinates[[1]]
    lon <- json$resourceSets[[1]]$resources[[1]]$point$coordinates[[2]]
  }  else {
    lat <- lon <- NA
  }
  
  random_zips$Latitude[i] <- lat
  random_zips$Longitude[i] <- lon
}

# Display ZIP code data with geocoded location
allegheny_zips[1:10,] %>%
  kbl(caption = "Allegheny County ZIP Codes with Geocoded Coordinates", 
      align = "r", row.names = FALSE) %>%
  kable_minimal("hover")
10 Randomly Selected ZIP Codes in Allegheny County with Geocoded Coordinates
ZIP City County State Latitude Longitude
15001 Aliquippa 42003 PA 40.60068 -80.28081
15003 Ambridge 42003 PA 40.59342 -80.22189
15005 Baden 42003 PA 40.63871 -80.19928
15006 Bairdford 42003 PA 40.63161 -79.88007
15007 Bakerstown 42003 PA 40.65100 -79.93566
15012 Belle Vernon 42003 PA 40.15799 -79.81738
15014 Brackenridge 42003 PA 40.60984 -79.74207
15015 Bradfordwoods 42003 PA 40.63476 -80.08659
15017 Bridgeville 42003 PA 40.34584 -80.11489
15018 Buena Vista 42003 PA 40.26614 -79.79324

Step 3: Load Hospital data

Destinations for each of the ZIP codes will be the nearest UPMC hospital, determined using ‘as the crow flies’.UPMC is a large healthcare provider in the region, headquartered in Pittsburgh. The addresses for the 12 UPMC hospitals in the Southwest PA region were taken from this public page on UPMC’s website.

# Load Southwest PA region UPMC hospital locations
upmc_hospitals <- read.csv("./data/UPMC Locations.csv")

# Display data
upmc_hospitals %>%
  kbl(caption = "UPMC Hospitals in Southwest PA", 
      align = "r", row.names = FALSE) %>%
  kable_minimal("hover")
UPMC Hospitals in Southwest PA
Location Address
UPMC Children’s Hospital of Pittsburgh 4401 Penn Ave, Pittsburgh, PA 15224
UPMC East 2775 Mosside Blvd, Monroeville, PA 15146
UPMC Magee-Womens Hospital 300 Halket St, Pittsburgh, PA 15146
UPMC McKeesport 1500 5th Ave, McKeesport, PA 15132
UPMC Mercy 1400 Locust St, Pittsburgh, PA 15219
UPMC Montefiore 3459 Fifth Ave, Pittsburgh, PA 15213
UPMC Passavant - Cranberry 1 St. Francis Way, Craberry Township, PA 16066
UPMC Passavant - McCandless 9100 Babcock Blvd, Pittsburgh, PA 15237
UPMC Presbyterian 200 Lothrop St, Pittsburgh, PA 15213
UPMC Shadyside 5230 Centre Ave, Pittsburgh, PA 15232
UPMC St. Margaret 815 Freeport Rd, Pittsburgh, PA 15215
UPMC Western Psychiatric Hospital 3811 O’Hara St, Pittsburgh, PA 15213

Step 4: Geocode UPMC Hospitals

The code below takes each of the UPMC Hospital addresses from step 3 above, stored in the field upmc_hospitals$Address, to request the coordinates from the Bing Maps API. If coordinates are found, then they will be added to the corresponding address in the new fields “Latitude” and “Longitude”; otherwise, if no match is found, then “NA” will be assigned for the two coordinate fields of that address.

# Loop through each hospital address and add geospatial coordinates
for(i in 1:nrow(upmc_hospitals)){

  url <- URLencode(paste0("http://dev.virtualearth.net/REST/v1/Locations?q=",
                          upmc_hospitals$Address[i],"&maxResults=1&key=",BingMapsKey))
  json <- fromJSON(getURL(url), simplify = FALSE)
  
  if (json$resourceSets[[1]]$estimatedTotal > 0) {
    lat <- json$resourceSets[[1]]$resources[[1]]$point$coordinates[[1]]
    lon <- json$resourceSets[[1]]$resources[[1]]$point$coordinates[[2]]
  }  else {
    lat <- lon <- NA
  }

  upmc_hospitals$Latitude[i] <- lat
  upmc_hospitals$Longitude[i] <- lon
}

# Display UPMC Hospital data for Southwest PA Region with geocoded location
upmc_hospitals %>%
  kbl(caption = "UPMC Hospitals in Southwest PA with Geocoded Coordinates", 
      align = "r", row.names = FALSE) %>%
  kable_minimal("hover")
UPMC Hospitals in Southwest PA with Geocoded Coordinates
Location Address Latitude Longitude
UPMC Children’s Hospital of Pittsburgh 4401 Penn Ave, Pittsburgh, PA 15224 40.46657 -79.95315
UPMC East 2775 Mosside Blvd, Monroeville, PA 15146 40.43666 -79.76028
UPMC Magee-Womens Hospital 300 Halket St, Pittsburgh, PA 15146 40.43681 -79.96070
UPMC McKeesport 1500 5th Ave, McKeesport, PA 15132 40.35162 -79.84907
UPMC Mercy 1400 Locust St, Pittsburgh, PA 15219 40.43623 -79.98560
UPMC Montefiore 3459 Fifth Ave, Pittsburgh, PA 15213 40.44058 -79.96178
UPMC Passavant - Cranberry 1 St. Francis Way, Craberry Township, PA 16066 40.68328 -80.09830
UPMC Passavant - McCandless 9100 Babcock Blvd, Pittsburgh, PA 15237 40.57290 -80.01395
UPMC Presbyterian 200 Lothrop St, Pittsburgh, PA 15213 40.44241 -79.96029
UPMC Shadyside 5230 Centre Ave, Pittsburgh, PA 15232 40.45428 -79.93960
UPMC St. Margaret 815 Freeport Rd, Pittsburgh, PA 15215 40.48925 -79.89633
UPMC Western Psychiatric Hospital 3811 O’Hara St, Pittsburgh, PA 15213 40.44373 -79.95988

Part II: Calculations

Step 1: Find the nearest Hospital

Using the geocoded coordinates of a street location within each ZIP code and the geocoded coordinates of each of the Southwest PA UPMC hospitals, the shortest combination of Euclidean distance for each ZIP code are calculated below.

# Find nearest UPMC Hospital to ZIP code point
nearest_hospital <- cbind(random_zips[,],
                          get.knnx(upmc_hospitals[,c("Latitude","Longitude")],
                          random_zips[,c("Latitude","Longitude")], k=1)$nn.index, 
                          get.knnx(upmc_hospitals[,c("Latitude","Longitude")],
                          random_zips[,c("Latitude","Longitude")], k=1)$nn.dist)

# Rename columns
colnames(nearest_hospital)[c(5:6,7:8)] <- c("ZIP_Latitude",
                                            "ZIP_Longitude",
                                            "Hospital_ID",
                                            "Euclidean_Distance")

# Add name of UPMC Hospital, its location, and its coordianates
nearest_hospital$UPMC_Location  <- upmc_hospitals[nearest_hospital[,7],1]
nearest_hospital$UPMC_Address   <- upmc_hospitals[nearest_hospital[,7],2]
nearest_hospital$UPMC_Latitude  <- upmc_hospitals[nearest_hospital[,7],3]
nearest_hospital$UPMC_Longitude <- upmc_hospitals[nearest_hospital[,7],4]

# Display nearest hospital data
nearest_hospital[,c("ZIP", "City", "UPMC_Location", "UPMC_Address")] %>%
  kbl(caption = "Nearest UPMC Hospital to Each Randomly Selected ZIP Code", 
      align = "r", row.names = FALSE) %>%
  kable_minimal("hover")
Nearest UPMC Hospital to Each Randomly Selected ZIP Code
ZIP City UPMC_Location UPMC_Address
15236 Pittsburgh UPMC Mercy 1400 Locust St, Pittsburgh, PA 15219
15005 Baden UPMC Passavant - Cranberry 1 St. Francis Way, Craberry Township, PA 16066
15065 Natrona Heights UPMC East 2775 Mosside Blvd, Monroeville, PA 15146
15143 Sewickley UPMC Passavant - McCandless 9100 Babcock Blvd, Pittsburgh, PA 15237
15229 Pittsburgh UPMC Passavant - McCandless 9100 Babcock Blvd, Pittsburgh, PA 15237
15020 Bunola UPMC McKeesport 1500 5th Ave, McKeesport, PA 15132
15126 Imperial UPMC Passavant - McCandless 9100 Babcock Blvd, Pittsburgh, PA 15237
15226 Pittsburgh UPMC Mercy 1400 Locust St, Pittsburgh, PA 15219
15134 Mckeesport UPMC McKeesport 1500 5th Ave, McKeesport, PA 15132
16059 Valencia UPMC Passavant - McCandless 9100 Babcock Blvd, Pittsburgh, PA 15237

Step 2: Calculate distance and time

The coordinates for each combination of nearest hospital and starting point within a ZIP code are plugged into the Bing Maps API to return the time and distance it takes to travel by car between those two locations. The code below calculates that drive starting at 9 AM local time on the day the code is run. To specifically pick a time and date for departure, then change the following line of code:

"&dt=",paste0(format(as.Date(Sys.Date(), "%B %d %Y"), "%m/%d/%Y"), " 09:00:00")

To a date and time in the format of:

## [1] "MM/DD/YYYY HH:MM:SS"
## [1] "Example: 02/22/2022 09:00:00"
# Note: uses todays date at 9 AM local time (reasonable traffic) for calculation
for(i in 1:nrow(nearest_hospital)){
  
  nearest_hospital$Drive_Distance[i] <- NA
  nearest_hospital$Drive_Time[i] <- NA
  
  url <- paste("http://dev.virtualearth.net/REST/V1/Routes/",
               "Driving",
               "?wp.0=",paste0(nearest_hospital$ZIP_Latitude[i],"+",nearest_hospital$ZIP_Longitude[i]),
               "&wp.1=",paste0(nearest_hospital$UPMC_Latitude[i],"+",nearest_hospital$UPMC_Longitude[i]),
               "&dt=",paste0(format(as.Date(Sys.Date(), "%B %d %Y"), "%m/%d/%Y"), " 09:00:00"),
               "&tt=","Departure",
               "&key=",BingMapsKey,
               sep="")
  
  suppressWarnings(json_bing <- fromJSON(paste(readLines(url), collapse=""), unexpected.escape = "skip"))
  
  # Convert distance calculation to miles and time to minutes
  travelDistance <- json_bing$resourceSets[[1]]$resources[[1]]$travelDistance / 1.609344
  travelDuration <- json_bing$resourceSets[[1]]$resources[[1]]$travelDuration / 60
  
  nearest_hospital$Drive_Distance[i] <- travelDistance
  nearest_hospital$Drive_Time[i] <- travelDuration

}

# Add a drive time range for 10 minute intervals, up to 30 minutes
nearest_hospital <- nearest_hospital %>%
  mutate(Range = ifelse(Drive_Time < 10, "0-10",
                 ifelse(Drive_Time < 20, "10-20",
                 ifelse(Drive_Time < 30, "20-30", "30+"))))

Note: Travel distance is returned by the API in kilometers and travel time is returned in hours. The above code converts these to miles and minutes, respectively.

Step 3: Plot Travel Time

# Create a color palette for the HERE 2 hour catchment areas
drive_time.colors <- c("#1a9641", "#a6d96a", "#fdae61", "#d7191c")
drive_time.pal <- colorFactor(drive_time.colors, domain = c(nearest_hospital$Range))

# URL for public use icon for hospitals:
icon <- paste0("https://icons.iconarchive.com/icons/google/",
        "noto-emoji-travel-places/256/42491-hospital-icon.png")

# Plot icons for each of the UMPC Hospital locations
# and color-coded markers at each ZIP code location
leaflet() %>% 
  addProviderTiles("CartoDB.Positron", group="Greyscale") %>% 
  addMarkers(upmc_hospitals$Longitude, upmc_hospitals$Latitude,
             icon = list(
              iconUrl = icon,
              iconSize = c(35, 35)),
              popup = paste0("<b>",upmc_hospitals$Location,
                            "</b><br>",upmc_hospitals$Address)) %>% 
  addCircles(nearest_hospital$ZIP_Longitude, nearest_hospital$ZIP_Latitude,
             popup = paste0("<b>",nearest_hospital$ZIP,
                            "</b><br>",nearest_hospital$City,
                            "<br> Nearest Hospital: ",round(nearest_hospital$Drive_Time,1)," minute drive"),
             color = drive_time.pal(as.factor(nearest_hospital$Range)),
             radius = 2500, opacity = 1) %>%
  
  # Add a legend
  addLegend("bottomleft", pal = drive_time.pal, values = nearest_hospital$Range,
            opacity = 0.35, title = "Drive time to nearest Hospital<br>(in minutes)")