JSON and APIs

Author

Harshini Karthikeyan

Published

May 19, 2025

library(httr)
library(jsonlite)
library(dplyr)
library(purrr)
library(readr)
library(tidyverse)
library(leaflet)
library(knitr)
library(tidyjson)
#eab39c3c6fb6d4f959dbcb4ad3ad9a8205551bc1
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")
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 <- full_join(latlon_df, capitals_df, by = "state")


full_capitals_dataset <- full_capitals_dataset |> 
  mutate(
    capital = str_remove_all(capital, '"'),
    latitude = as.numeric(latitude),
    longitude = as.numeric(longitude)
  )
response <- GET("https://api.g7vrd.co.uk/v1/satellite-passes/25544/51.45/-2.5833.json")

space <- response$content |> 
  rawToChar() |>  
  fromJSON()

str(space)
List of 11
 $ api_status        : chr "ALPHA"
 $ request_timestamp : chr "2025-06-09T16:43:59.782392226Z"
 $ norad_id          : int 25544
 $ satellite_name    : chr "ISS"
 $ tle_last_retrieved: chr "2025-06-08T21:52:58.294612771Z"
 $ lat               : num 51.5
 $ lon               : num -2.58
 $ hours             : int 48
 $ min_elevation     : int 30
 $ query_ms          : int 15
 $ passes            :'data.frame': 6 obs. of  6 variables:
  ..$ start        : chr [1:6] "2025-06-10T10:34:29.767Z" "2025-06-10T12:11:14.767Z" "2025-06-10T13:47:59.767Z" "2025-06-11T09:46:04.767Z" ...
  ..$ tca          : chr [1:6] "2025-06-10T10:39:59.767Z" "2025-06-10T12:16:44.767Z" "2025-06-10T13:52:59.767Z" "2025-06-11T09:51:34.767Z" ...
  ..$ end          : chr [1:6] "2025-06-10T10:45:24.767Z" "2025-06-10T12:22:09.767Z" "2025-06-10T13:58:34.767Z" "2025-06-11T09:56:54.767Z" ...
  ..$ aos_azimuth  : int [1:6] 256 277 283 248 273 283
  ..$ los_azimuth  : int [1:6] 80 100 131 78 93 122
  ..$ max_elevation: num [1:6] 80 86 35 67 84 48
json_tbl <- response$content |> 
  rawToChar() |> 
  as.tbl_json()

json_tbl |>  
  spread_all() |> 
  enter_object("passes") |> 
  gather_array() |>     # if it's a JSON array
  spread_all() |> 
  select(start)
# A tbl_json: 6 x 2 tibble with a "JSON" attribute
  ..JSON                  start                   
  <chr>                   <chr>                   
1 "{\"start\":\"2025-..." 2025-06-10T10:34:29.767Z
2 "{\"start\":\"2025-..." 2025-06-10T12:11:14.767Z
3 "{\"start\":\"2025-..." 2025-06-10T13:47:59.767Z
4 "{\"start\":\"2025-..." 2025-06-11T09:46:04.767Z
5 "{\"start\":\"2025-..." 2025-06-11T11:22:44.767Z
6 "{\"start\":\"2025-..." 2025-06-11T12:59:29.767Z
get_pass_times <- function(lat, lon) {
  #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 useable form
    
    if (!is.null(data$passes)) {
      return(head(data$passes$tca, 3)) #If passes exists in the data, extract first three pass times. Note that these are ordered descending, so it should get the earliest three times.
    }
  }

  return(rep(NA, 3))  #Return 3 NAs if unavailable
}
capitals_all_passes <- full_capitals_dataset |> 
  mutate(pass_times = pmap(list(latitude, longitude), get_pass_times))

capitals_with_passes <- capitals_all_passes |> 
  mutate(
    pass_time_1 = map_chr(pass_times, ~ if(length(.) >= 1) .[1] else NA), 
    pass_time_2 = map_chr(pass_times, ~ if(length(.) >= 2) .[2] else NA),
    pass_time_3 = map_chr(pass_times, ~ if(length(.) >= 3) .[3] else NA)) |>
  select(-pass_times) |>
  filter(!state %in% c('US', 'PR'))
  #There were two Districts of Columbia in the dataset, one with the wrong latitude, so we dropped the other DC and San Juan of Puerto Rico(not sure why this was showing up).
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("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)