As a simple project to tackle during the holidays I've set to write a simple wrapper around Citymapper's api to familiarise with httr
, testthat
and a simple CI setup (on CircleCI).
The API mainly revolves around 2 things:
- Cheking if a given point falls within their covered areas. As on their api docs there are 2 versions of this endpoint (a second one accepts multiple inputs via a JSON payload), I've implemented 2 different functions to reflect this, no idea if it'd make sense to collapse it into one.
- Computing travel time (using public transport) between two points.
Below you can find the main methods, here a link to the repo.
get_travel_time.R
#' Get travel time between two points
#'
#' Computes estimated travel time between two points using public transport.
#' If \code{time} and \code{time_type} are not specified, time of travel will be assumed to be the same time at which the request is made.
#'
#' @param start_coord Geographical coordinates of the start point in WGS84 '<latitude>,<longitude>' format.
#' @param end_coord Geographical coordinates of the arrival point in WGS84 '<latitude>,<longitude>' format.
#' @param time A date & time in ISO-8601 format (e.g \code{2014-11-06T19:00:02-0500}). If omitted travel time will be computed for travel at the time of the request.
#' @param time_type Required if `time` is provided. At the moment the only defined type is `arrival`, it computes the travel time for arriving at `end_coord` at the given time.
#' @inheritParams citymappr_setup
#' @return Estimated travel time in minutes (int).
#'
#' @examples
#' \dontrun{
#' get_travel_time(start_coord = "45.448643,9.207645",
#' end_coord = "45.452349,9.180225")
#' }
#'
#'
#' @importFrom httr GET stop_for_status content
#' @importFrom jsonlite fromJSON
#'
#' @export
get_travel_time <- function(start_coord,
end_coord,
time="",
time_type="",
api_token = Sys.getenv("CITYMAPPER_API_TOKEN")) {
resp <-GET(url = "https://developer.citymapper.com/api/1/traveltime/",
query = list(
key = api_token,
startcoord = start_coord,
endcoord = end_coord,
time = time,
time_type = time_type
)
)
stop_for_status(resp)
return(
fromJSON(
content(resp, "text"))$travel_time_minutes
)
}
check_coverage.R
#' Check if a point is in covered area
#'
#' Checks if a given point falls within Citymapper's covered areas.
#' It is good practice to refresh this value regularly as covered areas might change.
#'
#' @inheritParams citymappr_setup
#' @param point Geographical coordinates of the point in WGS84 \code{'<latitude>,<longitude>'} format.
#' @return A tibble containing boolean responses for each point.
#'
#' @importFrom magrittr %>%
#' @importFrom rlang .data
#' @importFrom httr GET content stop_for_status
#' @importFrom jsonlite fromJSON
#' @importFrom dplyr mutate
#' @importFrom tibble as_tibble
#'
#' @examples
#' \dontrun{
#' check_coverage("45.448643,9.207645")
#' }
#'
#' @export
check_coverage <- function(point,
api_token=Sys.getenv("CITYMAPPER_API_TOKEN")) {
resp <- GET(url="https://developer.citymapper.com/api/1/singlepointcoverage/",
query = list(key = api_token,
coord = point))
stop_for_status(resp)
return(
fromJSON(
content(resp, "text"))[["points"]]$covered
)
}
check_coverage_multi.R
#' Checks if multiple points are in covered area
#'
#' Checks if multiple points fall within Citymapper's covered areas.
#' It is good practice to refresh this values regularly as covered areas might change.
#'
#' @inheritParams citymappr_setup
#' @param points Dataframe containing geographical coordinates of the start point in WGS84 \code{'<latitude>,<longitude>'} format.
#' Columns should be set as \code{id (optional)} and \code{coord}.
#' @return A tibble containing boolean responses for each point. IDs column passed in \code{points} will be mirrored back in response.
#'
#' @examples
#' \dontrun{
#' # Make dataframes with coordinates and (optional) ids
#'
#' }
#'
#' @seealso \code{\link{check_coverage}}
#'
#' @importFrom magrittr %>%
#' @importFrom jsonlite fromJSON toJSON
#' @importFrom httr POST content stop_for_status
#' @importFrom tibble as_tibble
#'
#' @export
#TODO: there are problems with json parsin (coords are not parsed as a list)
check_coverage_multi <- function(points,
api_token=Sys.getenv("CITYMAPPER_API_TOKEN")) {
if (!is.data.frame(points)) {
points <- as_tibble(points)
}
# Check if column name respect c("id", "coord") convention
if(!all(c("coord") %in% names(points))) {
stop("Dataframe has wrong column names, make sure they are set as c('id', 'coord')")
}
# Get input in required JSON format
points$coord <- lapply(
strsplit(as.character(points$coord), ","),
as.numeric
)
cured_input <- toJSON(list(`points` = points))
resp <- POST(paste0("https://developer.citymapper.com/api/1/coverage/?key=",
api_token),
body = cured_input,
encode="json")
stop_for_status(resp)
return(
fromJSON(
content(resp, "text"))[["points"]]
)
}
Even being quite a simple package, it'd be good to understand if project structure makes sense and if there's any room for improvement.
check_coverage_multi.R
, are you sure you don't mean!all(c("id", "coord") %in% names(points))
? \$\endgroup\$coord
columns as it's mandatory (id
is optional as it'll be just mirrored back from the endpoint for quick reference). Is there a more elegant solution? I do believe I left thatif(!all(...))
there as I began checking for both fields in the first place. \$\endgroup\$