Using APIs

The purpose of this project was to familiarize myself with calling APIs in R. Specifically, I used the ISS API in order to create a visual displaying its pathway over U.S. capitals. This involved some function writing, pulling APIs, and visualization using leaflet
Functions
Data Visualization
Data Cleaning
APIs
Published

May 19, 2025

Note: This was initially a collaborative project, but I made some adjustments independently. There are two GitHub links below, one which contains the initial project, and one that contains the finalized assignment. However, the final project’s code is listed here.

View the collaborative code on GitHub

View the final code on GitHub

Making the U.S.Capitals dataset

capitals_names <- read_lines("https://people.sc.fsu.edu/~jburkardt/datasets/states/state_capitals_name.txt")
capitals_lat_long <- read_lines("https://people.sc.fsu.edu/~jburkardt/datasets/states/state_capitals_ll.txt")

We first created a dataset that contains all of the capital names, as well as their latitude and longitude coordinates.

latlon_df <- str_split(capitals_lat_long, "\\s+", simplify = TRUE) |>  
  as.data.frame() |> 
  rename(state = V1, latitude = V2, longitude = V3) |> 
  mutate(state = trimws(state))

capitals_df <- str_split(capitals_names, '"', simplify = TRUE) |>  
  as.data.frame() |> 
  rename(state = V1, capital = V2) |> 
  select(-V3) |> 
  mutate(state = trimws(state))

full_capitals_dataset <- left_join(latlon_df, capitals_df, by = "state")
#Technically a "join" is not necessary, since the columns are ordered the same, but we were worried it was going to cause issues

full_capitals_dataset <- full_capitals_dataset |> 
  mutate(
    latitude = as.numeric(latitude),
    longitude = as.numeric(longitude)
  )

Calling an API to get pass times

We then created a function that inputs latitudes and longitudes, and retrieves the next three times that the ISS passes over that location. Note that the API has a maximum of 72 hours. So, if the ISS does not pass over a location within 72 hours, it will return NA. Additionally, because this API is time dependent, re-running it after some time may produce new results.

Note: We used tca or “time of closest approach” as our pass time, but the API can also provide “start time” and “end time.” These are only about 10 minutes apart, however, so whichever value is chosen should not make a big difference.

get_pass_times <- function(lat, lon) {
  #Check inputs: lat and lon should be numeric (I had to Google what ranges they could have, did not know they were on two different scales!)
  if (!is.numeric(lat) || lat < -90 || lat > 90) {
    warning("Invalid latitude. Must be a numeric value between -90 and 90.")
    return(rep(NA, 3))
  }
  if (!is.numeric(lon) || lon < -180 || lon > 180) {
    warning("Invalid longitude. Must be a numeric value between -180 and 180.")
    return(rep(NA, 3))
  }
  
  #Construct the API URL for the given latitude and longitude
  url <- paste0("https://api.g7vrd.co.uk/v1/satellite-passes/25544/", lat, "/", lon, ".json")
  response <- GET(url) #Call the API 
  

  if (status_code(response) == 200) {
    data <- fromJSON(rawToChar(response$content)) #Convert raw JSON to usable form
  
  if (!is.null(data$passes)) {
    return(head(data$passes$tca, 3)) #Extract first three pass times (Ordered descending)
    } else {
      warning("No passes found in API response.")
      }
    } else {
    warning("API request failed or returned non-200 status.")
    }

  return(rep(NA, 3))  #Return 3 NAs if calling the API fails
}

We now take our function and apply it to every row, thus producing a dataframe which contains the top three pass times for each capital. Because we wanted to limit our dataset to U.S. state capitals, we removed Puerto Rico. Additionally, there was an issue with the initial dataframe that we read in. It included two District of Columbias, one with the correct coordinates, and one with incorrect coordinates. We made sure to remove the incorrect one from the datset.

capitals_all_passes <- full_capitals_dataset |> 
  mutate(pass_times = pmap(list(latitude, longitude), get_pass_times)) 
  #pass_times is a list of values

capitals_with_passes <- capitals_all_passes |> 
  unnest_wider(pass_times, names_sep = "_") |>  #Creates pass_times_1, pass_times_2, pass_times_3
  rename(
    pass_time_1 = pass_times_1,
    pass_time_2 = pass_times_2,
    pass_time_3 = pass_times_3
  ) |> 
  filter(!state %in% c('US', 'PR')) 

Making the ISS passing visual

The following takes our dataset and creates a visualization.

capitals_with_passes <- capitals_with_passes |> 
  arrange(is.na(pass_time_1), pass_time_1) |> 
  rowwise() |> 
  mutate(
    pop_up_html = if (is.na(pass_time_1)) {
      paste0("<b>", capital, "</b><br/>",
             "ISS will not pass over this location in the next 72 hours")
    } else {
      paste0("<b>", toupper(capital), "</b><br/>",
             "Next Three ISS Passtimes:<br/>",
             # read way many articles much about date time formatting and functions, but so happy with how it looks now
             # https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings
             format(as_datetime(pass_time_1), "%B %d, %Y  %I:%M %p %Z"),
             "<br/>",
             if_else(!is.na(pass_time_2),
                     (format(as_datetime(pass_time_2), "%B %d, %Y  %I:%M %p %Z")),
                     "No second pass"),
             "<br/>",
             if_else(!is.na(pass_time_3), (format(as_datetime(pass_time_3), "%B %d, %Y  %I:%M %p %Z")), 
                     "No third pass"))
    },  
    
    label_html = htmltools::HTML((paste0(
      "<b>", toupper(capital), "<b><br/>",
      if_else(is.na(pass_time_1),
              "ISS will not pass over this location in the next 72 hours",
              paste("Next ISS Passtime:",
                    format(as_datetime(pass_time_1), "%B %d, %Y  %I:%M %p %Z"))
     ))))
  ) |> 
  ungroup()

# created new columns in the dataframe, because the html options were having issues running within the leaflet plot and to make the code for the plot cleaner
#dropping NAs from the path
caps<- capitals_with_passes|> filter(!is.na(pass_time_1))
iss_icon <- icons(
  iconUrl = here::here("stat-541-posts/2025-05-19-lab-7/pngtree-space-station-probe-icon-png-image_4687961.png"), 
  iconWidth = 30, 
  iconHeight = 30
)

leaflet(capitals_with_passes) |> addTiles() |> 
  addMarkers(lng = ~longitude,
             lat = ~latitude,
             icon = iss_icon,
             label = ~label_html,
             popup = ~pop_up_html
            # so excited to have discovered the use of the tilda('~')
             ) |>
  addPolylines(lng = caps$longitude,
               lat = caps$latitude, 
               color = "pink", #I made the lines pink because it's coquette
               opacity = 1)

Looks so good!