├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ └── full-check-on-release.yaml ├── docs ├── CNAME ├── _config.yml ├── reference │ ├── Rplot001.png │ └── figures │ │ └── ptvapi.png ├── pkgdown.yml ├── link.svg ├── bootstrap-toc.css ├── docsearch.js ├── pkgdown.js ├── authors.html ├── bootstrap-toc.js ├── LICENSE-text.html └── 404.html ├── LICENSE ├── .gitignore ├── tests ├── testthat.R └── testthat │ ├── test-patterns.R │ ├── test-disruption-modes.R │ ├── test-insecure-connection.R │ ├── helper.R │ ├── test-helper-functions.R │ ├── test-search-routes.R │ ├── test-fare-estimate.R │ ├── test-outlets.R │ ├── test-route-types.R │ ├── test-search-stops.R │ ├── test-search-outlets.R │ ├── test-routes.R │ ├── test-directions.R │ ├── test-runs.R │ ├── test-disruptions.R │ ├── test-filter-departures.R │ ├── test-departures.R │ └── test-stops.R ├── man ├── figures │ └── ptvapi.png ├── make_url_friendly.Rd ├── convert_to_melbourne_time.Rd ├── map_and_rbind.Rd ├── process_response.Rd ├── to_datetime.Rd ├── geopath_to_tibble.Rd ├── parse_directions_content.Rd ├── stop_to_tibble.Rd ├── generate_request_url.Rd ├── to_integer.Rd ├── route_to_tibble.Rd ├── assert_correct_attributes.Rd ├── departure_to_tibble.Rd ├── describe_route_type.Rd ├── filter_departures.Rd ├── run_to_tibble.Rd ├── disruption_to_tibble.Rd ├── route_types.Rd ├── directions_on_route.Rd ├── outlet_to_tibble.Rd ├── parse_fare_estimate_content.Rd ├── disruption_modes.Rd ├── PTVGET.Rd ├── all_disruptions_to_tibble.Rd ├── cached_route_types.Rd ├── disruption_information.Rd ├── translate_route_type.Rd ├── disruptions_at_stop.Rd ├── stops_on_route.Rd ├── directions.Rd ├── route_information.Rd ├── routes.Rd ├── stop_information.Rd ├── outlets.Rd ├── disruptions_on_route.Rd ├── runs_on_route.Rd ├── ptvapi-package.Rd ├── stops_nearby.Rd ├── add_parameters.Rd ├── add_parameter.Rd ├── disruptions.Rd ├── search_stops.Rd ├── outlets_nearby.Rd ├── ptv_search.Rd ├── search_routes.Rd ├── search_outlets.Rd ├── fare_estimate.Rd ├── patterns.Rd ├── run_information.Rd └── departures.Rd ├── .Rbuildignore ├── R ├── zzz.R ├── globals.R ├── authentication.R ├── geopath.R ├── ptvapi-class.R ├── ptvapi.R ├── ptv-verbs.R ├── patterns.R ├── directions.R ├── outlets.R ├── helper-functions.R ├── routes.R ├── route-types.R └── fare-estimate.R ├── ptvapi.Rproj ├── NAMESPACE ├── DESCRIPTION ├── LICENSE.md ├── NEWS.md └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | ptvapi.mdneuzerling.com -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: David Neuzerling 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(ptvapi) 3 | 4 | test_check("ptvapi") 5 | -------------------------------------------------------------------------------- /man/figures/ptvapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdneuzerling/ptvapi/HEAD/man/figures/ptvapi.png -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^ptvapi\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^\.github$ 5 | ^releases$ 6 | ^docs$ 7 | -------------------------------------------------------------------------------- /docs/reference/Rplot001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdneuzerling/ptvapi/HEAD/docs/reference/Rplot001.png -------------------------------------------------------------------------------- /docs/reference/figures/ptvapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdneuzerling/ptvapi/HEAD/docs/reference/figures/ptvapi.png -------------------------------------------------------------------------------- /docs/pkgdown.yml: -------------------------------------------------------------------------------- 1 | pandoc: 2.11.4 2 | pkgdown: 1.6.1 3 | pkgdown_sha: ~ 4 | articles: {} 5 | last_built: 2021-05-02T07:04Z 6 | 7 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | # pkg_env is used for "private" package variables. 2 | # They're not truly private, but they are more difficult to access. 3 | # In our case, this is a useful place to cache results. 4 | pkg_env <- new.env(parent = emptyenv()) 5 | -------------------------------------------------------------------------------- /R/globals.R: -------------------------------------------------------------------------------- 1 | # NSE can trip up the lintr, especially with glue. 2 | # We add those variables here so that lintr is happy. 3 | utils::globalVariables(c( 4 | "version", "conjunction", "base_url", "version", "string", 5 | "departure" 6 | )) 7 | -------------------------------------------------------------------------------- /tests/testthat/test-patterns.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | pats <- patterns( 4 | run_ref = "1", 5 | route_type = "Train", 6 | departs = morning_test_time 7 | ) 8 | 9 | test_that("patterns returns list with right names", { 10 | expect_true(is.list(pats)) 11 | expect_equal( 12 | names(pats), 13 | c("departures", "stops", "routes", "runs", "directions", "disruptions") 14 | ) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /ptvapi.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /tests/testthat/test-disruption-modes.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | test_that("Disruption modes contain expected disruptions", { 3 | retrieved_disruption_modes <- disruption_modes() 4 | expect_true("metro_train" %in% retrieved_disruption_modes) 5 | expect_true("metro_tram" %in% retrieved_disruption_modes) 6 | expect_true("ferry" %in% retrieved_disruption_modes) 7 | expect_true("taxi" %in% retrieved_disruption_modes) 8 | expect_true("general" %in% retrieved_disruption_modes) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /man/make_url_friendly.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/url-manipulation.R 3 | \name{make_url_friendly} 4 | \alias{make_url_friendly} 5 | \title{Convert an input to a form that can be used in a URL.} 6 | \usage{ 7 | make_url_friendly(input) 8 | } 9 | \arguments{ 10 | \item{input}{Character.} 11 | } 12 | \value{ 13 | Character. 14 | } 15 | \description{ 16 | Before a character can be used as part of a html, spaces must be converted to 17 | "\%20". 18 | } 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/convert_to_melbourne_time.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/helper-functions.R 3 | \name{convert_to_melbourne_time} 4 | \alias{convert_to_melbourne_time} 5 | \title{Convert a datetime returned by the PTV API into Melbourne time} 6 | \usage{ 7 | convert_to_melbourne_time(datetime) 8 | } 9 | \arguments{ 10 | \item{datetime}{A datetime returned by the PTV API} 11 | } 12 | \value{ 13 | A datetime in the Melbourne timezone. 14 | } 15 | \description{ 16 | Convert a datetime returned by the PTV API into Melbourne time 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /R/authentication.R: -------------------------------------------------------------------------------- 1 | determine_user_id <- function() { 2 | user_id <- Sys.getenv("PTV_USER_ID") 3 | if (identical(user_id, "")) { 4 | stop("Could not find PTV_USER_ID environment variable. See ?ptvapi for ", 5 | "details on obtaining and providing this information.") 6 | } 7 | user_id 8 | } 9 | 10 | determine_api_key <- function() { 11 | api_key <- Sys.getenv("PTV_API_KEY") 12 | if (identical(api_key, "")) { 13 | stop("Could not find PTV_API_KEY environment variable. See ?ptvapi for ", 14 | "details on obtaining and providing this information.") 15 | } 16 | api_key 17 | } 18 | -------------------------------------------------------------------------------- /tests/testthat/test-insecure-connection.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | current_option <- getOption("use_insecure_ptv_connection") 4 | teardown(options(use_insecure_ptv_connection = current_option)) 5 | 6 | test_that("secure connection uses https", { 7 | secure_request <- run_information(1) 8 | expect_true(grepl("https", attr(secure_request, "request"))) 9 | }) 10 | 11 | test_that("insecure connection doesn't use https", { 12 | options(use_insecure_ptv_connection = TRUE) 13 | insecure_request <- run_information(1) 14 | expect_false(grepl("https", attr(insecure_request, "request"))) 15 | }) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,ptvapi) 4 | export(departures) 5 | export(directions) 6 | export(directions_on_route) 7 | export(disruption_information) 8 | export(disruption_modes) 9 | export(disruptions) 10 | export(disruptions_at_stop) 11 | export(disruptions_on_route) 12 | export(fare_estimate) 13 | export(outlets) 14 | export(outlets_nearby) 15 | export(patterns) 16 | export(route_information) 17 | export(route_types) 18 | export(routes) 19 | export(run_information) 20 | export(runs_on_route) 21 | export(search_outlets) 22 | export(search_routes) 23 | export(search_stops) 24 | export(stop_information) 25 | export(stops_nearby) 26 | export(stops_on_route) 27 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | # I've avoided this dependency for the package functions, because I truly 2 | # believe it's unnecessary. But we can be more liberal with dependencies for 3 | # the tests. 4 | 5 | library(dplyr, quietly = TRUE) 6 | library(lubridate, quietly = TRUE) 7 | 8 | # The API doesn't seem to keep a full history, so dates too far in the past 9 | # may return empty results. Our test times will be two days from today: one in 10 | # the morning, and one in the afternoon. 11 | 12 | morning_test_time <- paste( 13 | format(Sys.Date() + 2, format = "%Y-%m-%d", tz = "Australia/Melbourne"), 14 | "T07:48:08" 15 | ) 16 | 17 | afternoon_test_time <- paste( 18 | format(Sys.Date() + 2, format = "%Y-%m-%d", tz = "Australia/Melbourne"), 19 | "16:48:08" 20 | ) 21 | -------------------------------------------------------------------------------- /man/map_and_rbind.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/helper-functions.R 3 | \name{map_and_rbind} 4 | \alias{map_and_rbind} 5 | \title{Map and rbind a list of data frames} 6 | \usage{ 7 | map_and_rbind(.x, .f, ...) 8 | } 9 | \arguments{ 10 | \item{.x}{A list of data frames or tibbles.} 11 | 12 | \item{.f}{A function.} 13 | 14 | \item{...}{Additional arguments passed to the function.} 15 | } 16 | \value{ 17 | A data frame or tibble. 18 | } 19 | \description{ 20 | This function is a simple combination of \code{purrr::map} and \code{purrr::reduce} 21 | using \code{rbind}. This differs from \code{purrr::map_dfr}, which uses 22 | \code{dplyr::map_dfr} and therefore introduces \code{dplyr} as a dependency. If the 23 | provided list is empty, then an empty tibble will be returned. 24 | } 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /docs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /man/process_response.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ptv-verbs.R 3 | \name{process_response} 4 | \alias{process_response} 5 | \title{Process a raw httr response and return an object of class ptv_api} 6 | \usage{ 7 | process_response(response, request_url_without_auth) 8 | } 9 | \arguments{ 10 | \item{response}{A raw response generated by the \code{httr} package} 11 | 12 | \item{request_url_without_auth}{Character. The request \code{url}, without \code{devid} 13 | and signature} 14 | } 15 | \value{ 16 | An S3 object of class ptv_api 17 | } 18 | \description{ 19 | This S3 object returned by this function contains three elements: 20 | \itemize{ 21 | \item status_code, as an integer 22 | \item request, which is the request URL without authentication details 23 | \item content, the unparsed body of the response 24 | } 25 | } 26 | \keyword{internal} 27 | -------------------------------------------------------------------------------- /man/to_datetime.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/helper-functions.R 3 | \name{to_datetime} 4 | \alias{to_datetime} 5 | \title{Convert a POSIXct or character datetime to a format ready for a URL} 6 | \usage{ 7 | to_datetime(datetime) 8 | } 9 | \arguments{ 10 | \item{datetime}{POSIXct or Character.} 11 | } 12 | \value{ 13 | Character. 14 | } 15 | \description{ 16 | Datetimes accepted by the API need to be given in UTC. This function will 17 | accept a datetime or a character with a suitable datetime format, and output 18 | a character that is suitable for a URL. All URL input and output in this 19 | package should be in Melbourne time, and the UTC conversion should happen 20 | } 21 | \details{ 22 | The API seems to accept both "\%Y-\%m-\%dT\%H:\%M:\%OS" and "\%Y-\%m-\%d \%H:\%M:\%OS", 23 | but we opt for the former. 24 | } 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /man/geopath_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/geopath.R 3 | \name{geopath_to_tibble} 4 | \alias{geopath_to_tibble} 5 | \title{Convert a single geopath to a tibble} 6 | \usage{ 7 | geopath_to_tibble(geopath) 8 | } 9 | \arguments{ 10 | \item{geopath}{A \code{geopaths} object, as a list} 11 | } 12 | \value{ 13 | A tibble of routes, with the following columns: 14 | \itemize{ 15 | \item \code{direction_id} 16 | \item \code{valid_from} 17 | \item \code{valid_to} 18 | \item \code{paths} 19 | } 20 | } 21 | \description{ 22 | This function is designed to parse the \code{geopath} content returned by the 23 | interior steps of some functions. If geopath data is requested, that content 24 | will contain a list of \code{geopaths} for each route. This function is designed 25 | to parse \emph{one} of those geopaths into a tibble. 26 | } 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/parse_directions_content.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/directions.R 3 | \name{parse_directions_content} 4 | \alias{parse_directions_content} 5 | \title{Parse content of directions API call} 6 | \usage{ 7 | parse_directions_content(directions_content) 8 | } 9 | \arguments{ 10 | \item{directions_content}{A direction, as a list, returned by the 11 | \code{directions} API call.} 12 | } 13 | \value{ 14 | A tibble consisting of the following columns: \itemize{ 15 | \item \code{direction_id} 16 | \item \code{direction_name}, 17 | \item \code{route_id} 18 | \item \code{route_type} 19 | \item \code{route_type_description} 20 | \item \code{route_direction_description} 21 | } 22 | } 23 | \description{ 24 | This function is designed to parse the content returned by the interior 25 | steps of the \code{\link{directions}} function. 26 | } 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/stop_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/stops.R 3 | \name{stop_to_tibble} 4 | \alias{stop_to_tibble} 5 | \title{Convert a single stop to a tibble} 6 | \usage{ 7 | stop_to_tibble(stop) 8 | } 9 | \arguments{ 10 | \item{stop}{A stop, as a list, returned by a stops API call.} 11 | } 12 | \value{ 13 | A tibble with the following columns: \itemize{ 14 | \item{\code{stop_id}} 15 | \item{\code{stop_name}} 16 | \item{\code{stop_suburb}} 17 | \item{\code{route_type}} 18 | \item{\code{route_type_description}} 19 | \item{\code{stop_sequence}} 20 | \item{\code{stop_latitude}} 21 | \item{\code{stop_longitude}} 22 | \item{\code{disruption_ids}} 23 | } 24 | } 25 | \description{ 26 | This function is designed to parse the content returned by the interior 27 | steps of the \code{\link{stops_on_route}} and \code{\link{stops_nearby}} 28 | functions. 29 | } 30 | \keyword{internal} 31 | -------------------------------------------------------------------------------- /man/generate_request_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/url-manipulation.R 3 | \name{generate_request_url} 4 | \alias{generate_request_url} 5 | \title{Generate a URL with devid and signature} 6 | \usage{ 7 | generate_request_url( 8 | request, 9 | user_id = determine_user_id(), 10 | api_key = determine_api_key() 11 | ) 12 | } 13 | \arguments{ 14 | \item{request}{Character. A path without base URL or version, such as 15 | "routes" or "stop/1071".} 16 | 17 | \item{user_id}{Integer or character. A user ID or devid provided by Public 18 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 19 | 20 | \item{api_key}{Character. An API key, with dashes, provided by Public 21 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 22 | } 23 | \value{ 24 | A complete URL character that can be queried with httr 25 | } 26 | \description{ 27 | Generate a URL with devid and signature 28 | } 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/to_integer.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/helper-functions.R 3 | \name{to_integer} 4 | \alias{to_integer} 5 | \title{Strictly convert an object to an integer} 6 | \usage{ 7 | to_integer(x) 8 | } 9 | \arguments{ 10 | \item{x}{An input of any type.} 11 | } 12 | \value{ 13 | An integer interpretation of \code{x}, if possible. 14 | } 15 | \description{ 16 | R does not have a built-in function to determine if a value is an integer 17 | (\code{is.integer} will check if the class of an object is "integer", but 18 | \code{is.integer(3)} will return FALSE). This helper function fills that gap. It 19 | will attempt to convert the input to an integer, but will error on any input 20 | that cannot be confidently expressed as an integer. It serves as a stricter 21 | version of \code{as.integer}. For example, the function will convert \code{3} or \code{"3"} 22 | to integers, but will error on \code{3.5} or \code{"three"}. 23 | } 24 | \keyword{internal} 25 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ptvapi 2 | Title: Access the 'Public Transport Victoria' Timetable API 3 | Version: 2.0.5 4 | Authors@R: person(given = "David", 5 | family = "Neuzerling", 6 | role = c("aut", "cre", "cph"), 7 | email = "david@neuzerling.com") 8 | Description: Access the 'Public Transport Victoria' Timetable API 9 | , 10 | with results returned as familiar R data structures. Retrieve information on 11 | stops, routes, disruptions, departures, and more. 12 | License: MIT + file LICENSE 13 | Encoding: UTF-8 14 | Roxygen: list(markdown = TRUE) 15 | RoxygenNote: 7.2.3 16 | Imports: 17 | httr, 18 | glue, 19 | digest, 20 | jsonlite, 21 | purrr, 22 | tibble, 23 | assertthat 24 | Suggests: 25 | testthat (>= 2.1.0), 26 | dplyr, 27 | lubridate 28 | URL: https://github.com/mdneuzerling/ptvapi 29 | BugReports: https://github.com/mdneuzerling/ptvapi/issues 30 | -------------------------------------------------------------------------------- /man/route_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/routes.R 3 | \name{route_to_tibble} 4 | \alias{route_to_tibble} 5 | \title{Convert a single route to a tibble} 6 | \usage{ 7 | route_to_tibble(route) 8 | } 9 | \arguments{ 10 | \item{route}{A route, as a list, returned by the \code{\link{routes}} API 11 | call.} 12 | } 13 | \value{ 14 | A tibble of routes, with the following columns: 15 | \itemize{ 16 | \item \code{route_id} 17 | \item \code{route_gtfs_id} 18 | \item \code{route_name} 19 | \item \code{route_type} 20 | \item \code{route_type_description} 21 | \item \code{route_number} 22 | \item \code{geopath} 23 | \item \code{service_status} 24 | \item \code{service_status_timestamp} 25 | } 26 | } 27 | \description{ 28 | This function is designed to parse the content returned by the interior steps 29 | of the \code{\link{routes}} function. Service status information may be \code{NA}, 30 | depending on the API call that was used to populate the information. 31 | } 32 | \keyword{internal} 33 | -------------------------------------------------------------------------------- /man/assert_correct_attributes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/helper-functions.R 3 | \name{assert_correct_attributes} 4 | \alias{assert_correct_attributes} 5 | \title{Assert that the API has returned the expected attributes} 6 | \usage{ 7 | assert_correct_attributes(received_attributes, expected_attributes) 8 | } 9 | \arguments{ 10 | \item{received_attributes}{A character vector of attributes, in order.} 11 | 12 | \item{expected_attributes}{A character vector of expected attributes, in 13 | order.} 14 | } 15 | \value{ 16 | An error if the column names are not as expected. 17 | } 18 | \description{ 19 | The attributes returned by the API calls should be follow the API schema. 20 | This function compares received attributes against a vector of expected 21 | attributes, and returns an error if the two do not match. Unfortunately, 22 | there is no easy fix for this error: the package developer(s) must be 23 | notified, so that they can align the functions against the API schema. 24 | } 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /man/departure_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/departures.R 3 | \name{departure_to_tibble} 4 | \alias{departure_to_tibble} 5 | \title{Convert a single departure to a tibble} 6 | \usage{ 7 | departure_to_tibble(departure) 8 | } 9 | \arguments{ 10 | \item{departure}{A departure, as a list, returned by the \code{departures} API 11 | call.} 12 | } 13 | \value{ 14 | A tibble consisting of the following columns: \itemize{ 15 | \item \code{stop_id} 16 | \item \code{route_id} 17 | \item \code{run_id} (deprecated, use \code{run_ref} instead) 18 | \item \code{run_ref} 19 | \item \code{direction_id} 20 | \item \code{disruption_ids} 21 | \item \code{scheduled_departure} 22 | \item \code{estimated_departure} 23 | \item \code{at_platform} 24 | \item \code{platform_number} 25 | \item \code{flags} 26 | \item \code{departure_sequence} 27 | } 28 | } 29 | \description{ 30 | This function is designed to parse the content returned by the interior 31 | steps of the \code{departures} functions. 32 | } 33 | \keyword{internal} 34 | -------------------------------------------------------------------------------- /man/describe_route_type.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/route-types.R 3 | \name{describe_route_type} 4 | \alias{describe_route_type} 5 | \title{Convert a numeric route type to a human-friendly description} 6 | \usage{ 7 | describe_route_type( 8 | route_type, 9 | user_id = determine_user_id(), 10 | api_key = determine_api_key() 11 | ) 12 | } 13 | \arguments{ 14 | \item{route_type}{Atomic integer or character.} 15 | 16 | \item{user_id}{Integer or character. A user ID or devid provided by Public 17 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 18 | 19 | \item{api_key}{Character. An API key, with dashes, provided by Public 20 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 21 | } 22 | \value{ 23 | character 24 | } 25 | \description{ 26 | This function effectively wraps the results of \code{\link{route_types}} to 27 | translate a route type to a human-readable form, such as translating \code{0} to 28 | \code{"Train"}. This function is \emph{not} vectorised. 29 | } 30 | \keyword{Internal} 31 | -------------------------------------------------------------------------------- /man/filter_departures.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/departures.R 3 | \name{filter_departures} 4 | \alias{filter_departures} 5 | \title{Filter parsed departures content according to user input} 6 | \usage{ 7 | filter_departures(parsed, departs = NULL, route_id = NULL, max_results = NULL) 8 | } 9 | \arguments{ 10 | \item{parsed}{A tibble of parsed departures content.} 11 | 12 | \item{departs}{POSIXct in the "Australia/Melbourne" time zone.} 13 | 14 | \item{route_id}{Integer.} 15 | 16 | \item{max_results}{Integer max results.} 17 | } 18 | \value{ 19 | A filtered tibble. 20 | } 21 | \description{ 22 | The departures API call isn't always reliable. This function will take a 23 | tibble of parsed departures content and filter it according to the following 24 | inputs, if they are not \code{NULL}: \itemize{ 25 | \item Only departures after the given \code{departs} 26 | \item Only departures on the given route ID 27 | \item The next max_results departures per route ID, if \code{max_results} is not 28 | 0. 29 | } 30 | } 31 | \keyword{internal} 32 | -------------------------------------------------------------------------------- /man/run_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/runs.R 3 | \name{run_to_tibble} 4 | \alias{run_to_tibble} 5 | \title{Convert a single run to a tibble} 6 | \usage{ 7 | run_to_tibble(run) 8 | } 9 | \arguments{ 10 | \item{run}{A run, as a list, returned by the \code{runs} API call.} 11 | } 12 | \value{ 13 | A tibble with the following columns: \itemize{ 14 | \item \code{run_id} (deprecated, use \code{run_ref} instead) 15 | \item \code{run_ref} 16 | \item \code{route_id} 17 | \item \code{route_type} 18 | \item \code{route_type_description} 19 | \item \code{direction_id} 20 | \item \code{run_sequence} 21 | \item \code{final_stop_id} 22 | \item \code{destination_name} 23 | \item \code{status} 24 | \item \code{express_stop_count} 25 | \item \code{vehicle_position} 26 | \item \code{vehicle_descriptor} 27 | \item \code{geopath} 28 | } 29 | } 30 | \description{ 31 | This function is designed to parse the content returned by the interior steps 32 | of the \code{\link{runs_on_route}} and \code{\link{run_information}} 33 | functions. 34 | } 35 | \keyword{internal} 36 | -------------------------------------------------------------------------------- /man/disruption_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{disruption_to_tibble} 4 | \alias{disruption_to_tibble} 5 | \title{Convert a single disruption to a tibble} 6 | \usage{ 7 | disruption_to_tibble(disruption) 8 | } 9 | \arguments{ 10 | \item{disruption}{A disruption, as a list, returned by the \code{disruptions} API 11 | call.} 12 | } 13 | \value{ 14 | A tibble with the following columns: \itemize{ 15 | \item \code{disruption_id} 16 | \item \code{title} 17 | \item \code{url} 18 | \item \code{description} 19 | \item \code{disruption_status} 20 | \item \code{disruption_type} 21 | \item \code{published_on} 22 | \item \code{last_updated} 23 | \item \code{from_date} 24 | \item \code{to_date} 25 | \item \code{routes} 26 | \item \code{stops} 27 | \item \code{colour} 28 | \item \code{display_on_board} 29 | \item \code{display_status} 30 | } 31 | } 32 | \description{ 33 | This function is designed to parse the content returned by the interior 34 | steps of the \code{disruptions_on_route} and \code{disruptions_at_stop} functions. 35 | } 36 | \keyword{internal} 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 David Neuzerling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /R/geopath.R: -------------------------------------------------------------------------------- 1 | #' Convert a single geopath to a tibble 2 | #' 3 | #' This function is designed to parse the `geopath` content returned by the 4 | #' interior steps of some functions. If geopath data is requested, that content 5 | #' will contain a list of `geopaths` for each route. This function is designed 6 | #' to parse _one_ of those geopaths into a tibble. 7 | #' 8 | #' @param geopath A `geopaths` object, as a list 9 | #' 10 | #' @return A tibble of routes, with the following columns: 11 | #' \itemize{ 12 | #' \item `direction_id` 13 | #' \item `valid_from` 14 | #' \item `valid_to` 15 | #' \item `paths` 16 | #' } 17 | #' 18 | #' @keywords internal 19 | #' 20 | geopath_to_tibble <- function(geopath) { 21 | if (is.null(geopath)) { 22 | return( 23 | tibble::tibble( 24 | direction_id = integer(), 25 | valid_from = as.Date(NA), 26 | valid_to = as.Date(NA), 27 | paths = list() 28 | ) 29 | ) 30 | } 31 | tibble::tibble( 32 | direction_id = geopath$direction_id, 33 | valid_from = as.Date(geopath$valid_from), 34 | valid_to = as.Date(geopath$valid_to), 35 | paths = list(geopath$paths) 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /man/route_types.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/route-types.R 3 | \name{route_types} 4 | \alias{route_types} 5 | \title{Retrieve a translation from route type number to name} 6 | \usage{ 7 | route_types(user_id = determine_user_id(), api_key = determine_api_key()) 8 | } 9 | \arguments{ 10 | \item{user_id}{Integer or character. A user ID or devid provided by Public 11 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 12 | 13 | \item{api_key}{Character. An API key, with dashes, provided by Public 14 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 15 | } 16 | \value{ 17 | A named integer vector in which the values are the route type 18 | descriptions, and the names of the vector are the route type numbers. 19 | } 20 | \description{ 21 | Route types (tram, train, etc.) are provided to the PTV API as an integer 22 | code. This function retrieves a named vector in which the values are the 23 | route type descriptions, and the names of the vector are the route type 24 | numbers. Note that "Night Bus" is a separate route type. 25 | } 26 | \examples{ 27 | \dontrun{ 28 | route_types() 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/testthat/test-helper-functions.R: -------------------------------------------------------------------------------- 1 | test_that("Can convert only integer-like things to integers", { 2 | expect_equal(to_integer(3), 3) 3 | expect_equal(to_integer(0), 0) 4 | expect_equal(to_integer(-1), -1) 5 | expect_equal(to_integer("3"), 3) 6 | expect_error( 7 | to_integer(), 8 | 'Error in to_integer() : argument "x" is missing, with no default', 9 | fixed = TRUE 10 | ) 11 | expect_error(to_integer("three"), "Cannot convert three to an integer") 12 | expect_error(to_integer(3.5), "Cannot convert 3.5 to an integer") 13 | expect_error(to_integer(data.frame()), "Cannot convert list to an integer") 14 | }) 15 | 16 | test_that("Can convert API-provided timestamp to Melbourne time", { 17 | raw_time <- "2020-05-09T06:38:47.3194196+00:00" # this is what the API gives 18 | expect_equal( 19 | convert_to_melbourne_time(raw_time), 20 | as.POSIXct("2020-05-09 16:38:47.3194196 AEST", tz = "Australia/Melbourne") 21 | ) 22 | }) 23 | 24 | test_that("converting NULL datetime to NA", { 25 | na_datetime <- as.POSIXct(NA) 26 | attr(na_datetime, "tzone") <- "Australia/Melbourne" 27 | expect_identical(convert_to_melbourne_time(NULL), na_datetime) 28 | }) 29 | -------------------------------------------------------------------------------- /man/directions_on_route.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/directions.R 3 | \name{directions_on_route} 4 | \alias{directions_on_route} 5 | \title{Directions on a given route} 6 | \usage{ 7 | directions_on_route( 8 | route_id, 9 | user_id = determine_user_id(), 10 | api_key = determine_api_key() 11 | ) 12 | } 13 | \arguments{ 14 | \item{route_id}{Integer. These can be listed and described with the 15 | \code{\link{routes}} function.} 16 | 17 | \item{user_id}{Integer or character. A user ID or devid provided by Public 18 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 19 | 20 | \item{api_key}{Character. An API key, with dashes, provided by Public 21 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 22 | } 23 | \value{ 24 | A tibble consisting of the following columns: \itemize{ 25 | \item \code{direction_id} 26 | \item \code{direction_name}, 27 | \item \code{route_id} 28 | \item \code{route_type} 29 | \item \code{route_type_description} 30 | \item \code{route_direction_description} 31 | } 32 | } 33 | \description{ 34 | Directions on a given route 35 | } 36 | \examples{ 37 | \dontrun{ 38 | directions_on_route(6) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /R/ptvapi-class.R: -------------------------------------------------------------------------------- 1 | new_ptvapi_tibble <- function(response, parsed) { 2 | assertthat::assert_that(is.data.frame(parsed)) 3 | 4 | ptvapi_tibble <- tibble::new_tibble( 5 | parsed, 6 | nrow = nrow(parsed), 7 | class = "ptvapi", 8 | request = response$request, 9 | retrieved = response$retrieved, 10 | status_code = response$status_code, 11 | content = response$content 12 | ) 13 | 14 | tibble::validate_tibble(ptvapi_tibble) 15 | ptvapi_tibble 16 | } 17 | 18 | # This print function hides the custom attributes that we attach the objects 19 | # returned by the api functions. In particular, the "content" attribute contains 20 | # the raw content returned by the API, and is very ugly to print. The request 21 | # and status code are printed at the top, and everything else is printed as 22 | # normal. 23 | #' @export 24 | print.ptvapi <- function(x, ...) { 25 | if (!is.null(attr(x, "request"))) { 26 | cat("Request:", attr(x, "request"), "\n") 27 | } 28 | if (!is.null(attr(x, "retrieved"))) { 29 | cat("Retrieved:", attr(x, "retrieved"), "\n") 30 | } 31 | if (!is.null(attr(x, "status_code"))) { 32 | cat("Status code:", attr(x, "status_code"), "\n") 33 | } 34 | NextMethod() 35 | } 36 | -------------------------------------------------------------------------------- /man/outlet_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/outlets.R 3 | \name{outlet_to_tibble} 4 | \alias{outlet_to_tibble} 5 | \title{Convert a single outlet to a tibble} 6 | \usage{ 7 | outlet_to_tibble(outlet) 8 | } 9 | \arguments{ 10 | \item{outlet}{An outlet, as a list, returned by the \code{\link{outlets}} API 11 | call.} 12 | } 13 | \value{ 14 | A tibble with the following columns: \itemize{ 15 | \item {\code{outlet_slid_spid}} 16 | \item {\code{outlet_name}} 17 | \item {\code{outlet_business}} 18 | \item {\code{outlet_latitude}} 19 | \item {\code{outlet_longitude}} 20 | \item {\code{outlet_suburb}} 21 | \item {\code{outlet_postcode}} 22 | \item {\code{outlet_business_hour_mon}} 23 | \item {\code{outlet_business_hour_tue}} 24 | \item {\code{outlet_business_hour_wed}} 25 | \item {\code{outlet_business_hour_thu}} 26 | \item {\code{outlet_business_hour_fri}} 27 | \item {\code{outlet_business_hour_sat}} 28 | \item {\code{outlet_business_hour_sun}} 29 | \item {\code{outlet_notes}} 30 | } 31 | } 32 | \description{ 33 | This function is designed to parse the content returned by the interior 34 | steps of the \code{\link{outlets}} and \code{\link{outlets_nearby}} 35 | functions. 36 | } 37 | \keyword{internal} 38 | -------------------------------------------------------------------------------- /tests/testthat/test-search-routes.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | test_that("can find route number 11 with search_routes", { 4 | route_11 <- search_routes(11) 5 | expect_gte( 6 | nrow(dplyr::filter(route_11, route_number == 11)), 7 | 1 8 | ) 9 | }) 10 | 11 | pakenham <- search_routes("pakenham") 12 | 13 | test_that("Can find Pakenham route with search_routes", { 14 | expect_gte( 15 | nrow(dplyr::filter(pakenham, route_name == "Pakenham Line")), 16 | 1 17 | ) 18 | }) 19 | 20 | test_that("search_routes result has class \"ptvapi\"", { 21 | expect_s3_class(pakenham, "ptvapi") 22 | }) 23 | 24 | test_that("Errors when providing incomplete location arguments", { 25 | expect_error( 26 | search_routes("Bundoora", latitude = -37.818229), 27 | "If searching near a location, both latitude and longitude must be provided" 28 | ) 29 | expect_error( 30 | search_routes("Bundoora", longitude = 144.952404), 31 | "If searching near a location, both latitude and longitude must be provided" 32 | ) 33 | expect_error( 34 | search_routes("Bundoora", max_distance = 100), 35 | paste0( 36 | "Trying to limit search results with a maximum distance, but a latitude ", 37 | "and longitude wasn't provided" 38 | ) 39 | ) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /man/parse_fare_estimate_content.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/fare-estimate.R 3 | \name{parse_fare_estimate_content} 4 | \alias{parse_fare_estimate_content} 5 | \title{Parse content of fare estimates API call} 6 | \usage{ 7 | parse_fare_estimate_content(fare_estimate_content) 8 | } 9 | \arguments{ 10 | \item{fare_estimate_content}{A direction, as a list, returned by the 11 | \code{\link{fare_estimate}} API call.} 12 | } 13 | \value{ 14 | A data frame consisting of one row for each \code{passenger_type}, and the 15 | following columns: \itemize{ 16 | \item \code{min_zone} 17 | \item \code{max_zone} 18 | \item \code{unique_zones} 19 | \item \code{early_bird} 20 | \item \code{free_tram_zone} 21 | \item \code{weekend_journey} 22 | \item \code{passenger_type} 23 | \item \code{fare_2_hour_peak} 24 | \item \code{fare_2_hour_off_peak} 25 | \item \code{fare_daily_peak} 26 | \item \code{fare_daily_off_peak} 27 | \item \code{pass_7_days} 28 | \item \code{pass_28_to_69_day_per_day} 29 | \item \code{pass_70_plus_day_per_day} 30 | \item \code{weekend_cap} 31 | \item \code{holiday_cap} 32 | } 33 | } 34 | \description{ 35 | This function is designed to parse the content returned by the interior 36 | steps of the \code{\link{fare_estimate}} function. 37 | } 38 | \keyword{internal} 39 | -------------------------------------------------------------------------------- /man/disruption_modes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{disruption_modes} 4 | \alias{disruption_modes} 5 | \title{Retrieve a translation from description mode number to description mode name} 6 | \usage{ 7 | disruption_modes(user_id = determine_user_id(), api_key = determine_api_key()) 8 | } 9 | \arguments{ 10 | \item{user_id}{Integer or character. A user ID or devid provided by Public 11 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 12 | 13 | \item{api_key}{Character. An API key, with dashes, provided by Public 14 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 15 | } 16 | \value{ 17 | A named vector in which the values are the disruption mode 18 | descriptions, and the names of the vector are the description mode numbers. 19 | } 20 | \description{ 21 | Disruption mode types (eg. "metro_train", "metro_tram", "school_bus", "taxi") 22 | have corresponding integer IDs. This function retrieves a named vector in 23 | which the values are the disruption mode descriptions, and the names of the 24 | vector are the description mode numbers. Note that disruption mode names are 25 | in snake case, that is, all lower case with underscores between words. 26 | } 27 | \examples{ 28 | \dontrun{disruption_modes()} 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/testthat/test-fare-estimate.R: -------------------------------------------------------------------------------- 1 | # I'm unsure how to test for journey_route_types. It's not clear how it affects 2 | # the results. 3 | 4 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 5 | 6 | zone12 <- fare_estimate(1, 2) 7 | 8 | test_that("fare_estimate result has class \"ptvapi\"", { 9 | expect_s3_class(zone12, "ptvapi") 10 | }) 11 | 12 | test_that("basic fare estimate looks right", { 13 | expect_equal(nrow(zone12), 3) 14 | expect_equal(nrow(distinct(zone12, passenger_type)), 3) 15 | }) 16 | 17 | test_that("fare_estimate picks up on the weekday early bird free travel", { 18 | diff <- 2 - wday(today()) 19 | if (diff < 0) diff <- diff + 7 20 | monday <- today() + diff # Next Monday, or today, if today is Monday 21 | early_touch_on <- paste(as.character(monday), "06:00:00") 22 | early_touch_off <- paste(as.character(monday), "06:00:00") 23 | early_bird <- fare_estimate( 24 | min_zone = 1, 25 | max_zone = 1, 26 | journey_touch_on = early_touch_on, 27 | journey_touch_off = early_touch_off 28 | ) 29 | expect_true(all(early_bird$early_bird)) 30 | expect_false(any(early_bird$weekend_journey)) 31 | }) 32 | 33 | test_that("fare_estimate accepts free tram zone", { 34 | ftz <- fare_estimate(1, 1, journey_in_free_tram_zone = TRUE) 35 | expect_true(all(ftz$free_tram_zone)) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /tests/testthat/test-outlets.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | all_outlets <- outlets() 4 | test_that("outlets result has class \"ptvapi\"", { 5 | expect_s3_class(all_outlets, "ptvapi") 6 | }) 7 | 8 | test_that("We can find a 7-Eleven", { 9 | expect_true( 10 | any(grepl("7-Eleven", all_outlets$outlet_business)) 11 | ) 12 | }) 13 | 14 | outlets_near_flinders <- outlets_nearby( 15 | latitude = -37.8183, 16 | longitude = 144.9671 17 | ) 18 | 19 | test_that("outlets_nearby result has class \"ptvapi\"", { 20 | expect_s3_class(outlets_near_flinders, "ptvapi") 21 | }) 22 | 23 | test_that("There's a 7-Eleven near Flinders Street", { 24 | expect_true( 25 | any(grepl("7-Eleven", outlets_near_flinders$outlet_business)) 26 | ) 27 | }) 28 | 29 | test_that("outlets_nearby filtered by max_results", { 30 | one_outlet <- outlets_nearby( 31 | latitude = -37.8183, 32 | longitude = 144.9671, 33 | max_results = 1 34 | ) 35 | expect_equal(nrow(one_outlet), 1) 36 | }) 37 | 38 | test_that("outlets_nearby filtered by max_distance", { 39 | max_100 <- outlets_nearby( 40 | latitude = -37.8183, 41 | longitude = 144.9671, 42 | max_distance = 100 43 | ) 44 | max_1000 <- outlets_nearby( 45 | latitude = -37.8183, 46 | longitude = 144.9671, 47 | max_distance = 1000 48 | ) 49 | expect_gt( 50 | nrow(max_1000), 51 | nrow(max_100) 52 | ) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /man/PTVGET.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ptv-verbs.R 3 | \name{PTVGET} 4 | \alias{PTVGET} 5 | \title{Submit a GET request to the PTV API} 6 | \usage{ 7 | PTVGET( 8 | request, 9 | user_id = determine_user_id(), 10 | api_key = determine_api_key(), 11 | ... 12 | ) 13 | } 14 | \arguments{ 15 | \item{request}{A request or path for the API, eg. "routes".} 16 | 17 | \item{user_id}{Integer or character. A user ID or devid provided by Public 18 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 19 | 20 | \item{api_key}{Character. An API key, with dashes, provided by Public 21 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 22 | 23 | \item{...}{Additional arguments passed to \code{httr::GET}.} 24 | } 25 | \value{ 26 | A HTTP response. Content can be accessed with \code{httr::content}. 27 | } 28 | \description{ 29 | Submit a GET request to the PTV API 30 | } 31 | \section{Obtaining API authentication details}{ 32 | 33 | 34 | You will need to obtain a user ID (also called a devid) and an API key from 35 | Public Transport Victoria. These are obtained by email. Instructions are 36 | available at \url{https://www.ptv.vic.gov.au/footer/data-and-reporting/datasets/ptv-timetable-api/}. 37 | You may pass these two pieces of information directly to the function, or you 38 | can set the PTV_USER_ID and PTV_API_KEY environment variables. 39 | } 40 | 41 | \keyword{internal} 42 | -------------------------------------------------------------------------------- /man/all_disruptions_to_tibble.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{all_disruptions_to_tibble} 4 | \alias{all_disruptions_to_tibble} 5 | \title{Convert the contents of a disruptions API call to a single tibble} 6 | \usage{ 7 | all_disruptions_to_tibble(disruptions_content) 8 | } 9 | \arguments{ 10 | \item{disruptions_content}{The raw disruptions content returned by the 11 | \code{disruptions} API call.} 12 | } 13 | \value{ 14 | A tibble with the following columns: \itemize{ 15 | \item \code{disruption_mode} 16 | \item \code{disruption_mode_description} 17 | \item \code{disruption_id} 18 | \item \code{title} 19 | \item \code{url} 20 | \item \code{description} 21 | \item \code{disruption_status} 22 | \item \code{disruption_type} 23 | \item \code{published_on} 24 | \item \code{last_updated} 25 | \item \code{from_date} 26 | \item \code{to_date} 27 | \item \code{routes} 28 | \item \code{stops} 29 | \item \code{colour} 30 | \item \code{display_on_board} 31 | \item \code{display_status} 32 | } 33 | } 34 | \description{ 35 | Disruptions API responses contain an element for every service type, eg. 36 | metro train, taxis, Skybus. Normally we would map-reduce the content of an 37 | API call with a function analogous to \code{disruption_to_tibble}. But because of 38 | the extra layer of nesting in the response, we have to map-reduce the service 39 | types first. 40 | } 41 | \details{ 42 | Note that we return an empty tibble if there are no disruptions, so that 43 | this situation is omitted. 44 | } 45 | \keyword{internal} 46 | -------------------------------------------------------------------------------- /man/cached_route_types.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/route-types.R 3 | \name{cached_route_types} 4 | \alias{cached_route_types} 5 | \title{Retrieve route types, using cached values if possible} 6 | \usage{ 7 | cached_route_types( 8 | user_id = determine_user_id(), 9 | api_key = determine_api_key() 10 | ) 11 | } 12 | \arguments{ 13 | \item{user_id}{Integer or character. A user ID or devid provided by Public 14 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 15 | 16 | \item{api_key}{Character. An API key, with dashes, provided by Public 17 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 18 | } 19 | \value{ 20 | A named integer vector in which the values are the route type 21 | descriptions, and the names of the vector are the route type numbers. 22 | } 23 | \description{ 24 | Route types will change extraordinarily rarely --- this would require PTV to 25 | add a new route type akin to "train" or "bus". To avoid querying the API too 26 | much, we prefer to use cached values for route type translation wherever 27 | possible. This function effectively wraps \code{route_types}, returning cached 28 | results if possible or caching results otherwise. Note that if a user 29 | specifically calls \code{route_types} then we do \emph{not} return cached results. 30 | 31 | We use the \code{pkg_env} as a cache, which is an environment created on package 32 | load. This is not truly private --- users could still access this as an 33 | internal value. But it's effectively "out of the way". 34 | } 35 | \keyword{internal} 36 | -------------------------------------------------------------------------------- /man/disruption_information.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{disruption_information} 4 | \alias{disruption_information} 5 | \title{Information on a particular disruption} 6 | \usage{ 7 | disruption_information( 8 | disruption_id, 9 | user_id = determine_user_id(), 10 | api_key = determine_api_key() 11 | ) 12 | } 13 | \arguments{ 14 | \item{disruption_id}{Integer.} 15 | 16 | \item{user_id}{Integer or character. A user ID or devid provided by Public 17 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 18 | 19 | \item{api_key}{Character. An API key, with dashes, provided by Public 20 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 21 | } 22 | \value{ 23 | A tibble with the following columns: \itemize{ 24 | \item \code{disruption_mode} 25 | \item \code{disruption_mode_description} 26 | \item \code{disruption_id} 27 | \item \code{title} 28 | \item \code{url} 29 | \item \code{description} 30 | \item \code{disruption_status} 31 | \item \code{disruption_type} 32 | \item \code{published_on} 33 | \item \code{last_updated} 34 | \item \code{from_date} 35 | \item \code{to_date} 36 | \item \code{routes} 37 | \item \code{stops} 38 | \item \code{colour} 39 | \item \code{display_on_board} 40 | \item \code{display_status} 41 | } 42 | } 43 | \description{ 44 | This function can be used when the integer disruption ID is already known. 45 | This can be searched for with either \code{disruptions}, 46 | \code{disruptions_on_route}, or \code{disruptions_at_stop} functions. 47 | } 48 | \examples{ 49 | \dontrun{ 50 | disruption_information(206639) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /man/translate_route_type.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/route-types.R 3 | \name{translate_route_type} 4 | \alias{translate_route_type} 5 | \title{Translate a route type input into a numerical route type} 6 | \usage{ 7 | translate_route_type( 8 | route_type, 9 | user_id = determine_user_id(), 10 | api_key = determine_api_key() 11 | ) 12 | } 13 | \arguments{ 14 | \item{route_type}{A route type which can be provided either as a non-negative 15 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 16 | Bus". Character inputs are not case-sensitive. Use the 17 | \code{\link{route_types}} function to extract a vector of all route types.} 18 | 19 | \item{user_id}{Integer or character. A user ID or devid provided by Public 20 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 21 | 22 | \item{api_key}{Character. An API key, with dashes, provided by Public 23 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 24 | } 25 | \value{ 26 | An integer route type code, or NULL if the input is NULL 27 | } 28 | \description{ 29 | Many API calls require a route type (eg. "Tram" or "Train"). These must be 30 | provided as integers, which are translated to route type descriptions with 31 | the `route_types() function/API call. This function will: \itemize{ 32 | \item Translate a case-insensitive description such as "Tram" or "Train" to 33 | the corresponding route type code 34 | \item Check a given integer to see if it is a valid route type code, 35 | returning it if so and erroring otherwise 36 | \item Return NULL on NULL input 37 | } 38 | This function is \emph{not} vectorised. 39 | } 40 | \keyword{internal} 41 | -------------------------------------------------------------------------------- /man/disruptions_at_stop.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{disruptions_at_stop} 4 | \alias{disruptions_at_stop} 5 | \title{Disruptions at a given stop} 6 | \usage{ 7 | disruptions_at_stop( 8 | stop_id, 9 | disruption_status = NULL, 10 | user_id = determine_user_id(), 11 | api_key = determine_api_key() 12 | ) 13 | } 14 | \arguments{ 15 | \item{stop_id}{Integer stop ID.} 16 | 17 | \item{disruption_status}{Character. Can be used to filter to either "current" 18 | or "planned" disruptions. Defaults to NULL, in which case no filter is 19 | applied.} 20 | 21 | \item{user_id}{Integer or character. A user ID or devid provided by Public 22 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 23 | 24 | \item{api_key}{Character. An API key, with dashes, provided by Public 25 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 26 | } 27 | \value{ 28 | A tibble with the following columns: \itemize{ 29 | \item \code{disruption_mode} 30 | \item \code{disruption_mode_description} 31 | \item \code{disruption_id} 32 | \item \code{title} 33 | \item \code{url} 34 | \item \code{description} 35 | \item \code{disruption_status} 36 | \item \code{disruption_type} 37 | \item \code{published_on} 38 | \item \code{last_updated} 39 | \item \code{from_date} 40 | \item \code{to_date} 41 | \item \code{routes} 42 | \item \code{stops} 43 | \item \code{colour} 44 | \item \code{display_on_board} 45 | \item \code{display_status} 46 | } 47 | } 48 | \description{ 49 | Disruptions at a given stop 50 | } 51 | \examples{ 52 | \dontrun{ 53 | disruptions_at_stop(1071) 54 | disruptions_at_stop(1071, disruption_status = "current") 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/testthat/test-route-types.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | teardown(pkg_env$route_types <- NULL) 4 | 5 | route_types_results <- route_types() 6 | 7 | test_that("All 5 route types present", { 8 | expect_true("Train" %in% route_types_results) 9 | expect_true("Tram" %in% route_types_results) 10 | expect_true("Bus" %in% route_types_results) 11 | expect_true("Vline" %in% route_types_results) 12 | expect_true("Night Bus" %in% route_types_results) 13 | }) 14 | 15 | test_that("Route type translation is working", { 16 | expect_equal(translate_route_type(0), 0) 17 | expect_error(translate_route_type(99999)) 18 | 19 | expect_error(translate_route_type("notaroutetype")) 20 | train_route_code <- translate_route_type("Train") 21 | expect_type(train_route_code, "integer") 22 | expect_equal(translate_route_type("TRAIN"), train_route_code) 23 | expect_equal(translate_route_type("train"), train_route_code) 24 | }) 25 | 26 | test_that("Route type description is working", { 27 | expect_equal(describe_route_type(0), "Train") 28 | expect_equal(describe_route_type(1), "Tram") 29 | expect_equal(describe_route_type(2), "Bus") 30 | expect_equal(describe_route_type(3), "Vline") 31 | expect_equal(describe_route_type(4), "Night Bus") 32 | 33 | # This test has an alternative use --- if a new route type is added, the tests 34 | # will start failing, and I can be notified by a CICD pipeline 35 | expect_error(describe_route_type(5), "Route type 5 doesn't exist") 36 | }) 37 | 38 | test_that("route types are being cached", { 39 | cached_route_types() 40 | pkg_env$route_types <- c("0" = "fish") 41 | expect_equal(describe_route_type(0), "fish") 42 | }) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/testthat/test-search-stops.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | ascot_vale <- search_stops("Ascot Vale") 4 | 5 | test_that("search_stops result has class \"ptvapi\"", { 6 | expect_s3_class(ascot_vale, "ptvapi") 7 | }) 8 | 9 | test_that("results in search_stops can relate to stop name alone", { 10 | expect_gte( 11 | nrow( 12 | dplyr::filter( 13 | ascot_vale, 14 | grepl("Ascot Vale", stop_name, ignore.case = TRUE), 15 | !grepl("Ascot Vale", stop_suburb, ignore.case = TRUE) 16 | ) 17 | ), 18 | 1 19 | ) 20 | }) 21 | 22 | test_that("results in search_stops can relate to stop suburb alone", { 23 | expect_gte( 24 | nrow( 25 | dplyr::filter( 26 | ascot_vale, 27 | !grepl("Ascot Vale", stop_name, ignore.case = TRUE), 28 | grepl("Ascot Vale", stop_suburb, ignore.case = TRUE) 29 | ) 30 | ), 31 | 1 32 | ) 33 | }) 34 | 35 | test_that("all results in search_stops relate to search term somehow", { 36 | expect_equal( 37 | nrow( 38 | dplyr::filter( 39 | ascot_vale, 40 | !grepl("Ascot Vale", stop_name 41 | , ignore.case = TRUE), 42 | !grepl("Ascot Vale", stop_suburb, ignore.case = TRUE) 43 | ) 44 | ), 45 | 0 46 | ) 47 | }) 48 | 49 | test_that("search_stops can be filtered with multiple route types", { 50 | expect_equal( 51 | search_stops("South Yarra", route_types = c(0, 1)) %>% 52 | pull(route_type) %>% 53 | unique %>% 54 | sort, 55 | c(0, 1) 56 | ) 57 | expect_equal( 58 | search_stops("South Yarra", route_types = c(0, 2)) %>% 59 | pull(route_type) %>% 60 | unique %>% 61 | sort, 62 | c(0, 2) 63 | ) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /R/ptvapi.R: -------------------------------------------------------------------------------- 1 | # nolint start 2 | #' ptvapi: A package for accessing the Public Transport Victoria Timetable API 3 | #' 4 | #' @description 5 | #' \if{html}{\figure{ptvapi.png}{options: alt='logo'}} 6 | #' 7 | #' Accessing the Public Transport Victoria Timetable API reqiures a user ID 8 | #' (also called a `devid`) and an API key. These can be accessed by contacting 9 | #' Public Transport Victoria. See 10 | #' \url{https://www.ptv.vic.gov.au/footer/data-and-reporting/datasets/ptv-timetable-api/} 11 | #' 12 | #' The user ID and API key can be entered directly into all functions. 13 | #' Alternatively, all functions will pick up on the PTV_USER_ID and API_KEY 14 | #' environment variables, if defined. 15 | #' 16 | #' All API requests use SSL by default. To disable this, and to use the `http` 17 | #' API endpoints rather than the `https` API endpoints, set the option: 18 | #' ``` 19 | #' options(use_insecure_ptv_connection = TRUE) 20 | #' ``` 21 | #' 22 | #' @details 23 | #' This is an unofficial wrapper of the Public Transport Victoria Timetable API. 24 | #' The author(s) of this package are unaffiliated with Public Transport 25 | #' Victoria. 26 | #' 27 | #' @examples \dontrun{ 28 | #' # tibble of all routes 29 | #' routes() 30 | #' 31 | #' # Search for routes by name (case insensitive, partial matching supported) 32 | #' routes(route_name = "Frankston") 33 | #' 34 | #' # All current disruptions 35 | #' disruptions(disruption_status = "current") 36 | #' 37 | #' # Train stops near Flinders Street Station 38 | #' stops_nearby( 39 | #' latitude = -37.8183, 40 | #' longitude = 144.9671, 41 | #' route_types = "Train" 42 | #' ) 43 | #' 44 | #' # Upcoming train departures from Flinders Street Station 45 | #' departures(stop_id = 1071, route_type = "Train") 46 | #' } 47 | "_PACKAGE" 48 | # nolint end 49 | -------------------------------------------------------------------------------- /man/stops_on_route.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/stops.R 3 | \name{stops_on_route} 4 | \alias{stops_on_route} 5 | \title{Stops on a given route and route type} 6 | \usage{ 7 | stops_on_route( 8 | route_id, 9 | route_type, 10 | direction_id = NULL, 11 | user_id = determine_user_id(), 12 | api_key = determine_api_key() 13 | ) 14 | } 15 | \arguments{ 16 | \item{route_id}{Integer. These can be listed and described with the 17 | \code{\link{routes}} function.} 18 | 19 | \item{route_type}{A route type which can be provided either as a non-negative 20 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 21 | Bus". Character inputs are not case-sensitive. Use the 22 | \code{\link{route_types}} function to extract a vector of all route types.} 23 | 24 | \item{direction_id}{Optionally filter by a direction ID. These can be 25 | obtained with the \code{\link{directions_on_route}} function.} 26 | 27 | \item{user_id}{Integer or character. A user ID or devid provided by Public 28 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 29 | 30 | \item{api_key}{Character. An API key, with dashes, provided by Public 31 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 32 | } 33 | \value{ 34 | A tibble with the following columns: \itemize{ 35 | \item{\code{stop_id}} 36 | \item{\code{stop_name}} 37 | \item{\code{stop_suburb}} 38 | \item{\code{route_type}} 39 | \item{\code{route_type_description}} 40 | \item{\code{stop_sequence}} 41 | \item{\code{stop_latitude}} 42 | \item{\code{stop_longitude}} 43 | \item{\code{disruption_ids}} 44 | } 45 | } 46 | \description{ 47 | Stops on a given route and route type 48 | } 49 | \examples{ 50 | \dontrun{ 51 | stops_on_route(6, route_type = "Train") 52 | stops_on_route(6, route_type = 0) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /man/directions.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/directions.R 3 | \name{directions} 4 | \alias{directions} 5 | \title{Directions for a given direction ID} 6 | \usage{ 7 | directions( 8 | direction_id, 9 | route_type = NULL, 10 | user_id = determine_user_id(), 11 | api_key = determine_api_key() 12 | ) 13 | } 14 | \arguments{ 15 | \item{direction_id}{Integer.} 16 | 17 | \item{route_type}{Optionally filter results by a route type. A route type can 18 | be provided either as a non-negative integer code, or as a character: 19 | "Tram", "Train", "Bus", "Vline" or "Night Bus". Character inputs are not 20 | case-sensitive. Use the \code{\link{route_types}} function to extract a 21 | vector of all route types.} 22 | 23 | \item{user_id}{Integer or character. A user ID or devid provided by Public 24 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 25 | 26 | \item{api_key}{Character. An API key, with dashes, provided by Public 27 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 28 | } 29 | \value{ 30 | A tibble consisting of the following columns: \itemize{ 31 | \item \code{direction_id} 32 | \item \code{direction_name}, 33 | \item \code{route_id} 34 | \item \code{route_type} 35 | \item \code{route_type_description} 36 | \item \code{route_direction_description} 37 | } 38 | } 39 | \description{ 40 | This function returns all directions with a given ID. Directions that share 41 | an ID are not necessarily related, especially if not filtering by route type. 42 | It's advised to use to the \code{\link{directions_on_route}} function to 43 | search for directions of interest. 44 | } 45 | \examples{ 46 | \dontrun{ 47 | directions(direction_id = 5) 48 | directions(direction_id = 5, route_type = "Train") 49 | directions(direction_id = 5, route_type = 0) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /man/route_information.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/routes.R 3 | \name{route_information} 4 | \alias{route_information} 5 | \title{Information for a given route} 6 | \usage{ 7 | route_information( 8 | route_id, 9 | include_geopath = FALSE, 10 | geopath_utc = NULL, 11 | user_id = determine_user_id(), 12 | api_key = determine_api_key() 13 | ) 14 | } 15 | \arguments{ 16 | \item{route_id}{Integer. These can be listed and described with the 17 | \code{\link{routes}} function.} 18 | 19 | \item{include_geopath}{Logical. Whether to populate the \code{geopath} column. 20 | Defaults to FALSE.} 21 | 22 | \item{geopath_utc}{Date, or character that can be converted to a date. The 23 | UTC date for which the geopaths are effective. Defaults to the current 24 | date. Has no effect if \code{include_geopath = FALSE}. It's uncertain how much 25 | historical or future-dated data is available.} 26 | 27 | \item{user_id}{Integer or character. A user ID or devid provided by Public 28 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 29 | 30 | \item{api_key}{Character. An API key, with dashes, provided by Public 31 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 32 | } 33 | \value{ 34 | A tibble of routes, with the following columns: 35 | \itemize{ 36 | \item \code{route_id} 37 | \item \code{route_gtfs_id} 38 | \item \code{route_name} 39 | \item \code{route_type} 40 | \item \code{route_type_description} 41 | \item \code{route_number} 42 | \item \code{geopath} 43 | \item \code{service_status} 44 | \item \code{service_status_timestamp} 45 | } 46 | } 47 | \description{ 48 | Information for a given route 49 | } 50 | \examples{ 51 | \dontrun{ 52 | route_information(6) 53 | route_information(6, include_geopath = TRUE) 54 | route_information(6, include_geopath = TRUE, geopath_utc = "2020-07-01") 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /man/routes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/routes.R 3 | \name{routes} 4 | \alias{routes} 5 | \title{Information for all routes} 6 | \usage{ 7 | routes( 8 | route_types = NULL, 9 | route_name = NULL, 10 | user_id = determine_user_id(), 11 | api_key = determine_api_key() 12 | ) 13 | } 14 | \arguments{ 15 | \item{route_types}{Integer or character vector. Optionally filter by a vector 16 | of route types. A route type can be provided either as a non-negative 17 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 18 | Bus". Character inputs are not case-sensitive. Use the 19 | \code{\link{route_types}} function to extract a vector of all route types.} 20 | 21 | \item{route_name}{Character. Optionally filter by route name. Partial matches 22 | are accepted, and the matches are not case sensitive.} 23 | 24 | \item{user_id}{Integer or character. A user ID or devid provided by Public 25 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 26 | 27 | \item{api_key}{Character. An API key, with dashes, provided by Public 28 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 29 | } 30 | \value{ 31 | A tibble of routes, with the following columns: 32 | \itemize{ 33 | \item \code{route_id} 34 | \item \code{route_gtfs_id} 35 | \item \code{route_name} 36 | \item \code{route_type} 37 | \item \code{route_type_description} 38 | \item \code{route_number} 39 | \item \code{geopath} 40 | \item \code{service_status} 41 | \item \code{service_status_timestamp} 42 | } 43 | } 44 | \description{ 45 | Information for all routes 46 | } 47 | \examples{ 48 | \dontrun{ 49 | routes() 50 | routes(route_types = "Train") 51 | routes(route_types = 0) 52 | routes(route_types = c("Train", "Tram")) 53 | routes(route_name = "Frankston") 54 | routes(route_name = "Craigie") 55 | routes(route_name = "werribee") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /man/stop_information.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/stops.R 3 | \name{stop_information} 4 | \alias{stop_information} 5 | \title{Information for a given stop (metropolitan and V/Line stations only)} 6 | \usage{ 7 | stop_information( 8 | stop_id, 9 | route_type, 10 | user_id = determine_user_id(), 11 | api_key = determine_api_key() 12 | ) 13 | } 14 | \arguments{ 15 | \item{stop_id}{Integer stop ID.} 16 | 17 | \item{route_type}{A route type which can be provided either as a non-negative 18 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 19 | Bus". Character inputs are not case-sensitive. Use the 20 | \code{\link{route_types}} function to extract a vector of all route types.} 21 | 22 | \item{user_id}{Integer or character. A user ID or devid provided by Public 23 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 24 | 25 | \item{api_key}{Character. An API key, with dashes, provided by Public 26 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 27 | } 28 | \value{ 29 | A single-row tibble with the following columns: \itemize{ 30 | \item{\code{stop_id}} 31 | \item{\code{stop_name}} 32 | \item{\code{route_type}} 33 | \item{\code{route_type_description}} 34 | \item{\code{station_details_id}} 35 | \item{\code{station_type}} 36 | \item{\code{station_description}} 37 | \item{\code{point_id}} 38 | \item{\code{mode_id}} 39 | \item{\code{operating_hours}} 40 | \item{\code{flexible_stop_opening_hours}} 41 | \item{\code{stop_contact}} 42 | \item{\code{stop_ticket}} 43 | \item{\code{stop_location}} 44 | \item{\code{stop_amenities}} 45 | \item{\code{stop_accessibility}} 46 | \item{\code{stop_staffing}} 47 | \item{\code{disruption_ids}} 48 | } 49 | } 50 | \description{ 51 | This function can be used when integer stop ID is already known. This can be 52 | searched for with either the \code{\link{stops_on_route}} or 53 | \code{\link{stops_nearby}} functions. 54 | } 55 | -------------------------------------------------------------------------------- /man/outlets.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/outlets.R 3 | \name{outlets} 4 | \alias{outlets} 5 | \title{Information for all outlets} 6 | \usage{ 7 | outlets(user_id = determine_user_id(), api_key = determine_api_key()) 8 | } 9 | \arguments{ 10 | \item{user_id}{Integer or character. A user ID or devid provided by Public 11 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 12 | 13 | \item{api_key}{Character. An API key, with dashes, provided by Public 14 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 15 | } 16 | \value{ 17 | A tibble with the following columns: \itemize{ 18 | \item {\code{outlet_slid_spid}} 19 | \item {\code{outlet_name}} 20 | \item {\code{outlet_business}} 21 | \item {\code{outlet_latitude}} 22 | \item {\code{outlet_longitude}} 23 | \item {\code{outlet_suburb}} 24 | \item {\code{outlet_postcode}} 25 | \item {\code{outlet_business_hour_mon}} 26 | \item {\code{outlet_business_hour_tue}} 27 | \item {\code{outlet_business_hour_wed}} 28 | \item {\code{outlet_business_hour_thu}} 29 | \item {\code{outlet_business_hour_fri}} 30 | \item {\code{outlet_business_hour_sat}} 31 | \item {\code{outlet_business_hour_sun}} 32 | \item {\code{outlet_notes}} 33 | } 34 | } 35 | \description{ 36 | Information for all outlets 37 | } 38 | \details{ 39 | The \code{outlet_name} reported here is more accurately described as an outlet 40 | \emph{address}. We keep the \code{outlet_name} column name as this is how the PTV API 41 | describes it. 42 | 43 | The business hours are reported as characters. Usually they take on 44 | a format of "8.00AM - 10.00PM", but there variants such as "7.30AM - 11.00AM 45 | and 1.30PM - 6.00PM". For days on which an outlet is closed, the opening 46 | hours are usually reported as "CLOSED", but can also be an empty character. 47 | Some opening hours are "24 Hours". These fields are also filled with missing 48 | values and empty characters. 49 | } 50 | \examples{ 51 | \dontrun{ 52 | outlets() 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /man/disruptions_on_route.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{disruptions_on_route} 4 | \alias{disruptions_on_route} 5 | \title{Disruptions on a given route} 6 | \usage{ 7 | disruptions_on_route( 8 | route_id, 9 | stop_id = NULL, 10 | disruption_status = NULL, 11 | user_id = determine_user_id(), 12 | api_key = determine_api_key() 13 | ) 14 | } 15 | \arguments{ 16 | \item{route_id}{Integer. These can be listed and described with the 17 | \code{\link{routes}} function.} 18 | 19 | \item{stop_id}{Integer. Optionally filter results to a specific stop ID. 20 | These can be searched for with the \code{stops_on_route} and \code{stops_nearby} 21 | functions.} 22 | 23 | \item{disruption_status}{Character. Can be used to filter to either "current" 24 | or "planned" disruptions. Defaults to NULL, in which case no filter is 25 | applied.} 26 | 27 | \item{user_id}{Integer or character. A user ID or devid provided by Public 28 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 29 | 30 | \item{api_key}{Character. An API key, with dashes, provided by Public 31 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 32 | } 33 | \value{ 34 | A tibble with the following columns: \itemize{ 35 | \item \code{disruption_mode} 36 | \item \code{disruption_mode_description} 37 | \item \code{disruption_id} 38 | \item \code{title} 39 | \item \code{url} 40 | \item \code{description} 41 | \item \code{disruption_status} 42 | \item \code{disruption_type} 43 | \item \code{published_on} 44 | \item \code{last_updated} 45 | \item \code{from_date} 46 | \item \code{to_date} 47 | \item \code{routes} 48 | \item \code{stops} 49 | \item \code{colour} 50 | \item \code{display_on_board} 51 | \item \code{display_status} 52 | } 53 | } 54 | \description{ 55 | Disruptions on a given route 56 | } 57 | \examples{ 58 | \dontrun{ 59 | disruptions_on_route(6) 60 | disruptions_on_route(6, stop_id = 1071) 61 | disruptions_on_route(6, disruption_status = "current") 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /docs/bootstrap-toc.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) 3 | * Copyright 2015 Aidan Feldman 4 | * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ 5 | 6 | /* modified from https://github.com/twbs/bootstrap/blob/94b4076dd2efba9af71f0b18d4ee4b163aa9e0dd/docs/assets/css/src/docs.css#L548-L601 */ 7 | 8 | /* All levels of nav */ 9 | nav[data-toggle='toc'] .nav > li > a { 10 | display: block; 11 | padding: 4px 20px; 12 | font-size: 13px; 13 | font-weight: 500; 14 | color: #767676; 15 | } 16 | nav[data-toggle='toc'] .nav > li > a:hover, 17 | nav[data-toggle='toc'] .nav > li > a:focus { 18 | padding-left: 19px; 19 | color: #563d7c; 20 | text-decoration: none; 21 | background-color: transparent; 22 | border-left: 1px solid #563d7c; 23 | } 24 | nav[data-toggle='toc'] .nav > .active > a, 25 | nav[data-toggle='toc'] .nav > .active:hover > a, 26 | nav[data-toggle='toc'] .nav > .active:focus > a { 27 | padding-left: 18px; 28 | font-weight: bold; 29 | color: #563d7c; 30 | background-color: transparent; 31 | border-left: 2px solid #563d7c; 32 | } 33 | 34 | /* Nav: second level (shown on .active) */ 35 | nav[data-toggle='toc'] .nav .nav { 36 | display: none; /* Hide by default, but at >768px, show it */ 37 | padding-bottom: 10px; 38 | } 39 | nav[data-toggle='toc'] .nav .nav > li > a { 40 | padding-top: 1px; 41 | padding-bottom: 1px; 42 | padding-left: 30px; 43 | font-size: 12px; 44 | font-weight: normal; 45 | } 46 | nav[data-toggle='toc'] .nav .nav > li > a:hover, 47 | nav[data-toggle='toc'] .nav .nav > li > a:focus { 48 | padding-left: 29px; 49 | } 50 | nav[data-toggle='toc'] .nav .nav > .active > a, 51 | nav[data-toggle='toc'] .nav .nav > .active:hover > a, 52 | nav[data-toggle='toc'] .nav .nav > .active:focus > a { 53 | padding-left: 28px; 54 | font-weight: 500; 55 | } 56 | 57 | /* from https://github.com/twbs/bootstrap/blob/e38f066d8c203c3e032da0ff23cd2d6098ee2dd6/docs/assets/css/src/docs.css#L631-L634 */ 58 | nav[data-toggle='toc'] .nav > .active > ul { 59 | display: block; 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | R_KEEP_PKG_SOURCE: yes 18 | NOT_CRAN: true 19 | PTV_USER_ID: ${{ secrets.PTV_USER_ID }} 20 | PTV_API_KEY: ${{ secrets.PTV_API_KEY }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - uses: r-lib/actions/setup-r@v2 25 | with: 26 | use-public-rspm: true 27 | 28 | - uses: r-lib/actions/setup-r-dependencies@v2 29 | with: 30 | extra-packages: | 31 | rcmdcheck 32 | pkgdown 33 | covr 34 | lintr 35 | 36 | - uses: r-lib/actions/check-r-package@v2 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true 41 | shell: bash 42 | 43 | - name: Upload check results 44 | if: failure() 45 | uses: actions/upload-artifact@main 46 | with: 47 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 48 | path: check 49 | 50 | - name: Install package 51 | run: R CMD INSTALL . 52 | 53 | - name: Lint 54 | run: | 55 | lintr::lint_package() 56 | shell: Rscript {0} 57 | 58 | - name: Test coverage 59 | run: | 60 | covr::codecov() 61 | shell: Rscript {0} 62 | 63 | - name: Deploy pkgdown docs 64 | run: | 65 | git config --local user.name "$GITHUB_ACTOR" 66 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 67 | Rscript -e 'pkgdown::deploy_to_branch(branch = "gh-pages", new_process = FALSE)' 68 | -------------------------------------------------------------------------------- /man/runs_on_route.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/runs.R 3 | \name{runs_on_route} 4 | \alias{runs_on_route} 5 | \title{Runs on a given route} 6 | \usage{ 7 | runs_on_route( 8 | route_id, 9 | route_type = NULL, 10 | date_utc = NULL, 11 | user_id = determine_user_id(), 12 | api_key = determine_api_key() 13 | ) 14 | } 15 | \arguments{ 16 | \item{route_id}{Integer. These can be listed and described with the 17 | \code{\link{routes}} function.} 18 | 19 | \item{route_type}{Optionally filter results by a route type. A route type can 20 | be provided either as a non-negative integer code, or as a character: 21 | "Tram", "Train", "Bus", "Vline" or "Night Bus". Character inputs are not 22 | case-sensitive. Use the \code{\link{route_types}} function to extract a 23 | vector of all route types.} 24 | 25 | \item{date_utc}{Date, or character that can be converted to a date. The 26 | UTC date for which the results are effective. Defaults to the current date. 27 | It's uncertain how much historical or future-dated data is available. This 28 | argument is experimental and seems to not be functioning.} 29 | 30 | \item{user_id}{Integer or character. A user ID or devid provided by Public 31 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 32 | 33 | \item{api_key}{Character. An API key, with dashes, provided by Public 34 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 35 | } 36 | \value{ 37 | A tibble with the following columns: \itemize{ 38 | \item \code{run_id} (deprecated, use \code{run_ref} instead) 39 | \item \code{run_ref} 40 | \item \code{route_id} 41 | \item \code{route_type} 42 | \item \code{route_type_description} 43 | \item \code{direction_id} 44 | \item \code{run_sequence} 45 | \item \code{final_stop_id} 46 | \item \code{destination_name} 47 | \item \code{status} 48 | \item \code{express_stop_count} 49 | \item \code{vehicle_position} 50 | \item \code{vehicle_descriptor} 51 | \item \code{geopath} 52 | } 53 | } 54 | \description{ 55 | Runs on a given route 56 | } 57 | \examples{ 58 | \dontrun{ 59 | runs_on_route(6) 60 | runs_on_route(6, route_type = "Train") 61 | runs_on_route(6, route_type = 0) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /man/ptvapi-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ptvapi.R 3 | \docType{package} 4 | \name{ptvapi-package} 5 | \alias{ptvapi} 6 | \alias{ptvapi-package} 7 | \title{ptvapi: A package for accessing the Public Transport Victoria Timetable API} 8 | \description{ 9 | \if{html}{\figure{ptvapi.png}{options: alt='logo'}} 10 | 11 | Accessing the Public Transport Victoria Timetable API reqiures a user ID 12 | (also called a \code{devid}) and an API key. These can be accessed by contacting 13 | Public Transport Victoria. See 14 | \url{https://www.ptv.vic.gov.au/footer/data-and-reporting/datasets/ptv-timetable-api/} 15 | 16 | The user ID and API key can be entered directly into all functions. 17 | Alternatively, all functions will pick up on the PTV_USER_ID and API_KEY 18 | environment variables, if defined. 19 | 20 | All API requests use SSL by default. To disable this, and to use the \code{http} 21 | API endpoints rather than the \code{https} API endpoints, set the option: 22 | 23 | \if{html}{\out{
}}\preformatted{options(use_insecure_ptv_connection = TRUE) 24 | }\if{html}{\out{
}} 25 | } 26 | \details{ 27 | This is an unofficial wrapper of the Public Transport Victoria Timetable API. 28 | The author(s) of this package are unaffiliated with Public Transport 29 | Victoria. 30 | } 31 | \examples{ 32 | \dontrun{ 33 | # tibble of all routes 34 | routes() 35 | 36 | # Search for routes by name (case insensitive, partial matching supported) 37 | routes(route_name = "Frankston") 38 | 39 | # All current disruptions 40 | disruptions(disruption_status = "current") 41 | 42 | # Train stops near Flinders Street Station 43 | stops_nearby( 44 | latitude = -37.8183, 45 | longitude = 144.9671, 46 | route_types = "Train" 47 | ) 48 | 49 | # Upcoming train departures from Flinders Street Station 50 | departures(stop_id = 1071, route_type = "Train") 51 | } 52 | } 53 | \seealso{ 54 | Useful links: 55 | \itemize{ 56 | \item \url{https://github.com/mdneuzerling/ptvapi} 57 | \item Report bugs at \url{https://github.com/mdneuzerling/ptvapi/issues} 58 | } 59 | 60 | } 61 | \author{ 62 | \strong{Maintainer}: David Neuzerling \email{david@neuzerling.com} [copyright holder] 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/testthat/test-search-outlets.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | 3 | # Recall that "outlet_name" is more accurately outlet address 4 | 5 | st_kilda <- search_outlets("st kilda") 6 | seven_eleven <- search_outlets("7-Eleven") 7 | 8 | test_that("search_outlets result has class \"ptvapi\"", { 9 | expect_s3_class(st_kilda, "ptvapi") 10 | }) 11 | 12 | test_that("results in search_outlets can relate to outlet name alone", { 13 | expect_gte( 14 | nrow( 15 | dplyr::filter( 16 | st_kilda, 17 | grepl("st kilda", outlet_name, ignore.case = TRUE), 18 | !grepl("st kilda", outlet_business, ignore.case = TRUE), 19 | !grepl("st kilda", outlet_suburb, ignore.case = TRUE) 20 | ) 21 | ), 22 | 1 23 | ) 24 | }) 25 | 26 | test_that("results in search_outlets can relate to outlet business alone", { 27 | expect_gte(nrow(seven_eleven), 1) 28 | }) 29 | 30 | test_that("results in search_outlets can relate to outlet suburb alone", { 31 | expect_gte( 32 | nrow( 33 | dplyr::filter( 34 | st_kilda, 35 | !grepl("st kilda", outlet_name, ignore.case = TRUE), 36 | !grepl("st kilda", outlet_business, ignore.case = TRUE), 37 | grepl("st kilda", outlet_suburb, ignore.case = TRUE) 38 | ) 39 | ), 40 | 1 41 | ) 42 | }) 43 | 44 | test_that("all results in search_outlets relate to search term somehow", { 45 | expect_equal( 46 | nrow( 47 | dplyr::filter( 48 | st_kilda, 49 | !grepl("st kilda", outlet_name, ignore.case = TRUE), 50 | !grepl("st kilda", outlet_business, ignore.case = TRUE), 51 | !grepl("st kilda", outlet_suburb, ignore.case = TRUE) 52 | ) 53 | ), 54 | 0 55 | ) 56 | }) 57 | 58 | test_that("search_outlets filtered by max_distance", { 59 | # We'll try to find a 7-Eleven between 500 and 1000 metres from Southern Cross 60 | max_500 <- search_outlets( 61 | "7-Eleven", 62 | latitude = -37.818229, 63 | longitude = 144.952404, 64 | max_distance = 500 65 | ) 66 | max_1000 <- search_outlets( 67 | "7-Eleven", 68 | latitude = -37.818229, 69 | longitude = 144.952404, 70 | max_distance = 1000 71 | ) 72 | difference <- max_1000 %>% anti_join(max_500, by = "outlet_name") 73 | expect_gt(nrow(difference), 0) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /man/stops_nearby.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/stops.R 3 | \name{stops_nearby} 4 | \alias{stops_nearby} 5 | \title{Stops near a given location} 6 | \usage{ 7 | stops_nearby( 8 | latitude, 9 | longitude, 10 | max_distance = NULL, 11 | route_types = NULL, 12 | user_id = determine_user_id(), 13 | api_key = determine_api_key() 14 | ) 15 | } 16 | \arguments{ 17 | \item{latitude}{Numeric. Latitude in decimal degrees. For example, Flinders 18 | Street Station is at approximately -37.8183 latitude.} 19 | 20 | \item{longitude}{Numeric. Longitude in decimal degrees. For example, Flinders 21 | Street Station is at approximately 144.9671 longitude.} 22 | 23 | \item{max_distance}{Integer. Optionally filter by maximum distance from the 24 | given location, in metres.} 25 | 26 | \item{route_types}{Integer or character vector. Optionally filter by a vector 27 | of route types. A route type can be provided either as a non-negative 28 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 29 | Bus". Character inputs are not case-sensitive. Use the 30 | \code{\link{route_types}} function to extract a vector of all route types.} 31 | 32 | \item{user_id}{Integer or character. A user ID or devid provided by Public 33 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 34 | 35 | \item{api_key}{Character. An API key, with dashes, provided by Public 36 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 37 | } 38 | \value{ 39 | A tibble with the following columns: \itemize{ 40 | \item{\code{stop_id}} 41 | \item{\code{stop_name}} 42 | \item{\code{stop_suburb}} 43 | \item{\code{route_type}} 44 | \item{\code{route_type_description}} 45 | \item{\code{stop_sequence}} 46 | \item{\code{stop_latitude}} 47 | \item{\code{stop_longitude}} 48 | \item{\code{disruption_ids}} 49 | } 50 | } 51 | \description{ 52 | Stops near a given location 53 | } 54 | \examples{ 55 | \dontrun{ 56 | stops_nearby(latitude = -37.8183, longitude = 144.9671) 57 | stops_nearby(latitude = -37.8183, longitude = 144.9671, max_distance = 1000) 58 | stops_nearby( 59 | latitude = -37.8183, 60 | longitude = 144.9671, 61 | route_types = c("Train", "Tram") 62 | ) 63 | 64 | stops_nearby( 65 | latitude = -37.8183, 66 | longitude = 144.9671, 67 | route_types = 0 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/full-check-on-release.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | release: 9 | types: 10 | - created 11 | 12 | name: R-CMD-check 13 | 14 | jobs: 15 | R-CMD-check: 16 | runs-on: ${{ matrix.config.os }} 17 | 18 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | config: 24 | - {os: macOS-latest, r: 'release'} 25 | 26 | - {os: windows-latest, r: 'release'} 27 | # Use 3.6 to trigger usage of RTools35 28 | - {os: windows-latest, r: '3.6'} 29 | 30 | # Use older ubuntu to maximise backward compatibility 31 | - {os: ubuntu-20.04, r: 'devel', http-user-agent: 'release'} 32 | - {os: ubuntu-20.04, r: 'release'} 33 | - {os: ubuntu-20.04, r: 'oldrel-1'} 34 | - {os: ubuntu-20.04, r: 'oldrel-2'} 35 | - {os: ubuntu-20.04, r: 'oldrel-3'} 36 | - {os: ubuntu-20.04, r: 'oldrel-4'} 37 | 38 | env: 39 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 40 | R_KEEP_PKG_SOURCE: yes 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | 45 | - uses: r-lib/actions/setup-pandoc@v2 46 | 47 | - uses: r-lib/actions/setup-r@v2 48 | with: 49 | r-version: ${{ matrix.config.r }} 50 | http-user-agent: ${{ matrix.config.http-user-agent }} 51 | use-public-rspm: true 52 | 53 | - uses: r-lib/actions/setup-r-dependencies@v2 54 | with: 55 | extra-packages: rcmdcheck 56 | 57 | - uses: r-lib/actions/check-r-package@v2 58 | 59 | - name: Show testthat output 60 | if: always() 61 | run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true 62 | shell: bash 63 | 64 | - name: Upload check results 65 | if: failure() 66 | uses: actions/upload-artifact@main 67 | with: 68 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 69 | path: check 70 | -------------------------------------------------------------------------------- /docs/docsearch.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | // register a handler to move the focus to the search bar 4 | // upon pressing shift + "/" (i.e. "?") 5 | $(document).on('keydown', function(e) { 6 | if (e.shiftKey && e.keyCode == 191) { 7 | e.preventDefault(); 8 | $("#search-input").focus(); 9 | } 10 | }); 11 | 12 | $(document).ready(function() { 13 | // do keyword highlighting 14 | /* modified from https://jsfiddle.net/julmot/bL6bb5oo/ */ 15 | var mark = function() { 16 | 17 | var referrer = document.URL ; 18 | var paramKey = "q" ; 19 | 20 | if (referrer.indexOf("?") !== -1) { 21 | var qs = referrer.substr(referrer.indexOf('?') + 1); 22 | var qs_noanchor = qs.split('#')[0]; 23 | var qsa = qs_noanchor.split('&'); 24 | var keyword = ""; 25 | 26 | for (var i = 0; i < qsa.length; i++) { 27 | var currentParam = qsa[i].split('='); 28 | 29 | if (currentParam.length !== 2) { 30 | continue; 31 | } 32 | 33 | if (currentParam[0] == paramKey) { 34 | keyword = decodeURIComponent(currentParam[1].replace(/\+/g, "%20")); 35 | } 36 | } 37 | 38 | if (keyword !== "") { 39 | $(".contents").unmark({ 40 | done: function() { 41 | $(".contents").mark(keyword); 42 | } 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | mark(); 49 | }); 50 | }); 51 | 52 | /* Search term highlighting ------------------------------*/ 53 | 54 | function matchedWords(hit) { 55 | var words = []; 56 | 57 | var hierarchy = hit._highlightResult.hierarchy; 58 | // loop to fetch from lvl0, lvl1, etc. 59 | for (var idx in hierarchy) { 60 | words = words.concat(hierarchy[idx].matchedWords); 61 | } 62 | 63 | var content = hit._highlightResult.content; 64 | if (content) { 65 | words = words.concat(content.matchedWords); 66 | } 67 | 68 | // return unique words 69 | var words_uniq = [...new Set(words)]; 70 | return words_uniq; 71 | } 72 | 73 | function updateHitURL(hit) { 74 | 75 | var words = matchedWords(hit); 76 | var url = ""; 77 | 78 | if (hit.anchor) { 79 | url = hit.url_without_anchor + '?q=' + escape(words.join(" ")) + '#' + hit.anchor; 80 | } else { 81 | url = hit.url + '?q=' + escape(words.join(" ")); 82 | } 83 | 84 | return url; 85 | } 86 | -------------------------------------------------------------------------------- /man/add_parameters.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/url-manipulation.R 3 | \name{add_parameters} 4 | \alias{add_parameters} 5 | \title{Suffix one or many parameters to a HTML request} 6 | \usage{ 7 | add_parameters(request, ..., .combine = "repeat_name") 8 | } 9 | \arguments{ 10 | \item{request}{Character. The base URL or request which will be suffixed with 11 | the parameter.} 12 | 13 | \item{...}{The parameters to be suffixed, with name/value pairs provided as 14 | arguments.} 15 | 16 | \item{.combine}{How to combine parameters with multiple values. One 17 | of "repeat_name", "with_commas", "with_hex_commas". See Details.} 18 | } 19 | \value{ 20 | Character. The request with suffixed parameters. 21 | } 22 | \description{ 23 | Parameters are suffixed to a URL, like so: 24 | "request?para1=value1¶2=value2". The first parameter is suffixed with "?" 25 | and all others after that "&". This function will determine the correct 26 | suffix based on the presence of "&" in the request. 27 | } 28 | \details{ 29 | There is no standardised way to combine multiple values for a 30 | parameter. You should see how your API expects multiple values to be provided 31 | to the same parameter. This function allows for the following strategies. If 32 | any other value is provided, then the values will be concatenated and 33 | separated with the provided value. 34 | \itemize{ 35 | \item "repeat_name" (default). The values of the parameter are repeated 36 | with the parameter name. For example, \verb{("request", "para", c(1, 2))} will 37 | return "request?para=1¶=2". 38 | \item "with_commas". The values of the parameter are concatenated and 39 | separated with commas. For example, \verb{("request", "para", c(1, 2))} will 40 | return "request?para=1,2". 41 | \item "with_commas". The values of the parameter are concatenated and 42 | separated with the ASCII keycode in hexadecimal for a comma ("\%2C"). For 43 | example, \verb{("request", "para", c(1, 2))} will return "request?para=1\%2C2". 44 | } 45 | } 46 | \examples{ 47 | \dontrun{ 48 | ptvapi:::add_parameters("www.example.com", animal = crocodile) 49 | ptvapi:::add_parameters( 50 | "www.example.com", 51 | animal = crocodile, 52 | food = "cherries" 53 | ) 54 | ptvapi:::add_parameters( 55 | "www.example.com", 56 | animal = crocodile, 57 | numbers = c(1, 2, 3), 58 | .combine = "repeat_names" 59 | )} 60 | 61 | } 62 | \keyword{internal} 63 | -------------------------------------------------------------------------------- /man/add_parameter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/url-manipulation.R 3 | \name{add_parameter} 4 | \alias{add_parameter} 5 | \title{Suffix a parameter to a HTML request} 6 | \usage{ 7 | add_parameter( 8 | request, 9 | parameter_name, 10 | parameter_value, 11 | .combine = "repeat_name" 12 | ) 13 | } 14 | \arguments{ 15 | \item{request}{Character. The base URL or request which will be suffixed with 16 | the parameter.} 17 | 18 | \item{parameter_name}{Character. Name of parameter to suffix.} 19 | 20 | \item{parameter_value}{Character, or a value that can be coerced to a 21 | character. The value of the parameter to suffix.} 22 | 23 | \item{.combine}{How to combine parameters with multiple values. One 24 | of "repeat_name", "with_commas", "with_hex_commas". See Details.} 25 | } 26 | \value{ 27 | Character. The request with suffixed parameters. 28 | } 29 | \description{ 30 | Parameters are suffixed to a URL, like so: 31 | "request?para1=value1¶2=value2". The first parameter is suffixed with "?" 32 | and all others after that "&". This function will determine the correct 33 | suffix based on the presence of "&" in the request. 34 | } 35 | \details{ 36 | There is no standardised way to combine multiple values for a 37 | parameter. You should see how your API expects multiple values to be provided 38 | to the same parameter. This function allows for the following strategies. If 39 | any other value is provided, then the values will be concatenated and 40 | separated with the provided value. 41 | \itemize{ 42 | \item "repeat_name" (default). The values of the parameter are repeated 43 | with the parameter name. For example, \verb{("request", "para", c(1, 2))} will 44 | return "request?para=1¶=2". 45 | \item "with_commas". The values of the parameter are concatenated and 46 | separated with commas. For example, \verb{("request", "para", c(1, 2))} will 47 | return "request?para=1,2". 48 | \item "with_commas". The values of the parameter are concatenated and 49 | separated with the ASCII keycode in hexadecimal for a comma ("\%2C"). For 50 | example, \verb{("request", "para", c(1, 2))} will return "request?para=1\%2C2". 51 | } 52 | } 53 | \examples{ 54 | \dontrun{ 55 | ptvapi:::add_parameter("www.example.com", "animal", "crocodile") 56 | ptvapi:::add_parameter( 57 | "www.example.com", 58 | "numbers", 59 | c(1, 2, 3), 60 | .combine = "repeat_names" 61 | )} 62 | 63 | } 64 | \keyword{internal} 65 | -------------------------------------------------------------------------------- /tests/testthat/test-routes.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | # ---------------------------------------------------------------------------- # 3 | # ---- Define values if they haven't already been defined by another test ---- # 4 | # ---------------------------------------------------------------------------- # 5 | if (!exists("all_routes")) { 6 | all_routes <- routes() 7 | } 8 | 9 | if (!exists("frankston_route_id")) { 10 | frankston_route_id <- all_routes %>% 11 | dplyr::filter(route_name == "Frankston") %>% 12 | pull(route_id) 13 | } 14 | # ---------------------------------------------------------------------------- # 15 | 16 | single_route <- route_information(route_id = frankston_route_id) 17 | 18 | test_that("route_information() result has class \"ptvapi\"", { 19 | expect_s3_class(single_route, "ptvapi") 20 | }) 21 | 22 | test_that("we can query for a single route", { 23 | expect_equal(nrow(single_route), 1) 24 | }) 25 | 26 | test_that("routes() result has class \"ptvapi\"", { 27 | expect_s3_class(all_routes, "ptvapi") 28 | }) 29 | 30 | test_that("we can find the Frankston train route", { 31 | expect_true(length(frankston_route_id) == 1) 32 | }) 33 | 34 | test_that("we can filter route results by name", { 35 | frankston_named_routes <- routes(route_name = "frank") 36 | expect_gt(nrow(frankston_named_routes), 0) 37 | expect_equal( 38 | nrow(dplyr::filter( 39 | frankston_named_routes, 40 | !grepl("frank", route_name, ignore.case = TRUE) 41 | )), 42 | 0 43 | ) 44 | }) 45 | 46 | test_that("86 tram route can be found", { 47 | eighty_six_tram_routes <- all_routes %>% 48 | dplyr::filter( 49 | route_number == 86, 50 | route_type == translate_route_type("Tram") 51 | ) 52 | expect_equal(nrow(eighty_six_tram_routes), 1) 53 | }) 54 | 55 | test_that("we can filter routes by multiple route types", { 56 | expect_equal( 57 | routes(route_types = c(0, 1)) %>% pull(route_type) %>% unique %>% sort, 58 | c(0, 1) 59 | ) 60 | }) 61 | 62 | test_that("we can download geopath data with specific route", { 63 | frankston_with_geo <- route_information( 64 | frankston_route_id, 65 | include_geopath = TRUE 66 | ) 67 | geopaths <- frankston_with_geo$geopath[[1]] 68 | # One path for each of the two directions 69 | expect_gte(nrow(geopaths), 1) 70 | expect_identical( 71 | names(geopaths), 72 | c("direction_id", "valid_from", "valid_to", "paths") 73 | ) 74 | }) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /man/disruptions.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/disruptions.R 3 | \name{disruptions} 4 | \alias{disruptions} 5 | \title{Information for all disruptions} 6 | \usage{ 7 | disruptions( 8 | route_types = NULL, 9 | disruption_modes = NULL, 10 | disruption_status = NULL, 11 | user_id = determine_user_id(), 12 | api_key = determine_api_key() 13 | ) 14 | } 15 | \arguments{ 16 | \item{route_types}{Integer or character vector. Optionally filter by a vector 17 | of route types. A route type can be provided either as a non-negative 18 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 19 | Bus". Character inputs are not case-sensitive. Use the 20 | \code{\link{route_types}} function to extract a vector of all route types. 21 | The filter is applied to the disruption mode, rather than the routes that 22 | are affected by the disruption. For example, filtering by the "train" route 23 | type will restrict the disruptions returned to those with a mode 24 | corresponding to "metro_train".} 25 | 26 | \item{disruption_modes}{Integer vector. Optionally filter by disruption 27 | modes. For a full list of modes and their corresponding descriptions, use 28 | the \code{disruptions_modes} function.} 29 | 30 | \item{disruption_status}{Character. Can be used to filter to either "current" 31 | or "planned" disruptions. Defaults to NULL, in which case no filter is 32 | applied.} 33 | 34 | \item{user_id}{Integer or character. A user ID or devid provided by Public 35 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 36 | 37 | \item{api_key}{Character. An API key, with dashes, provided by Public 38 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 39 | } 40 | \value{ 41 | A tibble with the following columns: \itemize{ 42 | \item \code{disruption_mode} 43 | \item \code{disruption_mode_description} 44 | \item \code{disruption_id} 45 | \item \code{title} 46 | \item \code{url} 47 | \item \code{description} 48 | \item \code{disruption_status} 49 | \item \code{disruption_type} 50 | \item \code{published_on} 51 | \item \code{last_updated} 52 | \item \code{from_date} 53 | \item \code{to_date} 54 | \item \code{routes} 55 | \item \code{stops} 56 | \item \code{colour} 57 | \item \code{display_on_board} 58 | \item \code{display_status} 59 | } 60 | } 61 | \description{ 62 | Information for all disruptions 63 | } 64 | \examples{ 65 | \dontrun{ 66 | disruptions() 67 | disruptions(route_types = c("Train", "Tram")) 68 | disruptions(disruption_modes = c(0, 1)) 69 | disruptions(disruption_status = "current") 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /man/search_stops.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/search.R 3 | \name{search_stops} 4 | \alias{search_stops} 5 | \title{Search for stops using text} 6 | \usage{ 7 | search_stops( 8 | search_term, 9 | latitude = NULL, 10 | longitude = NULL, 11 | max_distance = NULL, 12 | route_types = NULL, 13 | user_id = determine_user_id(), 14 | api_key = determine_api_key() 15 | ) 16 | } 17 | \arguments{ 18 | \item{search_term}{Character. Term used to perform search.} 19 | 20 | \item{latitude}{Numeric. Latitude in decimal degrees. For example, Flinders 21 | Street Station is at approximately -37.8183 latitude.} 22 | 23 | \item{longitude}{Numeric. Longitude in decimal degrees. For example, Flinders 24 | Street Station is at approximately 144.9671 longitude.} 25 | 26 | \item{max_distance}{Integer. Optionally filter by maximum distance from the 27 | given location, in metres.} 28 | 29 | \item{route_types}{Integer or character vector. Optionally filter by a vector 30 | of route types. A route type can be provided either as a non-negative 31 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 32 | Bus". Character inputs are not case-sensitive. Use the 33 | \code{\link{route_types}} function to extract a vector of all route types.} 34 | 35 | \item{user_id}{Integer or character. A user ID or devid provided by Public 36 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 37 | 38 | \item{api_key}{Character. An API key, with dashes, provided by Public 39 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 40 | } 41 | \value{ 42 | A tibble with the following columns: \itemize{ 43 | \item{\code{stop_id}} 44 | \item{\code{stop_name}} 45 | \item{\code{stop_suburb}} 46 | \item{\code{route_type}} 47 | \item{\code{route_type_description}} 48 | \item{\code{stop_sequence}} 49 | \item{\code{stop_latitude}} 50 | \item{\code{stop_longitude}} 51 | \item{\code{disruption_ids}} 52 | } 53 | } 54 | \description{ 55 | This function will search stops in which the search term can be found in 56 | either the stop name or the stop suburb. The search is case-insensitive. 57 | The search term must contain at least 3 characters, and cannot be numeric. 58 | } 59 | \examples{ 60 | \dontrun{ 61 | search_stops("Ascot Vale") 62 | search_stops("Ascot Vale", route_types = c("Train", "Tram")) 63 | search_stops("Ascot Vale", route_types = 1) 64 | 65 | search_stops( 66 | "Ascot Vale", 67 | latitude = -37.774240, 68 | longitude = 144.915518 69 | ) 70 | search_stops( 71 | "Ascot Vale", 72 | latitude = -37.774240, 73 | longitude = 144.915518, 74 | max_distance = 100 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /man/outlets_nearby.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/outlets.R 3 | \name{outlets_nearby} 4 | \alias{outlets_nearby} 5 | \title{Information for outlets near a given location} 6 | \usage{ 7 | outlets_nearby( 8 | latitude, 9 | longitude, 10 | max_distance = NULL, 11 | max_results = 30, 12 | user_id = determine_user_id(), 13 | api_key = determine_api_key() 14 | ) 15 | } 16 | \arguments{ 17 | \item{latitude}{Numeric. Latitude in decimal degrees. For example, Flinders 18 | Street Station is at approximately -37.8183 latitude.} 19 | 20 | \item{longitude}{Numeric. Longitude in decimal degrees. For example, Flinders 21 | Street Station is at approximately 144.9671 longitude.} 22 | 23 | \item{max_distance}{Integer. Optionally filter by maximum distance from the 24 | given location, in metres.} 25 | 26 | \item{max_results}{Integer. Defaults to 30. Caps the number of results 27 | returned.} 28 | 29 | \item{user_id}{Integer or character. A user ID or devid provided by Public 30 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 31 | 32 | \item{api_key}{Character. An API key, with dashes, provided by Public 33 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 34 | } 35 | \value{ 36 | A tibble with the following columns: \itemize{ 37 | \item {\code{outlet_slid_spid}} 38 | \item {\code{outlet_name}} 39 | \item {\code{outlet_business}} 40 | \item {\code{outlet_latitude}} 41 | \item {\code{outlet_longitude}} 42 | \item {\code{outlet_suburb}} 43 | \item {\code{outlet_postcode}} 44 | \item {\code{outlet_business_hour_mon}} 45 | \item {\code{outlet_business_hour_tue}} 46 | \item {\code{outlet_business_hour_wed}} 47 | \item {\code{outlet_business_hour_thu}} 48 | \item {\code{outlet_business_hour_fri}} 49 | \item {\code{outlet_business_hour_sat}} 50 | \item {\code{outlet_business_hour_sun}} 51 | \item {\code{outlet_notes}} 52 | } 53 | } 54 | \description{ 55 | Information for outlets near a given location 56 | } 57 | \details{ 58 | The \code{outlet_name} reported here is more accurately described as an outlet 59 | \emph{address}. We keep the \code{outlet_name} column name as this is how the PTV API 60 | describes it. 61 | 62 | The business hours are reported as characters. Usually they take on 63 | a format of "8.00AM - 10.00PM", but there variants such as "7.30AM - 11.00AM 64 | and 1.30PM - 6.00PM". For days on which an outlet is closed, the opening 65 | hours are usually reported as "CLOSED", but can also be an empty character. 66 | Some opening hours are "24 Hours". These fields are also filled with missing 67 | values and empty characters. 68 | } 69 | \examples{ 70 | \dontrun{ 71 | outlets_nearby(latitude = -37.8183, longitude = 144.9671) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /tests/testthat/test-directions.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | # ---------------------------------------------------------------------------- # 3 | # ---- Define values if they haven't already been defined by another test ---- # 4 | # ---------------------------------------------------------------------------- # 5 | if (!exists("all_routes")) { 6 | all_routes <- routes() 7 | } 8 | 9 | if (!exists("frankston_route_id")) { 10 | frankston_route_id <- all_routes %>% 11 | dplyr::filter(route_name == "Frankston") %>% 12 | pull(route_id) 13 | } 14 | # ---------------------------------------------------------------------------- # 15 | 16 | # Testing directions_on_route 17 | # Simply running this functions asserts that column names, etc. are as expected 18 | frankston_directions_on_route <- directions_on_route( 19 | route_id = frankston_route_id 20 | ) 21 | test_that("directions_on_route result has class \"ptvapi\"", { 22 | expect_s3_class(frankston_directions_on_route, "ptvapi") 23 | }) 24 | 25 | city_directions <- frankston_directions_on_route %>% 26 | filter(grepl("City", direction_name, ignore.case = TRUE)) 27 | 28 | test_that("Frankston train goes to the city", { 29 | expect_equal(nrow(city_directions), 1) 30 | }) 31 | 32 | frankston_directions <- frankston_directions_on_route %>% 33 | filter(grepl("Frankston", direction_name, ignore.case = TRUE)) 34 | 35 | test_that("Frankston train goes to the city", { 36 | expect_equal(nrow(frankston_directions), 1) 37 | }) 38 | 39 | # Testing directions 40 | # We'll run the Frankston -> City direction from above through the directions 41 | # function, and see if we can recover the Frankston route ID. 42 | if (!exists("frankston_route_id")) { 43 | frankston_route_id <- routes() %>% 44 | dplyr::filter(route_name == "Frankston") %>% 45 | pull(route_id) 46 | } 47 | 48 | recovering_frankston_direction <- city_directions$direction_id %>% 49 | directions(direction_id = .) %>% 50 | filter( 51 | grepl("Frankston", route_direction_description, ignore.case = TRUE), 52 | route_id == frankston_route_id 53 | ) 54 | 55 | test_that("Frankston route can be recovered with directions call", { 56 | expect_equal(nrow(recovering_frankston_direction), 1) 57 | expect_equal(recovering_frankston_direction$route_id, frankston_route_id) 58 | }) 59 | 60 | 61 | # Testing directions with route type 62 | test_that("Frankstain train directions are all train routes", { 63 | city_train_directions <- city_directions$direction_id %>% 64 | directions(direction_id = ., route_type = "Train") 65 | expect_true( 66 | all( 67 | grepl( 68 | "train", 69 | city_train_directions$route_direction_description, 70 | ignore.case = TRUE 71 | ) 72 | ) 73 | ) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /man/ptv_search.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/search.R 3 | \name{ptv_search} 4 | \alias{ptv_search} 5 | \title{Use a character term to search for routes, stops, and outlets.} 6 | \usage{ 7 | ptv_search( 8 | search_term, 9 | latitude = NULL, 10 | longitude = NULL, 11 | max_distance = NULL, 12 | route_types = NULL, 13 | include_outlets = FALSE, 14 | match_stop_by_suburb = FALSE, 15 | match_route_by_suburb = FALSE, 16 | match_stop_by_gtfs_stop_id = FALSE, 17 | user_id = determine_user_id(), 18 | api_key = determine_api_key() 19 | ) 20 | } 21 | \arguments{ 22 | \item{search_term}{Character. Term used to perform search.} 23 | 24 | \item{latitude}{Numeric. Latitude in decimal degrees. For example, Flinders 25 | Street Station is at approximately -37.8183 latitude.} 26 | 27 | \item{longitude}{Numeric. Longitude in decimal degrees. For example, Flinders 28 | Street Station is at approximately 144.9671 longitude.} 29 | 30 | \item{max_distance}{Integer. Optionally filter by maximum distance from the 31 | given location, in metres.} 32 | 33 | \item{route_types}{Integer or character vector. Optionally filter by a vector 34 | of route types. A route type can be provided either as a non-negative 35 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 36 | Bus". Character inputs are not case-sensitive. Use the 37 | \code{\link{route_types}} function to extract a vector of all route types.} 38 | 39 | \item{include_outlets}{Boolean. Optional. Affects search results.} 40 | 41 | \item{match_stop_by_suburb}{Boolean. Optional. Affects search results.} 42 | 43 | \item{match_route_by_suburb}{Boolean. Optional. Affects search results.} 44 | 45 | \item{match_stop_by_gtfs_stop_id}{Boolean. Optional. Affects search results.} 46 | 47 | \item{user_id}{Integer or character. A user ID or devid provided by Public 48 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 49 | 50 | \item{api_key}{Character. An API key, with dashes, provided by Public 51 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 52 | } 53 | \value{ 54 | The response of the \code{search} API call. 55 | } 56 | \description{ 57 | There's only one search API call, and it covers stops, routes, and outlets. 58 | This function will return the response of this generic search. It is 59 | expected that other functions will take on of these three categories of 60 | search results, parse them, and return them to the user as a tibble. 61 | } 62 | \details{ 63 | If the search term is numeric and/or less than 3 characters, the API will 64 | return only routes. By default, as little matching is done as possible, and 65 | as little as possible is returned. We rely on functions that call on this 66 | function to specify what is needed. 67 | } 68 | \keyword{internal} 69 | -------------------------------------------------------------------------------- /man/search_routes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/search.R 3 | \name{search_routes} 4 | \alias{search_routes} 5 | \title{Search for routes using text} 6 | \usage{ 7 | search_routes( 8 | search_term, 9 | latitude = NULL, 10 | longitude = NULL, 11 | max_distance = NULL, 12 | route_types = NULL, 13 | user_id = determine_user_id(), 14 | api_key = determine_api_key() 15 | ) 16 | } 17 | \arguments{ 18 | \item{search_term}{Character. Term used to perform search.} 19 | 20 | \item{latitude}{Numeric. Latitude in decimal degrees. For example, Flinders 21 | Street Station is at approximately -37.8183 latitude.} 22 | 23 | \item{longitude}{Numeric. Longitude in decimal degrees. For example, Flinders 24 | Street Station is at approximately 144.9671 longitude.} 25 | 26 | \item{max_distance}{Integer. Optionally filter by maximum distance from the 27 | given location, in metres.} 28 | 29 | \item{route_types}{Integer or character vector. Optionally filter by a vector 30 | of route types. A route type can be provided either as a non-negative 31 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 32 | Bus". Character inputs are not case-sensitive. Use the 33 | \code{\link{route_types}} function to extract a vector of all route types.} 34 | 35 | \item{user_id}{Integer or character. A user ID or devid provided by Public 36 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 37 | 38 | \item{api_key}{Character. An API key, with dashes, provided by Public 39 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 40 | } 41 | \value{ 42 | A tibble of routes, with the following columns: 43 | \itemize{ 44 | \item \code{route_id} 45 | \item \code{route_gtfs_id} 46 | \item \code{route_name} 47 | \item \code{route_type} 48 | \item \code{route_type_description} 49 | \item \code{route_number} 50 | \item \code{geopath} 51 | \item \code{service_status} 52 | \item \code{service_status_timestamp} 53 | } 54 | } 55 | \description{ 56 | This function will search routes in which the search term can be found in 57 | one of many fields, such as \code{route_id}, \code{route_gtfs_id}, or \code{route_name}. 58 | The search is case-insensitive. Unlike \code{\link{search_stops}} and 59 | \code{\link{search_outlets}}, this function supports searching for numerics, 60 | and has no minimum character requirement for \code{search_term}. 61 | } 62 | \examples{ 63 | \dontrun{ 64 | search_routes("Pakenham") 65 | search_routes("Pakenham", route_types = c("Train", "Tram")) 66 | search_routes("Pakenham", route_types = 1) 67 | 68 | search_routes( 69 | "Pakenham", 70 | latitude = -38.077877, 71 | longitude = 145.484751 72 | ) 73 | search_routes( 74 | "Pakenham", 75 | latitude = -38.077877, 76 | longitude = 145.484751, 77 | max_distance = 100 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /man/search_outlets.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/search.R 3 | \name{search_outlets} 4 | \alias{search_outlets} 5 | \title{Search for outlets using text} 6 | \usage{ 7 | search_outlets( 8 | search_term, 9 | latitude = NULL, 10 | longitude = NULL, 11 | max_distance = NULL, 12 | route_types = NULL, 13 | user_id = determine_user_id(), 14 | api_key = determine_api_key() 15 | ) 16 | } 17 | \arguments{ 18 | \item{search_term}{Character. Term used to perform search.} 19 | 20 | \item{latitude}{Numeric. Latitude in decimal degrees. For example, Flinders 21 | Street Station is at approximately -37.8183 latitude.} 22 | 23 | \item{longitude}{Numeric. Longitude in decimal degrees. For example, Flinders 24 | Street Station is at approximately 144.9671 longitude.} 25 | 26 | \item{max_distance}{Integer. Optionally filter by maximum distance from the 27 | given location, in metres.} 28 | 29 | \item{route_types}{Integer or character vector. Optionally filter by a vector 30 | of route types. A route type can be provided either as a non-negative 31 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 32 | Bus". Character inputs are not case-sensitive. Use the 33 | \code{\link{route_types}} function to extract a vector of all route types.} 34 | 35 | \item{user_id}{Integer or character. A user ID or devid provided by Public 36 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 37 | 38 | \item{api_key}{Character. An API key, with dashes, provided by Public 39 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 40 | } 41 | \value{ 42 | A tibble with the following columns: \itemize{ 43 | \item {\code{outlet_slid_spid}} 44 | \item {\code{outlet_name}} 45 | \item {\code{outlet_business}} 46 | \item {\code{outlet_latitude}} 47 | \item {\code{outlet_longitude}} 48 | \item {\code{outlet_suburb}} 49 | \item {\code{outlet_postcode}} 50 | \item {\code{outlet_business_hour_mon}} 51 | \item {\code{outlet_business_hour_tue}} 52 | \item {\code{outlet_business_hour_wed}} 53 | \item {\code{outlet_business_hour_thu}} 54 | \item {\code{outlet_business_hour_fri}} 55 | \item {\code{outlet_business_hour_sat}} 56 | \item {\code{outlet_business_hour_sun}} 57 | \item {\code{outlet_notes}} 58 | } 59 | } 60 | \description{ 61 | This function will search outlets in which the search term can be found in 62 | either the outlet name, outlet business or outlet suburb. The search is 63 | case-insensitive. The search term must contain at least 3 characters, and 64 | cannot be numeric. 65 | } 66 | \examples{ 67 | \dontrun{ 68 | search_outlets("St Kilda") 69 | search_outlets("St Kilda", route_types = c("Train", "Tram")) 70 | search_outlets("St Kilda", route_types = 1) 71 | 72 | search_outlets( 73 | "St Kilda", 74 | latitude = -37.867647, 75 | longitude = 144.976809 76 | ) 77 | search_outlets( 78 | "St Kilda", 79 | latitude = -37.867647, 80 | longitude = 144.976809, 81 | max_distance = 100 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /man/fare_estimate.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/fare-estimate.R 3 | \name{fare_estimate} 4 | \alias{fare_estimate} 5 | \title{Calculate a fare estimate between zones} 6 | \usage{ 7 | fare_estimate( 8 | min_zone, 9 | max_zone, 10 | journey_touch_on = NULL, 11 | journey_touch_off = NULL, 12 | journey_in_free_tram_zone = FALSE, 13 | travelled_route_types = NULL, 14 | user_id = determine_user_id(), 15 | api_key = determine_api_key() 16 | ) 17 | } 18 | \arguments{ 19 | \item{min_zone}{Integer. Minimum zone travelled through.} 20 | 21 | \item{max_zone}{Integer. Maximum zone travelled through.} 22 | 23 | \item{journey_touch_on, journey_touch_off}{POSIXct or Character. Optionally 24 | filter results to a journey time. Values to both must be provided. 25 | Characters are automatically converted to datetimes, and are assumed to be 26 | given as Melbourne time.} 27 | 28 | \item{journey_in_free_tram_zone}{Boolean. Defaults to \code{FALSE}.} 29 | 30 | \item{travelled_route_types}{Integer or character vector. Optionally filter 31 | by a vector of route types. A route type can be provided either as a 32 | non-negative integer code, or as a character: "Tram", "Train", "Bus", 33 | "Vline" or "Night Bus". Character inputs are not case-sensitive. Use the 34 | \code{\link{route_types}} function to extract a vector of all route types.} 35 | 36 | \item{user_id}{Integer or character. A user ID or devid provided by Public 37 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 38 | 39 | \item{api_key}{Character. An API key, with dashes, provided by Public 40 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 41 | } 42 | \value{ 43 | A data frame consisting of one row for each \code{passenger_type}, and the 44 | following columns: \itemize{ 45 | \item \code{min_zone} 46 | \item \code{max_zone} 47 | \item \code{unique_zones} 48 | \item \code{early_bird} 49 | \item \code{free_tram_zone} 50 | \item \code{weekend_journey} 51 | \item \code{passenger_type} 52 | \item \code{fare_2_hour_peak} 53 | \item \code{fare_2_hour_off_peak} 54 | \item \code{fare_daily_peak} 55 | \item \code{fare_daily_off_peak} 56 | \item \code{pass_7_days} 57 | \item \code{pass_28_to_69_day_per_day} 58 | \item \code{pass_70_plus_day_per_day} 59 | \item \code{weekend_cap} 60 | \item \code{holiday_cap} 61 | } 62 | } 63 | \description{ 64 | Retrieve fare information for a journey through the given zones. Also 65 | supports journey touch on and off times, to accommodate for discounts. 66 | } 67 | \examples{ 68 | \dontrun{ 69 | fare_estimate(min_zone = 1, max_zone = 2) 70 | 71 | fare_estimate(min_zone = 1, max_zone = 1, journey_in_free_tram_zone = TRUE) 72 | 73 | fare_estimate( 74 | min_zone = 1, 75 | max_zone = 2, 76 | travelled_route_types = c("Train", "Tram") 77 | ) 78 | 79 | fare_estimate( 80 | min_zone = 1, 81 | max_zone = 2, 82 | journey_touch_on = "2020-06-21 07:31:00", 83 | journey_touch_off = "2020-06-21 08:45:00" 84 | ) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /man/patterns.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/patterns.R 3 | \name{patterns} 4 | \alias{patterns} 5 | \title{Stopping pattern for a given run} 6 | \usage{ 7 | patterns( 8 | run_ref, 9 | route_type, 10 | stop_id = NULL, 11 | departs = Sys.time(), 12 | user_id = determine_user_id(), 13 | api_key = determine_api_key() 14 | ) 15 | } 16 | \arguments{ 17 | \item{run_ref}{A character run reference. This supersedes the integer 18 | \code{run_id}. For backwards compatibility and since most run references are 19 | integers, this function will attempt to convert an the argument to a 20 | character. Run references may be retrieved from the 21 | \code{\link{departures}} or \code{\link{runs_on_route}} functions.} 22 | 23 | \item{route_type}{Optionally filter results by a route type. A route type can 24 | be provided either as a non-negative integer code, or as a character: 25 | "Tram", "Train", "Bus", "Vline" or "Night Bus". Character inputs are not 26 | case-sensitive. Use the \code{\link{route_types}} function to extract a 27 | vector of all route types.} 28 | 29 | \item{stop_id}{Integer. Optionally filter results to a specific stop ID. 30 | These can be searched for with the \code{stops_on_route} and \code{stops_nearby} 31 | functions.} 32 | 33 | \item{departs}{POSIXct or character. Optionally filter by date. See Details. 34 | Characters are automatically converted to departs, and are assumed to be 35 | given as Melbourne time. The behaviour of the API is unpredictable when 36 | using this argument --- see details. Defaults to the current system time.} 37 | 38 | \item{user_id}{Integer or character. A user ID or devid provided by Public 39 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 40 | 41 | \item{api_key}{Character. An API key, with dashes, provided by Public 42 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 43 | } 44 | \value{ 45 | An object of class "ptvapi", which is effectively a list with the 46 | following names: \itemize{ \item \code{departures} \item \code{stops} \item \code{routes} 47 | \item \code{runs} \item \code{directions} \item \code{disruptions} } 48 | } 49 | \description{ 50 | A pattern consists of all departures, stops, routes, runs, directions and 51 | disruptions associated with a particular run ID. This is returned as a list 52 | of tibbles, with output corresponding to their respective API calls. 53 | } 54 | \details{ 55 | The \code{stops} tibble has an output similar to that returned by 56 | \code{\link{stops_on_route}}. The \code{routes} tibble does not contain service 57 | status information. 58 | 59 | Departures: The API seems to return the earliest 7 departures. While 60 | the PTV Timetable API supports filtering patterns by datetimes, the 61 | behaviour of this argument is not reliable --- it appears to filter by day 62 | only, returning the earliest 7 departures of a different day. It is 63 | recommended that departures are retrieved via the \code{\link{departures}} 64 | function. 65 | } 66 | \examples{ 67 | \dontrun{ 68 | patterns(run_ref = "1", route_type = 0) 69 | patterns(run_ref = "1", route_type = "Train") 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tests/testthat/test-runs.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | # ---------------------------------------------------------------------------- # 3 | # ---- Define values if they haven't already been defined by another test ---- # 4 | # ---------------------------------------------------------------------------- # 5 | if (!exists("all_routes")) { 6 | all_routes <- routes() 7 | } 8 | 9 | if (!exists("frankston_route_id")) { 10 | frankston_route_id <- all_routes %>% 11 | dplyr::filter(route_name == "Frankston") %>% 12 | pull(route_id) 13 | } 14 | # ---------------------------------------------------------------------------- # 15 | 16 | # We need to be careful how we design this test. If there are planned works and 17 | # the trains aren't running, then we can expect that there will be no runs on 18 | # the Frankston line. In this case, the test should trivially pass. The 19 | # assertions within the `runs_on_route` function will still be run, and these 20 | # act as tests of sorts. 21 | 22 | runs_on_frankston_route <- runs_on_route( 23 | route_id = frankston_route_id, 24 | route_type = "Train" 25 | ) 26 | 27 | test_that("runs_on_route result has class \"ptvapi\"", { 28 | expect_s3_class(runs_on_frankston_route, "ptvapi") 29 | }) 30 | 31 | test_that("Runs on the Frankston route end at stops on the Frankston route", { 32 | frankston_runs_destinations <- runs_on_frankston_route %>% 33 | pull(destination_name) %>% 34 | unique 35 | frankston_route_train_stops <- stops_on_route( 36 | route_id = frankston_route_id, 37 | route_type = "Train" 38 | ) %>% 39 | pull(stop_name) 40 | for (suburb in frankston_runs_destinations) { 41 | expect_true(any(grepl(!!suburb, frankston_route_train_stops))) 42 | } 43 | }) 44 | 45 | run_one <- run_information(run_ref = "1") 46 | 47 | test_that("run_information() result has class \"ptvapi\"", { 48 | expect_s3_class(run_one, "ptvapi") 49 | }) 50 | 51 | test_that("Run 1 exists, and is unique up to route type", { 52 | expect_gte(nrow(run_one), 1) 53 | expect_equal(anyDuplicated(run_one$route_type), 0) 54 | }) 55 | 56 | run_one_train <- run_information(run_ref = "1", route_type = 0) 57 | 58 | test_that("run_information() with route type result has class \"ptvapi\"", { 59 | expect_s3_class(run_one_train, "ptvapi") 60 | }) 61 | 62 | test_that("run_information() with route type returns exactly one row", { 63 | expect_equal(nrow(run_one_train), 1) 64 | }) 65 | 66 | test_that("run_information can return geopath data", { 67 | run_one_with_geo <- run_information("1", include_geopath = TRUE) 68 | geopaths <- run_one_with_geo$geopath[[1]] 69 | expect_gte(nrow(geopaths), 1) 70 | expect_identical( 71 | names(geopaths), 72 | c("direction_id", "valid_from", "valid_to", "paths") 73 | ) 74 | 75 | run_one_train_with_geo <- run_information( 76 | "1", 77 | route_type = "Train", 78 | include_geopath = TRUE 79 | ) 80 | geopaths <- run_one_train_with_geo$geopath[[1]] 81 | expect_gte(nrow(geopaths), 1) 82 | expect_identical( 83 | names(geopaths), 84 | c("direction_id", "valid_from", "valid_to", "paths") 85 | ) 86 | }) 87 | 88 | } 89 | -------------------------------------------------------------------------------- /man/run_information.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/runs.R 3 | \name{run_information} 4 | \alias{run_information} 5 | \title{Information for a given run} 6 | \usage{ 7 | run_information( 8 | run_ref, 9 | route_type = NULL, 10 | include_geopath = FALSE, 11 | geopath_utc = NULL, 12 | date_utc = NULL, 13 | user_id = determine_user_id(), 14 | api_key = determine_api_key() 15 | ) 16 | } 17 | \arguments{ 18 | \item{run_ref}{A character run reference. This supersedes the integer 19 | \code{run_id}. For backwards compatibility and since most run references are 20 | integers, this function will attempt to convert an the argument to a 21 | character. Run references may be retrieved from the 22 | \code{\link{departures}} or \code{\link{runs_on_route}} functions.} 23 | 24 | \item{route_type}{Optionally filter results by a route type. A route type can 25 | be provided either as a non-negative integer code, or as a character: 26 | "Tram", "Train", "Bus", "Vline" or "Night Bus". Character inputs are not 27 | case-sensitive. Use the \code{\link{route_types}} function to extract a 28 | vector of all route types.} 29 | 30 | \item{include_geopath}{Logical. Whether to populate the \code{geopath} column. 31 | Defaults to FALSE.} 32 | 33 | \item{geopath_utc}{Date, or character that can be converted to a date. The 34 | UTC date for which the geopaths are effective. Defaults to the current 35 | date. Has no effect if \code{include_geopath = FALSE}. It's uncertain how much 36 | historical or future-dated data is available.} 37 | 38 | \item{date_utc}{Date, or character that can be converted to a date. The 39 | UTC date for which the results are effective. Defaults to the current date. 40 | It's uncertain how much historical or future-dated data is available. This 41 | argument is experimental and seems to not be functioning.} 42 | 43 | \item{user_id}{Integer or character. A user ID or devid provided by Public 44 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 45 | 46 | \item{api_key}{Character. An API key, with dashes, provided by Public 47 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 48 | } 49 | \value{ 50 | A tibble with the following columns: \itemize{ 51 | \item \code{run_id} (deprecated, use \code{run_ref} instead) 52 | \item \code{run_ref} 53 | \item \code{route_id} 54 | \item \code{route_type} 55 | \item \code{route_type_description} 56 | \item \code{direction_id} 57 | \item \code{run_sequence} 58 | \item \code{final_stop_id} 59 | \item \code{destination_name} 60 | \item \code{status} 61 | \item \code{express_stop_count} 62 | \item \code{vehicle_position} 63 | \item \code{vehicle_descriptor} 64 | \item \code{geopath} 65 | } 66 | } 67 | \description{ 68 | Run IDs are not unique across the network. If you are interested in a 69 | specific run, consider supplying a value to the optional \code{route_type} 70 | argument. 71 | } 72 | \examples{ 73 | \dontrun{ 74 | run_information("100") 75 | run_information("100", include_geopath = TRUE) 76 | run_information("100", include_geopath = TRUE, geopath_utc = "2020-07-01") 77 | run_information("100", date_utc = "2020-07-01") 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # ptvapi 2.0.5 2 | 3 | * Corrected argument names in documentation for some internal functions. 4 | 5 | # ptvapi 2.0.4 6 | 7 | * Documented package with "_PACKAGE" sentinel to meet new CRAN requirements. 8 | 9 | # ptvapi 2.0.3 10 | 11 | * Fixed 301 link in README 12 | 13 | # ptvapi 2.0.2 14 | 15 | * Remove HTML tags in logo unsupported in HTML5 to meet new CRAN policy. 16 | 17 | # ptvapi 2.0.1 18 | 19 | * Remove `lazyload` in DESCRIPTION to meet new CRAN policy (this package has no data) 20 | 21 | # ptvapi 2.0.0 22 | 23 | * All API request now use SSL by default. To force connections without SSL, set the option `use_insecure_ptv_connection` to `TRUE`. 24 | * In response to changes to the PTV API, `run_information` now requires a character `run_ref` instead of an integer `run_id`. Functions will attempt to convert the argument to a character, and so integer arguments will still work for runs that are not bus or nightrider runs. 25 | * Functions that return `route_type` will now also return `route_type_description`, which is a human-readable interpretation of the route type. For example, when `route_type` is 0, `route_type_description` is "Train". 26 | * In response to changes to the PTV API, run functions now support a `date_utc` argument for returning results as of a given date. This is experimental, and possibly non-functional. 27 | * Functions that internally convert between a route type and a human-readable description of a route type will use a cached version of `route_types`, in order to reduce the number of queries of the API. 28 | * Fixed failing assertion that was causing `run_information` to fail when specifying a route type. 29 | * The column `service_status_timestampe` in data returned by route functions has been changed to `service_status_timestamp` . 30 | 31 | # ptvapi 1.1.3 32 | 33 | * Changed unit tests to use lubridate instead of base for time zone declarations. The lubridate package better supports adding time zones to NA datetimes, as this was causing issues with R-devel (4.0.3). 34 | 35 | # ptvapi 1.1.2 36 | 37 | This release was the first to be accepted to CRAN. 38 | 39 | * Fixed misquoted metadata in package DESCRIPTION 40 | 41 | # ptvapi 1.1.1 42 | 43 | * Fixed invalid URL in package DESCRIPTION 44 | 45 | # ptvapi 1.1.0 46 | 47 | * Adjustments to package DESCRIPTION to meet CRAN standards 48 | * Removed internal function need_api_details, as it depended on changing the user's options. This function has been incorporated into the error messages in `determine_user_id` and `determine_api_key`. 49 | * Removed documentation for the following internal functions, which should resolve the issue of missing argument values in the respective .Rd files: `determine_user_id`, `determine_api_key`, `prefix_base_url`, `prefix_version`, `prefix_base_url_and_version` 50 | * Created `value` Rd-tag for `departures` function 51 | * Removed examples for the following internal functions: `convert_to_melbourne_time`, `PTVGET` 52 | * Added `ptvapi:::` to examples in the following internal functions: `add_parameter`, `add_parameters` 53 | * Removed internal function `route_types_cached`, and replaced all uses with `route_types`. With these changes, there are no references to user options remaining in the package. 54 | -------------------------------------------------------------------------------- /R/ptv-verbs.R: -------------------------------------------------------------------------------- 1 | # nolint start 2 | # Currently, only the GET verb is used. 3 | 4 | #' Submit a GET request to the PTV API 5 | #' 6 | #' @section Obtaining API authentication details: 7 | #' 8 | #' You will need to obtain a user ID (also called a devid) and an API key from 9 | #' Public Transport Victoria. These are obtained by email. Instructions are 10 | #' available at \url{https://www.ptv.vic.gov.au/footer/data-and-reporting/datasets/ptv-timetable-api/}. 11 | #' You may pass these two pieces of information directly to the function, or you 12 | #' can set the PTV_USER_ID and PTV_API_KEY environment variables. 13 | #' 14 | #' @param request A request or path for the API, eg. "routes". 15 | #' @param user_id Integer or character. A user ID or devid provided by Public 16 | #' Transport Victoria. Refer to `?ptvapi` for more details. 17 | #' @param api_key Character. An API key, with dashes, provided by Public 18 | #' Transport Victoria. Refer to `?ptvapi` for more details. 19 | #' @param ... Additional arguments passed to `httr::GET`. 20 | #' 21 | #' @return A HTTP response. Content can be accessed with `httr::content`. 22 | #' 23 | #' @keywords internal 24 | #' 25 | PTVGET <- function(request, 26 | user_id = determine_user_id(), 27 | api_key = determine_api_key(), 28 | ...) { 29 | request_url <- generate_request_url( 30 | request = request, 31 | user_id = user_id, 32 | api_key = api_key 33 | ) 34 | request_url_without_auth <- prefix_base_url_and_version(request) 35 | response <- httr::GET(url = request_url, ...) 36 | process_response(response, request_url_without_auth) 37 | } 38 | # nolint end 39 | 40 | #' Process a raw httr response and return an object of class ptv_api 41 | #' 42 | #' This S3 object returned by this function contains three elements: 43 | #' \itemize{ 44 | #' \item status_code, as an integer 45 | #' \item request, which is the request URL without authentication details 46 | #' \item content, the unparsed body of the response 47 | #' } 48 | #' 49 | #' @param response A raw response generated by the `httr` package 50 | #' @param request_url_without_auth Character. The request `url`, without `devid` 51 | #' and signature 52 | #' 53 | #' @return An S3 object of class ptv_api 54 | #' 55 | #' @keywords internal 56 | #' 57 | process_response <- function(response, request_url_without_auth) { 58 | 59 | status_code <- httr::status_code(response) 60 | 61 | if (status_code == 404) { 62 | stop("URL not found: ", request_url_without_auth) 63 | } 64 | 65 | content <- jsonlite::fromJSON( 66 | httr::content(response, "text", encoding = "UTF-8"), 67 | simplifyVector = FALSE 68 | ) 69 | 70 | if (status_code == 400) { 71 | stop("Invalid request: ", request_url_without_auth, " - ", content$message) 72 | } 73 | if (status_code == 403) { 74 | stop("Access denied.") 75 | } 76 | if (status_code != 200) { 77 | stop("Status code ", status_code) 78 | } 79 | 80 | list( 81 | request = request_url_without_auth, 82 | retrieved = format( 83 | Sys.time(), 84 | format = "%Y-%m-%d %H:%M:%OS %Z", 85 | tz = "Australia/Melbourne" 86 | ), 87 | status_code = status_code, 88 | content = content 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /tests/testthat/test-disruptions.R: -------------------------------------------------------------------------------- 1 | # Testing for disruptions is hard because there may be none! We rely on skipping 2 | # tests here if possible, but it should be rare to have no disruptions across 3 | # the network, even if we require a variety of disruption modes. 4 | 5 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 6 | # ---------------------------------------------------------------------------- # 7 | # ---- Define values if they haven't already been defined by another test ---- # 8 | # ---------------------------------------------------------------------------- # 9 | if (!exists("stops_near_flinders_street")) { 10 | stops_near_flinders_street <- stops_nearby( 11 | latitude = -37.8183, 12 | longitude = 144.9671 13 | ) 14 | } 15 | 16 | if (!exists("flinders_street_stop_id")) { 17 | flinders_street_stop_id <- stops_near_flinders_street %>% 18 | filter(stop_name == "Flinders Street Railway Station") %>% 19 | pull(stop_id) 20 | } 21 | # ---------------------------------------------------------------------------- # 22 | 23 | all_disruptions <- disruptions() 24 | disruptions_exist <- (nrow(all_disruptions) > 0) 25 | 26 | 27 | test_that("Can filter disruptions to current or planned", { 28 | skip_if(nrow(all_disruptions) == 0) 29 | skip_if_not("Current" %in% all_disruptions$disruption_status) 30 | skip_if_not("Planned" %in% all_disruptions$disruption_status) 31 | current_disruptions <- disruptions(disruption_status = "current") 32 | planned_disruptions <- disruptions(disruption_status = "planned") 33 | expect_gt(nrow(current_disruptions), 0) 34 | expect_equal(unique(current_disruptions$disruption_status), "Current") 35 | expect_gt(nrow(planned_disruptions), 0) 36 | expect_equal(unique(planned_disruptions$disruption_status), "Planned") 37 | }) 38 | 39 | test_that("Can filter by disruption modes", { 40 | skip_if(nrow(all_disruptions) == 0) 41 | dis_modes <- sort(unique(all_disruptions$disruption_mode)) 42 | 43 | skip_if(length(dis_modes) < 2) 44 | a_dis_mode <- sample(dis_modes, 1) 45 | disruptions_with_a_dis_mode <- disruptions(disruption_modes = a_dis_mode) 46 | expect_equal( 47 | unique(disruptions_with_a_dis_mode$disruption_mode), 48 | a_dis_mode 49 | ) 50 | 51 | skip_if(length(dis_modes) < 3) 52 | two_dis_modes <- sort(sample(dis_modes, 2)) 53 | disruptions_with_two_dis_modes <- disruptions( 54 | disruption_modes = two_dis_modes 55 | ) 56 | expect_equal( 57 | sort(unique(disruptions_with_two_dis_modes$disruption_mode)), 58 | two_dis_modes 59 | ) 60 | }) 61 | 62 | test_that("Can filter by route_types", { 63 | # Route types seem to correspond to disruption modes, not the contents of the 64 | # routes tibbles in the disruptions. If at least 3 disruption modes are 65 | # present, we test for the ability to filter to 2, using route_type. 66 | skip_if(nrow(all_disruptions) == 0) 67 | skip_if_not("metro_train" %in% all_disruptions$disruption_mode_description) 68 | skip_if_not("metro_tram" %in% all_disruptions$disruption_mode_description) 69 | skip_if_not("metro_bus" %in% all_disruptions$disruption_mode_description) 70 | 71 | train_route_type <- translate_route_type("Train") 72 | tram_route_type <- translate_route_type("Tram") 73 | two_route_types <- c(train_route_type, tram_route_type) 74 | disruptions_2_route_types <- disruptions( 75 | route_types = two_route_types 76 | ) 77 | expect_lt( 78 | nrow(disruptions_2_route_types), 79 | nrow(all_disruptions) 80 | ) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /R/patterns.R: -------------------------------------------------------------------------------- 1 | # This function ostensibly supports a date_utc function. It appears to change 2 | # the day on which departures are returned. However, only the earliest 7 3 | # departures are shown for the day corresponding to date_utc. 4 | 5 | #' Stopping pattern for a given run 6 | #' 7 | #' A pattern consists of all departures, stops, routes, runs, directions and 8 | #' disruptions associated with a particular run ID. This is returned as a list 9 | #' of tibbles, with output corresponding to their respective API calls. 10 | #' 11 | #' The `stops` tibble has an output similar to that returned by 12 | #' \code{\link{stops_on_route}}. The `routes` tibble does not contain service 13 | #' status information. 14 | #' 15 | #' @details Departures: The API seems to return the earliest 7 departures. While 16 | #' the PTV Timetable API supports filtering patterns by datetimes, the 17 | #' behaviour of this argument is not reliable --- it appears to filter by day 18 | #' only, returning the earliest 7 departures of a different day. It is 19 | #' recommended that departures are retrieved via the \code{\link{departures}} 20 | #' function. 21 | #' 22 | #' @inheritParams run_information 23 | #' @inheritParams translate_route_type 24 | #' @inheritParams disruptions_on_route 25 | #' @param departs POSIXct or character. Optionally filter by date. See Details. 26 | #' Characters are automatically converted to departs, and are assumed to be 27 | #' given as Melbourne time. The behaviour of the API is unpredictable when 28 | #' using this argument --- see details. Defaults to the current system time. 29 | #' @inheritParams PTVGET 30 | #' 31 | #' @return An object of class "ptvapi", which is effectively a list with the 32 | #' following names: \itemize{ \item `departures` \item `stops` \item `routes` 33 | #' \item `runs` \item `directions` \item `disruptions` } 34 | #' 35 | #' @export 36 | #' 37 | #' @examples \dontrun{ 38 | #' patterns(run_ref = "1", route_type = 0) 39 | #' patterns(run_ref = "1", route_type = "Train") 40 | #' } 41 | #' 42 | patterns <- function(run_ref, 43 | route_type, 44 | stop_id = NULL, 45 | departs = Sys.time(), 46 | user_id = determine_user_id(), 47 | api_key = determine_api_key()) { 48 | run_ref <- as.character(run_ref) 49 | route_type <- translate_route_type( 50 | route_type, 51 | user_id = user_id, 52 | api_key = api_key 53 | ) 54 | if (!is.null(stop_id)) stop_id <- to_integer(stop_id) 55 | departs <- to_datetime(departs) 56 | url_departs <- format(departs, format = "%Y-%m-%dT%H:%M:%OS", tz = "UTC") 57 | 58 | request <- add_parameters( 59 | glue::glue("pattern/run/{run_ref}/route_type/{route_type}"), 60 | expand = "all", 61 | stop_id = stop_id, 62 | date_utc = url_departs 63 | ) 64 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 65 | content <- response$content 66 | assert_correct_attributes( 67 | names(content), 68 | c("disruptions", "departures", "stops", "routes", "runs", "directions", 69 | "status") 70 | ) 71 | 72 | structure( 73 | list( 74 | departures = map_and_rbind(content$departures, departure_to_tibble), 75 | stops = map_and_rbind(content$stops, stop_to_tibble), 76 | routes = map_and_rbind(content$routes, route_to_tibble), 77 | runs = map_and_rbind(content$runs, run_to_tibble), 78 | directions = map_and_rbind(content$directions, tibble::as_tibble), 79 | disruptions = list(content$disruptions) 80 | ), 81 | class = "ptvapi", 82 | request = response$request, 83 | status_code = response$status_code, 84 | content = response$content 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /docs/pkgdown.js: -------------------------------------------------------------------------------- 1 | /* http://gregfranko.com/blog/jquery-best-practices/ */ 2 | (function($) { 3 | $(function() { 4 | 5 | $('.navbar-fixed-top').headroom(); 6 | 7 | $('body').css('padding-top', $('.navbar').height() + 10); 8 | $(window).resize(function(){ 9 | $('body').css('padding-top', $('.navbar').height() + 10); 10 | }); 11 | 12 | $('[data-toggle="tooltip"]').tooltip(); 13 | 14 | var cur_path = paths(location.pathname); 15 | var links = $("#navbar ul li a"); 16 | var max_length = -1; 17 | var pos = -1; 18 | for (var i = 0; i < links.length; i++) { 19 | if (links[i].getAttribute("href") === "#") 20 | continue; 21 | // Ignore external links 22 | if (links[i].host !== location.host) 23 | continue; 24 | 25 | var nav_path = paths(links[i].pathname); 26 | 27 | var length = prefix_length(nav_path, cur_path); 28 | if (length > max_length) { 29 | max_length = length; 30 | pos = i; 31 | } 32 | } 33 | 34 | // Add class to parent
  • , and enclosing
  • if in dropdown 35 | if (pos >= 0) { 36 | var menu_anchor = $(links[pos]); 37 | menu_anchor.parent().addClass("active"); 38 | menu_anchor.closest("li.dropdown").addClass("active"); 39 | } 40 | }); 41 | 42 | function paths(pathname) { 43 | var pieces = pathname.split("/"); 44 | pieces.shift(); // always starts with / 45 | 46 | var end = pieces[pieces.length - 1]; 47 | if (end === "index.html" || end === "") 48 | pieces.pop(); 49 | return(pieces); 50 | } 51 | 52 | // Returns -1 if not found 53 | function prefix_length(needle, haystack) { 54 | if (needle.length > haystack.length) 55 | return(-1); 56 | 57 | // Special case for length-0 haystack, since for loop won't run 58 | if (haystack.length === 0) { 59 | return(needle.length === 0 ? 0 : -1); 60 | } 61 | 62 | for (var i = 0; i < haystack.length; i++) { 63 | if (needle[i] != haystack[i]) 64 | return(i); 65 | } 66 | 67 | return(haystack.length); 68 | } 69 | 70 | /* Clipboard --------------------------*/ 71 | 72 | function changeTooltipMessage(element, msg) { 73 | var tooltipOriginalTitle=element.getAttribute('data-original-title'); 74 | element.setAttribute('data-original-title', msg); 75 | $(element).tooltip('show'); 76 | element.setAttribute('data-original-title', tooltipOriginalTitle); 77 | } 78 | 79 | if(ClipboardJS.isSupported()) { 80 | $(document).ready(function() { 81 | var copyButton = ""; 82 | 83 | $(".examples, div.sourceCode").addClass("hasCopyButton"); 84 | 85 | // Insert copy buttons: 86 | $(copyButton).prependTo(".hasCopyButton"); 87 | 88 | // Initialize tooltips: 89 | $('.btn-copy-ex').tooltip({container: 'body'}); 90 | 91 | // Initialize clipboard: 92 | var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', { 93 | text: function(trigger) { 94 | return trigger.parentNode.textContent; 95 | } 96 | }); 97 | 98 | clipboardBtnCopies.on('success', function(e) { 99 | changeTooltipMessage(e.trigger, 'Copied!'); 100 | e.clearSelection(); 101 | }); 102 | 103 | clipboardBtnCopies.on('error', function() { 104 | changeTooltipMessage(e.trigger,'Press Ctrl+C or Command+C to copy'); 105 | }); 106 | }); 107 | } 108 | })(window.jQuery || window.$) 109 | -------------------------------------------------------------------------------- /tests/testthat/test-filter-departures.R: -------------------------------------------------------------------------------- 1 | # Note that in all of these test cases we order the departures by departure 2 | # time: by estimated_departure (if defined), otherwise by scheduled departure. 3 | # 4 | # In the event of there being a tie, ie. the next 3 departures for a route are 5 | # all at the same time and we have max_results = 2, then the tie is arbitrarily 6 | # broken. This shouldn't be an issue because it would be highly unlikely that 7 | # there are two services of the same route departing at the same time from the 8 | # same stop. Maybe if they have different direction IDs? 9 | 10 | test_departures <- tibble::tribble( 11 | ~route_id, ~scheduled_departure, ~estimated_departure, 12 | 1, "2020-06-01 12:04:00", NA, 13 | 1, "2020-06-01 12:03:00", NA, 14 | 1, "2020-06-01 12:02:00", NA, 15 | 1, "2020-06-01 12:01:00", NA, 16 | 2, "2020-06-01 12:04:00", NA, 17 | 2, "2020-06-01 12:03:00", NA, 18 | 2, "2020-06-01 12:02:00", "2020-06-01 12:05:00", 19 | 2, "2020-06-01 12:01:00", NA, 20 | 3, "2020-06-01 12:01:00", NA, 21 | 3, "2020-06-01 12:01:00", NA, 22 | 3, "2020-06-01 12:01:00", NA, 23 | 3, "2020-06-01 12:02:00", NA 24 | ) %>% 25 | mutate_at( 26 | vars(contains("departure")), 27 | ~force_tz(as_datetime(.), tzone = "Australia/Melbourne") 28 | ) 29 | 30 | test_that("Can filter departures with max_results", { 31 | expect_equal( 32 | filter_departures(test_departures, max_results = 2), 33 | tibble::tribble( 34 | ~route_id, ~scheduled_departure, ~estimated_departure, 35 | 1, "2020-06-01 12:01:00", NA, 36 | 2, "2020-06-01 12:01:00", NA, 37 | 3, "2020-06-01 12:01:00", NA, 38 | 3, "2020-06-01 12:01:00", NA, 39 | 1, "2020-06-01 12:02:00", NA, 40 | 2, "2020-06-01 12:03:00", NA 41 | ) %>% 42 | mutate_at( 43 | vars(contains("departure")), 44 | ~force_tz(as_datetime(.), tzone = "Australia/Melbourne") 45 | ) 46 | ) 47 | }) 48 | 49 | test_that("Can filter departures with datetime", { 50 | expect_equal( 51 | filter_departures( 52 | test_departures, 53 | departs = as.POSIXct("2020-06-01 12:03:00", tz = "Australia/Melbourne") 54 | ), 55 | tibble::tribble( 56 | ~route_id, ~scheduled_departure, ~estimated_departure, 57 | 1, "2020-06-01 12:03:00", NA, 58 | 2, "2020-06-01 12:03:00", NA, 59 | 1, "2020-06-01 12:04:00", NA, 60 | 2, "2020-06-01 12:04:00", NA, 61 | 2, "2020-06-01 12:02:00", "2020-06-01 12:05:00" 62 | ) %>% 63 | mutate_at( 64 | vars(contains("departure")), 65 | ~force_tz(as_datetime(.), tzone = "Australia/Melbourne") 66 | ) 67 | ) 68 | }) 69 | 70 | test_that("Can filter departures with route_id", { 71 | expect_equal( 72 | filter_departures( 73 | test_departures, 74 | route_id = 2 75 | ), 76 | tibble::tribble( 77 | ~route_id, ~scheduled_departure, ~estimated_departure, 78 | 2, "2020-06-01 12:01:00", NA, 79 | 2, "2020-06-01 12:03:00", NA, 80 | 2, "2020-06-01 12:04:00", NA, 81 | 2, "2020-06-01 12:02:00", "2020-06-01 12:05:00" 82 | ) %>% 83 | mutate_at( 84 | vars(contains("departure")), 85 | ~force_tz(as_datetime(.), tzone = "Australia/Melbourne") 86 | ) 87 | ) 88 | }) 89 | 90 | test_that("Can filter departures with datetime and max_results", { 91 | expect_equal( 92 | filter_departures( 93 | test_departures, 94 | departs = as.POSIXct("2020-06-01 12:03:00", tz = "Australia/Melbourne"), 95 | max_results = 2 96 | ), 97 | tibble::tribble( 98 | ~route_id, ~scheduled_departure, ~estimated_departure, 99 | 1, "2020-06-01 12:03:00", NA, 100 | 2, "2020-06-01 12:03:00", NA, 101 | 1, "2020-06-01 12:04:00", NA, 102 | 2, "2020-06-01 12:04:00", NA, 103 | ) %>% 104 | mutate_at( 105 | vars(contains("departure")), 106 | ~force_tz(as_datetime(.), tzone = "Australia/Melbourne") 107 | ) 108 | ) 109 | }) 110 | -------------------------------------------------------------------------------- /R/directions.R: -------------------------------------------------------------------------------- 1 | #' Directions for a given direction ID 2 | #' 3 | #' This function returns all directions with a given ID. Directions that share 4 | #' an ID are not necessarily related, especially if not filtering by route type. 5 | #' It's advised to use to the \code{\link{directions_on_route}} function to 6 | #' search for directions of interest. 7 | #' 8 | #' @param direction_id Integer. 9 | #' @param route_type Optionally filter results by a route type. A route type can 10 | #' be provided either as a non-negative integer code, or as a character: 11 | #' "Tram", "Train", "Bus", "Vline" or "Night Bus". Character inputs are not 12 | #' case-sensitive. Use the \code{\link{route_types}} function to extract a 13 | #' vector of all route types. 14 | #' @inheritParams PTVGET 15 | #' 16 | #' @inherit parse_directions_content return 17 | #' @export 18 | #' 19 | #' @examples \dontrun{ 20 | #' directions(direction_id = 5) 21 | #' directions(direction_id = 5, route_type = "Train") 22 | #' directions(direction_id = 5, route_type = 0) 23 | #' } 24 | #' 25 | directions <- function(direction_id, 26 | route_type = NULL, 27 | user_id = determine_user_id(), 28 | api_key = determine_api_key()) { 29 | direction_id <- to_integer(direction_id) 30 | request <- glue::glue("directions/{direction_id}") 31 | if (!is.null(route_type)) { 32 | route_type <- translate_route_type( 33 | route_type, 34 | user_id = user_id, 35 | api_key = api_key 36 | ) 37 | request <- glue::glue(request, "/route_type/{route_type}") 38 | } 39 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 40 | content <- response$content 41 | parsed <- parse_directions_content(content) 42 | parsed$route_type_description <- purrr::map_chr( 43 | parsed$route_type, 44 | describe_route_type, 45 | user_id = user_id, 46 | api_key = api_key 47 | ) 48 | new_ptvapi_tibble(response, parsed) 49 | } 50 | 51 | #' Directions on a given route 52 | #' 53 | #' @param route_id Integer. These can be listed and described with the 54 | #' \code{\link{routes}} function. 55 | #' @inheritParams PTVGET 56 | #' 57 | #' @inherit parse_directions_content return 58 | #' 59 | #' @export 60 | #' 61 | #' @examples \dontrun{ 62 | #' directions_on_route(6) 63 | #' } 64 | directions_on_route <- function(route_id, 65 | user_id = determine_user_id(), 66 | api_key = determine_api_key()) { 67 | route_id <- to_integer(route_id) 68 | request <- glue::glue("directions/route/{route_id}") 69 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 70 | content <- response$content 71 | parsed <- parse_directions_content(content) 72 | parsed$route_type_description <- purrr::map_chr( 73 | parsed$route_type, 74 | describe_route_type, 75 | user_id = user_id, 76 | api_key = api_key 77 | ) 78 | new_ptvapi_tibble(response, parsed) 79 | } 80 | 81 | #' Parse content of directions API call 82 | #' 83 | #' This function is designed to parse the content returned by the interior 84 | #' steps of the \code{\link{directions}} function. 85 | #' 86 | #' @param directions_content A direction, as a list, returned by the 87 | #' `directions` API call. 88 | #' 89 | #' @return A tibble consisting of the following columns: \itemize{ 90 | #' \item `direction_id` 91 | #' \item `direction_name`, 92 | #' \item `route_id` 93 | #' \item `route_type` 94 | #' \item `route_type_description` 95 | #' \item `route_direction_description` 96 | #' } 97 | #' 98 | #' @keywords internal 99 | #' 100 | parse_directions_content <- function(directions_content) { 101 | assert_correct_attributes( 102 | names(directions_content), 103 | c("directions", "status") 104 | ) 105 | parsed <- map_and_rbind(directions_content$directions, tibble::as_tibble) 106 | 107 | assert_correct_attributes( 108 | colnames(parsed), 109 | c("route_direction_description", "direction_id", "direction_name", 110 | "route_id", "route_type") 111 | ) 112 | 113 | parsed$route_type_description <- NA_character_ 114 | 115 | parsed[c("direction_id", "direction_name", "route_id", "route_type", 116 | "route_type_description", "route_direction_description")] 117 | } 118 | -------------------------------------------------------------------------------- /tests/testthat/test-departures.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | # ---------------------------------------------------------------------------- # 3 | # ---- Define values if they haven't already been defined by another test ---- # 4 | # ---------------------------------------------------------------------------- # 5 | if (!exists("all_routes")) { 6 | all_routes <- routes() 7 | } 8 | 9 | if (!exists("frankston_route_id")) { 10 | frankston_route_id <- all_routes %>% 11 | dplyr::filter(route_name == "Frankston") %>% 12 | pull(route_id) 13 | } 14 | 15 | if (!exists("stops_near_flinders_street")) { 16 | stops_near_flinders_street <- stops_nearby( 17 | latitude = -37.8183, 18 | longitude = 144.9671 19 | ) 20 | } 21 | 22 | if (!exists("flinders_street_stop_id")) { 23 | flinders_street_stop_id <- stops_near_flinders_street %>% 24 | filter(stop_name == "Flinders Street Railway Station") %>% 25 | pull(stop_id) 26 | } 27 | # ---------------------------------------------------------------------------- # 28 | 29 | flinders_departures <- departures( 30 | stop_id = flinders_street_stop_id, 31 | route_type = "Train" 32 | ) 33 | 34 | test_that("departures result has class \"ptvapi\"", { 35 | expect_s3_class(flinders_departures, "ptvapi") 36 | }) 37 | 38 | flinders_street_stop_id <- stops_nearby( 39 | latitude = -37.8183, 40 | longitude = 144.9671 41 | ) %>% 42 | filter(stop_name == "Flinders Street Railway Station") %>% 43 | pull(stop_id) 44 | 45 | test_that("Frankston train departs from Flinders Street Station", 46 | expect_true(frankston_route_id %in% flinders_departures$route_id) 47 | ) 48 | 49 | test_that("Departures filtered by platform_number", { 50 | platform_5_6_departures <- departures( 51 | stop_id = flinders_street_stop_id, 52 | route_type = "Train", 53 | platform_numbers = c("5", "6"), # despite the name, they are characters 54 | ) 55 | expect_gt(nrow(platform_5_6_departures), 0) # must have some results 56 | expect_true( 57 | # disruptions may mean that platform 5 or 6 not active 58 | all(platform_5_6_departures$platform_number %in% c("5", "6")) 59 | ) 60 | }) 61 | 62 | test_that("Departures filtered by datetime", { 63 | flinders_morning_departures <- departures( 64 | stop_id = flinders_street_stop_id, 65 | route_type = "Train", 66 | departs = morning_test_time 67 | ) 68 | expect_gt(nrow(flinders_morning_departures), 0) # must have some results 69 | expect_gte( 70 | min(flinders_morning_departures$scheduled_departure), 71 | as.POSIXct(morning_test_time, tz = "Australia/Melbourne") 72 | ) 73 | }) 74 | 75 | test_that("Departures filtered by datetime with max_results = 0", { 76 | flinders_all_day_departures <- departures( 77 | stop_id = flinders_street_stop_id, 78 | route_type = "Train", 79 | departs = afternoon_test_time, 80 | max_results = 0 81 | ) 82 | expect_gt(nrow(flinders_all_day_departures), 0) # must have some results 83 | 84 | # When max_results = 0, all departures after the given datetime for the entire 85 | # day are returned. Because of how a "day" is considered in public transport, 86 | # this may include some departures in the early hours of the next morning. 87 | expect_gte( 88 | min(flinders_all_day_departures$scheduled_departure), 89 | as.POSIXct(afternoon_test_time, tz = "Australia/Melbourne") 90 | ) 91 | departure_dates <- as.Date( 92 | flinders_all_day_departures$scheduled_departure, 93 | tz = "Australia/Melbourne" 94 | ) 95 | expect_true( 96 | all(departure_dates %in% c( 97 | as.Date(afternoon_test_time, tz = "Australia/Melbourne"), 98 | as.Date(afternoon_test_time, tz = "Australia/Melbourne") + 1) 99 | ) 100 | ) 101 | }) 102 | 103 | test_that("Departures filtered by datetime and max_results", { 104 | flinders_afternoon_departures <- departures( 105 | stop_id = flinders_street_stop_id, 106 | route_type = "Train", 107 | departs = afternoon_test_time, 108 | max_results = 3 109 | ) 110 | expect_gt(nrow(flinders_afternoon_departures), 0) # must have some results 111 | expect_gte( 112 | min(flinders_afternoon_departures$scheduled_departure), 113 | as.POSIXct(afternoon_test_time, tz = "Australia/Melbourne") 114 | ) 115 | departures_per_route_id <- count(flinders_afternoon_departures, route_id) 116 | expect_true(all(departures_per_route_id$n == 3)) 117 | }) 118 | 119 | test_that("Departures filtered by route_id", { 120 | flinders_route_6_departures <- departures( 121 | stop_id = flinders_street_stop_id, 122 | route_type = "Train", 123 | route_id = 6 124 | ) 125 | expect_equal( 126 | distinct(flinders_route_6_departures, route_id)$route_id, 127 | 6 128 | ) 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /man/departures.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/departures.R 3 | \name{departures} 4 | \alias{departures} 5 | \title{Departures from a given stop} 6 | \usage{ 7 | departures( 8 | stop_id, 9 | route_type, 10 | route_id = NULL, 11 | direction_id = NULL, 12 | platform_numbers = NULL, 13 | departs = Sys.time(), 14 | look_backwards = FALSE, 15 | max_results = 5, 16 | include_cancelled = FALSE, 17 | validate_results = TRUE, 18 | user_id = determine_user_id(), 19 | api_key = determine_api_key() 20 | ) 21 | } 22 | \arguments{ 23 | \item{stop_id}{An integer stop ID returned by the \code{stops_on_route} or 24 | \code{stops_nearby} functions.} 25 | 26 | \item{route_type}{A route type which can be provided either as a non-negative 27 | integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 28 | Bus". Character inputs are not case-sensitive. Use the 29 | \code{\link{route_types}} function to extract a vector of all route types.} 30 | 31 | \item{route_id}{Optionally filter by a route ID. These can be obtained with 32 | the \code{routes} function.} 33 | 34 | \item{direction_id}{Optionally filter by a direction ID. These can be 35 | obtained with the \code{\link{directions_on_route}} function.} 36 | 37 | \item{platform_numbers}{Character vector. Optionally filter results by 38 | platform number. Despite the name, these are characters.} 39 | 40 | \item{departs}{POSIXct or Character. Optionally filter results to departures 41 | on or after the given value, according to either scheduled or estimated 42 | departure time. Characters are automatically converted to datetimes, and 43 | are assumed to be given as Melbourne time. Defaults to the current system 44 | time.} 45 | 46 | \item{look_backwards}{Boolean. Whether to look before \code{departs}. Use with 47 | caution (see Details). Defaults to \code{FALSE}.} 48 | 49 | \item{max_results}{Integer. The maximum number of departures to return for 50 | each route_id. Departures are ordered by estimated departure time, when 51 | available, and scheduled departure time otherwise. When set to 0, all 52 | departures after the given \code{departs} for the entire day are shown, and 53 | potentially some in the early hours of the next morning. Defaults to 5.} 54 | 55 | \item{include_cancelled}{Logical. Whether results should be returned if they 56 | have been cancelled. Metropolitan train services only. Defaults to FALSE.} 57 | 58 | \item{validate_results}{Boolean. If TRUE (the default), will apply additional 59 | filters to ensure that the arguments to \code{departs}, \code{max_results}, and 60 | \code{route_id} are respected if given.} 61 | 62 | \item{user_id}{Integer or character. A user ID or devid provided by Public 63 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 64 | 65 | \item{api_key}{Character. An API key, with dashes, provided by Public 66 | Transport Victoria. Refer to \code{?ptvapi} for more details.} 67 | } 68 | \value{ 69 | A tibble consisting of the following columns: \itemize{ 70 | \item \code{stop_id} 71 | \item \code{route_id} 72 | \item \code{run_id} (deprecated, use \code{run_ref} instead) 73 | \item \code{run_ref} 74 | \item \code{direction_id} 75 | \item \code{disruption_ids} 76 | \item \code{scheduled_departure} 77 | \item \code{estimated_departure} 78 | \item \code{at_platform} 79 | \item \code{platform_number} 80 | \item \code{flags} 81 | \item \code{departure_sequence} 82 | } 83 | } 84 | \description{ 85 | \code{departures} retrieves all upcoming departures for a given stop ID and route 86 | type. 87 | } 88 | \details{ 89 | Filtering departures: The API supports filtering by departure time, 90 | to show the departures after the given time. However, its behaviour is 91 | unpredictable, returning departures around the given time, both before and 92 | after. We apply an additional filter once the results are retrieved to 93 | ensure that only departures at or after the given \code{departs} datetime are 94 | shown. 95 | 96 | It's not clear what functionality \code{look_backwards} has. It's included here 97 | regardless. Moreover, it's not clear how the API treats \code{route_id} or 98 | \code{max_results}. We filter the results after retrieval, to ensure that 99 | \code{departs}, \code{max_results}, and \code{route_id} are respected. This additional 100 | validation can be disabled by setting \code{validate_results = TRUE}. 101 | } 102 | \examples{ 103 | \dontrun{ 104 | departures(stop_id = 1071, route_type = "Train") 105 | departures(stop_id = 1071, route_type = 0) 106 | 107 | departures( 108 | stop_id = 1071, 109 | route_type = "Train", 110 | platform_numbers = c(4, 5) 111 | ) 112 | 113 | departures( 114 | stop_id = 1071, 115 | route_type = "Train", 116 | route_id = 6 117 | ) 118 | 119 | departures( 120 | stop_id = 1071, 121 | route_type = "Train", 122 | departs = "2020-06-23 17:05:00" 123 | ) 124 | 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /tests/testthat/test-stops.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | # ---------------------------------------------------------------------------- # 3 | # ---- Define values if they haven't already been defined by another test ---- # 4 | # ---------------------------------------------------------------------------- # 5 | if (!exists("all_routes")) { 6 | all_routes <- routes() 7 | } 8 | 9 | if (!exists("frankston_route_id")) { 10 | frankston_route_id <- all_routes %>% 11 | dplyr::filter(route_name == "Frankston") %>% 12 | pull(route_id) 13 | } 14 | 15 | if (!exists("stops_near_flinders_street")) { 16 | stops_near_flinders_street <- stops_nearby( 17 | latitude = -37.8183, 18 | longitude = 144.9671 19 | ) 20 | } 21 | 22 | if (!exists("flinders_street_stop_id")) { 23 | flinders_street_stop_id <- stops_near_flinders_street %>% 24 | filter(stop_name == "Flinders Street Railway Station") %>% 25 | pull(stop_id) 26 | } 27 | # ---------------------------------------------------------------------------- # 28 | 29 | stops_on_frankston_line <- stops_on_route( 30 | route_id = frankston_route_id, 31 | route_type = "Train" 32 | ) 33 | 34 | test_that("stops_on_route result has class \"ptvapi\"", { 35 | expect_s3_class(stops_on_frankston_line, "ptvapi") 36 | }) 37 | 38 | test_that("Stop names along a route are unique", { 39 | expect_equal(anyDuplicated(stops_on_frankston_line$stop_name), 0) 40 | }) 41 | 42 | test_that("Expected stops on Frankston train line", { 43 | expect_true("South Yarra Station" %in% stops_on_frankston_line$stop_name) 44 | expect_true("Cheltenham Station" %in% stops_on_frankston_line$stop_name) 45 | }) 46 | 47 | # Stops with a direction_id parameters 48 | frankston_directions_on_route <- directions_on_route(frankston_route_id) 49 | 50 | test_that("Stop sequence when travelling City to Frankston", { 51 | frankston_direction_id <- frankston_directions_on_route %>% 52 | filter(direction_name == "Frankston") %>% 53 | pull(direction_id) 54 | city_to_frankston <- stops_on_route( 55 | route_id = frankston_route_id, 56 | route_type = "Train", 57 | direction = frankston_direction_id 58 | ) 59 | city_to_frankston_ends <- city_to_frankston %>% 60 | filter(stop_sequence == max(stop_sequence)) 61 | expect_equal(nrow(city_to_frankston_ends), 1) 62 | expect_equal(city_to_frankston_ends$stop_suburb, "Frankston") 63 | }) 64 | 65 | test_that("Stop sequence when travelling Frankston to City", { 66 | city_direction_id <- frankston_directions_on_route %>% 67 | filter(grepl("City", direction_name)) %>% 68 | pull(direction_id) 69 | frankston_to_city <- stops_on_route( 70 | route_id = frankston_route_id, 71 | route_type = "Train", 72 | direction_id = city_direction_id 73 | ) 74 | frankston_to_city_ends <- frankston_to_city %>% 75 | filter(stop_sequence == max(stop_sequence)) 76 | expect_equal(nrow(frankston_to_city_ends), 1) 77 | expect_equal(frankston_to_city_ends$stop_suburb, "Melbourne City") 78 | }) 79 | 80 | flinders_info <- stop_information( 81 | flinders_street_stop_id, 82 | route_type = "Train" 83 | ) 84 | 85 | test_that("stop_information result has class \"ptvapi\"", { 86 | expect_s3_class(flinders_info, "ptvapi") 87 | }) 88 | 89 | test_that("Flinders Street stop_information is complete", { 90 | expect_lt(mean(is.na(flinders_info)), 0.25) # max prop of NA columns 91 | }) 92 | 93 | # stops_near_flinders_street defined at top of file 94 | test_that("stops_nearby result has class \"ptvapi\"", { 95 | expect_s3_class(stops_near_flinders_street, "ptvapi") 96 | }) 97 | 98 | # flinders_street_stop_id defined at top of file 99 | test_that("Can find Flinders Street Station stop with latitude and longitude", { 100 | expect_true(length(flinders_street_stop_id) == 1) 101 | }) 102 | 103 | test_that("stops_nearby filtered by max_distance", { 104 | max_100 <- stops_nearby( 105 | latitude = -37.8183, 106 | longitude = 144.9671, 107 | max_distance = 100 108 | ) 109 | max_1000 <- stops_nearby( 110 | latitude = -37.8183, 111 | longitude = 144.9671, 112 | max_distance = 1000 113 | ) 114 | expect_true("Collins St/Queen St" %in% max_1000$stop_name) 115 | expect_false("Collins St/Queen St" %in% max_100$stop_name) 116 | }) 117 | 118 | test_that("stops_nearby() can be filtered with multiple route types", { 119 | # We use Southern Cross because it has a variety of route types 120 | expect_equal( 121 | stops_nearby( 122 | latitude = -37.818229, 123 | longitude = 144.952404, 124 | route_types = c(0, 1) 125 | ) %>% 126 | pull(route_type) %>% 127 | unique %>% 128 | sort, 129 | c(0, 1) 130 | ) 131 | expect_equal( 132 | stops_nearby( 133 | latitude = -37.818229, 134 | longitude = 144.952404, 135 | route_types = c(0, 3) 136 | ) %>% 137 | pull(route_type) %>% 138 | unique %>% 139 | sort, 140 | c(0, 3) 141 | ) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /R/outlets.R: -------------------------------------------------------------------------------- 1 | #' Information for all outlets 2 | #' 3 | #' @details 4 | #' The `outlet_name` reported here is more accurately described as an outlet 5 | #' _address_. We keep the `outlet_name` column name as this is how the PTV API 6 | #' describes it. 7 | #' 8 | #' The business hours are reported as characters. Usually they take on 9 | #' a format of "8.00AM - 10.00PM", but there variants such as "7.30AM - 11.00AM 10 | #' and 1.30PM - 6.00PM". For days on which an outlet is closed, the opening 11 | #' hours are usually reported as "CLOSED", but can also be an empty character. 12 | #' Some opening hours are "24 Hours". These fields are also filled with missing 13 | #' values and empty characters. 14 | #' 15 | #' @inheritParams PTVGET 16 | #' 17 | #' @inherit outlet_to_tibble return 18 | #' 19 | #' @export 20 | #' 21 | #' @examples \dontrun{ 22 | #' outlets() 23 | #' } 24 | #' 25 | outlets <- function(user_id = determine_user_id(), 26 | api_key = determine_api_key()) { 27 | 28 | # This API call nominally supports an `max_results` parameter, which defaults 29 | # to 30. It seems to have no impact on the results, so it hasn't been 30 | # included here. It's possibly accidentally copied from the API call used in 31 | # the `outlets_nearby` function. 32 | 33 | request <- "outlets" 34 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 35 | content <- response$content 36 | assert_correct_attributes( 37 | names(content), 38 | c("outlets", "status") 39 | ) 40 | 41 | parsed <- map_and_rbind(content$outlets, outlet_to_tibble) 42 | new_ptvapi_tibble(response, parsed) 43 | } 44 | 45 | #' Information for outlets near a given location 46 | #' 47 | #' @inherit outlets details 48 | #' 49 | #' @inheritParams stops_nearby 50 | #' @param max_results Integer. Defaults to 30. Caps the number of results 51 | #' returned. 52 | #' @inheritParams PTVGET 53 | #' 54 | #' @inherit outlet_to_tibble return 55 | #' 56 | #' @export 57 | #' 58 | #' @examples \dontrun{ 59 | #' outlets_nearby(latitude = -37.8183, longitude = 144.9671) 60 | #' } 61 | #' 62 | outlets_nearby <- function(latitude, 63 | longitude, 64 | max_distance = NULL, 65 | max_results = 30, 66 | user_id = determine_user_id(), 67 | api_key = determine_api_key()) { 68 | 69 | assertthat::assert_that(is.numeric(latitude)) 70 | assertthat::assert_that(is.numeric(longitude)) 71 | if (!is.null(max_distance)) max_distance <- to_integer(max_distance) 72 | max_results <- to_integer(max_results) 73 | 74 | request <- add_parameters( 75 | glue::glue("outlets/location/{latitude},{longitude}"), 76 | max_distance = max_distance, 77 | max_results = max_results 78 | ) 79 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 80 | content <- response$content 81 | assert_correct_attributes( 82 | names(content), 83 | c("outlets", "status") 84 | ) 85 | 86 | parsed <- map_and_rbind(content$outlets, outlet_to_tibble) 87 | new_ptvapi_tibble(response, parsed) 88 | } 89 | 90 | #' Convert a single outlet to a tibble 91 | #' 92 | #' This function is designed to parse the content returned by the interior 93 | #' steps of the \code{\link{outlets}} and \code{\link{outlets_nearby}} 94 | #' functions. 95 | #' 96 | #' @param outlet An outlet, as a list, returned by the \code{\link{outlets}} API 97 | #' call. 98 | #' 99 | #' @return A tibble with the following columns: \itemize{ 100 | #' \item {`outlet_slid_spid`} 101 | #' \item {`outlet_name`} 102 | #' \item {`outlet_business`} 103 | #' \item {`outlet_latitude`} 104 | #' \item {`outlet_longitude`} 105 | #' \item {`outlet_suburb`} 106 | #' \item {`outlet_postcode`} 107 | #' \item {`outlet_business_hour_mon`} 108 | #' \item {`outlet_business_hour_tue`} 109 | #' \item {`outlet_business_hour_wed`} 110 | #' \item {`outlet_business_hour_thu`} 111 | #' \item {`outlet_business_hour_fri`} 112 | #' \item {`outlet_business_hour_sat`} 113 | #' \item {`outlet_business_hour_sun`} 114 | #' \item {`outlet_notes`} 115 | #' } 116 | #' 117 | #' @keywords internal 118 | #' 119 | outlet_to_tibble <- function(outlet) { 120 | null_to_na_char <- function(x) ifelse(is.null(x), NA_character_, x) 121 | tibble::tibble( 122 | outlet_slid_spid = outlet$outlet_slid_spid, 123 | outlet_name = outlet$outlet_name, 124 | outlet_business = outlet$outlet_business, 125 | outlet_latitude = outlet$outlet_latitude, 126 | outlet_longitude = outlet$outlet_longitude, 127 | outlet_suburb = outlet$outlet_suburb, 128 | outlet_postcode = outlet$outlet_postcode, 129 | outlet_business_hour_mon = null_to_na_char(outlet$outlet_business_hour_mon), 130 | outlet_business_hour_tue = null_to_na_char(outlet$outlet_business_hour_tue), 131 | outlet_business_hour_wed = null_to_na_char(outlet$outlet_business_hour_wed), 132 | outlet_business_hour_thu = null_to_na_char(outlet$outlet_business_hour_thu), 133 | outlet_business_hour_fri = null_to_na_char(outlet$outlet_business_hour_fri), 134 | outlet_business_hour_sat = null_to_na_char(outlet$outlet_business_hour_sat), 135 | outlet_business_hour_sun = null_to_na_char(outlet$outlet_business_hour_sun), 136 | outlet_notes = null_to_na_char(outlet$outlet_notes) 137 | ) 138 | } 139 | -------------------------------------------------------------------------------- /docs/authors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Authors • ptvapi 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 |
      117 |
    • 118 |

      David Neuzerling. Author, maintainer, copyright holder. 119 |

      120 |
    • 121 |
    122 | 123 |
    124 | 125 |
    126 | 127 | 128 | 129 |
    130 | 133 | 134 |
    135 |

    Site built with pkgdown 1.6.1.

    136 |
    137 | 138 |
    139 |
    140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/bootstrap-toc.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) 3 | * Copyright 2015 Aidan Feldman 4 | * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ 5 | (function() { 6 | 'use strict'; 7 | 8 | window.Toc = { 9 | helpers: { 10 | // return all matching elements in the set, or their descendants 11 | findOrFilter: function($el, selector) { 12 | // http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/ 13 | // http://stackoverflow.com/a/12731439/358804 14 | var $descendants = $el.find(selector); 15 | return $el.filter(selector).add($descendants).filter(':not([data-toc-skip])'); 16 | }, 17 | 18 | generateUniqueIdBase: function(el) { 19 | var text = $(el).text(); 20 | var anchor = text.trim().toLowerCase().replace(/[^A-Za-z0-9]+/g, '-'); 21 | return anchor || el.tagName.toLowerCase(); 22 | }, 23 | 24 | generateUniqueId: function(el) { 25 | var anchorBase = this.generateUniqueIdBase(el); 26 | for (var i = 0; ; i++) { 27 | var anchor = anchorBase; 28 | if (i > 0) { 29 | // add suffix 30 | anchor += '-' + i; 31 | } 32 | // check if ID already exists 33 | if (!document.getElementById(anchor)) { 34 | return anchor; 35 | } 36 | } 37 | }, 38 | 39 | generateAnchor: function(el) { 40 | if (el.id) { 41 | return el.id; 42 | } else { 43 | var anchor = this.generateUniqueId(el); 44 | el.id = anchor; 45 | return anchor; 46 | } 47 | }, 48 | 49 | createNavList: function() { 50 | return $(''); 51 | }, 52 | 53 | createChildNavList: function($parent) { 54 | var $childList = this.createNavList(); 55 | $parent.append($childList); 56 | return $childList; 57 | }, 58 | 59 | generateNavEl: function(anchor, text) { 60 | var $a = $(''); 61 | $a.attr('href', '#' + anchor); 62 | $a.text(text); 63 | var $li = $('
  • '); 64 | $li.append($a); 65 | return $li; 66 | }, 67 | 68 | generateNavItem: function(headingEl) { 69 | var anchor = this.generateAnchor(headingEl); 70 | var $heading = $(headingEl); 71 | var text = $heading.data('toc-text') || $heading.text(); 72 | return this.generateNavEl(anchor, text); 73 | }, 74 | 75 | // Find the first heading level (`

    `, then `

    `, etc.) that has more than one element. Defaults to 1 (for `

    `). 76 | getTopLevel: function($scope) { 77 | for (var i = 1; i <= 6; i++) { 78 | var $headings = this.findOrFilter($scope, 'h' + i); 79 | if ($headings.length > 1) { 80 | return i; 81 | } 82 | } 83 | 84 | return 1; 85 | }, 86 | 87 | // returns the elements for the top level, and the next below it 88 | getHeadings: function($scope, topLevel) { 89 | var topSelector = 'h' + topLevel; 90 | 91 | var secondaryLevel = topLevel + 1; 92 | var secondarySelector = 'h' + secondaryLevel; 93 | 94 | return this.findOrFilter($scope, topSelector + ',' + secondarySelector); 95 | }, 96 | 97 | getNavLevel: function(el) { 98 | return parseInt(el.tagName.charAt(1), 10); 99 | }, 100 | 101 | populateNav: function($topContext, topLevel, $headings) { 102 | var $context = $topContext; 103 | var $prevNav; 104 | 105 | var helpers = this; 106 | $headings.each(function(i, el) { 107 | var $newNav = helpers.generateNavItem(el); 108 | var navLevel = helpers.getNavLevel(el); 109 | 110 | // determine the proper $context 111 | if (navLevel === topLevel) { 112 | // use top level 113 | $context = $topContext; 114 | } else if ($prevNav && $context === $topContext) { 115 | // create a new level of the tree and switch to it 116 | $context = helpers.createChildNavList($prevNav); 117 | } // else use the current $context 118 | 119 | $context.append($newNav); 120 | 121 | $prevNav = $newNav; 122 | }); 123 | }, 124 | 125 | parseOps: function(arg) { 126 | var opts; 127 | if (arg.jquery) { 128 | opts = { 129 | $nav: arg 130 | }; 131 | } else { 132 | opts = arg; 133 | } 134 | opts.$scope = opts.$scope || $(document.body); 135 | return opts; 136 | } 137 | }, 138 | 139 | // accepts a jQuery object, or an options object 140 | init: function(opts) { 141 | opts = this.helpers.parseOps(opts); 142 | 143 | // ensure that the data attribute is in place for styling 144 | opts.$nav.attr('data-toggle', 'toc'); 145 | 146 | var $topContext = this.helpers.createChildNavList(opts.$nav); 147 | var topLevel = this.helpers.getTopLevel(opts.$scope); 148 | var $headings = this.helpers.getHeadings(opts.$scope, topLevel); 149 | this.helpers.populateNav($topContext, topLevel, $headings); 150 | } 151 | }; 152 | 153 | $(function() { 154 | $('nav[data-toggle="toc"]').each(function(i, el) { 155 | var $nav = $(el); 156 | Toc.init($nav); 157 | }); 158 | }); 159 | })(); 160 | -------------------------------------------------------------------------------- /R/helper-functions.R: -------------------------------------------------------------------------------- 1 | #' Convert a datetime returned by the PTV API into Melbourne time 2 | #' 3 | #' @param datetime A datetime returned by the PTV API 4 | #' 5 | #' @return A datetime in the Melbourne timezone. 6 | #' 7 | #' @keywords internal 8 | #' 9 | convert_to_melbourne_time <- function(datetime) { 10 | 11 | if (is.null(datetime)) { 12 | na_datetime <- as.POSIXct(NA) 13 | attr(na_datetime, "tzone") <- "Australia/Melbourne" 14 | return(na_datetime) 15 | } 16 | 17 | converted <- as.POSIXct( 18 | datetime, 19 | tz = "GMT", 20 | format = "%Y-%m-%dT%H:%M:%OS" 21 | ) 22 | attr(converted, "tzone") <- "Australia/Melbourne" 23 | converted 24 | } 25 | 26 | #' Convert a POSIXct or character datetime to a format ready for a URL 27 | #' 28 | #' Datetimes accepted by the API need to be given in UTC. This function will 29 | #' accept a datetime or a character with a suitable datetime format, and output 30 | #' a character that is suitable for a URL. All URL input and output in this 31 | #' package should be in Melbourne time, and the UTC conversion should happen 32 | #' 33 | #' The API seems to accept both "%Y-%m-%dT%H:%M:%OS" and "%Y-%m-%d %H:%M:%OS", 34 | #' but we opt for the former. 35 | #' 36 | #' @param datetime POSIXct or Character. 37 | #' 38 | #' @return Character. 39 | #' 40 | #' @keywords internal 41 | #' 42 | to_datetime <- function(datetime) { 43 | if (is.character(datetime)) { 44 | datetime <- as.POSIXct( 45 | datetime, 46 | tz = "Australia/Melbourne", 47 | tryFormats = c("%Y-%m-%dT%H:%M:%OS", 48 | "%Y-%m-%d %H:%M:%OS", 49 | "%Y/%m/%d %H:%M:%OS", 50 | "%Y-%m-%d %H:%M", 51 | "%Y/%m/%d %H:%M", 52 | "%Y-%m-%d", 53 | "%Y/%m/%d") 54 | ) 55 | } 56 | 57 | assertthat::assert_that( 58 | inherits(datetime, "POSIXct"), 59 | msg = "datetime must be POSIXct, or character that parses as POSIXct" 60 | ) 61 | 62 | datetime 63 | } 64 | 65 | #' Map and rbind a list of data frames 66 | #' 67 | #' This function is a simple combination of `purrr::map` and `purrr::reduce` 68 | #' using `rbind`. This differs from `purrr::map_dfr`, which uses 69 | #' `dplyr::map_dfr` and therefore introduces `dplyr` as a dependency. If the 70 | #' provided list is empty, then an empty tibble will be returned. 71 | #' 72 | #' @param .x A list of data frames or tibbles. 73 | #' @param .f A function. 74 | #' @param ... Additional arguments passed to the function. 75 | #' 76 | #' @return A data frame or tibble. 77 | #' 78 | #' @keywords internal 79 | #' 80 | map_and_rbind <- function(.x, .f, ...) { 81 | if (length(.x) == 0) { 82 | return(tibble::tibble()) 83 | } 84 | do.call(rbind, lapply(.x, .f, ...)) 85 | } 86 | 87 | #' Assert that the API has returned the expected attributes 88 | #' 89 | #' The attributes returned by the API calls should be follow the API schema. 90 | #' This function compares received attributes against a vector of expected 91 | #' attributes, and returns an error if the two do not match. Unfortunately, 92 | #' there is no easy fix for this error: the package developer(s) must be 93 | #' notified, so that they can align the functions against the API schema. 94 | #' 95 | #' @param received_attributes A character vector of attributes, in order. 96 | #' @param expected_attributes A character vector of expected attributes, in 97 | #' order. 98 | #' 99 | #' @return An error if the column names are not as expected. 100 | #' 101 | #' @keywords internal 102 | #' 103 | assert_correct_attributes <- function(received_attributes, 104 | expected_attributes) { 105 | assertthat::assert_that( 106 | identical(received_attributes, expected_attributes), 107 | msg = paste( 108 | "The attributes returned by the API were not as expected.", 109 | "Please contact the package developer.", 110 | "Expected", paste(expected_attributes, collapse = ", "), 111 | "but got", paste(received_attributes, collapse = ", ") 112 | ) 113 | ) 114 | } 115 | 116 | #' Strictly convert an object to an integer 117 | #' 118 | #' R does not have a built-in function to determine if a value is an integer 119 | #' (`is.integer` will check if the class of an object is "integer", but 120 | #' `is.integer(3)` will return FALSE). This helper function fills that gap. It 121 | #' will attempt to convert the input to an integer, but will error on any input 122 | #' that cannot be confidently expressed as an integer. It serves as a stricter 123 | #' version of `as.integer`. For example, the function will convert `3` or `"3"` 124 | #' to integers, but will error on `3.5` or `"three"`. 125 | #' 126 | #' @param x An input of any type. 127 | #' 128 | #' @return An integer interpretation of `x`, if possible. 129 | #' 130 | #' @keywords internal 131 | #' 132 | to_integer <- function(x) { 133 | if (missing(x)) { 134 | stop("Error in to_integer() : argument \"x\" is missing, with no default") 135 | } 136 | 137 | if (!(typeof(x) %in% c("integer", "double", "character"))) { 138 | stop(paste("Cannot convert", typeof(x), "to an integer")) 139 | } 140 | 141 | error_message <- paste("Cannot convert", x, "to an integer") 142 | 143 | # Try numeric first, then cast to integer 144 | x_numeric <- suppressWarnings(as.numeric(x)) 145 | if (is.na(x_numeric)) stop(error_message) 146 | x_integer <- as.integer(x_numeric) 147 | if (x_integer != x_numeric) stop(error_message) 148 | x_integer 149 | } 150 | -------------------------------------------------------------------------------- /docs/LICENSE-text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | License • ptvapi 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 |
    YEAR: 2020
    117 | COPYRIGHT HOLDER: David Neuzerling
    118 | 
    119 | 120 |
    121 | 122 | 127 | 128 |
    129 | 130 | 131 | 132 |
    133 | 136 | 137 |
    138 |

    Site built with pkgdown 1.6.1.

    139 |
    140 | 141 |
    142 |
    143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Page not found (404) • ptvapi 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    62 |
    63 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 |
    112 | 115 | 116 | Content not found. Please use links in the navbar. 117 | 118 |
    119 | 120 | 125 | 126 |
    127 | 128 | 129 | 130 |
    131 | 134 | 135 |
    136 |

    Site built with pkgdown 1.6.1.

    137 |
    138 | 139 |
    140 |
    141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /R/routes.R: -------------------------------------------------------------------------------- 1 | #' Information for a given route 2 | #' 3 | #' @inheritParams directions_on_route 4 | #' @inheritParams PTVGET 5 | #' @param include_geopath Logical. Whether to populate the `geopath` column. 6 | #' Defaults to FALSE. 7 | #' @param geopath_utc Date, or character that can be converted to a date. The 8 | #' UTC date for which the geopaths are effective. Defaults to the current 9 | #' date. Has no effect if `include_geopath = FALSE`. It's uncertain how much 10 | #' historical or future-dated data is available. 11 | #' 12 | #' @inherit route_to_tibble return 13 | #' 14 | #' @export 15 | #' 16 | #' @examples \dontrun{ 17 | #' route_information(6) 18 | #' route_information(6, include_geopath = TRUE) 19 | #' route_information(6, include_geopath = TRUE, geopath_utc = "2020-07-01") 20 | #' } 21 | #' 22 | route_information <- function(route_id, 23 | include_geopath = FALSE, 24 | geopath_utc = NULL, 25 | user_id = determine_user_id(), 26 | api_key = determine_api_key()) { 27 | route_id <- to_integer(route_id) 28 | if (!include_geopath && !is.null(geopath_utc)) { 29 | warning("`geopath_utc` is ignored when `include_geopath` is `TRUE`") 30 | geopath_utc <- NULL 31 | } 32 | if (!is.null(geopath_utc)) { 33 | geopath_utc <- as.Date(geopath_utc) 34 | } 35 | 36 | request <- add_parameters( 37 | glue::glue("routes/{route_id}"), 38 | include_geopath = include_geopath, 39 | geopath_utc = geopath_utc 40 | ) 41 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 42 | content <- response$content 43 | 44 | assert_correct_attributes(names(content), c("route", "status")) 45 | parsed <- route_to_tibble(content$route) # may be an empty tibble 46 | parsed$route_type_description <- purrr::map_chr( 47 | parsed$route_type, 48 | describe_route_type, 49 | user_id = user_id, 50 | api_key = api_key 51 | ) 52 | new_ptvapi_tibble(response, parsed) 53 | } 54 | 55 | #' Information for all routes 56 | #' 57 | #' @inheritParams stops_nearby 58 | #' @param route_name Character. Optionally filter by route name. Partial matches 59 | #' are accepted, and the matches are not case sensitive. 60 | #' @inheritParams PTVGET 61 | #' 62 | #' @inherit route_to_tibble return 63 | #' 64 | #' @export 65 | #' 66 | #' @examples \dontrun{ 67 | #' routes() 68 | #' routes(route_types = "Train") 69 | #' routes(route_types = 0) 70 | #' routes(route_types = c("Train", "Tram")) 71 | #' routes(route_name = "Frankston") 72 | #' routes(route_name = "Craigie") 73 | #' routes(route_name = "werribee") 74 | #' } 75 | routes <- function(route_types = NULL, 76 | route_name = NULL, 77 | user_id = determine_user_id(), 78 | api_key = determine_api_key()) { 79 | if (!is.null(route_types)) { 80 | route_types <- purrr::map_int( 81 | route_types, 82 | translate_route_type, 83 | user_id = user_id, 84 | api_key = api_key 85 | ) 86 | } 87 | if (!is.null(route_name)) assertthat::assert_that(is.character(route_name)) 88 | 89 | request <- add_parameters( 90 | "routes", 91 | route_types = route_types, 92 | route_name = route_name 93 | ) 94 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 95 | content <- response$content 96 | parsed <- map_and_rbind(content$routes, route_to_tibble) 97 | parsed$route_type_description <- purrr::map_chr( 98 | parsed$route_type, 99 | describe_route_type, 100 | user_id = user_id, 101 | api_key = api_key 102 | ) 103 | new_ptvapi_tibble(response, parsed) 104 | } 105 | 106 | #' Convert a single route to a tibble 107 | #' 108 | #' This function is designed to parse the content returned by the interior steps 109 | #' of the \code{\link{routes}} function. Service status information may be `NA`, 110 | #' depending on the API call that was used to populate the information. 111 | #' 112 | #' @param route A route, as a list, returned by the \code{\link{routes}} API 113 | #' call. 114 | #' 115 | #' @return A tibble of routes, with the following columns: 116 | #' \itemize{ 117 | #' \item `route_id` 118 | #' \item `route_gtfs_id` 119 | #' \item `route_name` 120 | #' \item `route_type` 121 | #' \item `route_type_description` 122 | #' \item `route_number` 123 | #' \item `geopath` 124 | #' \item `service_status` 125 | #' \item `service_status_timestamp` 126 | #' } 127 | #' 128 | #' @keywords internal 129 | #' 130 | route_to_tibble <- function(route) { 131 | if (is.null(route)) { 132 | return( 133 | tibble::tibble( 134 | route_id = integer(), 135 | route_gtfs_id = character(), 136 | route_name = character(), 137 | route_type = integer(), 138 | route_type_description = character(), 139 | route_number = character(), 140 | geopaths = list(), 141 | service_status = character(), 142 | service_status_timestamp = as.POSIXct(NA) 143 | ) 144 | ) 145 | } 146 | tibble::tibble( 147 | route_id = route$route_id, 148 | route_gtfs_id = route$route_gtfs_id, 149 | route_name = route$route_name, 150 | route_type = route$route_type, 151 | route_type_description = NA_character_, 152 | route_number = ifelse( 153 | # Not always a number, eg. "745a" 154 | route$route_number == "", NA_character_, route$route_number 155 | ), 156 | geopath = if (is.null(route$geopath)) { 157 | list(geopath_to_tibble(NULL)) 158 | } else { 159 | list(purrr::map_dfr(route$geopath, geopath_to_tibble)) 160 | }, 161 | service_status = ifelse( 162 | is.null(route$route_service_status$description), 163 | NA_character_, 164 | route$route_service_status$description 165 | ), 166 | service_status_timestamp = convert_to_melbourne_time( 167 | route$route_service_status$timestamp 168 | ) 169 | ) 170 | } 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ptvapi 3 | 4 | 5 | [![CRAN status](https://www.r-pkg.org/badges/version/ptvapi)](https://cran.r-project.org/package=ptvapi) 6 | [![CRAN downloads](https://cranlogs.r-pkg.org/badges/ptvapi)](https://cran.r-project.org/package=ptvapi) 7 | [![Last commit](https://img.shields.io/github/last-commit/mdneuzerling/ptvapi/main.svg)](https://github.com/mdneuzerling/ptvapi/tree/main) 8 | [![R build status](https://github.com/mdneuzerling/ptvapi/workflows/R-CMD-check/badge.svg)](https://github.com/mdneuzerling/ptvapi/actions) 9 | [![Lint](https://github.com/mdneuzerling/ptvapi/workflows/lint/badge.svg)](https://github.com/mdneuzerling/ptvapi/actions?query=workflow%3Alint) 10 | [![Codecov test coverage](https://codecov.io/gh/mdneuzerling/ptvapi/branch/main/graph/badge.svg)](https://app.codecov.io/gh/mdneuzerling/ptvapi?branch=main) 11 | [![license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://choosealicense.com/licenses/mit/) 12 | 13 | 14 | 15 | This package provides a friendly interface to the Public Transport Victoria (PTV) Timetable API. Results are returned as data frames --- using real-time data where available --- and authentication is handled under the hood. 16 | 17 | **This package is an unofficial wrapper of the Public Transport Victoria timetable API. The authors of this package are not associated with Public Transport Victoria.** 18 | 19 | ## Installing 20 | 21 | ### CRAN version 22 | 23 | ``` 24 | install.packages("ptvapi") 25 | ``` 26 | 27 | ### Development version 28 | 29 | ``` 30 | remotes::install_github("mdneuzerling/ptvapi") 31 | ``` 32 | 33 | ## Authentication 34 | 35 | Using the API requires a user ID (also called a `devid`) and an API key from Public Transport Victoria. These can be requested in an email. Refer to the [PTV website](https://www.ptv.vic.gov.au/footer/data-and-reporting/datasets/ptv-timetable-api/) for instructions. 36 | 37 | The user ID and API key be provided directly to the functions, for example: 38 | ``` 39 | routes( 40 | user_id = 1234567, 41 | api_key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 42 | ) 43 | ``` 44 | 45 | Alternatively, this information can be set as environment variables. These can be configured directly within R: 46 | ``` 47 | Sys.setenv("PTV_USER_ID" = 1234567) 48 | Sys.setenv("PTV_API_KEY" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") 49 | ``` 50 | 51 | If a `user_id` or `api_key` value is not provided to the functions within this package, then they will be retrieved from the "PTV_USER_ID" and "PTV_API_KEY" environment variables, if possible. 52 | 53 | ## Example usage 54 | 55 | The code examples below assume that you've set environment variables for authentication. 56 | 57 | ```r 58 | # tibble of all routes 59 | routes() 60 | # A tibble: 828 x 7 61 | # route_id route_gtfs_id route_name route_type route_number service_status 62 | # 63 | ``` 64 | 65 | ```r 66 | # Search for routes by name (case insensitive, partial matching supported) 67 | routes(route_name = "Frankston") 68 | # A tibble: 27 x 7 69 | # route_id route_gtfs_id route_name route_type route_number service_status 70 | # 71 | ``` 72 | 73 | ```r 74 | # All current disruptions 75 | disruptions(disruption_status = "current") 76 | # A tibble: 244 x 17 77 | disruption_mode disruption_mode… disruption_id title url description 78 | 79 | # … with 177 more rows, and 17 more variables … 80 | ``` 81 | 82 | ```r 83 | # Train stops near Flinders Street Station 84 | stops_nearby( 85 | latitude = -37.8183, 86 | longitude = 144.9671, 87 | route_types = "Train" 88 | ) 89 | # # A tibble: 1 x 8 90 | # stop_id stop_name stop_suburb route_type stop_sequence stop_latitude stop_longitude 91 | # 92 | ``` 93 | 94 | ```r 95 | # Upcoming train departures from Flinders Street Station 96 | > departures(stop_id = 1071, route_type = "Train") 97 | # A tibble: 75 x 12 98 | direction_id stop_id route_id run_id run_ref platform_number at_platform 99 | 100 | # … with 65 more rows, and 5 more variables: departure_sequence , 101 | # scheduled_departure , estimated_departure , flags , 102 | # disruption_ids 103 | ``` 104 | 105 | ## A note about route types 106 | 107 | The API recognises five route types: "Train", "Tram", "Bus", "Vline", and "Night Bus". Many functions have arguments such as `route_type` and `route_types` that expect a non-negative integer code representing these route types. To simplify calling the API, these functions will also accept a character description like those above. Under the hood, the functions will translate these descriptions to the non-negative integer codes that the API expects. For example, `routes(route_type = "Train")` is automatically translated to `routes(route_type = 0)`. 108 | 109 | ## Available functions 110 | 111 | All API calls have been implemented. Some API calls have been combined into a single function with arguments, and some have been split into multiple functions. The following functions are available through this package: 112 | 113 | * `departures` 114 | * `directions` 115 | * `directions_on_route` 116 | * `disruption_information` 117 | * `disruption_modes` 118 | * `disruptions` 119 | * `disruptions_at_stop` 120 | * `disruptions_on_route` 121 | * `fare_estimate` 122 | * `outlets` 123 | * `outlets_nearby` 124 | * `patterns` 125 | * `route_information` 126 | * `route_types` 127 | * `routes` 128 | * `run_information` 129 | * `runs_on_route` 130 | * `search_outlets` 131 | * `search_routes` 132 | * `search_stops` 133 | * `stop_information` 134 | * `stops_nearby` 135 | * `stops_on_route` 136 | 137 | --- 138 | 139 | Hex logo by Phizz Leeder 140 | -------------------------------------------------------------------------------- /R/route-types.R: -------------------------------------------------------------------------------- 1 | #' Retrieve a translation from route type number to name 2 | #' 3 | #' Route types (tram, train, etc.) are provided to the PTV API as an integer 4 | #' code. This function retrieves a named vector in which the values are the 5 | #' route type descriptions, and the names of the vector are the route type 6 | #' numbers. Note that "Night Bus" is a separate route type. 7 | #' 8 | #' @inheritParams PTVGET 9 | #' 10 | #' @return A named integer vector in which the values are the route type 11 | #' descriptions, and the names of the vector are the route type numbers. 12 | #' 13 | #' @export 14 | #' 15 | #' @examples \dontrun{ 16 | #' route_types() 17 | #' } 18 | #' 19 | route_types <- function(user_id = determine_user_id(), 20 | api_key = determine_api_key()) { 21 | response <- PTVGET( 22 | request = "route_types", 23 | user_id = user_id, 24 | api_key = api_key 25 | ) 26 | content_route_types <- response$content$route_types 27 | 28 | route_type_names <- purrr::map_chr( 29 | content_route_types, 30 | function(x) x$route_type_name 31 | ) 32 | route_type_numbers <- purrr::map_int( 33 | content_route_types, 34 | function(x) x$route_type 35 | ) 36 | names(route_type_names) <- route_type_numbers 37 | route_type_names 38 | } 39 | 40 | #' Retrieve route types, using cached values if possible 41 | #' 42 | #' @inheritParams PTVGET 43 | #' 44 | #' @description 45 | #' Route types will change extraordinarily rarely --- this would require PTV to 46 | #' add a new route type akin to "train" or "bus". To avoid querying the API too 47 | #' much, we prefer to use cached values for route type translation wherever 48 | #' possible. This function effectively wraps `route_types`, returning cached 49 | #' results if possible or caching results otherwise. Note that if a user 50 | #' specifically calls `route_types` then we do _not_ return cached results. 51 | #' 52 | #' We use the `pkg_env` as a cache, which is an environment created on package 53 | #' load. This is not truly private --- users could still access this as an 54 | #' internal value. But it's effectively "out of the way". 55 | #' 56 | #' @inherit route_types return 57 | #' 58 | #' @keywords internal 59 | cached_route_types <- function(user_id = determine_user_id(), 60 | api_key = determine_api_key()) { 61 | if (is.null(pkg_env$route_types)) { 62 | pkg_env$route_types <- route_types(user_id = user_id, api_key = api_key) 63 | } 64 | pkg_env$route_types 65 | } 66 | 67 | #' Translate a route type input into a numerical route type 68 | #' 69 | #' Many API calls require a route type (eg. "Tram" or "Train"). These must be 70 | #' provided as integers, which are translated to route type descriptions with 71 | #' the `route_types() function/API call. This function will: \itemize{ 72 | #' \item Translate a case-insensitive description such as "Tram" or "Train" to 73 | #' the corresponding route type code 74 | #' \item Check a given integer to see if it is a valid route type code, 75 | #' returning it if so and erroring otherwise 76 | #' \item Return NULL on NULL input 77 | #' } 78 | #' This function is _not_ vectorised. 79 | #' 80 | #' @inheritParams PTVGET 81 | #' @param route_type A route type which can be provided either as a non-negative 82 | #' integer code, or as a character: "Tram", "Train", "Bus", "Vline" or "Night 83 | #' Bus". Character inputs are not case-sensitive. Use the 84 | #' \code{\link{route_types}} function to extract a vector of all route types. 85 | #' 86 | #' @return An integer route type code, or NULL if the input is NULL 87 | #' 88 | #' @keywords internal 89 | #' 90 | translate_route_type <- function(route_type, 91 | user_id = determine_user_id(), 92 | api_key = determine_api_key()) { 93 | 94 | route_type_vector <- cached_route_types(user_id = user_id, api_key = api_key) 95 | 96 | if (is.numeric(route_type)) { 97 | if (as.character(route_type) %in% names(route_type_vector)) { 98 | translation <- route_type 99 | } else { 100 | stop( 101 | route_type, 102 | " is not a valid route type code. Valid codes are ", 103 | paste(names(route_type_vector), collapse = ", ") 104 | ) 105 | } 106 | } else if (is.character(route_type)) { 107 | if (toupper(route_type) %in% toupper(route_type_vector)) { 108 | translation <- names( 109 | route_type_vector[ 110 | which(toupper(route_type_vector) == toupper(route_type))] 111 | ) 112 | } else { 113 | stop( 114 | route_type, 115 | " is not a valid route type description. Valid descriptions are ", 116 | paste(route_type_vector, collapse = ", ") 117 | ) 118 | } 119 | } else { 120 | stop( 121 | "Couldn't determine route type code. Route types can be provided either ", 122 | "as a non-negative integer code, or as a character: \"Tram\", ", 123 | "\"Train\" \"Bus\", \"Vline\" or \"Night Bus\". Character inputs are ", 124 | "not case-sensitive." 125 | ) 126 | } 127 | 128 | as.integer(translation) 129 | } 130 | 131 | #' Convert a numeric route type to a human-friendly description 132 | #' 133 | #' This function effectively wraps the results of \code{\link{route_types}} to 134 | #' translate a route type to a human-readable form, such as translating `0` to 135 | #' `"Train"`. This function is _not_ vectorised. 136 | #' 137 | #' @param route_type Atomic integer or character. 138 | #' @inheritParams PTVGET 139 | #' 140 | #' @return character 141 | #' @keywords Internal 142 | describe_route_type <- function(route_type, 143 | user_id = determine_user_id(), 144 | api_key = determine_api_key()) { 145 | route_type <- to_integer(route_type) 146 | route_type_vector <- cached_route_types(user_id = user_id, api_key = api_key) 147 | if (!(as.character(route_type) %in% names(route_type_vector))) { 148 | stop(glue::glue("Route type {route_type} doesn't exist")) 149 | } 150 | unname(route_type_vector[as.character(route_type)]) 151 | } 152 | -------------------------------------------------------------------------------- /R/fare-estimate.R: -------------------------------------------------------------------------------- 1 | #' Calculate a fare estimate between zones 2 | #' 3 | #' Retrieve fare information for a journey through the given zones. Also 4 | #' supports journey touch on and off times, to accommodate for discounts. 5 | #' 6 | #' @param min_zone Integer. Minimum zone travelled through. 7 | #' @param max_zone Integer. Maximum zone travelled through. 8 | #' @param journey_touch_on,journey_touch_off POSIXct or Character. Optionally 9 | #' filter results to a journey time. Values to both must be provided. 10 | #' Characters are automatically converted to datetimes, and are assumed to be 11 | #' given as Melbourne time. 12 | #' @param journey_in_free_tram_zone Boolean. Defaults to `FALSE`. 13 | #' @param travelled_route_types Integer or character vector. Optionally filter 14 | #' by a vector of route types. A route type can be provided either as a 15 | #' non-negative integer code, or as a character: "Tram", "Train", "Bus", 16 | #' "Vline" or "Night Bus". Character inputs are not case-sensitive. Use the 17 | #' \code{\link{route_types}} function to extract a vector of all route types. 18 | #' @inheritParams PTVGET 19 | #' 20 | #' @inherit parse_fare_estimate_content return 21 | #' 22 | #' @export 23 | #' 24 | #' @examples \dontrun{ 25 | #' fare_estimate(min_zone = 1, max_zone = 2) 26 | #' 27 | #' fare_estimate(min_zone = 1, max_zone = 1, journey_in_free_tram_zone = TRUE) 28 | #' 29 | #' fare_estimate( 30 | #' min_zone = 1, 31 | #' max_zone = 2, 32 | #' travelled_route_types = c("Train", "Tram") 33 | #' ) 34 | #' 35 | #' fare_estimate( 36 | #' min_zone = 1, 37 | #' max_zone = 2, 38 | #' journey_touch_on = "2020-06-21 07:31:00", 39 | #' journey_touch_off = "2020-06-21 08:45:00" 40 | #' ) 41 | #' } 42 | #' 43 | fare_estimate <- function(min_zone, 44 | max_zone, 45 | journey_touch_on = NULL, 46 | journey_touch_off = NULL, 47 | journey_in_free_tram_zone = FALSE, 48 | travelled_route_types = NULL, 49 | user_id = determine_user_id(), 50 | api_key = determine_api_key()) { 51 | min_zone <- to_integer(min_zone) 52 | max_zone <- to_integer(max_zone) 53 | 54 | if (xor(is.null(journey_touch_on), is.null(journey_touch_off))) { 55 | stop("If providing journey touch on/off times, both must be provided") 56 | } 57 | if (!is.null(journey_touch_on)) { 58 | journey_touch_on <- to_datetime(journey_touch_on) 59 | journey_touch_off <- to_datetime(journey_touch_off) 60 | journey_touch_on_url <- format( 61 | journey_touch_on, format = "%Y-%m-%dT%H:%M:%OS", tz = "UTC" 62 | ) 63 | journey_touch_off_url <- format( 64 | journey_touch_off, format = "%Y-%m-%dT%H:%M:%OS", tz = "UTC" 65 | ) 66 | } else { 67 | journey_touch_on_url <- NULL 68 | journey_touch_off_url <- NULL 69 | } 70 | 71 | if (!is.null(travelled_route_types)) { 72 | travelled_route_types <- purrr::map_int( 73 | travelled_route_types, 74 | translate_route_type, 75 | user_id = user_id, 76 | api_key = api_key 77 | ) 78 | } 79 | 80 | request <- add_parameters( 81 | glue::glue("fare_estimate/min_zone/{min_zone}/max_zone/{max_zone}"), 82 | journey_touch_on_utc = journey_touch_on_url, 83 | journey_touch_off_utc = journey_touch_off_url, 84 | is_journey_in_free_tram_zone = journey_in_free_tram_zone, 85 | travelled_route_types = travelled_route_types 86 | ) 87 | 88 | response <- PTVGET(request, user_id = user_id, api_key = api_key) 89 | content <- response$content 90 | assert_correct_attributes( 91 | names(content), 92 | c("FareEstimateResultStatus", "FareEstimateResult") 93 | ) 94 | 95 | parsed <- parse_fare_estimate_content(content$FareEstimateResult) 96 | new_ptvapi_tibble(response, parsed) 97 | } 98 | 99 | #' Parse content of fare estimates API call 100 | #' 101 | #' This function is designed to parse the content returned by the interior 102 | #' steps of the \code{\link{fare_estimate}} function. 103 | #' 104 | #' @param fare_estimate_content A direction, as a list, returned by the 105 | #' \code{\link{fare_estimate}} API call. 106 | #' 107 | #' @return A data frame consisting of one row for each `passenger_type`, and the 108 | #' following columns: \itemize{ 109 | #' \item `min_zone` 110 | #' \item `max_zone` 111 | #' \item `unique_zones` 112 | #' \item `early_bird` 113 | #' \item `free_tram_zone` 114 | #' \item `weekend_journey` 115 | #' \item `passenger_type` 116 | #' \item `fare_2_hour_peak` 117 | #' \item `fare_2_hour_off_peak` 118 | #' \item `fare_daily_peak` 119 | #' \item `fare_daily_off_peak` 120 | #' \item `pass_7_days` 121 | #' \item `pass_28_to_69_day_per_day` 122 | #' \item `pass_70_plus_day_per_day` 123 | #' \item `weekend_cap` 124 | #' \item `holiday_cap` 125 | #' } 126 | #' 127 | #' @keywords internal 128 | #' 129 | parse_fare_estimate_content <- function(fare_estimate_content) { 130 | assert_correct_attributes( 131 | names(fare_estimate_content), 132 | c("IsEarlyBird", "IsJourneyInFreeTramZone", "IsThisWeekendJourney", 133 | "ZoneInfo", "PassengerFares") 134 | ) 135 | 136 | base_columns <- tibble::tibble( 137 | min_zone = fare_estimate_content$ZoneInfo$MinZone, 138 | max_zone = fare_estimate_content$ZoneInfo$MaxZone, 139 | unique_zones = list(unlist(fare_estimate_content$ZoneInfo$UniqueZones)), 140 | early_bird = fare_estimate_content$IsEarlyBird, 141 | free_tram_zone = fare_estimate_content$IsJourneyInFreeTramZone, 142 | weekend_journey = fare_estimate_content$IsThisWeekendJourney 143 | ) 144 | passenger_fares <- map_and_rbind( 145 | fare_estimate_content$PassengerFares, 146 | function(x) { 147 | tibble::tibble( 148 | passenger_type = x$PassengerType, 149 | fare_2_hour_peak = x$Fare2HourPeak, 150 | fare_2_hour_off_peak = x$Fare2HourOffPeak, 151 | fare_daily_peak = x$FareDailyPeak, 152 | fare_daily_off_peak = x$FareDailyOffPeak, 153 | pass_7_days = x$Pass7Days, 154 | pass_28_to_69_day_per_day = x$Pass28To69DayPerDay, 155 | pass_70_plus_day_per_day = x$Pass70PlusDayPerDay, 156 | weekend_cap = x$WeekendCap, 157 | holiday_cap = x$HolidayCap 158 | ) 159 | } 160 | ) 161 | 162 | # cbind converts to data.frame, but this shouldn't matter 163 | cbind(base_columns, passenger_fares) 164 | } 165 | --------------------------------------------------------------------------------