Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated UI and DGMs #64

Merged
merged 6 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/com/github/thed2lab/analysis/Analysis.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class Analysis {
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 final static int MIN_SEQUENCE_SIZE = 2;

private Parameters params;

Expand Down
98 changes: 94 additions & 4 deletions src/main/java/com/github/thed2lab/analysis/SaccadeVelocity.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@
public class SaccadeVelocity {

/**
* Calculates the average peak saccade velocity. Iterates over all rows of a participant’s gaze.
* Calculates the descriptive gaze measures for 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, 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}.
* @return saccade velocity’s header mapped to the calculated value as a {@code String}.
*/
static public LinkedHashMap<String,String> analyze(DataEntry allGazeData, DataEntry fixationData) {
LinkedHashMap<String,String> results = new LinkedHashMap<String,String>();

List<List<Double[]>> positionProfiles = new ArrayList<List<Double[]>>();
List<Double[]> positionProfile = new ArrayList<Double[]>();
ArrayList<Double> peakSaccadeVelocities = new ArrayList<Double>();
ArrayList<Double> meanSaccadeVelocities = new ArrayList<Double>();

int fixDataIndex = 0;
int gazeDataIndex = 0;
int targetFixId = -1;
Expand Down Expand Up @@ -80,20 +82,71 @@ static public LinkedHashMap<String,String> analyze(DataEntry allGazeData, DataEn

for (int i = 0; i < positionProfiles.size(); i++) {
List<Double[]> saccadePoints = positionProfiles.get(i);

Double peakSaccadeVelocity = getPeakVelocity(saccadePoints);
Double meanSaccadeVelocity = getMeanVelocity(saccadePoints);

if (!Double.isNaN(peakSaccadeVelocity)) peakSaccadeVelocities.add(peakSaccadeVelocity);
if (!Double.isNaN(meanSaccadeVelocity)) meanSaccadeVelocities.add(meanSaccadeVelocity);
}

// Peak saccade velocity
results.put(
"average_peak_saccade_velocity",
"sum_peak_saccade_velocity",
String.valueOf(DescriptiveStats.getSumOfDoubles(peakSaccadeVelocities))
);
results.put(
"mean_peak_saccade_velocity",
String.valueOf(DescriptiveStats.getMeanOfDoubles(peakSaccadeVelocities))
);
results.put(
"median_peak_saccade_velocity",
String.valueOf(DescriptiveStats.getMedianOfDoubles(peakSaccadeVelocities))
);
results.put(
"std_peak_saccade_velocity",
String.valueOf(DescriptiveStats.getStDevOfDoubles(peakSaccadeVelocities))
);
results.put(
"min_peak_saccade_velocity",
String.valueOf(DescriptiveStats.getMinOfDoubles(peakSaccadeVelocities))
);
results.put(
"max_peak_saccade_velocity",
String.valueOf(DescriptiveStats.getMaxOfDoubles(peakSaccadeVelocities))
);

// Mean saccade velocity
results.put(
"sum_mean_saccade_velocity",
String.valueOf(DescriptiveStats.getSumOfDoubles(meanSaccadeVelocities))
);
results.put(
"mean_mean_saccade_velocity",
String.valueOf(DescriptiveStats.getMeanOfDoubles(meanSaccadeVelocities))
);
results.put(
"median_mean_saccade_velocity",
String.valueOf(DescriptiveStats.getMedianOfDoubles(meanSaccadeVelocities))
);
results.put(
"std_mean_saccade_velocity",
String.valueOf(DescriptiveStats.getStDevOfDoubles(meanSaccadeVelocities))
);
results.put(
"min_mean_saccade_velocity",
String.valueOf(DescriptiveStats.getMinOfDoubles(meanSaccadeVelocities))
);
results.put(
"max_mean_saccade_velocity",
String.valueOf(DescriptiveStats.getMaxOfDoubles(meanSaccadeVelocities))
);

return results;
}

/**
* Returns the peak velocity of a given saccade calculated using a two point central difference algorithm.
* Returns the peak velocity of a given saccade calculated using a two point difference algorithm.
*
* @param saccadePoints A list of saccade data points, where each data point is a double array.
* [0] = X position
Expand Down Expand Up @@ -135,4 +188,41 @@ static double getPeakVelocity(List<Double[]> saccadePoints) {

return peakVelocity;
}

static double getMeanVelocity(List<Double[]> saccadePoints) {
if (saccadePoints.size() == 0 || saccadePoints.size() == 1) {
return Double.NaN;
}

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 totalVelocity = 0;
double discardedDataCount = 0;

for (int i = 1; i < saccadePoints.size(); i++) {
Double[] currPoint = saccadePoints.get(i);
Double[] prevPoint = saccadePoints.get(i - 1);

double x1 = currPoint[0];
double y1 = currPoint[1];
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)) * PIXELS_TO_CM;
double dt = Math.abs(currPoint[2] - prevPoint[2]);
double amplitude = RADIAN_TO_DEGREES * Math.atan(dx/PARTICIPANT_DISTANCE);

double velocity = amplitude/dt;

if (velocity <= VELOCITY_THRESHOLD) {
totalVelocity += velocity;
} else {
discardedDataCount++;
}
}

return totalVelocity/(saccadePoints.size() - discardedDataCount);
}
}
61 changes: 60 additions & 1 deletion src/main/java/com/github/thed2lab/analysis/Saccades.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ static public LinkedHashMap<String,String> analyze(DataEntry data) {

Double[] allSaccadeLengths = getAllSaccadeLengths(allCoordinates);
ArrayList<Double> allSaccadeDurations = getAllSaccadeDurations(saccadeDetails);

ArrayList<Double> allSaccadeAmplitudes = getAllSaccadeAmplitudes(allCoordinates);

results.put(
"total_number_of_saccades", //Output Header
Expand Down Expand Up @@ -107,6 +107,36 @@ static public LinkedHashMap<String,String> analyze(DataEntry data) {
"max_saccade_duration",
String.valueOf(DescriptiveStats.getMaxOfDoubles(allSaccadeDurations))
);

results.put(
"sum_of_all_saccade_amplitudes",
String.valueOf(DescriptiveStats.getSumOfDoubles(allSaccadeAmplitudes))
);

results.put(
"mean_saccade_amplitude",
String.valueOf(DescriptiveStats.getMeanOfDoubles(allSaccadeAmplitudes))
);

results.put(
"median_saccade_amplitude",
String.valueOf(DescriptiveStats.getMedianOfDoubles(allSaccadeAmplitudes))
);

results.put(
"stdev_of_saccade_amplitude",
String.valueOf(DescriptiveStats.getStDevOfDoubles(allSaccadeAmplitudes))
);

results.put(
"min_saccade_amplitude",
String.valueOf(DescriptiveStats.getMinOfDoubles(allSaccadeAmplitudes))
);

results.put(
"max_saccade_amplitude",
String.valueOf(DescriptiveStats.getMaxOfDoubles(allSaccadeAmplitudes))
);

results.put(
"scanpath_duration",
Expand Down Expand Up @@ -176,4 +206,33 @@ public static double getFixationToSaccadeRatio(ArrayList<Double> allFixationDura
double saccadeDuration = DescriptiveStats.getSumOfDoubles(allSaccadeDurations);
return fixationDuration/saccadeDuration;
}

public static ArrayList<Double> getAllSaccadeAmplitudes(ArrayList<Coordinate> allCoordinates) {
if (allCoordinates.size() == 0 || allCoordinates.size() == 1) return new ArrayList<Double>();

ArrayList<Double> allSaccadeAmplitudes = new ArrayList<>();

final double PIXELS_TO_CM = 0.0264583333; // Convert from pixels to cms
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;

for (int i = 1; i < allCoordinates.size(); i++) {
Coordinate currCoordinate = allCoordinates.get(i);
Coordinate prevCoordinate = allCoordinates.get(i - 1);

if (prevCoordinate.fid == currCoordinate.fid - 1) {
double x1 = currCoordinate.x;
double y1 = currCoordinate.y;
double x2 = prevCoordinate.x;
double y2 = prevCoordinate.y;

double dx = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) * PIXELS_TO_CM;
double amplitude = RADIAN_TO_DEGREES * Math.atan(dx/PARTICIPANT_DISTANCE);

allSaccadeAmplitudes.add(amplitude);
}
}

return allSaccadeAmplitudes;
}
}
20 changes: 10 additions & 10 deletions src/main/java/com/github/thed2lab/analysis/UserInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ private void buildWindowsPanel() {
componentGBC.gridwidth = 1;
windowPanel1.add(hoppingHopSizeField, componentGBC);

eventCheckBox = new JCheckBox("Event-Based");
String eventToolTip = "An event-based view of the gaze data using a session window that is non-overlapping and non-fixed in size.";
eventCheckBox = new JCheckBox("Session");
String eventToolTip = "An session view of the gaze data using a session window that is non-overlapping and non-fixed in size.";
eventCheckBox.setToolTipText("<html><p width=\"500\">" + eventToolTip + "</p></html>");
eventCheckBox.setFont(STANDARD_FONT);
componentGBC.gridx = 0;
Expand All @@ -359,7 +359,7 @@ private void buildWindowsPanel() {
windowPanel2.add(timeoutLabel, componentGBC);

eventTimeoutField = new JTextField(windowSettings.eventTimeout + "", 10); // Default value
eventTimeoutField.setToolTipText("Enter the timeout length in seconds for event-based windows.");
eventTimeoutField.setToolTipText("Enter the timeout length in seconds for session windows.");
eventTimeoutField.setFont(STANDARD_FONT);
componentGBC.insets = new Insets(0, 0, 0, 0);
componentGBC.gridx = 1;
Expand All @@ -376,7 +376,7 @@ private void buildWindowsPanel() {
windowPanel2.add(maxDurationLabel, componentGBC);

eventMaxDurationField = new JTextField(windowSettings.eventMaxDuration + "", 10); // Default value
eventMaxDurationField.setToolTipText("Enter the maximum duration in seconds for event-based windows.");
eventMaxDurationField.setToolTipText("Enter the maximum duration in seconds for session windows.");
eventMaxDurationField.setFont(STANDARD_FONT);
componentGBC.insets = new Insets(0, 0, 0, 0);
componentGBC.gridx = 1;
Expand All @@ -393,7 +393,7 @@ private void buildWindowsPanel() {
windowPanel2.add(baselineDurationLabel, componentGBC);

eventBaselineDurationField = new JTextField(windowSettings.eventBaselineDuration + "", 10); // Default value
eventBaselineDurationField.setToolTipText("Enter the baseline duration in seconds for event-based windows.");
eventBaselineDurationField.setToolTipText("Enter the baseline duration in seconds for session windows.");
eventBaselineDurationField.setFont(STANDARD_FONT);
componentGBC.insets = new Insets(0, 0, 0, 0);
componentGBC.gridx = 1;
Expand All @@ -415,7 +415,7 @@ private void buildWindowsPanel() {

eventComboBox = new JComboBox<String>(itemSet.toArray(new String[itemSet.size()]));
windowSettings.event = (String) eventComboBox.getSelectedItem();
eventComboBox.setToolTipText("Select the event-defining analytic for event-based windows.");
eventComboBox.setToolTipText("Select the event-defining analytic for session windows.");
eventComboBox.setFont(STANDARD_FONT);
componentGBC.insets = new Insets(0, 0, 0, 0);
componentGBC.gridx = 1;
Expand Down Expand Up @@ -498,7 +498,7 @@ private void buildHelpPagePanel() {
Window settings allow users to perform window-based analyses of the DGMs, wherein the participant’s gaze file can be analyzed over time by: <br/> <br/>
• taking a scheduled digest view of the gaze data using a tumbling window that is non-overlapping and fixed in size. <br/>
• taking the most recent snapshot view of the gaze data using a hopping window that is overlapping and fixed in size. <br/>
• taking an event-based view of the gaze data using a session window that is non-overlapping and non-fixed in size. <br/>
• taking an session view of the gaze data using a session window that is non-overlapping and non-fixed in size. <br/>
• taking a cumulative view of the gaze data using an expanding window that is overlapping and non-fixed in size. <br/> <br/>
To enable a window, simply select the checkbox appearing next to the window’s name and enter the desired parameters corresponding to each window.
All window parameters are defined in seconds. Once users have selected one or more files and filled out their desired fields, they can press the “Run Analysis”
Expand Down Expand Up @@ -585,7 +585,7 @@ then moves on to the next bordering time zone to make subsequent predictions (i.

windowsHelpPanel.add(Box.createRigidArea(new Dimension(0, 20)));

JLabel eventLabel = new JLabel("Event-Based Window");
JLabel eventLabel = new JLabel("Session Window");
eventLabel.setFont(BOLD_FONT);
windowsHelpPanel.add(eventLabel);

Expand All @@ -605,7 +605,7 @@ then moves on to the next bordering time zone to make subsequent predictions (i.
• POGD, the duration of a fixation (in seconds); and <br/>
• BKPMIN, the number of blinks in the previous 60 second period of time (count). <br/> <br/>

A non-overlapping, non-fixed-size session window is used to achieve event-based gaze analytics, shown in Fig. 4.
A non-overlapping, non-fixed-size session window is used to achieve session gaze analytics, shown in Fig. 4.
A session window begins when the first event is found; it then keeps searching for the next event within a specified time period.
If nothing is found, it would close at a specified timeout (e.g., window 1); if another event is found, the session window would extend the
search within another timeout period and repeat this process (e.g., window 3) until a specified maximum duration (e.g., window 2). For example,
Expand All @@ -623,7 +623,7 @@ to define a notable gaze event, a baseline (i.e., the average saccadic length ob

String fig4Description = """
<html><p width=\"750\">
Fig. 4. Taking an event-based view of the gaze data using a session window that is non-overlapping and non-fixed in size.
Fig. 4. Taking an session view of the gaze data using a session window that is non-overlapping and non-fixed in size.
<html><p width=\"750\">
""";
JLabel fig4DescriptionLabel = new JLabel(fig4Description);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@ public WindowSettings() {
this.hoppingHopSize = 30;

this.eventEnabled = false;
this.eventTimeout = 4;
this.eventMaxDuration = 60;
this.eventBaselineDuration = 120;
this.eventTimeout = 10;
this.eventMaxDuration = 90;
this.eventBaselineDuration = 30;
this.event = "";
}

@Override
public String toString() {
return "Tumbling: " + tumblingEnabled + " Window Size: " + tumblingWindowSize + "\n" +
"Expanding: " + expandingEnabled + " Window Size: " + expandingWindowSize +"\n" +
"Hopping: " + hoppingEnabled + " Window Size: " + hoppingWindowSize + " Hop Size: " + hoppingHopSize + "\n" +
"Event: " + eventEnabled + ", " + event + " Timeout: " + eventTimeout + " Max Duration: " + eventMaxDuration + " Baseline Duration: " + eventBaselineDuration;
"Session (Event): " + eventEnabled + ", " + event + " Timeout: " + eventTimeout + " Max Duration: " + eventMaxDuration + " Baseline Duration: " + eventBaselineDuration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void testSaccadeVelocityAnalyze_nonContinuousWholeScreen() {
final String GAZE_PATH = "./src/test/resources/valid_gaze_screenApplied.csv";
final String FIXATION_PATH = "./src/test/resources/valid_fixation_screenApplied.csv";
final double EXPECTED_AVG_PEAK = 179.96919273273;
final String KEY = "average_peak_saccade_velocity";
final String KEY = "mean_peak_saccade_velocity";
DataEntry gazeData = FileHandler.buildDataEntry(new File(GAZE_PATH));
DataEntry fixationData = FileHandler.buildDataEntry(new File(FIXATION_PATH));
double actualAvgPeak = Double.parseDouble(SaccadeVelocity.analyze(gazeData, fixationData).get(KEY));
Expand All @@ -71,7 +71,7 @@ public void testSaccadeVelocityAnalyze_nonContinuousAoiA() {
final String GAZE_PATH = "./src/test/resources/filtered_by_validity.csv";
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";
final String KEY = "mean_peak_saccade_velocity";
DataEntry gazeData = DataFilter.applyScreenSize(FileHandler.buildDataEntry(new File(GAZE_PATH)), SCREEN_WIDTH, SCREEN_HEIGHT);
DataEntry fixationData = FileHandler.buildDataEntry(new File(FIXATION_PATH));
double actualAvgPeak = Double.parseDouble(SaccadeVelocity.analyze(gazeData, fixationData).get(KEY));
Expand Down
7 changes: 7 additions & 0 deletions src/test/java/com/github/thed2lab/analysis/SaccadesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ public void testSaccadeAnalyze_validFixations_returnHeadersAndValues() {
expectedResults.put("min_saccade_duration", 0.00598);
expectedResults.put("max_saccade_duration", 0.03028);

expectedResults.put("sum_of_all_saccade_amplitudes", 58.834398303511215);
expectedResults.put("mean_saccade_amplitude", 3.677149893969451);
expectedResults.put("median_saccade_amplitude", 2.781254831659127);
expectedResults.put("stdev_of_saccade_amplitude", 2.4654446860609887);
expectedResults.put("min_saccade_amplitude", 0.48295537661957966);
expectedResults.put("max_saccade_amplitude", 8.094871649121);

expectedResults.put("scanpath_duration", 4.95373);
expectedResults. put("fixation_to_saccade_ratio", 24.9479859620);

Expand Down
Loading
Loading