#' Request and download pressure time series at a given location
#'
#' @description
#' This function returns the surface atmospheric pressure time series from ERA5 at any requested
#' location.
#'
#' If the location queried is over water, the location will be moved to the closest onshore
#' location.
#'
#' The ERA5 pressure time series of the response \eqn{P_{ERA}} will be provided on a hourly basis
#' between `start_time` and `end_time` or the same as `pressure$date` if `pressure` is supplied.
#'
#' If you supply the `pressure` of the geolocator \eqn{P_{gl}}, the function will
#' additionally return the altitude of the geolocator above sea level \eqn{z_{gl}} using the
#' barometric equation,
#' \deqn{ z_{{gl}}(x)=z_{ERA5}(x) + \frac{T_{ERA5}(x)}{L_b} \left( \frac{P_{gl}}{P_{ERA5}(x)}
#' \right)^{\frac{RL_b}{g M}-1},}
#' where \eqn{z_{ERA}}, \eqn{T_{ERA}}, and \eqn{P_{ERA}} respectively correspond to the ground level
#' elevation, temperature at 2m, and ground level pressure of ERA5, \eqn{L_b} is the standard
#' temperature lapse rate, \eqn{R} is the universal gas constant, \eqn{g} is the gravity constant
#' and \eqn{M} is the molar mass of air. See more information at
#' [the GeoPressureAPI documentation](https://raphaelnussbaumer.com/GeoPressureAPI/#description-1).
#'
#' To be able to compare the temporal variation of the retrieved pressure of ERA5 \eqn{P_{ERA}} to
#' the geolocator pressure \eqn{P_{gl}}, the function also returns the ERA pressure normalized with
#' the geolocator mean pressure measurement as `surface_pressure_norm`.
#' \deqn{ P_{ERA5,0}(\boldsymbol{x})[t] = \left( P_{ERA5}(\boldsymbol{x})[t]-P_{gl}[t]\right) -
#' \left( \frac{1}{n}\sum_{i=1}^{n} P_{ERA5}(\boldsymbol{x})[i]-P_{gl}[i] \right).}
#'
#' @param lon Longitude to query (-180° to 180°).
#' @param lat Latitude to query (0° to 90°).
#' @param pressure A data.frame of pressure time series, containing at least a `"date"` and
#' `"value"` column.
#' @param start_time If `pressure` is not provided, `start_time` defines the start time of
#' the time series as POSIXlt.
#' @param end_time If `pressure` is not provided, `end_time` defines the end time of
#' the time series as POSIXlt.
#' @param timeout duration before the code is interrupted both for the request on
#' GeoPressureAPI and on GEE (in seconds, see `httr2::req_timeout()`).
#' @param quiet logical to hide messages about the progress
#' @param debug logical to display additional information to debug a request
#'
#' @return A data.frame containing
#' - `date` POSIXct date time
#' - `surface_pressure` pressure (hPa)
#' - `lon` same as input `lon` except if over water
#' - `lat` same as input `lat` except if over water.
#' - `surface_pressure_norm` only if `pressure` is provided as input
#' - `altitude` only if `pressure` is provided as input
#'
#' @examples
#' # Request pressure at a given location
#' pressurepath <- geopressure_timeseries(
#' lat = 46, lon = 6,
#' start_time = "2017-01-01 00:00",
#' end_time = "2017-01-02 00:00",
#' quiet = TRUE
#' )
#'
#' str(pressurepath)
#'
#' plot(pressurepath$date, pressurepath$surface_pressure,
#' type = "b", ylab = "Pressure (hPa)", xlab = "Datetime"
#' )
#'
#' # Retrieve the altitude of a bird being at this location adding random noise on the sensor.
#' pressurepath <- geopressure_timeseries(
#' lat = 46, lon = 6,
#' pressure = data.frame(
#' data.frame(
#' date = pressurepath$date,
#' value = pressurepath$surface_pressure + rnorm(nrow(pressurepath))
#' )
#' ),
#' quiet = TRUE
#' )
#'
#' str(pressurepath)
#'
#' plot(pressurepath$date, pressurepath$altitude,
#' type = "b", ylab = "Altitude (m)", xlab = "Datetime"
#' )
#' @family pressurepath
#' @references{ Nussbaumer, Raphaël, Mathieu Gravey, Martins Briedis, and Felix Liechti. 2023.
#' Global Positioning with Animal‐borne Pressure Sensors. *Methods in Ecology and Evolution*, 14,
#' 1118–1129 <https://doi.org/10.1111/2041-210X.14043>.}
#' @export
geopressure_timeseries <- function(lat,
lon,
pressure = NULL,
start_time = NULL,
end_time = NULL,
timeout = 60 * 5,
quiet = FALSE,
debug = FALSE) {
# Check input
assertthat::assert_that(is.numeric(lon))
assertthat::assert_that(is.numeric(lat))
assertthat::assert_that(lon >= -180 & lon <= 180)
assertthat::assert_that(lat >= -90 & lat <= 90)
if (!is.null(pressure)) {
assertthat::assert_that(is.data.frame(pressure))
assertthat::assert_that("date" %in% names(pressure))
assertthat::assert_that(assertthat::is.time(pressure$date))
assertthat::assert_that("value" %in% names(pressure))
assertthat::assert_that(is.numeric(pressure$value))
end_time <- NULL
start_time <- NULL
} else {
start_time <- as.POSIXct(start_time, tz = "UTC")
end_time <- as.POSIXct(end_time, tz = "UTC")
assertthat::assert_that(start_time <= end_time)
}
assertthat::assert_that(is.logical(quiet))
# Format query
body <- list(lon = lon, lat = lat)
if (!is.null(pressure)) {
assertthat::assert_that(nrow(pressure) > 0)
body$time <- as.numeric(as.POSIXct(pressure$date))
body$pressure <- pressure$value * 100
} else {
body$startTime <- as.numeric(as.POSIXct(start_time))
body$endTime <- as.numeric(as.POSIXct(end_time))
}
if (!quiet) {
cli::cli_progress_step(
"Generate request on {.url glp.mgravey.com/GeoPressure/v2/timeseries}"
)
}
if (debug) {
temp_file <- tempfile("log_geopressure_timeseries_", fileext = ".json")
write(jsonlite::toJSON(body, auto_unbox = TRUE, pretty = TRUE), temp_file)
cli::cli_text("Body request file: {.file {temp_file}}")
}
req <- httr2::request("https://glp.mgravey.com/GeoPressure/v2/timeseries/") |>
httr2::req_body_json(body) |>
httr2::req_timeout(timeout) |>
httr2::req_retry(max_tries = 3) |>
httr2::req_error(body = function(resp) {
if (debug) {
print(httr2::resp_body_json(resp))
}
c(
"x" = "Error with your request on \
{.url https://glp.mgravey.com/GeoPressure/v2/timeseries/}",
">" = httr2::resp_body_json(resp)$errorMessage,
"i" = "Please try again with `debug=TRUE`"
)
})
if (debug) {
req <- httr2::req_verbose(req, body_req = TRUE, body_resp = TRUE, info = TRUE)
}
# Perform the request and convert the response to json
resp <- httr2::req_perform(req)
resp_data <- httr2::resp_body_json(resp)$data
# Check for change in position
if (resp_data$distInter > 0) {
cli::cli_bullets(c("!" = "Requested position is on water and will be move to the closet point \\
on shore \\
({.url https://www.google.com/maps/dir/{lat},{lon}/{resp_data$lat},{resp_data$lon}}) \\
located {round(resp_data$distInter / 1000)} km away."))
}
if (!quiet) cli::cli_progress_step("Sending request")
# Prepare request
req <- httr2::request(resp_data$url) |>
httr2::req_timeout(timeout) |>
httr2::req_retry(max_tries = 3)
if (debug) {
req <- httr2::req_verbose(req, body_req = TRUE, body_resp = TRUE, info = TRUE)
}
# Perform request
resp <- httr2::req_perform(req)
# Convert the response to data.frame
out <- utils::read.csv(text = httr2::resp_body_string(resp))
# check for errors
if (nrow(out) == 0) {
temp_file <- tempfile("log_pressurepath_create", fileext = ".json")
write(jsonlite::toJSON(body, auto_unbox = TRUE, pretty = TRUE), temp_file)
cli::cli_abort(c(
x = "Returned csv file is empty.",
i = "Check that the time range is none-empty. Log of your JSON request: {.file {temp_file}}"
))
}
# convert Pa to hPa and rename
out$pressure <- out$pressure / 100
names(out)[names(out) == "pressure"] <- "surface_pressure"
# convert time into date
out$time <- as.POSIXct(out$time, origin = "1970-01-01", tz = "UTC")
names(out)[names(out) == "time"] <- "date"
# Add exact location
out$lat <- resp_data$lat
out$lon <- resp_data$lon
# Compute the ERA5 pressure normalized to the pressure level (i.e. altitude) of the bird
if (!is.null(pressure)) {
if (nrow(out) != nrow(pressure)) {
cli::cli_warn(
"The returned data.frame is had a different number of element than the requested\\
pressure."
)
}
if (!quiet) cli::cli_progress_step("Compute normalized ERA5 pressure")
# Use a merge to combine all information possible from out into pressure.
out <- merge(pressure, out, all.x = TRUE)
names(out)[names(out) == "value"] <- "pressure_tag"
# find when the bird was in flight or not to be considered
if (!("stap_id" %in% names(pressure))) {
pressure$stap_id <- 1
}
if (!("label" %in% names(pressure))) {
pressure$label <- ""
}
# We compute the mean pressure of the geolocator only when the bird is on the ground
# (id_q==0) and when not labelled as flight or discard
id_norm <- pressure$stap_id != 0 & pressure$label != "discard"
# If no ground (ie. only flight) is present, surface_pressure_norm has no meaning
if (sum(id_norm) > 0) {
pressure$elev <- ifelse(
startsWith(pressure$label, "elev_"),
gsub("^.*?elev_", "", pressure$label),
"0"
)
elev <- unique(pressure$elev)
for (elev_i in elev) {
id_elev <- pressure$elev == elev_i
pressure_tag_m <- mean(pressure$value[id_elev & id_norm])
surface_pressure_m <- mean(out$surface_pressure[id_elev & id_norm])
out$surface_pressure_norm[id_elev] <- out$surface_pressure[id_elev] - surface_pressure_m +
pressure_tag_m
}
}
}
return(out)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.