R/measurement.R

# ==============================================================================
#
#' Fetch the Measurements of a Phenomenon on opensensemap.org
#'
#' Measurements can be retrieved either for a set of boxes, or through a spatial
#' bounding box filter. To get all measurements, the \code{default} function applies
#' a bounding box spanning the whole world.
#'
#' @param x Depending on the method, either
#'   \enumerate{
#'     \item a \code{chr} specifying the phenomenon, see \code{phenomenon}
#'     \item a \code{\link[sf]{st_bbox}} to select sensors spatially,
#'     \item a \code{sensebox data.frame} to select boxes from which
#'       measurements will be retrieved,
#'   }
#' @param ... see parameters below
#' @param phenomenon The phenomenon to retrieve measurements for
#' @param exposure Filter sensors by their exposure ('indoor', 'outdoor', 'mobile')
#' @param from A \code{POSIXt} like object to select a time interval
#' @param to A \code{POSIXt} like object to select a time interval
#' @param columns Select specific column in the output (see openSenseMap API documentation)
#' @param endpoint The URL of the openSenseMap API
#' @param progress Whether to print download progress information
#' @param cache Whether to cache the result, defaults to false.
#'   If a valid path to a directory is given, the response will be cached there. Subsequent identical requests will return the cached data instead.
#'
#' @return An \code{osem_measurements data.frame} containing the
#'   requested measurements
#'
#' @export
#' @seealso \href{https://docs.opensensemap.org/#api-Measurements-getDataMulti}{openSenseMap API documentation (web)}
#' @seealso \code{\link{osem_box}}
#' @seealso \code{\link{osem_boxes}}
#' @seealso \code{\link{osem_clear_cache}}
osem_measurements = function (x, ...) UseMethod('osem_measurements')

# ==============================================================================
#
#' @describeIn osem_measurements Get measurements from \strong{all} senseBoxes.
#' @export
#' @examples
#' \dontrun{
#'   # get measurements from all boxes on the phenomenon 'PM10' from the last 48h
#'   m = osem_measurements('PM10')
#'
#'   # get measurements from all mobile boxes on the phenomenon 'PM10' from the last 48h
#'   m = osem_measurements('PM10', exposure = 'mobile')
#'
#'   # get measurements and cache them locally in the working directory.
#'   # subsequent identical requests will load from the cache instead, ensuring
#'   # reproducibility and saving time and bandwidth!
#'   m = osem_measurements('PM10', exposure = 'mobile', cache = getwd())
#'   m = osem_measurements('PM10', exposure = 'mobile', cache = getwd())
#'
#'   # get measurements returning a custom selection of columns
#'   m = osem_measurements('PM10', exposure = 'mobile', columns = c(
#'     'value',
#'     'boxId',
#'     'sensorType',
#'     'lat',
#'     'lon',
#'     'height'
#'   ))
#' }
osem_measurements.default = function (x, ...) {
  bbox = structure(c(-180, -90, 180, 90), class = 'bbox')
  osem_measurements(bbox, x, ...)
}

# ==============================================================================
#
#' @describeIn osem_measurements Get measurements by a spatial filter.
#' @export
#' @examples
#' \dontrun{
#'   # get measurements from sensors within a custom WGS84 bounding box
#'   bbox = structure(c(7, 51, 8, 52), class = 'bbox')
#'   m = osem_measurements(bbox, 'Temperatur')
#'
#'   # construct a bounding box 12km around berlin using the sf package,
#'   # and get measurements from stations within that box
#'   library(sf)
#'   library(units)
#'   bbox2 = st_point(c(13.4034, 52.5120)) %>%
#'     st_sfc(crs = 4326) %>%
#'     st_transform(3857) %>% # allow setting a buffer in meters
#'     st_buffer(set_units(12, km)) %>%
#'     st_transform(4326) %>% # the opensensemap expects WGS 84
#'     st_bbox()
#'   m = osem_measurements(bbox2, 'Temperatur', exposure = 'outdoor')
#'
#'   # construct a bounding box from two points,
#'   # and get measurements from stations within that box
#'   points = st_multipoint(matrix(c(7.5, 7.8, 51.7, 52), 2, 2))
#'   bbox3 = st_bbox(points)
#'   m = osem_measurements(bbox2, 'Temperatur', exposure = 'outdoor')
#' }
osem_measurements.bbox = function (x, phenomenon, exposure = NA,
                                   from = NA, to = NA, columns = NA,
                                   ...,
                                   endpoint = osem_endpoint(),
                                   progress = TRUE,
                                   cache = NA) {
  bbox = x
  environment() %>%
    as.list() %>%
    parse_get_measurements_params() %>%
    paged_measurements_req()
}

# ==============================================================================
#
#' @describeIn osem_measurements Get measurements from a set of senseBoxes.
#' @export
#' @examples
#' \donttest{
#'   # get measurements from a set of boxes
#'   b = osem_boxes(grouptag = 'ifgi')
#'   m4 = osem_measurements(b, phenomenon = 'Temperatur')
#'
#'   # ...or a single box
#'   b = osem_box('57000b8745fd40c8196ad04c')
#'   m5 = osem_measurements(b, phenomenon = 'Temperatur')
#'
#'   # get measurements from a single box on the from the last 40 days.
#'   # requests are paged for long time frames, so the APIs limitation
#'   # does not apply!
#'   library(lubridate)
#'   m1 = osem_measurements(
#'     b,
#'     'Temperatur',
#'     to = now(),
#'     from = now() - days(40)
#'   )
#' }
osem_measurements.sensebox = function (x, phenomenon, exposure = NA,
                                       from = NA, to = NA, columns = NA,
                                       ...,
                                       endpoint = osem_endpoint(),
                                       progress = TRUE,
                                       cache = NA) {
  boxes = x
  environment() %>%
    as.list() %>%
    parse_get_measurements_params() %>%
    paged_measurements_req()
}

# ==============================================================================
#
#' Validates and parses the Parameters for use in \code{osem_measurements()}
#' and sets a default selection of columns, if unspecified.
#' Dates are not stringified!
#'
#' @param params A named \code{list} of parameters
#' @return A named \code{list} of parsed parameters.
#' @noRd
parse_get_measurements_params = function (params) {
  if (is.symbol(params$phenomenon) || is.null(params$phenomenon) || is.na(params$phenomenon))
    stop('Parameter "phenomenon" is required')

  if (
    (!is.na(params$from) && is.na(params$to)) ||
    (!is.na(params$to) && is.na(params$from))
  ) stop('specify "from" only together with "to"')

  if (
    (!is.null(params$bbox) && !is.null(params$boxes)) ||
    (is.null(params$bbox) && is.null(params$boxes))
  ) stop('Specify either "bbox" or "boxes"')

  query = list(
    endpoint = params$endpoint,
    phenomenon = params$phenomenon,
    progress = params$progress,
    cache = params$cache
  )

  if (!is.null(params$boxes))  query$boxId = paste(params$boxes$X_id, collapse = ',')
  if (!is.null(params$bbox))   query$bbox = paste(params$bbox, collapse = ',')

  if (!is.na(params$from) && !is.na(params$to)) {
    parse_dateparams(params$from, params$to) # only for validation sideeffect
    query$`from-date` = date_as_utc(params$from)
    query$`to-date` = date_as_utc(params$to)
  }

  if (!is.na(params$exposure)) query$exposure = params$exposure
  if (!any(is.na(params$columns)))
    query$columns = paste(params$columns, collapse = ',')
  else
    query$columns = 'value,createdAt,lon,lat,sensorId,unit'

  query
}

paged_measurements_req = function (query) {
  # no paged requests when dates are not provided
  if (!all(c('from-date', 'to-date') %in% names(query)))
    return(do.call(get_measurements_, query))

  # auto paging: make a request for one 31day interval each (max supprted length)
  # generate a list 31day intervals
  from = query$`from-date`
  to = query$`to-date`
  dates = list()
  while (from < to) {
    in31days = from + as.difftime(31, units = 'days')
    dates = append(dates, list(list(from = from, to = min(in31days, to))))
    from = in31days + as.difftime(1, units = 'secs')
  }

  # use the dates as pages for multiple requests
  df = lapply(dates, function(page) {
    query$`from-date` = date_as_isostring(page$from)
    query$`to-date` = date_as_isostring(page$to)
    res = do.call(get_measurements_, query)

    if (query$progress && !is_non_interactive())
      cat(paste(query$`from-date`, query$`to-date`, sep = ' - '), '\n')

    res
  }) %>%
    dplyr::bind_rows()

  # coerce all character columns (sensorId, unit, ...) to factors AFTER binding
  df[sapply(df, is.character)] = lapply(df[sapply(df, is.character)], as.factor)
  df
}

Try the opensensmapr package in your browser

Any scripts or data that you put into this service are public.

opensensmapr documentation built on March 31, 2023, 11:45 p.m.