parseDbl <- function(header, field, name) {
header %>%
stringr::str_subset(pattern = field) %>%
stringr::str_split(pattern = "=") %>%
purrr::map_chr(2) %>%
as.numeric() %>%
tibble::as_tibble() %>%
purrr::set_names(name)
}
parseBuild <- function(header, field, prefix) {
.parsed <- header %>%
stringr::str_subset(pattern = field) %>%
stringr::str_split(pattern = "=") %>%
purrr::map_chr(2) %>%
tibble::as_tibble() %>%
dplyr::rename(Build = .data$value) %>%
dplyr::mutate(
Version = stringr::str_extract(string = .data$Build,
pattern = "[0-9]+\\.[0-9]+"),
Version = as.numeric(.data$Version)
) %>%
dplyr::mutate(
Revision = stringr::str_extract(string = .data$Build,
pattern = "[0-9]+$"),
Revision = as.numeric(.data$Revision)
)
.parsed_named <- purrr::set_names(.parsed, nm = stringr::str_c(prefix, names(.parsed)))
return(.parsed_named)
}
parseTimestamp <- function(header, field, name) {
.tz <- Sys.timezone()
if (! (.tz %in% OlsonNames())) {
Sys.setenv(TZ = "UTC")
}
.timezone <-
header %>%
stringr::str_subset(pattern = field) %>%
stringr::str_split(pattern = "=") %>%
purrr::map_chr(2) %>%
lubridate::ymd_hms(tz = Sys.timezone()) %>%
tibble::as_tibble() %>%
purrr::set_names(name)
if (! (.tz %in% OlsonNames())) {
Sys.unsetenv("TZ")
}
return(.timezone)
}
parseTaxonomic <- function(header, statistic) {
.normpos_version <- parseBuild(header, "normpos.version", "") %>%
dplyr::select(.data$Version) %>%
as.numeric()
.field <- stringr::str_interp("Taxonomic_Distance_${statistic}")
.taxonomic <- ifelse(.normpos_version >= 2.4,
as.numeric(parseDbl(header, .field, "x")),
as.numeric(NA))
.tax_tibble <- purrr::set_names(tibble::tibble(.taxonomic),
stringr::str_replace_all(.field, "_", ""))
return(.tax_tibble)
}
parseCalcposFilter <- function(filter) {
tibble::tibble(
Name = filter %>%
stringr::str_split(pattern = "=") %>%
purrr::map_chr(2),
Application = "Calcpos"
)
}
parseNormposFilter <- function(filter, sensors) {
tibble::tibble(
Socket = filter %>%
stringr::str_split(pattern = "=") %>%
purrr::map_chr(2) %>%
stringr::str_split(pattern = ",", simplify = TRUE) %>%
as.numeric(),
Sensor = sensors[.data$Socket],
Name = filter %>%
stringr::str_split(pattern = "\\.") %>%
purrr::map_chr(2) %>%
stringr::str_split(pattern = "=") %>%
purrr::map_chr(1),
Application = "Normpos"
)
}
#' Read the Header of the .pos File for a Sweep
#'
#' Read the header of a \code{.pos} file, which contains the metadata for the
#' positional data recorded during a sweep.
#'
#' \code{ReadSweepMetadata} is vectorized over both \code{pos} and \code{sensors}.
#' If \code{pos} is a vector of more than one path to a \code{.pos} file, and
#' if \code{sensors} is a character vector, then the elements of \code{sensors}
#' are recycled as the sensor-names for each \code{.pos} file. To specify
#' different sensor names for each \code{.pos} file, \code{sensors} should be
#' a list with the same length as \code{pos} and whose elements are character
#' vectors.
#'
#' @param pos A character vector of paths to \code{.pos} files.
#' @param sensors A character vector or list of character vectors, the
#' names for sensors that were attached to the participant and connected to
#' sockets in the Carstens Sensin box. The position of a name in this vector
#' is interpreted as the number of the socket to which the sensor was connected.
#' Default is \code{character()}. See \code{\link{FormatSensors}}.
#'
#' @return If \code{pos} is a single character string, then a single-row data
#' table with the following variables:
#' @template sweep-metadata
#'
#' If \code{pos} is a character vector with more than one element, then a list
#' whose elements are data tables structured as described above.
#'
#' @export
ReadSweepMetadata <- function(pos, sensors = character()) {
.ReadSinglePOS <- function(.pos, .sens = sensors) {
if (!is.character(.pos)) {
stop(".ReadSinglePOS requires a character string as the .pos argument")
}
if (length(.pos) > 1) {
stop(".ReadSinglePOS can only handle a single path in the .pos argument")
}
if (!is.character(.sens)) {
stop(".ReadSinglePOS requires a character vector as the .sens argument ")
}
# Read the lines of the `.pos` file. Select just the header.
.lines <- readr::read_lines(.pos)
.header <- .lines[1:(match("", .lines) - 1)]
# Format the names of the `sensors`.
.n <- as.numeric(parseDbl(.header, field = "NumberOfChannels", name = "x"))
.sensors <- FormatSensors(sensors = .sens, n = .n)
# Parse the filters applied by cs5normpos.
.normpos_filters <- .header %>%
stringr::str_subset(pattern = "normpos") %>%
stringi::stri_subset_regex(pattern = "version|timestamp|Taxonomic_Distance",
negate = TRUE) %>%
purrr::map(parseNormposFilter, sensors = .sensors) %>%
purrr::reduce(dplyr::bind_rows)
# Parse the filter applied by cs5calcpos.
.calcpos_filters <- .header %>%
stringr::str_subset(pattern = "calcpos") %>%
stringi::stri_subset_regex(pattern = "version|timestamp", negate = TRUE) %>%
parseCalcposFilter() %>%
dplyr::sample_n(size = nrow(.normpos_filters), replace = TRUE) %>%
dplyr::bind_cols(dplyr::select(.normpos_filters, .data$Socket, .data$Sensor)) %>%
dplyr::select(.data$Socket, .data$Sensor, .data$Name, .data$Application)
# Bind the normpos-filters and the calcpos-filters.
.post_filters <- .calcpos_filters %>%
dplyr::bind_rows(.normpos_filters) %>%
dplyr::arrange(.data$Socket, .data$Application)
.metadata <- tibble::tibble(
Sweep = basename(.pos) %>%
stringr::str_replace(pattern = ".pos", replacement = ""),
Format = .header %>%
stringr::str_subset(pattern = "AG50xDATA")
) %>%
dplyr::bind_cols(
parseDbl(header = .header,
field = "SamplingFrequency",
name = "SamplingRate"),
parseBuild(header = .header,
field = "sweepsaver.version",
prefix = "Sweepsaver"),
parseTimestamp(header = .header,
field = "recorded",
name = "SweepsaverTimestamp"),
parseBuild(header = .header,
field = "calcpos.version",
prefix = "Calcpos"),
parseTimestamp(header = .header,
field = "calcpos.timestamp",
name = "CalcposTimestamp"),
parseBuild(header = .header,
field = "normpos.version",
prefix = "Normpos"),
parseTimestamp(header = .header,
field = "normpos.timestamp",
name = "NormposTimestamp"),
parseTaxonomic(header = .header,
statistic = "Mean"),
parseTaxonomic(header = .header,
statistic = "StdDev")
) %>%
dplyr::mutate(PostFilters = list(.post_filters))
return(.metadata)
}
if (length(pos) > 1) {
if (is.character(sensors)) {
sensors <- rep(list(sensors), times = length(pos))
} else {
if (is.list(sensors)) {
if (length(sensors) == 1) {
sensors <- rep(sensors, times = length(pos))
} else if (length(sensors) != length(pos)) {
.stop_msg <- sprintf("length of @sensors list (%d) must be either length of @pos (%d) or 1.",
length(sensors), length(pos))
stop(.stop_msg)
}
}
}
.sweep_metadata <- purrr::map2(pos, sensors, .ReadSinglePOS)
} else {
.sweep_metadata <- .ReadSinglePOS(pos, sensors)
}
return(.sweep_metadata)
}
#' Read the Contents of the .txt File for a Sweep
#'
#' Read the contents of a \code{.txt} file, which contains the positional data
#' recorded during a sweep.
#'
#' \code{ReadSweepData} is vectorized over both \code{txt} and \code{sensors}.
#' If \code{txt} is a vector of more than one path to a \code{.txt} file, and
#' if \code{sensors} is a character vector, then the elements of \code{sensors}
#' are recycled as the sensor-names for each \code{.txt} file. To specify
#' different sensor names for each \code{.txt} file, \code{sensors} should be
#' a list with the same length as \code{txt} and whose elements are character
#' vectors.
#'
#' @param txt A character vector of paths to \code{.txt} files.
#' @param sensors A character vector or list of character vectors, the
#' names for sensors that were attached to the participant and connected to
#' sockets in the Carstens Sensin box. The position of a name in this vector
#' is interpreted as the number of the socket to which the sensor was connected.
#' Default is \code{character()}. See \code{\link{FormatSensors}}.
#' @param n A numeric, the number of data channels that are recorded
#' per sensor. Default is \code{7}, which is consistent with Carstens data
#' format \code{AG50xDATA_V003}, under which each sensor comprises data
#' channels: \code{x}, \code{y}, \code{z}, \code{phi}, \code{theta},
#' \code{rms}, \code{extra}.
#' See \code{\link{FormatChannels}}.
#' @param dropExtra If \code{TRUE}, then the \code{extra} channel for each sensor
#' is dropped from the data, since this channel is only a placeholder for
#' future development by Carstens and contains no data.
#' If \code{FALSE}, then the \code{extra} channels are kept.
#'
#' @return If \code{txt} is a single character string, then a data table,
#' the names of whose variables are determined by expanding the \code{sensors}
#' argument through \code{\link{FormatChannels}}, and optionally dropping
#' the channels suffixed by \code{extra}.
#'
#' If \code{txt} is a character vector with more than one element, then a list
#' whose elements are data tables structured as described above.
#'
#' @export
ReadSweepData <- function(txt, sensors = character(), n = 7, dropExtra = TRUE) {
.ReadSingleTXT <- function(.txt, .sens = sensors, .n = n, .drop = dropExtra) {
if (!is.character(.txt)) {
stop(".ReadSingleTXT requires a character string as the .txt argument")
}
if (length(.txt) > 1) {
stop(".ReadSingleTXT can only handle a single path in the .txt argument")
}
if (!is.character(.sens)) {
stop(".ReadSingleTXT requires a character vector as the .sens argument ")
}
.channel_data <- suppressMessages(readr::read_delim(.txt, delim = "\t", col_names = FALSE))
.channel_names <- FormatChannels(sensors = .sens, n = ncol(.channel_data)/.n)
.sweep_data <- purrr::set_names(.channel_data, .channel_names)
if (dropExtra) {
.sweep_data <- dplyr::select(.sweep_data, -dplyr::ends_with("extra"))
}
return(.sweep_data)
}
if (length(txt) > 1) {
if (is.character(sensors)) {
sensors <- rep(list(sensors), times = length(txt))
} else {
if (is.list(sensors)) {
if (length(sensors) == 1) {
sensors <- rep(sensors, times = length(txt))
} else if (length(sensors) != length(txt)) {
.stop_msg <- sprintf("length of @sensors list (%d) must be either length of @txt (%d) or 1.",
length(sensors), length(txt))
stop(.stop_msg)
}
}
}
.sweep_data <- purrr::map2(txt, sensors, .ReadSingleTXT, .n = n, .drop = dropExtra)
} else {
.sweep_data <- .ReadSingleTXT(txt, sensors, .n = n, .drop = dropExtra)
}
return(.sweep_data)
}
#' Read the Sensor Data Recorded During a Sweep
#'
#' Read the head-corrected (and biteplane-rotated) position and rotation data from a
#' \code{.txt} file and its metadata from the header of a \code{.pos} file.
#'
#' \code{ReadSweep} is vectorized over \code{pos}, \code{txt}, and \code{sensors}.
#' \code{pos} and \code{txt} should be vectors of the same length ordered such
#' that the positions on these vectors correspond to the \code{.pos} and \code{.txt}
#' files, respectively, of the same sweep. Alternatively, multiple sweeps can
#' be specified by passing a character vector to the \code{sweep} argument.
#' If multiple sweeps are specified, and if \code{sensors} is a character vector,
#' then the elements of \code{sensors} are recycled as the sensor-names for each
#' sweep. To specify different sensor names for each sweep, \code{sensors} should be
#' a list with the same length as the number of sweeps and whose elements are
#' character vectors.
#'
#' @param pos A character vector of paths to \code{.pos} files.
#' @param txt A character vector of paths to \code{.txt} files.
#' @param sweep A character vector, the path to and basenames (without extensions)
#' of sweeps. The names of the \code{.pos} and \code{.txt} files for the
#' sweeps will be constructed by appending the appropriate file extensions.
#' Default is \code{NULL}. If not \code{NULL}, then \code{-.pos} and
#' \code{-.txt} completions of \code{sweep} override any provided values of
#' the \code{pos} and \code{txt} arguments.
#' @param sensors A character vector, names for sensors that were attached to
#' the participant and connected to sockets in the Carstens Sensin box. The
#' position of a name in this vector is interpreted as the number of the
#' socket to which the sensor was connected. Default is \code{character()}.
#' See \code{\link{FormatSensors}} and \code{\link{FormatChannels}}.
#' @param dropExtra If \code{TRUE}, then the \code{extra} channel for each sensor
#' is dropped from the data, since this channel is only a placeholder for
#' future development by Carstens and contains no data.
#' If \code{FALSE}, then the \code{extra} channels are kept.
#' @param simplify If \code{TRUE}, then the data tables for the sweeps
#' are row-binded together into a single data table.
#' If \code{FALSE}, then the data tables for the sweeps are returned as
#' elements of a list.
#'
#' @return A list or nested data frame with the following variables:
#' @template sweep-metadata
#' @template sweep-data
#'
#' @export
ReadSweep <- function(pos = "", txt = "", sweep = NULL, sensors = character(), dropExtra = TRUE, simplify = TRUE) {
.ReadSingleSweep <- function(.pos, .txt, .sens = sensors, .drop = dropExtra) {
if (!is.character(.pos)) {
stop(".ReadSingleSweep requires a character string as the .pos argument")
}
if (length(.pos) > 1) {
stop(".ReadSingleSweep can only handle a single path in the .pos argument")
}
if (!is.character(.txt)) {
stop(".ReadSingleSweep requires a character string as the .txt argument")
}
if (length(.txt) > 1) {
stop(".ReadSingleSweep can only handle a single path in the .txt argument")
}
if (!is.character(.sens)) {
stop(".ReadSingleSweep requires a character vector as the .sens argument ")
}
.metadata <- ReadSweepMetadata(pos = .pos, sensors = .sens)
.active_chs <-
.metadata %>%
dplyr::pull(.data$PostFilters) %>%
purrr::flatten_df() %>%
dplyr::filter(.data$Application == "Normpos") %>%
dplyr::pull(.data$Sensor) %>%
paste(collapse = "|")
.channel_data <-
ReadSweepData(txt = .txt, sensors = .sens, dropExtra = .drop) %>%
dplyr::select(dplyr::matches(.active_chs))
.time_samples <- seq(from = 0, by = 1/dplyr::pull(.metadata, .data$SamplingRate),
length.out = nrow(.channel_data))
.time_data <- tibble::tibble(Time = .time_samples)
.sensor_data <- dplyr::bind_cols(.time_data, .channel_data)
.sweep <- list(dplyr::mutate(.metadata, SensorData = list(.sensor_data)))
.named <- purrr::set_names(.sweep, stringr::str_replace(basename(.pos), "\\.pos$", ""))
return(.named)
}
if (!is.null(sweep)) {
pos <- sprintf(fmt = "%s.pos", sweep)
txt <- sprintf(fmt = "%s.txt", sweep)
}
if (length(pos) != length(txt)) {
.stop_msg <- sprintf("The length of @pos (%d) must be equal to the length of @txt (%d)",
length(pos), length(txt))
stop(.stop_msg)
}
if (is.character(sensors)) {
sensors <- list(sensors)
}
if (length(sensors) == 1) {
sensors <- rep(sensors, times = length(txt))
} else if (length(sensors) != length(txt)) {
.stop_msg <- sprintf("length of @sensors list (%d) must be either length of @txt (%d) or 1.",
length(sensors), length(txt))
stop(.stop_msg)
}
.sweeps <- purrr::pmap(list(pos, txt, sensors), function(.p, .t, .s) {
.ReadSingleSweep(.pos = .p, .txt = .t, .sens = .s, .drop = dropExtra)
})
if (simplify) {
if (length(.sweeps) == 1) {
.sweeps <- purrr::flatten_df(.sweeps)
} else {
.sweeps <- purrr::reduce(.sweeps, dplyr::bind_rows)
}
} else {
.sweeps <- purrr::flatten(.sweeps)
}
return(.sweeps)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.