├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── pkgdown.yaml ├── .gitignore ├── DESCRIPTION ├── NAMESPACE ├── NEWS.md ├── R ├── RcppExports.R ├── adjust_message_time.R ├── check_consistency_flag.R ├── check_logical_flag.R ├── check_string_parameter.R ├── check_that_compiled.R ├── compute_cyclopean_samples.R ├── convert_codes.R ├── data.R ├── extract_aois.R ├── extract_blinks.R ├── extract_display_coords.R ├── extract_fixations.R ├── extract_saccades.R ├── extract_triggers.R ├── extract_variables.R ├── eyelinkReader-classes.R ├── eyelinkReader-package.R ├── logical_index_for_sample_attributes.R ├── plot.R ├── print.R ├── read_edf.R ├── read_preamble.R └── zzz.R ├── README.md ├── _pkgdown.yml ├── data └── gaze.rda ├── eyelinkReader.Rproj ├── inst ├── WORDLIST ├── cpp │ └── edf_interface.cpp └── extdata │ └── example.edf ├── man ├── adjust_message_time.Rd ├── check_consistency_flag.Rd ├── check_logical_flag.Rd ├── check_string_parameter.Rd ├── check_that_compiled.Rd ├── compiled_library_status.Rd ├── compute_cyclopean_samples.Rd ├── convert_NAs.Rd ├── convert_header_codes.Rd ├── convert_recording_codes.Rd ├── dot-onAttach.Rd ├── dot-onLoad.Rd ├── extract_AOIs.Rd ├── extract_blinks.Rd ├── extract_display_coords.Rd ├── extract_fixations.Rd ├── extract_saccades.Rd ├── extract_triggers.Rd ├── extract_variables.Rd ├── eyelinkReader.Rd ├── eyelinkRecording-class.Rd ├── figures │ └── logo.svg ├── gaze.Rd ├── logical_index_for_sample_attributes.Rd ├── plot.eyelinkRecording.Rd ├── print.eyelinkRecording.Rd ├── read_edf.Rd ├── read_edf_file.Rd ├── read_preamble.Rd └── read_preamble_str.Rd ├── pkgdown └── favicon │ ├── apple-touch-icon.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── site.webmanifest │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png ├── src ├── .gitignore ├── Makevars ├── Makevars.win ├── RcppExports.cpp ├── compiled_library_status.cpp ├── convert_NAs.cpp ├── read_edf_file.cpp └── read_preamble_str.cpp ├── tests ├── testthat.R └── testthat │ ├── test-safety_checks.R │ └── test-sample-attributes.R └── vignettes ├── .gitignore ├── installation.Rmd ├── plotting.Rmd └── usage.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^_pkgdown\.yml$ 4 | ^docs$ 5 | ^pkgdown$ 6 | ^\.github$ 7 | eyelinkReader.svg 8 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/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 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | name: pkgdown.yaml 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | pkgdown: 17 | runs-on: ubuntu-latest 18 | # Only restrict concurrency for non-PR jobs 19 | concurrency: 20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | permissions: 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::pkgdown, local::. 37 | needs: website 38 | 39 | - name: Build site 40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 41 | shell: Rscript {0} 42 | 43 | - name: Deploy to GitHub pages 🚀 44 | if: github.event_name != 'pull_request' 45 | uses: JamesIves/github-pages-deploy-action@v4.5.0 46 | with: 47 | clean: false 48 | branch: gh-pages 49 | folder: docs 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | docs 6 | inst/doc 7 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: eyelinkReader 2 | Title: Import Gaze Data for EyeLink Eye Tracker 3 | Version: 1.0.3 4 | Date: 2024-10-08 5 | Authors@R: 6 | person(given = "Alexander", 7 | family = "Pastukhov", 8 | role = c("aut", "cre"), 9 | email ="pastukhov.alexander@gmail.com", 10 | comment = c(ORCID = "0000-0002-8738-8591")) 11 | Description: Import gaze data from edf files generated by 12 | the SR Research EyeLink eye tracker. Gaze data, 13 | both recorded events and samples, is imported per trial. The package allows to extract 14 | events of interest, such as saccades, blinks, etc. as well as 15 | recorded variables and custom events (areas of interest, triggers) into separate tables. 16 | The package requires EDF API library that can be obtained at . 17 | License: GPL (>= 3) 18 | URL: https://github.com/alexander-pastukhov/eyelinkReader/, 19 | https://alexander-pastukhov.github.io/eyelinkReader/ 20 | BugReports: https://github.com/alexander-pastukhov/eyelinkReader/issues 21 | Depends: 22 | R (>= 4.1.0), 23 | RcppProgress, 24 | rlang 25 | Encoding: UTF-8 26 | NeedsCompilation: yes 27 | VignetteBuilder: 28 | knitr 29 | LazyData: true 30 | LinkingTo: Rcpp, RcppProgress 31 | Imports: 32 | dplyr, 33 | fs, 34 | purrr, 35 | Rcpp, 36 | stringr, 37 | tidyr, 38 | methods, 39 | ggplot2 40 | RoxygenNote: 7.3.2 41 | Roxygen: list(markdown = TRUE) 42 | SystemRequirements: GNU make 43 | Suggests: 44 | rmarkdown, 45 | knitr, 46 | testthat (>= 3.0.0) 47 | Config/testthat/edition: 3 48 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(adjust_message_time,data.frame) 4 | S3method(adjust_message_time,eyelinkRecording) 5 | S3method(compute_cyclopean_samples,data.frame) 6 | S3method(compute_cyclopean_samples,eyelinkRecording) 7 | S3method(extract_AOIs,data.frame) 8 | S3method(extract_AOIs,eyelinkRecording) 9 | S3method(extract_blinks,data.frame) 10 | S3method(extract_blinks,eyelinkRecording) 11 | S3method(extract_display_coords,data.frame) 12 | S3method(extract_display_coords,eyelinkRecording) 13 | S3method(extract_fixations,data.frame) 14 | S3method(extract_fixations,eyelinkRecording) 15 | S3method(extract_saccades,data.frame) 16 | S3method(extract_saccades,eyelinkRecording) 17 | S3method(extract_triggers,data.frame) 18 | S3method(extract_triggers,eyelinkRecording) 19 | S3method(extract_variables,data.frame) 20 | S3method(extract_variables,eyelinkRecording) 21 | S3method(plot,eyelinkRecording) 22 | S3method(print,eyelinkPreamble) 23 | S3method(print,eyelinkRecording) 24 | export(.onAttach) 25 | export(.onLoad) 26 | export(adjust_message_time) 27 | export(check_consistency_flag) 28 | export(check_logical_flag) 29 | export(check_string_parameter) 30 | export(check_that_compiled) 31 | export(compiled_library_status) 32 | export(compute_cyclopean_samples) 33 | export(convert_NAs) 34 | export(convert_header_codes) 35 | export(convert_recording_codes) 36 | export(extract_AOIs) 37 | export(extract_blinks) 38 | export(extract_display_coords) 39 | export(extract_fixations) 40 | export(extract_saccades) 41 | export(extract_triggers) 42 | export(extract_variables) 43 | export(logical_index_for_sample_attributes) 44 | export(read_edf) 45 | export(read_edf_file) 46 | export(read_preamble) 47 | export(read_preamble_str) 48 | import(Rcpp) 49 | import(RcppProgress) 50 | importFrom(Rcpp,evalCpp) 51 | importFrom(dplyr,"%>%") 52 | importFrom(dplyr,all_of) 53 | importFrom(dplyr,arrange) 54 | importFrom(dplyr,filter) 55 | importFrom(dplyr,mutate) 56 | importFrom(dplyr,mutate_if) 57 | importFrom(dplyr,select) 58 | importFrom(fs,file_exists) 59 | importFrom(ggplot2,aes_string) 60 | importFrom(ggplot2,coord_equal) 61 | importFrom(ggplot2,geom_point) 62 | importFrom(ggplot2,geom_segment) 63 | importFrom(ggplot2,ggplot) 64 | importFrom(ggplot2,labs) 65 | importFrom(ggplot2,scale_x_continuous) 66 | importFrom(ggplot2,scale_y_reverse) 67 | importFrom(methods,hasArg) 68 | importFrom(methods,is) 69 | importFrom(purrr,map_chr) 70 | importFrom(purrr,map_int) 71 | importFrom(rlang,.data) 72 | importFrom(stringr,str_detect) 73 | importFrom(stringr,str_extract) 74 | importFrom(stringr,str_remove) 75 | importFrom(stringr,str_remove_all) 76 | importFrom(stringr,str_replace) 77 | importFrom(stringr,str_split) 78 | importFrom(tidyr,separate) 79 | useDynLib(eyelinkReader) 80 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # eyelinkReader 1.0.0 2 | * Initial CRAN Release 3 | 4 | # eyelinkReader 1.0.1 5 | ## Enhancements 6 | * Optional GRUB for plotting 7 | 8 | ## Bug Fixes 9 | * Internal documentation structure changes. 10 | 11 | # eyelinkReader 1.0.2 12 | ## Bug Fixes 13 | * Correct path for compilation on Mac 14 | * Bug fix in plot function 15 | * UTF-8 encoding for event message strings 16 | * AOI parsing function now allows for labels with white spaces 17 | 18 | # eyelinkReader 1.0.3 19 | ## Bug Fixes 20 | * Format string for the error message 21 | -------------------------------------------------------------------------------- /R/RcppExports.R: -------------------------------------------------------------------------------- 1 | # Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | #' @title Status of compiled library 5 | #' @description Return status of compiled library 6 | #' @return logical 7 | #' @export 8 | #' @examples 9 | #' compiled_library_status() 10 | compiled_library_status <- function() { 11 | .Call('_eyelinkReader_compiled_library_status', PACKAGE = 'eyelinkReader') 12 | } 13 | 14 | #' @title Convert -32767 (missing info) to NA 15 | #' @description Converts all -32767 (smallest INT16 value indicating missing info) to NA. 16 | #' You don't need to call this function directly, as it is automatically evoked within 17 | #' \code{\link{read_edf}} function. 18 | #' @param original_frame data.frame to be processed 19 | #' @return processed data.frame 20 | #' @export 21 | #' @examples 22 | #' \donttest{ 23 | #' data(gaze) 24 | #' gaze$samples <- convert_NAs(gaze$samples) 25 | #' } 26 | convert_NAs <- function(original_frame) { 27 | .Call('_eyelinkReader_convert_NAs', PACKAGE = 'eyelinkReader', original_frame) 28 | } 29 | 30 | #' @title Internal function that reads EDF file 31 | #' @description Reads EDF file into a list that contains events, samples, and recordings. 32 | #' DO NOT call this function directly. Instead, use read_edf function that implements 33 | #' parameter checks and additional postprocessing. 34 | #' @param filename full name of the EDF file 35 | #' @param consistency consistency check control (for the time stamps of the start 36 | #' and end events, etc). 0, no consistency check. 1, check consistency and report. 37 | #' 2, check consistency and fix. 38 | #' @param import_events load/skip loading events. 39 | #' @param import_recordings load/skip loading recordings. 40 | #' @param import_samples load/skip loading of samples. 41 | #' @param sample_attr_flag boolean vector that indicates which sample fields are to be stored 42 | #' @param start_marker_string event that marks trial start. Defaults to "TRIALID", if empty. 43 | #' @param end_marker_string event that marks trial end 44 | #' @param verbose whether to show progressbar and report number of trials 45 | #' @export 46 | #' @keywords internal 47 | #' @return contents of the EDF file. Please see read_edf for details. 48 | read_edf_file <- function(filename, consistency, import_events, import_recordings, import_samples, sample_attr_flag, start_marker_string, end_marker_string, verbose) { 49 | .Call('_eyelinkReader_read_edf_file', PACKAGE = 'eyelinkReader', filename, consistency, import_events, import_recordings, import_samples, sample_attr_flag, start_marker_string, end_marker_string, verbose) 50 | } 51 | 52 | #' @title Reads preamble of the EDF file as a single string. 53 | #' @description Reads preamble of the EDF file as a single string. 54 | #' Please, do not use this function directly. Instead, call \code{\link{read_preamble}} function 55 | #' that provides a more consistent interface. 56 | #' @return string with the preamble 57 | #' @export 58 | #' @keywords internal 59 | #' @examples 60 | #' \donttest{ 61 | #' if (eyelinkReader::compiled_library_status()) { 62 | #' read_preamble(system.file("extdata", "example.edf", package = "eyelinkReader")) 63 | #' } 64 | #' } 65 | read_preamble_str <- function(filename) { 66 | .Call('_eyelinkReader_read_preamble_str', PACKAGE = 'eyelinkReader', filename) 67 | } 68 | 69 | -------------------------------------------------------------------------------- /R/adjust_message_time.R: -------------------------------------------------------------------------------- 1 | #' Adjusts message time based on embedded text offset 2 | #' 3 | #' Uses text in the message to adjust its time. E.g., 4 | #' for a message \code{"-50 TARGET_ONSET"} that was sent at \code{105600} 5 | #' the actual onset occurred 50 milliseconds earlier (\code{-50}). The function 6 | #' adjusts the event timing and removes the timing offset information from 7 | #' the message. I.e., the example message becomes \code{"TARGET_ONSET"} and 8 | #' its time become \code{105550}. 9 | #' 10 | #' @param object An \code{\link{eyelinkRecording}} object or data.frame with events, 11 | #' i.e., \code{events} slot of the \code{\link{eyelinkRecording}} object. 12 | #' @param prefix String with a regular expression that defines the offset. 13 | #' Defaults to \code{"^[-+]?[:digit:]+[:space:]+"} (a string starts with a positive 14 | #' or negative integer offset followed by a white space and the rest of the message). 15 | #' 16 | #' @return Object of the same time as input, i.e., either a \code{\link{eyelinkRecording}} object 17 | #' with \emph{modified} \code{events} slot or a data.frame with offset-adjusted events. 18 | #' @export 19 | #' 20 | #' @examples 21 | #' data(gaze) 22 | #' 23 | #' # by passing events table 24 | #' adjusted_events <- adjust_message_time(gaze$events) 25 | #' 26 | #' # by passing the recording 27 | #' gaze <- adjust_message_time(gaze) 28 | 29 | adjust_message_time <- function(object, prefix) { UseMethod("adjust_message_time") } 30 | 31 | #' @rdname adjust_message_time 32 | #' @importFrom stringr str_detect str_extract str_replace 33 | #' @importFrom dplyr arrange 34 | #' @export 35 | adjust_message_time.data.frame <- function(object, prefix = "^[-+]?[:digit:]+[:space:]+"){ 36 | # find messages that need adjusting 37 | need_adjusting <- which(stringr::str_detect(object$message, prefix)) 38 | 39 | if (length(need_adjusting) > 0) { 40 | # get temporal offset as a number 41 | offset <- as.numeric(str_extract(object$message[need_adjusting], prefix)) 42 | 43 | # remove offset from the message 44 | object$message[need_adjusting] <- stringr::str_replace(object$message[need_adjusting], prefix, "") 45 | 46 | # adjust time 47 | object$sttime[need_adjusting] <- object$sttime[need_adjusting] + offset 48 | object$sttime_rel[need_adjusting] <- object$sttime_rel[need_adjusting] + offset 49 | 50 | # sort events based on adjusted time 51 | object <- dplyr::arrange(object, .data$sttime) 52 | } 53 | 54 | object 55 | } 56 | 57 | #' @rdname adjust_message_time 58 | #' @export 59 | adjust_message_time.eyelinkRecording <- function(object, prefix = "^[-+]?[:digit:]+[:space:]+"){ 60 | # check that events are in the recording at all 61 | if (!("events" %in% names(object))) { 62 | warning("No events in an eyelinkRecording object, nothing to do.") 63 | return(object) 64 | } 65 | 66 | # modify in place 67 | object$events <- adjust_message_time(object$events, prefix) 68 | object 69 | } 70 | -------------------------------------------------------------------------------- /R/check_consistency_flag.R: -------------------------------------------------------------------------------- 1 | #' Checks consistency flag, stops if invalid, returns code if valid. 2 | #' 3 | #' @param consistency consistency check control for the time stamps of the start 4 | #' and end events, etc. Could be \code{'no consistency check'} (0), 5 | #' \code{'check consistency and report'} (1), \code{'check consistency and fix'} (2). 6 | #' 7 | #' @return integer index 8 | #' @export 9 | #' @keywords internal 10 | #' 11 | #' @examples 12 | #' check_consistency_flag('no consistency check') 13 | check_consistency_flag <- function(consistency) { 14 | # converting consistency to integer constant that C-code understands 15 | requested_consistency <- factor(consistency, 16 | levels= c('no consistency check', 'check consistency and report', 'check consistency and fix')) 17 | if (length(requested_consistency) < 1) stop("Null value for consistency flag") 18 | if (length(requested_consistency) > 1) stop("Multiple values for consistency flag") 19 | if (is.na(requested_consistency)) stop(sprintf('Bad consistency check value "%s".', consistency)) 20 | 21 | # zero-based index 22 | as.numeric(requested_consistency) - 1 23 | } 24 | -------------------------------------------------------------------------------- /R/check_logical_flag.R: -------------------------------------------------------------------------------- 1 | #' Checks for validity of a logical flag, stops if not valid 2 | #' 3 | #' @param logical_flag Logical scalar 4 | #' 5 | #' @return logical, value of the logical_flag 6 | #' @export 7 | #' @keywords internal 8 | #' 9 | #' @examples 10 | #' import_samples <- TRUE 11 | #' check_logical_flag(import_samples) 12 | check_logical_flag <- function(logical_flag){ 13 | param_name <- as.character(match.call()[[2]]) 14 | if (length(logical_flag) != 1) stop(sprintf("%s must be a scalar logical value", param_name)) 15 | if (is.null(logical_flag)) stop(sprintf("%s must be a scalar logical value, not NULL", param_name)) 16 | if (!is.logical(logical_flag)) stop(sprintf("%s must be a scalar logical value", param_name)) 17 | if (is.na(logical_flag)) stop(sprintf("%s must be a scalar logical value, NA not allowed", param_name)) 18 | 19 | logical_flag 20 | } 21 | -------------------------------------------------------------------------------- /R/check_string_parameter.R: -------------------------------------------------------------------------------- 1 | #' Checks for validity of a string parameter, stops if not valid 2 | #' 3 | #' @param string_parameter String scalar 4 | #' 5 | #' @return character, value of the string_parameter 6 | #' @export 7 | #' @keywords internal 8 | #' 9 | #' @examples 10 | #' start_marker <- "TRIALID" 11 | #' check_string_parameter(start_marker) 12 | check_string_parameter <- function(string_parameter){ 13 | param_name <- as.character(match.call()[[2]]) 14 | if (length(string_parameter) != 1) stop(sprintf("%s must be a scalar string value", param_name)) 15 | if (is.null(string_parameter)) stop(sprintf("%s must be a scalar logical value, not NULL", param_name)) 16 | if (!is.character(string_parameter)) stop(sprintf("%s must be a scalar string value", param_name)) 17 | if (nchar(string_parameter) == 0) warning(sprintf("Empty string for %s parameter", param_name)) 18 | if (is.na(string_parameter)) stop(sprintf("%s must be a scalar logical value, NA not allowed", param_name)) 19 | 20 | string_parameter 21 | } 22 | -------------------------------------------------------------------------------- /R/check_that_compiled.R: -------------------------------------------------------------------------------- 1 | #' Checks whether EDF API library was present and 2 | #' interface was successfully be compiled 3 | #' 4 | #' @param fail_loudly logical, whether lack of compiled library means 5 | #' error (\code{TRUE}), just warning (\code{FALSE}), or silent (\code{NA}, 6 | #' for test use only). 7 | #' 8 | #' @export 9 | #' @return No return value. Stops the computation, if compiled interface to EDF API in missing. 10 | #' @keywords internal 11 | #' 12 | #' @examples 13 | #' check_that_compiled(fail_loudly = FALSE) 14 | check_that_compiled <- function(fail_loudly = TRUE){ 15 | # try to compile 16 | if (!eyelinkReader::compiled_library_status() && !is.na(fail_loudly)) { 17 | if (fail_loudly) { 18 | stop("Failed to compile interface to EDF API, function will return NULL. Please read the manual for further details.") 19 | } else { 20 | warning("Failed to compile interface to EDF API, function will return NULL. Please read the manual for further details.") 21 | } 22 | } 23 | 24 | eyelinkReader::compiled_library_status() 25 | } 26 | -------------------------------------------------------------------------------- /R/compute_cyclopean_samples.R: -------------------------------------------------------------------------------- 1 | #' Computes cyclopean samples by averaging over binocular data 2 | #' 3 | #' Computes cyclopean samples by averaging over binocular recorded properties 4 | #' such as \code{pxL}/\code{pxR}, \code{pyL}/\code{pyR}, \code{hxL}/\code{hxR}, 5 | #' etc. Uses function specified via \code{fun} 6 | #' parameter to compute the average with \code{na.rm = TRUE} option. 7 | #' In case of a monocular recording or when the information from one eye missing, 8 | #' uses information from one eye only, ignoring the other column. 9 | #' In both binocular and monocular recording cases, simplifies column names 10 | #' so that \code{pxL} and/or \code{pxR} are replaced 11 | #' with a single column \code{px}, \code{pyL}/\code{pyR} with \code{py}, etc. 12 | #' 13 | #' @param object Either an \code{\link{eyelinkRecording}} object or data.frame with samples, 14 | #' i.e., \code{samples} slot of the \code{\link{eyelinkRecording}} object. 15 | #' @param fun Function used to average across eyes, defaults to \code{\link{mean}}. 16 | #' 17 | #' @return Object of the same time as input, i.e., either a \code{\link{eyelinkRecording}} object 18 | #' with \emph{modified} \code{samples} slot or a data.frame with cyclopean samples. 19 | 20 | #' @export 21 | #' 22 | #' @examples 23 | #' data(gaze) 24 | #' 25 | #' # by passing samples table 26 | #' cyclopean_samples <- compute_cyclopean_samples(gaze$samples) 27 | #' 28 | #' # storing cyclopean samples as a separate table in recording 29 | #' gaze$cyclopean_samples <- compute_cyclopean_samples(gaze$samples) 30 | #' 31 | #' # by passing the recording, cyclopean samples replace original ones 32 | #' gaze <- compute_cyclopean_samples(gaze) 33 | compute_cyclopean_samples <- function(object, fun = mean) { UseMethod("compute_cyclopean_samples") } 34 | 35 | 36 | #' @rdname compute_cyclopean_samples 37 | #' @export 38 | #' @importFrom dplyr %>% select all_of mutate_if 39 | #' @importFrom tidyr separate 40 | #' @importFrom stringr str_detect str_remove 41 | #' @importFrom rlang .data 42 | compute_cyclopean_samples.data.frame <- function(object, fun = mean) { 43 | # figuring out columns that we need to compute the mean over 44 | i_eye_specific_column <- stringr::str_detect(names(object), "[L|R]$") 45 | eye_specific_columns <- names(object)[i_eye_specific_column] 46 | joint_eye_columns <- unique(stringr::str_remove(eye_specific_columns, "[L|R]$")) 47 | resulting_columns <- unique(stringr::str_remove(names(object), "[L|R]$")) 48 | 49 | # averaging over eye-specific columns 50 | for(current_column in joint_eye_columns){ 51 | eye_components <- eye_specific_columns[stringr::str_detect(eye_specific_columns, sprintf("^%s[L|R]$", current_column))] 52 | if (identical(fun, mean)) { 53 | # rowMeans are MUCH faster than applying mean to margin row 54 | object[[current_column]] <- rowMeans(object[, eye_components], na.rm = TRUE) 55 | } else { 56 | # universal solution for everything else 57 | object[[current_column]] <- apply(object[, eye_components], MARGIN = 1, FUN = fun, na.rm = TRUE) 58 | } 59 | } 60 | 61 | object %>% 62 | # drop original eye-specific columns 63 | select(all_of(resulting_columns)) %>% 64 | 65 | # convert NaN to NA because mean(c(NA, NA), na.rm = TRUE)) returns NaN, not NA 66 | mutate_if(is.numeric, ~ifelse(is.nan(.x), NA, .x)) 67 | } 68 | 69 | 70 | #' @rdname compute_cyclopean_samples 71 | #' @export 72 | compute_cyclopean_samples.eyelinkRecording <- function(object, fun = mean) { 73 | # check that samples are in the recording at all 74 | if (!("samples" %in% names(object))) { 75 | stop("No samples in an eyelinkRecording object.") 76 | } 77 | 78 | # modify in place 79 | object$samples <- compute_cyclopean_samples(object$samples, fun) 80 | object 81 | } 82 | -------------------------------------------------------------------------------- /R/convert_codes.R: -------------------------------------------------------------------------------- 1 | #' Converts integer constants in trial headers to factor with explicit labels 2 | #' 3 | #' @description Converts integer constants in trial headers to factor with explicit labels. 4 | #' Please refer to EDF API manual for further details. 5 | #' @param trial_headers data.frame that contains trial headers. 6 | #' 7 | #' @return a modified trial_headers table 8 | #' @keywords internal 9 | #' @export 10 | convert_header_codes <- function(trial_headers){ 11 | trial_headers$rec_state <- factor(trial_headers$rec_state, levels = c(0, 1), labels= c('END', 'START')) 12 | trial_headers$rec_record_type <- factor(trial_headers$rec_record_type, levels = c(1, 2, 3), labels= c('SAMPLES', 'EVENTS', 'SAMPLES and EVENTS')) 13 | trial_headers$rec_pupil_type <- factor(trial_headers$rec_pupil_type, levels = c(0, 1), labels= c('AREA', 'DIAMETER')) 14 | trial_headers$rec_recording_mode <- factor(trial_headers$rec_recording_mode, levels = c(0, 1), labels= c('PUPIL', 'CR')) 15 | # trial_headers$rec_pos_type <- factor(trial_headers$rec_pos_type, levels = c(0, 1, 2), labels= c('GAZE', 'HREF', 'RAW')) 16 | trial_headers$rec_eye <- factor(trial_headers$rec_eye, levels = c(1, 2, 3), labels= c('LEFT', 'RIGHT', 'LEFT and RIGHT')) 17 | 18 | return (trial_headers); 19 | } 20 | 21 | #' Converts integer constants in recordings to factor with explicit labels 22 | #' 23 | #' @description Converts integer constants in trial recordings information to factor with explicit labels. 24 | #' Please refer to EDF API manual for further details. 25 | #' @param trial_recordings data.frame that contains trial recordings. 26 | #' 27 | #' @return a modified trial_recordings table 28 | #' @keywords internal 29 | #' @export 30 | convert_recording_codes <- function(trial_recordings){ 31 | trial_recordingsstate <- factor(trial_recordings$state, levels = c(0, 1), labels= c('END', 'START')) 32 | trial_recordings$record_type <- factor(trial_recordings$record_type, levels = c(1, 2, 3), labels= c('SAMPLES', 'EVENTS', 'SAMPLES and EVENTS')) 33 | trial_recordings$pupil_type <- factor(trial_recordings$pupil_type, levels = c(0, 1), labels= c('AREA', 'DIAMETER')) 34 | trial_recordings$recording_mode <- factor(trial_recordings$recording_mode, levels = c(0, 1), labels= c('PUPIL', 'CR')) 35 | # trial_recordings$pos_type <- factor(trial_recordings$pos_type, levels = c(0, 1, 2), labels= c('GAZE', 'HREF', 'RAW')) 36 | trial_recordings$eye <- factor(trial_recordings$eye, levels = c(1, 2, 3), labels= c('LEFT', 'RIGHT', 'LEFT and RIGHT')) 37 | 38 | return (trial_recordings); 39 | } 40 | -------------------------------------------------------------------------------- /R/data.R: -------------------------------------------------------------------------------- 1 | #' Imported example.edf, events and samples 2 | #' 3 | #' An \code{\link{eyelinkRecording}} for _example.edf_ via 4 | #' \code{read_edf(system.file("extdata", "example.edf", package = "eyelinkReader"), import_samples = TRUE)}). 5 | #' Contains all extracted events including triggers, areas of interested, and display coordinates. The original 6 | #' recording consist of ten trials with a participant fixating on a target that jumps to a new location after 7 | #' one second and stays on the screen for another second. Includes all relevant events. 8 | #' 9 | #' See \code{\link{eyelinkRecording}} for details. 10 | #' @seealso \code{\link{eyelinkRecording}}, \code{\link{read_edf}} 11 | "gaze" 12 | -------------------------------------------------------------------------------- /R/extract_aois.R: -------------------------------------------------------------------------------- 1 | #' Extracts rectangular areas of interest (AOI) 2 | #' 3 | #' @description Extracts rectangular areas of interest (AOI), 4 | #' as defined by \code{"!V IAREA RECTANGLE"} command. 5 | #' Specifically, we expect it to be in format 6 | #' \code{!V IAREA RECTANGLE