diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 282279e..37cbfab 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' diff --git a/src/main/java/com/github/thed2lab/analysis/Analysis.java b/src/main/java/com/github/thed2lab/analysis/Analysis.java index bcc377a..e77cd77 100644 --- a/src/main/java/com/github/thed2lab/analysis/Analysis.java +++ b/src/main/java/com/github/thed2lab/analysis/Analysis.java @@ -1,5 +1,8 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.SCREEN_HEIGHT; +import static com.github.thed2lab.analysis.Constants.SCREEN_WIDTH; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -12,8 +15,6 @@ public class Analysis { * Its role is to iterate over each file, and process it into DataEntry objects for gaze, * validity, and fixations. */ - private final static int SCREEN_WIDTH = 1920; - private final static int SCREEN_HEIGHT = 1080; private final static int MIN_PATTERN_LENGTH = 3; private final static int MAX_PATTERN_LENGTH = 7; @@ -54,10 +55,10 @@ public boolean run() { System.out.println("Analyzing " + pName); // Build DataEntrys - DataEntry allGaze = FileHandler.buildDataEntry(f); - DataEntry validGaze = DataFilter.filterByValidity(allGaze); + DataEntry allGaze = DataFilter.applyScreenSize(FileHandler.buildDataEntry(f), SCREEN_WIDTH, SCREEN_HEIGHT); + DataEntry validGaze = DataFilter.filterByValidity(allGaze, SCREEN_WIDTH, SCREEN_HEIGHT); DataEntry fixations = DataFilter.filterByFixations(allGaze); - DataEntry validFixations = DataFilter.filterByValidity(fixations); + DataEntry validFixations = DataFilter.filterByValidity(fixations, SCREEN_HEIGHT, SCREEN_WIDTH); // Write DataEntrys to file validGaze.writeToCSV(pDirectory, pName + "_valid_all_gaze"); @@ -81,7 +82,7 @@ public boolean run() { allParticipantDGMs.add(dgms); // Generate AOIs - AreaOfInterests.generateAOIs(allGaze, pDirectory, pName); + AreaOfInterests.generateAOIs(allGaze, fixations, pDirectory, pName); // Generate windows Windows.generateWindows(allGaze, pDirectory, settings); @@ -149,9 +150,7 @@ public boolean run() { * @return a {@code List} where the first inner-list is the measure names, and second inner-list is the calculated values. */ static List> generateResults(DataEntry allGaze, DataEntry fixations) { - DataEntry validGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT); - DataEntry validAoiFixations = DataFilter.applyScreenSize(DataFilter.filterByValidity(fixations), SCREEN_WIDTH, SCREEN_HEIGHT); - var results = generateResultsHelper(validGaze, validGaze, validAoiFixations); + var results = generateResultsHelper(allGaze, allGaze, fixations); return results; } @@ -164,33 +163,35 @@ static List> generateResults(DataEntry allGaze, DataEntry fixations * @return a {@code List} where the first inner-list is the measure names, and second inner-list is the calculated values. */ static List> generateResults(DataEntry allGaze, DataEntry aoiGaze, DataEntry aoiFixations) { - DataEntry validAllGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT); - DataEntry validAoiGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT); - DataEntry validAoiFixations = DataFilter.applyScreenSize(DataFilter.filterByValidity(aoiFixations), SCREEN_WIDTH, SCREEN_HEIGHT); - var results = generateResultsHelper(validAllGaze, validAoiGaze, validAoiFixations); + var results = generateResultsHelper(allGaze, aoiGaze, aoiFixations); return results; } /** * Helper methods that generates the descriptive gaze measures. - * @param validAllGaze all gaze data, filtered by validity. - * @param validAoiGaze the gaze data that ocurred within an aoi, filtered by validity. For the whole screen, this is the same - * data as validAllGaze. - * @param validAoiFixation the gaze data that ocurred within an aoi, filtered by fixation and validity. - * @return a list the descriptive gaze measures where the first row is the headers and the second row is the values. + * @param allGaze all gaze data for the whole screen, with screen size applied. + * @param areaGaze the gaze data that ocurred within a target portion of screen, i.e., either the whole screen or an AOI, with screen + * size applied. + * @param areaFixations the gaze data that ocurred within a target portion of screen, i.e., either the whole screen or an AOI, + * and filtered by fixation with screen size applied. + * @return a {@code List} where the first inner-list is the measure names, and second inner-list is the calculated values. */ - private static List> generateResultsHelper(DataEntry validAllGaze, DataEntry validAoiGaze, DataEntry validAoiFixation) { + private static List> generateResultsHelper(DataEntry allGaze, DataEntry areaGaze, DataEntry areaFixations) { + // an argument could be made to filter before entering this function; there is a code smell due to blink rate and saccadeV changing + DataEntry validAllGaze = DataFilter.filterByValidity(allGaze, SCREEN_WIDTH, SCREEN_HEIGHT); + DataEntry validAreaGaze = DataFilter.filterByValidity(areaGaze, SCREEN_WIDTH, SCREEN_HEIGHT); + DataEntry validAreaFixations = DataFilter.filterByValidity(areaFixations, SCREEN_WIDTH, SCREEN_HEIGHT); LinkedHashMap resultsMap = new LinkedHashMap(); - resultsMap.putAll(Fixations.analyze(validAoiFixation)); - resultsMap.putAll(Saccades.analyze(validAoiFixation)); - resultsMap.putAll(SaccadeVelocity.analyze(validAllGaze, validAoiFixation)); - resultsMap.putAll(Angles.analyze(validAoiFixation)); - resultsMap.putAll(ConvexHull.analyze(validAoiFixation)); - resultsMap.putAll(GazeEntropy.analyze(validAoiFixation)); - resultsMap.putAll(Blinks.analyze(validAoiGaze)); - resultsMap.putAll(Gaze.analyze(validAoiGaze)); - resultsMap.putAll(Event.analyze(validAoiGaze)); + resultsMap.putAll(Fixations.analyze(validAreaFixations)); + resultsMap.putAll(Saccades.analyze(validAreaFixations)); + resultsMap.putAll(SaccadeVelocity.analyze(validAllGaze, validAreaFixations)); + resultsMap.putAll(Angles.analyze(validAreaFixations)); + resultsMap.putAll(ConvexHull.analyze(validAreaFixations)); + resultsMap.putAll(GazeEntropy.analyze(validAreaFixations)); + resultsMap.putAll(Blinks.analyze(areaGaze)); + resultsMap.putAll(Gaze.analyze(validAreaGaze)); + resultsMap.putAll(Event.analyze(validAreaGaze)); var resultsList = new ArrayList>(2); resultsList.add(new ArrayList<>(resultsMap.keySet())); diff --git a/src/main/java/com/github/thed2lab/analysis/Angles.java b/src/main/java/com/github/thed2lab/analysis/Angles.java index 7de333d..b7c8f70 100644 --- a/src/main/java/com/github/thed2lab/analysis/Angles.java +++ b/src/main/java/com/github/thed2lab/analysis/Angles.java @@ -1,12 +1,13 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.FIXATION_ID; +import static com.github.thed2lab.analysis.Constants.FIXATION_X; +import static com.github.thed2lab.analysis.Constants.FIXATION_Y; + import java.util.ArrayList; import java.util.LinkedHashMap; public class Angles { - final static String FIXATIONID_INDEX = "FPOGID"; - final static String FIXATIONX_INDEX = "FPOGX"; - final static String FIXATIONY_INDEX = "FPOGY"; static public LinkedHashMap analyze(DataEntry data) { LinkedHashMap results = new LinkedHashMap(); @@ -14,9 +15,9 @@ static public LinkedHashMap analyze(DataEntry data) { for (int row = 0; row < data.rowCount(); row++) { Coordinate eachCoordinate = new Coordinate( - Double.valueOf(data.getValue(FIXATIONX_INDEX, row)), - Double.valueOf(data.getValue(FIXATIONY_INDEX, row)), - Integer.valueOf(data.getValue(FIXATIONID_INDEX, row)) + Double.valueOf(data.getValue(FIXATION_X, row)), + Double.valueOf(data.getValue(FIXATION_Y, row)), + Integer.valueOf(data.getValue(FIXATION_ID, row)) ); allCoordinates.add(eachCoordinate); } diff --git a/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java b/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java index 8d3192b..593861c 100644 --- a/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java +++ b/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java @@ -1,26 +1,28 @@ package com.github.thed2lab.analysis; -import java.util.Arrays; +import static com.github.thed2lab.analysis.Constants.AOI_LABEL; +import static com.github.thed2lab.analysis.Constants.FIXATION_DURATION; +import static com.github.thed2lab.analysis.Constants.FIXATION_ID; +import static com.github.thed2lab.analysis.Constants.SCREEN_HEIGHT; +import static com.github.thed2lab.analysis.Constants.SCREEN_WIDTH; + import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; public class AreaOfInterests { - final static String FIXATIONID_INDEX = "FPOGID"; //CNT - final static String DURATION_INDEX = "FPOGD"; - final static String AOI_INDEX = "AOI"; - private static final String[] additionalHeaders = {"aoi", "proportion_of_fixations_spent_in_aoi","proportion_of_fixations_durations_spent_in_aoi"}; private static final String[] perAoiHeaders = {"aoi_pair", "transition_count", "proportion_including_self_transitions", "proportion_excluding_self_transitions"}; - public static void generateAOIs(DataEntry allGazeData, String outputDirectory, String fileName) { + public static void generateAOIs(DataEntry allGazeData, DataEntry fixationData, String outputDirectory, String fileName) { LinkedHashMap aoiMetrics = new LinkedHashMap<>(); for (int i = 0; i < allGazeData.rowCount(); i++) { - String aoi = allGazeData.getValue(AOI_INDEX, i); - String aoiKey = aoi.equals("") ? "No AOI" : aoi; + String aoi = allGazeData.getValue(AOI_LABEL, i); + String aoiKey = aoi.equals("") ? "Undefined Area" : aoi; if (!aoiMetrics.containsKey(aoiKey)) { DataEntry d = new DataEntry(allGazeData.getHeaders()); aoiMetrics.put(aoiKey, d); @@ -28,18 +30,17 @@ public static void generateAOIs(DataEntry allGazeData, String outputDirectory, S aoiMetrics.get(aoiKey).process(allGazeData.getRow(i)); } - + DataEntry filteredFixations = DataFilter.filterByValidity(fixationData, SCREEN_WIDTH, SCREEN_HEIGHT); LinkedHashMap aoiFixationMetrics = new LinkedHashMap<>(); - DataEntry allFixations = DataFilter.filterByValidity(DataFilter.filterByFixations(allGazeData)); //System.out.println(allFixations.rowCount()); - for (int i = 0; i < allFixations.rowCount(); i++) { - String aoi = allFixations.getValue(AOI_INDEX, i); - String aoiKey = aoi.equals("") ? "No AOI" : aoi; + for (int i = 0; i < filteredFixations.rowCount(); i++) { + String aoi = filteredFixations.getValue(AOI_LABEL, i); + String aoiKey = aoi.equals("") ? "Undefined Area" : aoi; if (!aoiFixationMetrics.containsKey(aoiKey)) { - DataEntry d = new DataEntry(allFixations.getHeaders()); + DataEntry d = new DataEntry(filteredFixations.getHeaders()); aoiFixationMetrics.put(aoiKey, d); } - aoiFixationMetrics.get(aoiKey).process(allFixations.getRow(i)); + aoiFixationMetrics.get(aoiKey).process(filteredFixations.getRow(i)); } // For any AOIs not in aoiFixationMetrics, add an empty DataEntry @@ -59,7 +60,7 @@ public static void generateAOIs(DataEntry allGazeData, String outputDirectory, S ArrayList> metrics = new ArrayList<>(); metrics.add(new ArrayList()); - double totalDuration = getDuration(allFixations); + double totalDuration = getDuration(filteredFixations); LinkedHashMap validAOIs = new LinkedHashMap<>(); boolean isFirst = true; Set aoiKeySet = aoiMetrics.keySet(); @@ -80,11 +81,11 @@ public static void generateAOIs(DataEntry allGazeData, String outputDirectory, S } results.get(1).add(aoiKey); metrics.add(results.get(1)); - metrics.get(row).addAll(getProportions(allFixations, singleAoiFixations, totalDuration)); + metrics.get(row).addAll(getProportions(filteredFixations, singleAoiFixations, totalDuration)); validAOIs.put(aoiKey, singleAoiFixations); row++; } - ArrayList> pairResults = generatePairResults(allFixations, aoiMetrics); + ArrayList> pairResults = generatePairResults(filteredFixations, aoiMetrics); FileHandler.writeToCSV(metrics, outputDirectory, fileName + "_AOI_DGMs"); FileHandler.writeToCSV(pairResults, outputDirectory, fileName+"_AOI_Transitions"); } @@ -102,7 +103,7 @@ public static ArrayList getProportions(DataEntry fixations, DataEntry ao public static double getDuration(DataEntry fixations) { double durationSum = 0.0; for (int i = 0; i < fixations.rowCount(); i++) { - durationSum += Double.valueOf(fixations.getValue(DURATION_INDEX, i)); + durationSum += Double.valueOf(fixations.getValue(FIXATION_DURATION, i)); } return durationSum; @@ -112,12 +113,12 @@ public static ArrayList> generatePairResults(DataEntry fixations, L LinkedHashMap> totalTransitions = new LinkedHashMap<>(); // ArrayList(Transtions, Inclusive, Exlusive); LinkedHashMap> transitionCounts = new LinkedHashMap<>(); for (int i = 0; i < fixations.rowCount()-1; i++) { - String curAoi = fixations.getValue(AOI_INDEX, i); - curAoi = curAoi.equals("") ? "No AOI" : curAoi; - int curId = Integer.valueOf(fixations.getValue(FIXATIONID_INDEX, i)); - String nextAoi = fixations.getValue(AOI_INDEX, i+1); - nextAoi = nextAoi.equals("") ? "No AOI" : nextAoi; - int nextId = Integer.valueOf(fixations.getValue(FIXATIONID_INDEX, i+1)); + String curAoi = fixations.getValue(AOI_LABEL, i); + curAoi = curAoi.equals("") ? "Undefined Area" : curAoi; + int curId = Integer.valueOf(fixations.getValue(FIXATION_ID, i)); + String nextAoi = fixations.getValue(AOI_LABEL, i+1); + nextAoi = nextAoi.equals("") ? "Undefined Area" : nextAoi; + int nextId = Integer.valueOf(fixations.getValue(FIXATION_ID, i+1)); boolean isValidAOI = (validAOIs.containsKey(curAoi) && validAOIs.containsKey(nextAoi)); if (isValidAOI && nextId == curId + 1) { //Check if fixations are subsequent if (!totalTransitions.containsKey(curAoi)) { //Ensure AOI is initialized in map. diff --git a/src/main/java/com/github/thed2lab/analysis/Blinks.java b/src/main/java/com/github/thed2lab/analysis/Blinks.java index 93e4295..09d16c4 100644 --- a/src/main/java/com/github/thed2lab/analysis/Blinks.java +++ b/src/main/java/com/github/thed2lab/analysis/Blinks.java @@ -1,13 +1,15 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.BLINK_ID; +import static com.github.thed2lab.analysis.Constants.DATA_ID; +import static com.github.thed2lab.analysis.Constants.TIMESTAMP; + import java.util.LinkedHashMap; public class Blinks { - final static String BLINK_ID_INDEX = "BKID"; - final static String TIME_INDEX = "TIME"; - final static String DATA_ID_INDEX = "CNT"; // unique for each line of raw data - final static String DEFAULT_BKID = "0"; // BKID when no blink is detected + /** Blink id when no blink is being detected. */ + final static String DEFAULT_BKID = "0"; static public LinkedHashMap analyze(DataEntry allGazeData) { @@ -18,14 +20,14 @@ static public LinkedHashMap analyze(DataEntry allGazeData) { double prevTimestamp = 0; int prevDataId = -10; // IDs are always non-negative for (int i = 0; i < allGazeData.rowCount(); i++) { - int curDataId = Integer.parseInt(allGazeData.getValue(DATA_ID_INDEX, i)); - double curTimestamp = Double.parseDouble(allGazeData.getValue(TIME_INDEX, i)); + int curDataId = Integer.parseInt(allGazeData.getValue(DATA_ID, i)); + double curTimestamp = Double.parseDouble(allGazeData.getValue(TIMESTAMP, i)); // calculate time window between data records if they are consecutive if (curDataId == prevDataId + 1) { timeTotal += curTimestamp - prevTimestamp; } - String curBlinkId = allGazeData.getValue(BLINK_ID_INDEX, i); + String curBlinkId = allGazeData.getValue(BLINK_ID, i); if (!curBlinkId.equals(DEFAULT_BKID) && !curBlinkId.equals(prevBlinkId)) { blinkCnt++; // new blink occurred } diff --git a/src/main/java/com/github/thed2lab/analysis/Constants.java b/src/main/java/com/github/thed2lab/analysis/Constants.java new file mode 100644 index 0000000..1609c57 --- /dev/null +++ b/src/main/java/com/github/thed2lab/analysis/Constants.java @@ -0,0 +1,64 @@ +package com.github.thed2lab.analysis; + +/** + * Hardcoded constants that are used throughout the package so there are fewer duplicated + * constants throughout the files. + */ +final class Constants { + /* + * Notice the access modifier is default (package-private). + * We could make this an injectable if we wanted to be better OO programmers, but I think + * this package is closely coupled enough that we don't care anymore. + */ + + private Constants() { + // do not instantiate this class EVER!!! + } + + /** Screen width in pixels */ + final static int SCREEN_WIDTH = 1920; + /** Screen height in pixels */ + final static int SCREEN_HEIGHT = 1080; + + /** Header for timestamp of when the data line was recorded since the start of the recording in seconds */ + final static String TIMESTAMP = "TIME"; + /** Header for the unique ID given to each line of data */ + final static String DATA_ID = "CNT"; + + /** Header for fixation ID. */ + final static String FIXATION_ID = "FPOGID"; + /** Header for the fixations starting timestamp */ + final static String FIXATION_START = "FPOGS"; + /** Header for fixation validity. 1 for true and 2 for false */ + final static String FIXATION_VALIDITY = "FPOGV"; + /** Header for the x-coordinate of the fixation point of gaze */ + final static String FIXATION_X = "FPOGX"; + /** Header for the y-coordinate of the fixation point of gaze */ + final static String FIXATION_Y = "FPOGY"; + /** Header for the duration of a fixation */ + final static String FIXATION_DURATION = "FPOGD"; + + /** Header for the diameter of the left pupil in mm */ + final static String LEFT_PUPIL_DIAMETER = "LPMM"; + /** Header for the valid flag for the left pupil. A value of 1 is valid */ + final static String LEFT_PUPIL_VALIDITY = "LPMMV"; + /** Header for the diameter of the right pupil in mm */ + final static String RIGHT_PUPIL_DIAMETER = "RPMM"; + /** Header for the valid flag for the right pupil. A value of 1 is valid */ + final static String RIGHT_PUPIL_VALIDITY = "RPMMV"; + + /** Header for cursor events */ + final static String CURSOR_EVENT = "CS"; + /** Header for the blink ID */ + final static String BLINK_ID = "BKID"; + /** Header for the blink rate per minute */ + final static String BLINK_RATE = "BKPMIN"; + /** Header for the AOI Label */ + final static String AOI_LABEL = "AOI"; + + /** Header for the saccade magnitude */ + final static String SACCADE_MAGNITUDE = "SACCADE_MAG"; + /** Header for the saccade direction */ + final static String SACCADE_DIR = "SACCADE_DIR"; + +} diff --git a/src/main/java/com/github/thed2lab/analysis/ConvexHull.java b/src/main/java/com/github/thed2lab/analysis/ConvexHull.java index ee55c4b..8dbb912 100644 --- a/src/main/java/com/github/thed2lab/analysis/ConvexHull.java +++ b/src/main/java/com/github/thed2lab/analysis/ConvexHull.java @@ -1,5 +1,8 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.FIXATION_X; +import static com.github.thed2lab.analysis.Constants.FIXATION_Y; + import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -11,16 +14,14 @@ import java.awt.Point; public class ConvexHull { - final static String FIXATIONX_INDEX = "FPOGX"; - final static String FIXATIONY_INDEX = "FPOGY"; static public LinkedHashMap analyze(DataEntry data) { LinkedHashMap results = new LinkedHashMap(); List allPoints = new ArrayList<>(); for (int row = 0; row < data.rowCount(); row++) { - double x = Double.valueOf(data.getValue(FIXATIONX_INDEX, row)); - double y = Double.valueOf(data.getValue(FIXATIONY_INDEX, row)); + double x = Double.valueOf(data.getValue(FIXATION_X, row)); + double y = Double.valueOf(data.getValue(FIXATION_Y, row)); allPoints.add(new Point2D.Double(x, y)); } List boundingPoints = getConvexHull(allPoints); diff --git a/src/main/java/com/github/thed2lab/analysis/DataFilter.java b/src/main/java/com/github/thed2lab/analysis/DataFilter.java index b421c21..6781204 100644 --- a/src/main/java/com/github/thed2lab/analysis/DataFilter.java +++ b/src/main/java/com/github/thed2lab/analysis/DataFilter.java @@ -1,7 +1,15 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.FIXATION_ID; +import static com.github.thed2lab.analysis.Constants.FIXATION_VALIDITY; +import static com.github.thed2lab.analysis.Constants.FIXATION_X; +import static com.github.thed2lab.analysis.Constants.FIXATION_Y; +import static com.github.thed2lab.analysis.Constants.LEFT_PUPIL_DIAMETER; +import static com.github.thed2lab.analysis.Constants.LEFT_PUPIL_VALIDITY; +import static com.github.thed2lab.analysis.Constants.RIGHT_PUPIL_DIAMETER; +import static com.github.thed2lab.analysis.Constants.RIGHT_PUPIL_VALIDITY; + import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; // Note: Order used for data filtering matters and can invalidate DataEntrys if used incorrectly @@ -14,8 +22,8 @@ static public DataEntry filterByFixations(DataEntry data) { int currFixation = 1; for (int row = 0; row < data.rowCount(); row++) { - int fixationID = Integer.parseInt(data.getValue("FPOGID", row)); - int fixationValidity = Integer.parseInt(data.getValue("FPOGV", row)); + int fixationID = Integer.parseInt(data.getValue(FIXATION_ID, row)); + int fixationValidity = Integer.parseInt(data.getValue(FIXATION_VALIDITY, row)); if (fixationID != currFixation) { if (lastValidFixation != null) filtered.process(lastValidFixation); if (fixationValidity == 1) lastValidFixation = data.getRow(row); // Edge case; check to see if the first line associated with a given fixation is valid @@ -35,37 +43,51 @@ static public DataEntry filterByFixations(DataEntry data) { /** * Cleanses data by filtering out invalid data. Valid data entries must occur within * the bounds of the monitor and have humanly possible pupil dilation. - * @param data Data to be cleansed + * @param data data to be cleansed. */ public static DataEntry filterByValidity(DataEntry data) { + // GazePoint scales point of gaze location from 0 to 1 when on screen + final int MAX_SCREEN_WIDTH = 1; + final int MAX_SCREEN_HEIGHT = 1; + return filterByValidity(data, MAX_SCREEN_WIDTH, MAX_SCREEN_HEIGHT); + } + + /** + * Cleanses data by filtering out invalid data. Valid data entries must occur within + * the bounds of the monitor and have humanly possible pupil dilation. + * @param data data to be cleansed. + * @param screenWidth screen width scaler that was previously applied to the data. + * @param screenHeight screen height scaler that was previously applied to the data. + */ + public static DataEntry filterByValidity(DataEntry data, int screenWidth, int screenHeight) { // humanly possible pupil diameter is between 2 and 8 mm final int MIN_DIAMETER = 2; final int MAX_DIAMETER = 8; final int MAX_PUPIL_DIFF = 1; - // GazePoint scales point of gaze location from 0 to 1 when on screen - final int MIN_SCREEN_DIM = 0; - final int MAX_SCREEN_DIM = 1; + // GazePoint scales point of gaze location from 0 to 1 when on screen. The data + // may have been scaled so we only hardcode the 0 + final int MIN_SCREEN_DIM = 0; DataEntry filtered = new DataEntry(data.getHeaders()); for (int rowNum = 0; rowNum < data.rowCount(); rowNum++) { // Note: It is extremely slow to parse a string over and over again // Check if Gazepoint could detect the pupils - boolean leftValid = Integer.parseInt(data.getValue("LPMMV", rowNum)) == 1; - boolean rightValid = Integer.parseInt(data.getValue("RPMMV", rowNum)) == 1; + boolean leftValid = Integer.parseInt(data.getValue(LEFT_PUPIL_VALIDITY, rowNum)) == 1; + boolean rightValid = Integer.parseInt(data.getValue(RIGHT_PUPIL_VALIDITY, rowNum)) == 1; if (!(leftValid && rightValid)) { continue; // skip invalid entry } // Check if POG is on the screen - float xCoordinate = Float.parseFloat(data.getValue("FPOGX" ,rowNum)); - float yCoordinate = Float.parseFloat(data.getValue("FPOGY" ,rowNum)); - if (xCoordinate < MIN_SCREEN_DIM || xCoordinate > MAX_SCREEN_DIM) { + float xCoordinate = Float.parseFloat(data.getValue(FIXATION_X ,rowNum)); + float yCoordinate = Float.parseFloat(data.getValue(FIXATION_Y ,rowNum)); + if (xCoordinate < MIN_SCREEN_DIM || xCoordinate > screenWidth) { continue; // off screen in x-direction, invalid - } else if (yCoordinate < MIN_SCREEN_DIM || yCoordinate > MAX_SCREEN_DIM) { + } else if (yCoordinate < MIN_SCREEN_DIM || yCoordinate > screenHeight) { continue; // off screen in y-direction, invalid entry } // Check if pupils are valid sizes individually and compared to each other. - float leftDiameter = Float.parseFloat(data.getValue("LPMM", rowNum)); - float rightDiameter = Float.parseFloat(data.getValue("RPMM", rowNum)); + float leftDiameter = Float.parseFloat(data.getValue(LEFT_PUPIL_DIAMETER, rowNum)); + float rightDiameter = Float.parseFloat(data.getValue(RIGHT_PUPIL_DIAMETER, rowNum)); if ( leftDiameter >= MIN_DIAMETER && leftDiameter <= MAX_DIAMETER @@ -87,8 +109,8 @@ static public DataEntry applyScreenSize(DataEntry data, int screenWidth, int scr List newRow = new ArrayList(); newRow.addAll(currentRow); - int fixationXIndex = data.getHeaderIndex("FPOGX"); - int fixationYIndex = data.getHeaderIndex("FPOGY"); + int fixationXIndex = data.getHeaderIndex(FIXATION_X); + int fixationYIndex = data.getHeaderIndex(FIXATION_Y); newRow.set(fixationXIndex,String.valueOf(Double.valueOf(newRow.get(fixationXIndex)) * screenWidth)); newRow.set(fixationYIndex,String.valueOf(Double.valueOf(newRow.get(fixationYIndex)) * screenHeight)); @@ -98,10 +120,4 @@ static public DataEntry applyScreenSize(DataEntry data, int screenWidth, int scr return filtered; } - - - static public LinkedHashMap filterByAOI(DataEntry data ){ - - return new LinkedHashMap(); - } } diff --git a/src/main/java/com/github/thed2lab/analysis/Event.java b/src/main/java/com/github/thed2lab/analysis/Event.java index b361401..d3b14f8 100644 --- a/src/main/java/com/github/thed2lab/analysis/Event.java +++ b/src/main/java/com/github/thed2lab/analysis/Event.java @@ -1,16 +1,17 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.CURSOR_EVENT; + import java.util.LinkedHashMap; public class Event { - final static String INPUT_INDEX = "CS"; static public LinkedHashMap analyze(DataEntry data) { LinkedHashMap results = new LinkedHashMap(); int leftMouseClicks = 0; for (int row = 0; row < data.rowCount(); row++) { - if (data.getValue(INPUT_INDEX, row).equals("1")) { + if (data.getValue(CURSOR_EVENT, row).equals("1")) { leftMouseClicks += 1; } } diff --git a/src/main/java/com/github/thed2lab/analysis/Fixations.java b/src/main/java/com/github/thed2lab/analysis/Fixations.java index e8cf6c9..55fa52b 100644 --- a/src/main/java/com/github/thed2lab/analysis/Fixations.java +++ b/src/main/java/com/github/thed2lab/analysis/Fixations.java @@ -1,10 +1,11 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.FIXATION_DURATION; + import java.util.ArrayList; import java.util.LinkedHashMap; public class Fixations { - final static String DURATION_INDEX = "FPOGD"; static public LinkedHashMap analyze(DataEntry data) { LinkedHashMap results = new LinkedHashMap(); @@ -13,7 +14,7 @@ static public LinkedHashMap analyze(DataEntry data) { int fixationCount = data.rowCount(); for (int row = 0; row < data.rowCount(); row++) { - Double fixationDurationSeconds = Double.valueOf(data.getValue(DURATION_INDEX, row)); + Double fixationDurationSeconds = Double.valueOf(data.getValue(FIXATION_DURATION, row)); allFixationDurations.add(fixationDurationSeconds); } diff --git a/src/main/java/com/github/thed2lab/analysis/Gaze.java b/src/main/java/com/github/thed2lab/analysis/Gaze.java index e7e1c6a..1cad24d 100644 --- a/src/main/java/com/github/thed2lab/analysis/Gaze.java +++ b/src/main/java/com/github/thed2lab/analysis/Gaze.java @@ -1,10 +1,10 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.*; + import java.util.LinkedHashMap; public class Gaze { - final static String PUPIL_LEFT_DIAMETER_INDEX = "LPMM"; - final static String PUPIL_RIGHT_DIAMETER_INDEX = "RPMM"; static public LinkedHashMap analyze(DataEntry data) { LinkedHashMap results = new LinkedHashMap(); @@ -14,8 +14,8 @@ static public LinkedHashMap analyze(DataEntry data) { int count = data.rowCount(); for (int row = 0; row < data.rowCount(); row++) { - double leftSize = Double.valueOf(data.getValue(PUPIL_LEFT_DIAMETER_INDEX, row)); - double rightSize = Double.valueOf(data.getValue(PUPIL_RIGHT_DIAMETER_INDEX, row)); + double leftSize = Double.valueOf(data.getValue(LEFT_PUPIL_DIAMETER, row)); + double rightSize = Double.valueOf(data.getValue(RIGHT_PUPIL_DIAMETER, row)); leftSum += leftSize; rightSum += rightSize; bothSum += (leftSize + rightSize) / 2.0; diff --git a/src/main/java/com/github/thed2lab/analysis/GazeEntropy.java b/src/main/java/com/github/thed2lab/analysis/GazeEntropy.java index ede3d95..bc90ee1 100644 --- a/src/main/java/com/github/thed2lab/analysis/GazeEntropy.java +++ b/src/main/java/com/github/thed2lab/analysis/GazeEntropy.java @@ -1,44 +1,39 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.AOI_LABEL; + import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; public class GazeEntropy { - final static String AOI_INDEX = "AOI"; - - static public LinkedHashMap analyze(DataEntry data) { - LinkedHashMap results = new LinkedHashMap(); - HashMap aoiProbability = new HashMap(); - HashMap> transitionProbability = new HashMap>(); - ArrayList aoiSequence = new ArrayList(); - String lastAoi = ""; + /** + * Calculates the stationary entropy and transition entropy measures. The method iterates over the participant’s + * fixations, determining the probability of viewing each AOI and the probability of transitioning from one AOI + * to another. All lines of data not labeled with an AOI are treated as if they appear in a single AOI. + * Therefore, n in the entropy formula will be the total number of labeled AOIs plus one. . + * @param fixations the users gaze data, filtered by fixations and validity with screen size applied. + * @return an ordered map containing the stationary and transition entropy headers as keys mapped to their computed values. + */ + static public LinkedHashMap analyze(DataEntry fixations) { + var aoiProbability = new HashMap(); + var transitionProbability = new HashMap>(); + var aoiSequence = new ArrayList(); + String lastAoi = null; - int fixationCount = data.rowCount(); + int fixationCount = fixations.rowCount(); - for (int row = 0; row < data.rowCount(); row++) { - String aoi = data.getValue(AOI_INDEX, row); + for (int row = 0; row < fixations.rowCount(); row++) { + String aoi = fixations.getValue(AOI_LABEL, row); aoiSequence.add(aoi); - - if (aoi.equals("")) - continue; - else if (aoiProbability.containsKey(aoi)) { - aoiProbability.put(aoi, aoiProbability.get(aoi) + 1); - if (!lastAoi.equals("")) { - HashMap relationMatrix = transitionProbability.get(lastAoi); - if (relationMatrix.containsKey(aoi)) { - double count = relationMatrix.get(aoi); - relationMatrix.put(aoi, count + 1); - } else { - relationMatrix.put(aoi, 1.0); - } - } - - } else { - aoiProbability.put(aoi, 1.0); - transitionProbability.put(aoi, new HashMap()); + aoiProbability.put(aoi, aoiProbability.getOrDefault(aoi, 0.0) + 1); + if (lastAoi != null) { // skips the first loop + Map relationMatrix = transitionProbability.getOrDefault(lastAoi, new HashMap()); + double count = relationMatrix.getOrDefault(aoi, 0.0); + relationMatrix.put(aoi, count + 1); + transitionProbability.put(lastAoi, relationMatrix); } lastAoi = aoi; } @@ -50,7 +45,7 @@ else if (aoiProbability.containsKey(aoi)) { } - for (Map.Entry> entry : transitionProbability.entrySet()) { + for (Map.Entry> entry : transitionProbability.entrySet()) { int aoiTransitions = 0; for (Map.Entry edge : entry.getValue().entrySet()) { aoiTransitions += edge.getValue(); @@ -60,6 +55,8 @@ else if (aoiProbability.containsKey(aoi)) { } } + var results = new LinkedHashMap(); + results.put( "stationary_entropy", //Output Header String.valueOf(getStationaryEntropy(aoiProbability)) //Output Value @@ -73,7 +70,12 @@ else if (aoiProbability.containsKey(aoi)) { return results; } - public static double getStationaryEntropy(HashMap aoiProbability) { + /** + * Calculates the stationary entropy score. + * @param aoiProbability the AOI labels mapped to the probability of viewing the AOI. + * @return the stationary entropy score. + */ + static double getStationaryEntropy(Map aoiProbability) { double stationaryEntropy = 0; for (Map.Entry entry : aoiProbability.entrySet()) { double probability = entry.getValue(); @@ -83,9 +85,17 @@ public static double getStationaryEntropy(HashMap aoiProbability return stationaryEntropy; } - public static double getTransitionEntropy(HashMap aoiProbability, HashMap> transitionMatrix){ + /** + * Calculates the transition entropy score. + * @param aoiProbability the AOI labels mapped to the probability of viewing the AOI. + * @param transitionMatrix the outer map has a key for each AOI (A). The value for each AOI is another map + * containing all AOIs (inclusive) (B). The value for each of those is the probability of transitioning + * from A to B. + * @return the stationary entropy score. + */ + static double getTransitionEntropy(Map aoiProbability, Map> transitionMatrix){ double transitionEntropy = 0; - for (Map.Entry> entry : transitionMatrix.entrySet()) { + for (Map.Entry> entry : transitionMatrix.entrySet()) { double pijSum = 0; for (Map.Entry edge : entry.getValue().entrySet()) { pijSum += edge.getValue() * Math.log10(edge.getValue()); diff --git a/src/main/java/com/github/thed2lab/analysis/Parameters.java b/src/main/java/com/github/thed2lab/analysis/Parameters.java index 3cb11e4..0a6c6e7 100644 --- a/src/main/java/com/github/thed2lab/analysis/Parameters.java +++ b/src/main/java/com/github/thed2lab/analysis/Parameters.java @@ -47,19 +47,6 @@ public String toString() { return "--Parameters-- \n InputFiles: ["+inputFiles.length+"] "+Arrays.toString(inputFiles)+" \n OutputDirectory: "+outputDirectory +"\n --End of Parameters--"; } - /* public static void main(String[] args) { - System.out.println("Creating and saving Parameters!"); - - // Parameters p = new Parameters(new String[]{"data\\Kayla_all_gaze.csv","data\\Esther Jung_all_gaze.csv"},"data\\presets", new HashMap<>()); - // p.saveToJSON("data\\presets","TestConfig.json"); - - // System.out.println(p.toString()); - // System.out.println("Loading parameters!"); - - Parameters p2 = new Parameters(new File("data\\presets\\TestConfig.json")); - System.out.println(p2.toString()); - } */ - public File[] getInputFiles() { return this.inputFiles.clone(); } diff --git a/src/main/java/com/github/thed2lab/analysis/Patterns.java b/src/main/java/com/github/thed2lab/analysis/Patterns.java index e27813f..b4d6586 100644 --- a/src/main/java/com/github/thed2lab/analysis/Patterns.java +++ b/src/main/java/com/github/thed2lab/analysis/Patterns.java @@ -34,12 +34,6 @@ public static ArrayList> discoverPatterns(List sequences, i double averagePatternFrequency = (double) frequencyMap.get(pattern)/sequences.size(); double proportionalPatternFrequency = (double) frequencyMap.get(pattern)/totalPatternCount; - // System.out.print(pattern + " "); - // System.out.print(frequency + " "); - // System.out.print(sequenceSupport + " "); - // System.out.print(averagePatternFrequency + " "); - // System.out.println(proportionalPatternFrequency + " "); - if (frequency >= minFrequency && sequenceMap.get(pattern).size() >= minSequenceSize) { List patternData = new ArrayList(); patternData.add(pattern); diff --git a/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java b/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java index 8501f45..4be9668 100644 --- a/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java +++ b/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java @@ -1,24 +1,24 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.FIXATION_ID; +import static com.github.thed2lab.analysis.Constants.FIXATION_VALIDITY; +import static com.github.thed2lab.analysis.Constants.FIXATION_X; +import static com.github.thed2lab.analysis.Constants.FIXATION_Y; +import static com.github.thed2lab.analysis.Constants.TIMESTAMP; + import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; public class SaccadeVelocity { - final static String TIME_INDEX = "TIME"; - final static String FIXATIONID_INDEX = "FPOGID"; - final static String FIXATIONX_INDEX = "FPOGX"; - final static String FIXATIONY_INDEX = "FPOGY"; - final static String FIXATION_VALIDITY_INDEX = "FPOGV"; - final static String FIXATION_DURATION_INDEX = "FPOGD"; /** * Calculates the average peak saccade velocity. Iterates over all rows of a participant’s gaze. * If the current row’s fixation ID (“FID”) and the next consecutive fixation ID both appear in * the fixation data as well as if the fixation validity (“FPOGV”) is set to 0, then the row is * considered part of a saccade. - * @param allGazeData all gaze data, filtered by validity with screen size applied. When calculating - * for AOIs, use all gaze data, not just the AOI specific gaze data. + * @param allGazeData all gaze data, with screen size applied. When calculating for AOIs, use all + * gaze data, not just the AOI specific gaze data. * @param fixationData the gaze data, filtered by fixation and validity with screen size applied. * When calculating for AOIs, use only fixation data that occurs within the AOI. * @return average peak saccade velocity’s header mapped to the calculated value as a {@code String}. @@ -36,8 +36,8 @@ static public LinkedHashMap analyze(DataEntry allGazeData, DataEn while ((fixDataIndex < fixationData.rowCount() - 1) && (gazeDataIndex < allGazeData.rowCount())) { // Get the fixation Id of the next saccade that occurs completely within portion of screen (whole or AOI) while (fixDataIndex < fixationData.rowCount() - 1) { - int curFixId = Integer.parseInt(fixationData.getValue(FIXATIONID_INDEX, fixDataIndex)); - int nextFixId = Integer.parseInt(fixationData.getValue(FIXATIONID_INDEX, fixDataIndex + 1)); + int curFixId = Integer.parseInt(fixationData.getValue(FIXATION_ID, fixDataIndex)); + int nextFixId = Integer.parseInt(fixationData.getValue(FIXATION_ID, fixDataIndex + 1)); fixDataIndex++; if (nextFixId == curFixId + 1) { targetFixId = curFixId; @@ -46,7 +46,7 @@ static public LinkedHashMap analyze(DataEntry allGazeData, DataEn } while (gazeDataIndex < allGazeData.rowCount()) { - int curId = Integer.parseInt(allGazeData.getValue(FIXATIONID_INDEX, gazeDataIndex)); + int curId = Integer.parseInt(allGazeData.getValue(FIXATION_ID, gazeDataIndex)); if (curId < targetFixId) { gazeDataIndex++; continue; @@ -54,16 +54,16 @@ static public LinkedHashMap analyze(DataEntry allGazeData, DataEn break; // could not find target, look for next fixation } - boolean saccade = Integer.parseInt(allGazeData.getValue(FIXATION_VALIDITY_INDEX, gazeDataIndex)) == 0 ? true : false; + boolean saccade = Integer.parseInt(allGazeData.getValue(FIXATION_VALIDITY, gazeDataIndex)) == 0 ? true : false; // Check if not a saccade if (!saccade) { gazeDataIndex++; continue; // go to next data point } - Double x = Double.parseDouble(allGazeData.getValue(FIXATIONX_INDEX, gazeDataIndex)); - Double y = Double.parseDouble(allGazeData.getValue(FIXATIONY_INDEX, gazeDataIndex)); - Double t = Double.parseDouble(allGazeData.getValue(TIME_INDEX, gazeDataIndex)); + Double x = Double.parseDouble(allGazeData.getValue(FIXATION_X, gazeDataIndex)); + Double y = Double.parseDouble(allGazeData.getValue(FIXATION_Y, gazeDataIndex)); + Double t = Double.parseDouble(allGazeData.getValue(TIMESTAMP, gazeDataIndex)); positionProfile.add(new Double[] {x, y, t}); gazeDataIndex++; } diff --git a/src/main/java/com/github/thed2lab/analysis/Saccades.java b/src/main/java/com/github/thed2lab/analysis/Saccades.java index bb84287..a479ad1 100644 --- a/src/main/java/com/github/thed2lab/analysis/Saccades.java +++ b/src/main/java/com/github/thed2lab/analysis/Saccades.java @@ -1,14 +1,15 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.FIXATION_DURATION; +import static com.github.thed2lab.analysis.Constants.FIXATION_ID; +import static com.github.thed2lab.analysis.Constants.FIXATION_X; +import static com.github.thed2lab.analysis.Constants.FIXATION_Y; +import static com.github.thed2lab.analysis.Constants.FIXATION_START; + import java.util.ArrayList; import java.util.LinkedHashMap; public class Saccades { - final static String DURATION_INDEX = "FPOGD"; - final static String TIMESTAMP_INDEX = "FPOGS"; - final static String FIXATIONID_INDEX = "FPOGID"; - final static String FIXATIONX_INDEX = "FPOGX"; - final static String FIXATIONY_INDEX = "FPOGY"; static public LinkedHashMap analyze(DataEntry data) { LinkedHashMap results = new LinkedHashMap(); @@ -18,18 +19,18 @@ static public LinkedHashMap analyze(DataEntry data) { ArrayList allCoordinates = new ArrayList<>(); for (int row = 0; row < data.rowCount(); row++) { - Double fixationDurationSeconds = Double.valueOf(data.getValue(DURATION_INDEX, row));; + Double fixationDurationSeconds = Double.valueOf(data.getValue(FIXATION_DURATION, row));; Double[] eachSaccadeDetail = new Double[3]; - eachSaccadeDetail[0] = Double.valueOf(data.getValue(TIMESTAMP_INDEX, row)); - eachSaccadeDetail[1] = Double.valueOf(data.getValue(DURATION_INDEX, row)); - eachSaccadeDetail[2] = Double.valueOf(data.getValue(FIXATIONID_INDEX, row)); + eachSaccadeDetail[0] = Double.valueOf(data.getValue(FIXATION_START, row)); + eachSaccadeDetail[1] = Double.valueOf(data.getValue(FIXATION_DURATION, row)); + eachSaccadeDetail[2] = Double.valueOf(data.getValue(FIXATION_ID, row)); saccadeDetails.add(eachSaccadeDetail); Coordinate eachCoordinate = new Coordinate( - Double.valueOf(data.getValue(FIXATIONX_INDEX, row)), - Double.valueOf(data.getValue(FIXATIONY_INDEX, row)), - Integer.valueOf(data.getValue(FIXATIONID_INDEX, row)) + Double.valueOf(data.getValue(FIXATION_X, row)), + Double.valueOf(data.getValue(FIXATION_Y, row)), + Integer.valueOf(data.getValue(FIXATION_ID, row)) ); allCoordinates.add(eachCoordinate); allFixationDurations.add(fixationDurationSeconds); diff --git a/src/main/java/com/github/thed2lab/analysis/Sequences.java b/src/main/java/com/github/thed2lab/analysis/Sequences.java index ebb3379..f1cacc9 100644 --- a/src/main/java/com/github/thed2lab/analysis/Sequences.java +++ b/src/main/java/com/github/thed2lab/analysis/Sequences.java @@ -1,11 +1,11 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.AOI_LABEL; + import java.util.HashMap; import java.util.List; public class Sequences { - - final static String AOI_INDEX = "AOI"; public static void generateSequenceFiles(DataEntry data, String outputDirectory, List sequences, HashMap map) { String aoiDescriptions = ""; @@ -15,18 +15,18 @@ public static void generateSequenceFiles(DataEntry data, String outputDirectory, // Build aoiDescriptions string for (String s: map.keySet()) { int asciiValue = map.get(s); - String description = s == "" ? "No AOI" : s; + String description = s == "" ? "Undefined Area" : s; aoiDescriptions += (char)asciiValue + ", " + description + "\n"; } // Generate sequence for (int i = 0; i < data.rowCount(); i++) { - String aoi = data.getValue(AOI_INDEX, i); + String aoi = data.getValue(AOI_LABEL, i); if (!map.containsKey(aoi)) { map.put(aoi, map.size() + ascii); - String description = aoi == "" ? "No AOI" : aoi; + String description = aoi == "" ? "Undefined Area" : aoi; aoiDescriptions += (char)(map.size() + ascii - 1) + ", " + description + "\n"; } diff --git a/src/main/java/com/github/thed2lab/analysis/Windows.java b/src/main/java/com/github/thed2lab/analysis/Windows.java index 4160cba..1f1b933 100644 --- a/src/main/java/com/github/thed2lab/analysis/Windows.java +++ b/src/main/java/com/github/thed2lab/analysis/Windows.java @@ -1,5 +1,13 @@ package com.github.thed2lab.analysis; +import static com.github.thed2lab.analysis.Constants.TIMESTAMP; +import static com.github.thed2lab.analysis.Constants.BLINK_RATE; +import static com.github.thed2lab.analysis.Constants.FIXATION_DURATION; +import static com.github.thed2lab.analysis.Constants.LEFT_PUPIL_DIAMETER; +import static com.github.thed2lab.analysis.Constants.RIGHT_PUPIL_DIAMETER; +import static com.github.thed2lab.analysis.Constants.SACCADE_DIR; +import static com.github.thed2lab.analysis.Constants.SACCADE_MAGNITUDE; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -9,29 +17,29 @@ public class Windows { - private final static String TIME_INDEX = "TIME"; private final static int BASELINE_LENGTH = 120; + private final static String LEFT_RIGHT_DIAMETER = LEFT_PUPIL_DIAMETER + " + " + RIGHT_PUPIL_DIAMETER; // Set of supported events that utilize the fixation file final static Set fixationEvents = new HashSet( Arrays.asList( - "FPOGD", - "SACCADE_MAG", - "SACCADE_DIR" + FIXATION_DURATION, + SACCADE_MAGNITUDE, + SACCADE_DIR )); // Set of supported events that utilize the allGaze file final static Set allGazeEvents = new HashSet( Arrays.asList( - "LPMM", - "RPMM", - "BKPMIN", - "LPMM + RPMM" + LEFT_PUPIL_DIAMETER, + RIGHT_PUPIL_DIAMETER, + BLINK_RATE, + LEFT_RIGHT_DIAMETER )); public static void generateWindows(DataEntry allGaze, String outputDirectory, WindowSettings settings) { List headers = allGaze.getHeaders(); - double t0 = Double.valueOf(allGaze.getValue(TIME_INDEX, 0)); + double t0 = Double.valueOf(allGaze.getValue(TIMESTAMP, 0)); // Generate baseline file generateBaselineFile(allGaze, outputDirectory + "/baseline"); @@ -46,7 +54,7 @@ public static void generateWindows(DataEntry allGaze, String outputDirectory, Wi for (int i = 0; i < allGaze.rowCount(); i++) { List currRow = allGaze.getRow(i); - Double t = Double.valueOf(allGaze.getValue(TIME_INDEX, i)); + Double t = Double.valueOf(allGaze.getValue(TIMESTAMP, i)); if (t > end) { end += windowSize; @@ -74,7 +82,7 @@ public static void generateWindows(DataEntry allGaze, String outputDirectory, Wi for (int i = 0; i < allGaze.rowCount(); i++) { List currRow = allGaze.getRow(i); - Double t = Double.valueOf(allGaze.getValue(TIME_INDEX, i)); + Double t = Double.valueOf(allGaze.getValue(TIMESTAMP, i)); if (t > end) { end += windowSize; @@ -102,12 +110,12 @@ public static void generateWindows(DataEntry allGaze, String outputDirectory, Wi double end = start + windowSize; for (int i = 0; i < allGaze.rowCount(); i++) { - double t1 = Double.parseDouble(allGaze.getValue(TIME_INDEX, i)); + double t1 = Double.parseDouble(allGaze.getValue(TIMESTAMP, i)); if (t1 >= start) { for (int j = i; j < allGaze.rowCount(); j++) { List row2 = allGaze.getRow(j); - double t2 = Double.parseDouble(allGaze.getValue(TIME_INDEX, j)); + double t2 = Double.parseDouble(allGaze.getValue(TIMESTAMP, j)); if (t2 >= end || j == allGaze.rowCount() - 1) { window.process(row2); @@ -142,7 +150,7 @@ public static void generateWindows(DataEntry allGaze, String outputDirectory, Wi double baselineValue = getEventBaselineValue(outputDirectory, event); for (int i = 0; i < allGaze.rowCount(); i++) { - Double t = Double.valueOf(allGaze.getValue(TIME_INDEX, i)); + Double t = Double.valueOf(allGaze.getValue(TIMESTAMP, i)); Double windowValue = getEventWindowValue(allGaze, event, i); // Get the initial timestamp @@ -174,18 +182,19 @@ public static void generateWindows(DataEntry allGaze, String outputDirectory, Wi static void outputWindowFiles(ArrayList windows, double t0, String outputDirectory) { int windowCount = 1; List> allWindowDGMs = new ArrayList>(); - for (DataEntry w : windows) { + for (DataEntry windowGaze : windows) { String fileName = "window" + windowCount; String windowDirectory = outputDirectory + "/" + fileName; - w.writeToCSV(windowDirectory, fileName); + windowGaze.writeToCSV(windowDirectory, fileName); // windows are continuous and raw, therefore fixation filtering will be valid - List> results = Analysis.generateResults(w, DataFilter.filterByFixations(w)); + DataEntry windowFixations = DataFilter.filterByFixations(windowGaze); + List> results = Analysis.generateResults(windowGaze, windowFixations); // Calculate beginning time stamp, ending timestamp, window duration, initial/final seconds elapsed since window start - double t1 = Double.parseDouble(w.getValue(TIME_INDEX, 0)); - double t2 = Double.parseDouble(w.getValue(TIME_INDEX, w.rowCount() - 1)); + double t1 = Double.parseDouble(windowGaze.getValue(TIMESTAMP, 0)); + double t2 = Double.parseDouble(windowGaze.getValue(TIMESTAMP, windowGaze.rowCount() - 1)); double windowDuration = t2 - t1; double initialDuration = t1 - t0; double finalDuration = t2 - t0; @@ -211,8 +220,7 @@ static void outputWindowFiles(ArrayList windows, double t0, String ou } FileHandler.writeToCSV(results, windowDirectory, fileName + "_DGMs"); - AreaOfInterests.generateAOIs(w, windowDirectory, fileName); - + AreaOfInterests.generateAOIs(windowGaze, windowFixations, windowDirectory, fileName); windowCount++; } @@ -221,11 +229,11 @@ static void outputWindowFiles(ArrayList windows, double t0, String ou static void generateBaselineFile(DataEntry allGaze, String outputDirectory) { DataEntry baseline = new DataEntry(allGaze.getHeaders()); - double startTime = Double.valueOf(allGaze.getValue(TIME_INDEX, 0)); + double startTime = Double.valueOf(allGaze.getValue(TIMESTAMP, 0)); double endTime = startTime + BASELINE_LENGTH; for (int i = 0; i < allGaze.rowCount(); i++) { - Double t = Double.parseDouble(allGaze.getValue(TIME_INDEX, i)); + Double t = Double.parseDouble(allGaze.getValue(TIMESTAMP, i)); if (t >= endTime) { break; @@ -264,8 +272,8 @@ static double getAveragePupilDilationBaseline(String fileDirectory) { baseline = DataFilter.filterByValidity(baseline); // Filter by validity for (int i = 0; i < baseline.rowCount(); i++) { - double left = Double.parseDouble(baseline.getValue("LPMM", i)); - double right = Double.parseDouble(baseline.getValue("RPMM", i)); + double left = Double.parseDouble(baseline.getValue(LEFT_PUPIL_DIAMETER, i)); + double right = Double.parseDouble(baseline.getValue(RIGHT_PUPIL_DIAMETER, i)); eventValue += ((left + right) / 2); } @@ -276,7 +284,7 @@ static double getAveragePupilDilationBaseline(String fileDirectory) { static double getEventBaselineValue(String fileDirectory, String event) { switch(event) { - case "LPMM + RPMM": + case LEFT_RIGHT_DIAMETER: return getAveragePupilDilationBaseline(fileDirectory); default: return getRawEventBaselineValue(fileDirectory, event); @@ -285,9 +293,9 @@ static double getEventBaselineValue(String fileDirectory, String event) { static double getEventWindowValue(DataEntry d, String event, int row) { switch (event) { - case "LPMM + RPMM": - double left = Double.parseDouble(d.getValue("LPMM", row)); - double right = Double.parseDouble(d.getValue("RPMM", row)); + case LEFT_RIGHT_DIAMETER: + double left = Double.parseDouble(d.getValue(LEFT_PUPIL_DIAMETER, row)); + double right = Double.parseDouble(d.getValue(RIGHT_PUPIL_DIAMETER, row)); return (left + right) / 2; default: return Double.parseDouble(d.getValue(event, row)); diff --git a/src/test/java/com/github/thed2lab/analysis/GazeEntropyTest.java b/src/test/java/com/github/thed2lab/analysis/GazeEntropyTest.java new file mode 100644 index 0000000..e58db8d --- /dev/null +++ b/src/test/java/com/github/thed2lab/analysis/GazeEntropyTest.java @@ -0,0 +1,92 @@ +package com.github.thed2lab.analysis; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +public class GazeEntropyTest { + + private final double PRECISION = 0.000000001; // allowable floating point error + private final String STATIONARY_ENTROPY = "stationary_entropy"; + private final String TRANSITION_ENTROPY = "transition_entropy"; + + @Test + public void testGazeEntropyAnalyze_singleAoi_0entropy() { + final double EXPECTED_STATIONARY = 0.0; + final double EXPECTED_TRANSITION = 0.0; + DataEntry data = new DataEntry(Arrays.asList("AOI")) {{ + process(Arrays.asList("A")); + process(Arrays.asList("A")); + process(Arrays.asList("A")); + process(Arrays.asList("A")); + }}; + var results = GazeEntropy.analyze(data); + assertEquals( + "Unexpected stationary entropy.", + EXPECTED_STATIONARY, + Double.parseDouble(results.get(STATIONARY_ENTROPY)), + PRECISION + ); + assertEquals( + "Unexpected transition entropy.", + EXPECTED_TRANSITION, Double.parseDouble(results.get(TRANSITION_ENTROPY)), + PRECISION + ); + } + + @Test + public void testGazeEntropyAnalyze_threeAoi() { + final double EXPECTED_STATIONARY = 0.4699915470362; + final double EXPECTED_TRANSITION = 0.282583442123752; + DataEntry data = new DataEntry(Arrays.asList("AOI")) {{ + process(Arrays.asList("A")); + process(Arrays.asList("B")); + process(Arrays.asList("B")); + process(Arrays.asList("A")); + process(Arrays.asList("A")); + process(Arrays.asList("B")); + process(Arrays.asList("C")); + process(Arrays.asList("C")); + }}; + var results = GazeEntropy.analyze(data); + assertEquals( + "Unexpected stationary entropy.", + EXPECTED_STATIONARY, + Double.parseDouble(results.get(STATIONARY_ENTROPY)), + PRECISION + ); + assertEquals( + "Unexpected transition entropy.", + EXPECTED_TRANSITION, Double.parseDouble(results.get(TRANSITION_ENTROPY)), + PRECISION + ); + } + + @Test + public void testGazeEntropyAnalyze_undefinedAoi() { + final double EXPECTED_STATIONARY = 0.301029995663981; + final double EXPECTED_TRANSITION = 0.288732293303828; + DataEntry data = new DataEntry(Arrays.asList("AOI")) {{ + process(Arrays.asList("A")); + process(Arrays.asList("")); + process(Arrays.asList("")); + process(Arrays.asList("A")); + process(Arrays.asList("A")); + process(Arrays.asList("")); + }}; + var results = GazeEntropy.analyze(data); + assertEquals( + "Unexpected stationary entropy.", + EXPECTED_STATIONARY, + Double.parseDouble(results.get(STATIONARY_ENTROPY)), + PRECISION + ); + assertEquals( + "Unexpected transition entropy.", + EXPECTED_TRANSITION, Double.parseDouble(results.get(TRANSITION_ENTROPY)), + PRECISION + ); + } +}