From 02451d734d1511f53d3e55dbb2d068f0703f9c90 Mon Sep 17 00:00:00 2001 From: ashkjones Date: Tue, 6 Aug 2024 20:04:27 -0700 Subject: [PATCH 1/3] Update access modifiers on windows.java --- .../com/github/thed2lab/analysis/Windows.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/github/thed2lab/analysis/Windows.java b/src/main/java/com/github/thed2lab/analysis/Windows.java index 0af7d7c..4160cba 100644 --- a/src/main/java/com/github/thed2lab/analysis/Windows.java +++ b/src/main/java/com/github/thed2lab/analysis/Windows.java @@ -9,8 +9,8 @@ public class Windows { - final static String TIME_INDEX = "TIME"; - final static int BASELINE_LENGTH = 120; + private final static String TIME_INDEX = "TIME"; + private final static int BASELINE_LENGTH = 120; // Set of supported events that utilize the fixation file final static Set fixationEvents = new HashSet( @@ -171,7 +171,7 @@ public static void generateWindows(DataEntry allGaze, String outputDirectory, Wi } } - public static void outputWindowFiles(ArrayList windows, double t0, String outputDirectory) { + static void outputWindowFiles(ArrayList windows, double t0, String outputDirectory) { int windowCount = 1; List> allWindowDGMs = new ArrayList>(); for (DataEntry w : windows) { @@ -181,7 +181,7 @@ public static void outputWindowFiles(ArrayList windows, double t0, St w.writeToCSV(windowDirectory, fileName); // windows are continuous and raw, therefore fixation filtering will be valid - ArrayList> results = Analysis.generateResults(w, DataFilter.filterByFixations(w)); + List> results = Analysis.generateResults(w, DataFilter.filterByFixations(w)); // Calculate beginning time stamp, ending timestamp, window duration, initial/final seconds elapsed since window start double t1 = Double.parseDouble(w.getValue(TIME_INDEX, 0)); @@ -219,7 +219,7 @@ public static void outputWindowFiles(ArrayList windows, double t0, St FileHandler.writeToCSV(allWindowDGMs, outputDirectory, "all_window_DGMs"); } - public static void generateBaselineFile(DataEntry allGaze, String outputDirectory) { + static void generateBaselineFile(DataEntry allGaze, String outputDirectory) { DataEntry baseline = new DataEntry(allGaze.getHeaders()); double startTime = Double.valueOf(allGaze.getValue(TIME_INDEX, 0)); double endTime = startTime + BASELINE_LENGTH; @@ -239,7 +239,7 @@ public static void generateBaselineFile(DataEntry allGaze, String outputDirector FileHandler.writeToCSV(Analysis.generateResults(baseline, DataFilter.filterByFixations(baseline)), outputDirectory, "baseline_DGMs"); } - public static double getRawEventBaselineValue(String fileDirectory, String event) { + static double getRawEventBaselineValue(String fileDirectory, String event) { double eventValue = 0; File baselineFile = new File(fileDirectory + "/baseline/baseline.csv"); @@ -256,7 +256,7 @@ public static double getRawEventBaselineValue(String fileDirectory, String event return eventValue; } - public static double getAveragePupilDilationBaseline(String fileDirectory) { + static double getAveragePupilDilationBaseline(String fileDirectory) { double eventValue = 0; File baselineFile = new File(fileDirectory + "/baseline/baseline.csv"); @@ -274,7 +274,7 @@ public static double getAveragePupilDilationBaseline(String fileDirectory) { return eventValue; } - public static double getEventBaselineValue(String fileDirectory, String event) { + static double getEventBaselineValue(String fileDirectory, String event) { switch(event) { case "LPMM + RPMM": return getAveragePupilDilationBaseline(fileDirectory); @@ -283,7 +283,7 @@ public static double getEventBaselineValue(String fileDirectory, String event) { } } - public static double getEventWindowValue(DataEntry d, String event, int row) { + static double getEventWindowValue(DataEntry d, String event, int row) { switch (event) { case "LPMM + RPMM": double left = Double.parseDouble(d.getValue("LPMM", row)); From 1972cb53857d68c5d912665f05ca90975279ee7b Mon Sep 17 00:00:00 2001 From: ashkjones Date: Tue, 6 Aug 2024 20:06:20 -0700 Subject: [PATCH 2/3] rename variables in AreaOfInterests to be more intuitive --- .../thed2lab/analysis/AreaOfInterests.java | 52 ++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java b/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java index 6935f36..8d3192b 100644 --- a/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java +++ b/src/main/java/com/github/thed2lab/analysis/AreaOfInterests.java @@ -64,49 +64,31 @@ public static void generateAOIs(DataEntry allGazeData, String outputDirectory, S boolean isFirst = true; Set aoiKeySet = aoiMetrics.keySet(); - //int aoiFixationCount = 0; int row = 1; for (String aoiKey : aoiKeySet) { - DataEntry aoi = aoiMetrics.get(aoiKey); - DataEntry aoiFixations = aoiFixationMetrics.get(aoiKey); - //aoiFixationCount += aoiFixations.rowCount(); - - aoi.writeToCSV(outputDirectory + "/AOIs", aoiKey + "_all_gaze"); - - // if (aoi.rowCount() >= 2) { - ArrayList> results = Analysis.generateResults(aoi, aoiFixations); - if (isFirst) { // - isFirst = false; - List headers = results.get(0); - metrics.get(0).addAll(headers); //Adds all headers generated by an analysis, but only for the first AOI - metrics.get(0).addAll(Arrays.asList(additionalHeaders)); - } - results.get(1).add(aoiKey); - metrics.add(results.get(1)); - metrics.get(row).addAll(getProportions(allFixations, aoiFixations, totalDuration)); - validAOIs.put(aoiKey, aoiFixations); - row++; - // } + DataEntry singleAoiGaze = aoiMetrics.get(aoiKey); + DataEntry singleAoiFixations = aoiFixationMetrics.get(aoiKey); + + singleAoiGaze.writeToCSV(outputDirectory + "/AOIs", aoiKey + "_all_gaze"); + + List> results = Analysis.generateResults(allGazeData, singleAoiGaze, singleAoiFixations); + if (isFirst) { // + isFirst = false; + List headers = results.get(0); + metrics.get(0).addAll(headers); //Adds all headers generated by an analysis, but only for the first AOI + metrics.get(0).addAll(Arrays.asList(additionalHeaders)); + } + results.get(1).add(aoiKey); + metrics.add(results.get(1)); + metrics.get(row).addAll(getProportions(allFixations, singleAoiFixations, totalDuration)); + validAOIs.put(aoiKey, singleAoiFixations); + row++; } ArrayList> pairResults = generatePairResults(allFixations, aoiMetrics); - /*for (int i = 0; i < pairResults.size(); i++) { //Write values to all rows - for (String s : perAoiHeaders) { - metrics.get(0).add(s + "_" + i); //Adds headersfor each pair. - } - metrics.get(i + 1).addAll(pairResults.get(i)); - }*/ FileHandler.writeToCSV(metrics, outputDirectory, fileName + "_AOI_DGMs"); FileHandler.writeToCSV(pairResults, outputDirectory, fileName+"_AOI_Transitions"); } - // public static ArrayList generateAreaOfInterestResults(DataEntry all,DataEntry aoi, double totalDuration) { - // DataEntry valid = DataFilter.filterByValidity(all); - // ArrayList results = new ArrayList<>(); - // List proportions = getProportions(valid, aoi, totalDuration); - // results.addAll(proportions); - // return results; - // } - public static ArrayList getProportions(DataEntry fixations, DataEntry aoiFixations, double totalDuration) { ArrayList results = new ArrayList<>(); double fixationProportion = (double)aoiFixations.rowCount()/fixations.rowCount(); //Number of fixations in AOI divided by total fixations From 0e12cd960fd0d58e49b445c5f811e4ab0b879c01 Mon Sep 17 00:00:00 2001 From: ashkjones Date: Tue, 6 Aug 2024 20:08:08 -0700 Subject: [PATCH 3/3] Changed how SaccadeVelocity sorts saccade data in AOIs related to issue #40. Also added tests for SaccadeVelocity. Added some javadoc and will add more later --- .../github/thed2lab/analysis/Analysis.java | 139 ++++++++++-------- .../thed2lab/analysis/SaccadeVelocity.java | 90 ++++++++---- .../analysis/SaccadeVelocityTest.java | 51 +++---- src/test/resources/aoi_a_fixation.csv | 15 ++ 4 files changed, 173 insertions(+), 122 deletions(-) create mode 100644 src/test/resources/aoi_a_fixation.csv diff --git a/src/main/java/com/github/thed2lab/analysis/Analysis.java b/src/main/java/com/github/thed2lab/analysis/Analysis.java index 29293d3..bcc377a 100644 --- a/src/main/java/com/github/thed2lab/analysis/Analysis.java +++ b/src/main/java/com/github/thed2lab/analysis/Analysis.java @@ -7,22 +7,35 @@ import java.util.List; public class Analysis { - final static int SCREEN_WIDTH = 1920; - final static int SCREEN_HEIGHT = 1080; - - final static int MIN_PATTERN_LENGTH = 3; - final static int MAX_PATTERN_LENGTH = 7; - final static int MIN_PATTERN_FREQUENCY = 2; - final static int MIN_SEQUENCE_SIZE = 3; - - final static String TIME_INDEX = "TIME"; + /** + * The analysis class drives the entire analysis on one or multiple files of gaze data. + * 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; + private final static int MIN_PATTERN_FREQUENCY = 2; + private final static int MIN_SEQUENCE_SIZE = 3; private Parameters params; + /** + * Used to construct a single Analysis object. + * @param params information about the files to analyze and the types of analysis run. + */ public Analysis(Parameters params) { this.params = params; } + /** + * Runs all data analysis and writes the results to files. The method iterates over all the provided files, + * create DataEntry objects to represent them, and call methods to analyze those DataEntry objects. + * Finally, the results of these methods are output to CSV files. + * @return {@code Boolean} indicating if the run was successful. + */ public boolean run() { try { File[] inputFiles = params.getInputFiles(); @@ -30,15 +43,6 @@ public boolean run() { List> allParticipantDGMs = new ArrayList>(); LinkedHashMap aoiMap = new LinkedHashMap(); - // aoiMap.put("", 65); - // aoiMap.put("Alt_VSI", 66); - // aoiMap.put("AI", 67); - // aoiMap.put("TI_HSI", 68); - // aoiMap.put("SSI", 69); - // aoiMap.put("ASI", 70); - // aoiMap.put("RPM", 71); - // aoiMap.put("Window", 72); - WindowSettings settings = params.getWindowSettings(); for (int i = 0; i < inputFiles.length; i++) { @@ -61,7 +65,7 @@ public boolean run() { fixations.writeToCSV(pDirectory, pName + "_fixations"); // Generate DGMs - ArrayList> descriptiveGazeMeasures = generateResults(allGaze, fixations); + List> descriptiveGazeMeasures = generateResults(allGaze, fixations); FileHandler.writeToCSV(descriptiveGazeMeasures, pDirectory, pName + "_DGMs"); // If empty, add header row @@ -138,53 +142,60 @@ public boolean run() { } // This function should only take in raw gaze data as a parameter, otherwise derived DataEntrys will be produced with incorrect data - public static ArrayList> generateResults(DataEntry allGaze, DataEntry fixations) { + /** + * Generates descriptive gaze measures from a single participant’s raw gaze data for the whole screen. + * @param allGaze all the participant's gaze data. + * @param fixations the participant gaze data, filtered by 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 fixations) { DataEntry validGaze = DataFilter.applyScreenSize(DataFilter.filterByValidity(allGaze), SCREEN_WIDTH, SCREEN_HEIGHT); - DataEntry validFixations = DataFilter.applyScreenSize(DataFilter.filterByValidity(fixations), SCREEN_WIDTH, SCREEN_HEIGHT); - - // DataEntry validGaze = DataFilter.applyScreenSize(allGaze, SCREEN_WIDTH, SCREEN_HEIGHT); - // DataEntry validFixations = DataFilter.applyScreenSize(fixations, SCREEN_WIDTH, SCREEN_HEIGHT); - - ArrayList> results = new ArrayList>(); - results.add(new ArrayList()); //Headers - results.add(new ArrayList()); //Values - - LinkedHashMap fixation = Fixations.analyze(validFixations); - results.get(0).addAll(fixation.keySet()); - results.get(1).addAll(fixation.values()); - - LinkedHashMap saccades = Saccades.analyze(validFixations); - results.get(0).addAll(saccades.keySet()); - results.get(1).addAll(saccades.values()); - - LinkedHashMap saccadeVelocity = SaccadeVelocity.analyze(validGaze); - results.get(0).addAll(saccadeVelocity.keySet()); - results.get(1).addAll(saccadeVelocity.values()); - - LinkedHashMap angles = Angles.analyze(validFixations); - results.get(0).addAll(angles.keySet()); - results.get(1).addAll(angles.values()); - - LinkedHashMap convexHull = ConvexHull.analyze(validFixations); - results.get(0).addAll(convexHull.keySet()); - results.get(1).addAll(convexHull.values()); - - LinkedHashMap entropy = GazeEntropy.analyze(validFixations); - results.get(0).addAll(entropy.keySet()); - results.get(1).addAll(entropy.values()); - - LinkedHashMap blinks = Blinks.analyze(allGaze); - results.get(0).addAll(blinks.keySet()); - results.get(1).addAll(blinks.values()); - - LinkedHashMap gaze = Gaze.analyze(validGaze); - results.get(0).addAll(gaze.keySet()); - results.get(1).addAll(gaze.values()); - - LinkedHashMap event = Event.analyze(validGaze); - results.get(0).addAll(event.keySet()); - results.get(1).addAll(event.values()); + DataEntry validAoiFixations = DataFilter.applyScreenSize(DataFilter.filterByValidity(fixations), SCREEN_WIDTH, SCREEN_HEIGHT); + var results = generateResultsHelper(validGaze, validGaze, validAoiFixations); + return results; + } + // This function should only take in raw gaze data as a parameter, otherwise derived DataEntrys will be produced with incorrect data + /** + * Generates descriptive gaze measures from a single participant’s raw gaze data for a single AOI. + * @param allGaze all the of participant gaze data. + * @param aoiGaze the participant gaze data that occurred inside the target AOI. + * @param aoiFixations the participant gaze data, filtered by fixations that occurred inside the target AOI. + * @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); 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. + */ + private static List> generateResultsHelper(DataEntry validAllGaze, DataEntry validAoiGaze, DataEntry validAoiFixation) { + + 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)); + + var resultsList = new ArrayList>(2); + resultsList.add(new ArrayList<>(resultsMap.keySet())); + resultsList.add(new ArrayList<>(resultsMap.values())); + + return resultsList; + } } diff --git a/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java b/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java index fc1ec22..8501f45 100644 --- a/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java +++ b/src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java @@ -10,41 +10,69 @@ public class SaccadeVelocity { 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"; - static public LinkedHashMap analyze(DataEntry data) { + /** + * 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 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}. + */ + static public LinkedHashMap analyze(DataEntry allGazeData, DataEntry fixationData) { LinkedHashMap results = new LinkedHashMap(); List> positionProfiles = new ArrayList>(); List positionProfile = new ArrayList(); ArrayList peakSaccadeVelocities = new ArrayList(); + int fixDataIndex = 0; + int gazeDataIndex = 0; + int targetFixId = -1; + + 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)); + fixDataIndex++; + if (nextFixId == curFixId + 1) { + targetFixId = curFixId; + break; + } + } + + while (gazeDataIndex < allGazeData.rowCount()) { + int curId = Integer.parseInt(allGazeData.getValue(FIXATIONID_INDEX, gazeDataIndex)); + if (curId < targetFixId) { + gazeDataIndex++; + continue; + } else if (curId > targetFixId) { + break; // could not find target, look for next fixation + } - String prevID = ""; - for (int i = 0; i < data.rowCount(); i++) { - boolean saccade = Integer.parseInt(data.getValue(FIXATION_VALIDITY_INDEX, i)) == 0 ? true : false; - if (saccade) { - // Check to see if these saccade points are part of the same saccade - String currID = data.getValue(FIXATIONID_INDEX, i); - if (!prevID.equals(currID) && positionProfile.size() != 0) { - positionProfiles.add(positionProfile); - positionProfile = new ArrayList(); + boolean saccade = Integer.parseInt(allGazeData.getValue(FIXATION_VALIDITY_INDEX, gazeDataIndex)) == 0 ? true : false; + // Check if not a saccade + if (!saccade) { + gazeDataIndex++; + continue; // go to next data point } - Double x = Double.parseDouble(data.getValue(FIXATIONX_INDEX, i)); - Double y = Double.parseDouble(data.getValue(FIXATIONY_INDEX, i)); - Double t = Double.parseDouble(data.getValue(TIME_INDEX, i)); + 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)); positionProfile.add(new Double[] {x, y, t}); - prevID = currID; - - } else if (positionProfile.size() != 0) { + gazeDataIndex++; + } + + if (positionProfile.size() > 0) { positionProfiles.add(positionProfile); positionProfile = new ArrayList(); } - } - - // add the last saccade profile if it isn't already added - // see issue #39 and #40 for explanation - if (positionProfile.size() > 0) { - positionProfiles.add(positionProfile); + } for (int i = 0; i < positionProfiles.size(); i++) { @@ -61,7 +89,6 @@ static public LinkedHashMap analyze(DataEntry data) { return results; } - /** * Returns the peak velocity of a given saccade calculated using a two point central difference algorithm. * @@ -72,15 +99,16 @@ static public LinkedHashMap analyze(DataEntry data) { * * @return The peak velocity of a saccade */ - public static double getPeakVelocity(List saccadePoints) { + static double getPeakVelocity(List saccadePoints) { if (saccadePoints.size() == 0 || saccadePoints.size() == 1) { return Double.NaN; } - double peakVelocity = 0; - double conversionRate = 0.0264583333; // Convert from pixels to cms - double velocityThreshold = 700; // Maximum possible saccadic velocity - int participantDistance = 65; // assume an average distance of 65cm from the participant to the screen + final double PIXELS_TO_CM = 0.0264583333; // Convert from pixels to cms + final double VELOCITY_THRESHOLD = 700; // Maximum possible saccadic velocity + final double PARTICIPANT_DISTANCE = 65; // assume an average distance of 65cm from the participant to the screen + final double RADIAN_TO_DEGREES = 180/Math.PI; + double peakVelocity = 0; for (int i = 1; i < saccadePoints.size(); i++) { Double[] currPoint = saccadePoints.get(i); @@ -91,13 +119,13 @@ public static double getPeakVelocity(List saccadePoints) { double x2 = prevPoint[0]; double y2 = prevPoint[1]; - double dx = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2)) * conversionRate; + double dx = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2)) * PIXELS_TO_CM; double dt = Math.abs(currPoint[2] - prevPoint[2]); - double amplitude = 180/Math.PI * Math.atan(dx/participantDistance); + double amplitude = RADIAN_TO_DEGREES * Math.atan(dx/PARTICIPANT_DISTANCE); double velocity = amplitude/dt; - if (velocity > peakVelocity && velocity <= velocityThreshold) { + if (velocity > peakVelocity && velocity <= VELOCITY_THRESHOLD) { peakVelocity = velocity; } } diff --git a/src/test/java/com/github/thed2lab/analysis/SaccadeVelocityTest.java b/src/test/java/com/github/thed2lab/analysis/SaccadeVelocityTest.java index cfc5777..1b0c5cf 100644 --- a/src/test/java/com/github/thed2lab/analysis/SaccadeVelocityTest.java +++ b/src/test/java/com/github/thed2lab/analysis/SaccadeVelocityTest.java @@ -5,17 +5,15 @@ import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Map; import org.junit.Test; public class SaccadeVelocityTest { - final double PRECISION = 0.000000001; // allowable floating point error - final static int SCREEN_WIDTH = 1920; - final static int SCREEN_HEIGHT = 1080; + private final double PRECISION = 0.000000001; // allowable floating point error + private final static int SCREEN_WIDTH = 1920; + private final static int SCREEN_HEIGHT = 1080; @Test public void testGetPeakVelocity_emptyPoints_returnNaN() { @@ -46,40 +44,39 @@ public void testGetPeakVelocity_aboveThreshold_return0() { @Test public void testGetPeakVelocity_normalUseCase_returnVelocityValue() { - + final double EXPECTED_VELOCITY = 78.3228634680; + List saccadePoints = new ArrayList<>() {{ + add(new Double[]{1027.2576, 431.892, 0.70996}); + add(new Double[]{1014.1824, 412.5168, 0.71692}); + add(new Double[]{1008.096, 391.2408, 0.72363}); + }}; + + double actualVelocity = SaccadeVelocity.getPeakVelocity(saccadePoints); + assertEquals(EXPECTED_VELOCITY, actualVelocity, PRECISION); } @Test - public void testSaccadeVelocityAnalyze_nonContinuousEndsWithSaccadeData() { + public void testSaccadeVelocityAnalyze_nonContinuousWholeScreen() { final String GAZE_PATH = "./src/test/resources/filtered_by_validity.csv"; - final double EXPECTED_AVG_PEAK = 169.15239533934; + final String FIXATION_PATH = "./src/test/resources/valid_fixations.csv"; + final double EXPECTED_AVG_PEAK = 179.96919273273; final String KEY = "average_peak_saccade_velocity"; DataEntry gazeData = DataFilter.applyScreenSize(FileHandler.buildDataEntry(new File(GAZE_PATH)), SCREEN_WIDTH, SCREEN_HEIGHT); + DataEntry fixationData = DataFilter.applyScreenSize(FileHandler.buildDataEntry(new File(FIXATION_PATH)), SCREEN_WIDTH, SCREEN_HEIGHT); - Map actualMap = SaccadeVelocity.analyze(gazeData); - assertEquals(1, actualMap.size()); - assertEquals(EXPECTED_AVG_PEAK, Double.parseDouble(actualMap.get(KEY)), PRECISION); - + double actualAvgPeak = Double.parseDouble(SaccadeVelocity.analyze(gazeData, fixationData).get(KEY)); + assertEquals(EXPECTED_AVG_PEAK, actualAvgPeak, PRECISION); } @Test - public void testSaccadeVelocityAnalyze_nonContinuousEndsWithFixationData() { + public void testSaccadeVelocityAnalyze_nonContinuousAoiA() { final String GAZE_PATH = "./src/test/resources/filtered_by_validity.csv"; - final double EXPECTED_AVG_PEAK = 169.15239533934; + final String FIXATION_PATH = "./src/test/resources/aoi_a_fixation.csv"; + final double EXPECTED_AVG_PEAK = 133.30276061352; final String KEY = "average_peak_saccade_velocity"; DataEntry gazeData = DataFilter.applyScreenSize(FileHandler.buildDataEntry(new File(GAZE_PATH)), SCREEN_WIDTH, SCREEN_HEIGHT); - gazeData.process(Arrays.asList( - "0","X-Plane","971","6.53005","5.88E+12","0.47815","0.45335", - "6.53005","0.14856","25","1","0.46056","0.45713","1","0.60729", - "0.47315","0"," ","0","","0.41066","0.36362","16.25439","1.08789", - "1","0.61855","0.37037","15.69818","1.08789","1","0","0","11", - "4.43149","1","4.03482","1","0","0","0","0","0","0","0","0","1", - "1","1","1","1","1","1","0","0","0","AOI_A","0","0","0" - )); - - Map actualMap = SaccadeVelocity.analyze(gazeData); - assertEquals(1, actualMap.size()); - assertEquals(EXPECTED_AVG_PEAK, Double.parseDouble(actualMap.get(KEY)), PRECISION); - + DataEntry fixationData = FileHandler.buildDataEntry(new File(FIXATION_PATH)); + double actualAvgPeak = Double.parseDouble(SaccadeVelocity.analyze(gazeData, fixationData).get(KEY)); + assertEquals(EXPECTED_AVG_PEAK, actualAvgPeak, PRECISION); } } diff --git a/src/test/resources/aoi_a_fixation.csv b/src/test/resources/aoi_a_fixation.csv new file mode 100644 index 0000000..93a34ac --- /dev/null +++ b/src/test/resources/aoi_a_fixation.csv @@ -0,0 +1,15 @@ +"MEDIA_ID","MEDIA_NAME","CNT","TIME(2023/02/22 14:26:24.897)","TIMETICK(f=10000000)","FPOGX","FPOGY","FPOGS","FPOGD","FPOGID","FPOGV","BPOGX","BPOGY","BPOGV","CX","CY","CS","KB","KBS","USER","LPCX","LPCY","LPD","LPS","LPV","RPCX","RPCY","RPD","RPS","RPV","BKID","BKDUR","BKPMIN","LPMM","LPMMV","RPMM","RPMMV","DIAL","DIALV","GSR","GSRV","HR","HRV","HRP","IBI","TTL0","TTL1","TTL2","TTL3","TTL4","TTL5","TTL6","TTLV","PIXS","PIXV","AOI","SACCADE_MAG","SACCADE_DIR","VID_FRAME" +"0","X-Plane","105","0.70361","5.88E+12","0.55532","0.37966","0.04236","0.66125","2","1","0.51227","0.4405","1","0.39896","0.96667","0"," ","0","","0.37635","0.36126","16.18326","0.98898","1","0.58384","0.36132","17.10145","0.98898","1","0","0","10","4.37419","1","4.37949","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","90.89242","328.39877","0" +"0","X-Plane","170","1.14563","5.88E+12","0.60302","0.47807","1.02502","0.12061","4","1","0.51292","0.55406","1","0.36875","0.97407","0"," ","0","","0.37832","0.35979","14.0872","1.06316","1","0.58606","0.35848","17.1052","1.05078","1","0","0","11","4.45777","1","4.38095","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","195.7713","335.22574","0" +"0","X-Plane","195","1.31287","5.88E+12","0.5417","0.50501","1.15222","0.16064","5","1","0.58271","0.56753","1","0.36875","0.97407","0"," ","0","","0.37915","0.36145","17.69883","1.03842","1","0.58641","0.36015","17.05352","1.01574","1","0","0","11","4.8244","1","4.79207","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","121.27631","193.88113","0" +"0","X-Plane","214","1.44702","5.88E+12","0.6017","0.49948","1.31982","0.1272","6","1","0.61164","0.43869","1","0.36875","0.97407","0"," ","0","","0.3805","0.35773","18.65766","1.0879","1","0.58796","0.35573","16.47065","1.0879","1","0","0","11","4.76412","1","4.75056","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","115.35478","2.96778","0" +"0","X-Plane","267","1.80212","5.88E+12","0.59532","0.48402","1.45374","0.34839","7","1","0.52798","0.54946","1","0.45521","0.65926","0"," ","0","","0.3855","0.35311","17.34099","0.99513","1","0.59383","0.35234","17.22945","1.01781","1","0","0","11","4.69776","1","4.48169","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","20.7084","126.26564","0" +"0","X-Plane","301","2.02979","5.88E+12","0.51246","0.50446","1.80884","0.22095","8","1","0.4986","0.63023","1","0.48906","0.46667","0"," ","0","","0.3791","0.35579","17.26946","1.02608","1","0.587","0.35428","16.91505","1.03845","1","0","0","10","4.63761","1","4.42349","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","160.61545","187.89983","0" +"0","X-Plane","455","3.06165","5.88E+12","0.56004","0.50484","2.57239","0.48926","11","1","0.66262","0.51919","1","0.60729","0.47315","0"," ","0","","0.38286","0.34484","19.87031","1.0879","1","0.59006","0.34435","18.37679","1.07552","1","0","0","10","4.89634","1","4.74066","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","256.58389","17.07036","0" +"0","X-Plane","482","3.24194","5.88E+12","0.6961","0.45194","3.06799","0.17395","12","1","0.80537","0.42519","1","0.60729","0.47315","0"," ","0","","0.3808","0.3419","20.03168","1.0137","1","0.58614","0.34192","18.966","1.0137","1","0","0","10","4.99377","1","4.87158","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","267.40952","12.33633","0" +"0","X-Plane","527","3.54358","5.88E+12","0.8177","0.36305","3.26904","0.27454","13","1","0.82291","0.36648","1","0.60729","0.47315","0"," ","0","","0.38277","0.33067","18.89176","1.0384","1","0.58888","0.32927","18.47846","1.05077","1","0","0","10","4.87204","1","4.638","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","252.43895","22.35194","0" +"0","X-Plane","540","3.63074","5.88E+12","0.68927","0.39487","3.55029","0.08044","14","1","0.59499","0.3775","1","0.60729","0.47315","0"," ","0","","0.38348","0.32802","15.91045","1.13326","1","0.59059","0.32877","16.65091","1.11814","1","0","0","10","4.5452","1","4.42635","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","248.96875","187.93396","0" +"0","X-Plane","630","4.24011","5.88E+12","0.63812","0.52132","4.10193","0.13818","16","1","0.46777","0.60101","1","0.60729","0.47315","0"," ","0","","0.39122","0.35429","14.82566","1.0384","1","0.60049","0.35801","15.83811","1.0384","1","0","0","10","3.92854","1","4.26184","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","107.59187","333.29495","0" +"0","X-Plane","909","6.11572","5.88E+12","0.51429","0.47915","5.61011","0.50562","22","1","0.51094","0.47216","1","0.60729","0.47315","0"," ","0","","0.41894","0.36276","16.4916","1.08789","1","0.62635","0.37182","16.70486","1.10301","1","0","0","11","4.56582","1","4.28076","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","349.41483","161.06587","0" +"0","X-Plane","944","6.3501","5.88E+12","0.49332","0.43387","6.12244","0.22766","23","1","0.48903","0.38343","1","0.60729","0.47315","0"," ","0","","0.41379","0.36169","15.53086","1.06314","1","0.62167","0.36961","15.83425","1.07551","1","0","0","11","4.47751","1","4.09084","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","63.34434","129.46535","0" +"0","X-Plane","967","65.0562","5.88E+12","0.47815","0.45335","6.35706","0.14856","24","1","0.46056","0.45713","1","0.60729","0.47315","0"," ","0","","0.41066","0.36362","16.25439","1.08789","1","0.61855","0.37037","15.69818","1.08789","1","0","0","11","4.43149","1","4.03482","1","0","0","0","0","0","0","0","0","1","1","1","1","1","1","1","0","0","0","AOI_A","35.92992","215.84119","0"