package edu.cmu.cs.eyetrack.analysis.io;
import java.awt.Dimension;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import au.com.bytecode.opencsv.CSVReader;
import edu.cmu.cs.eyetrack.analysis.struct.Trajectory;
import edu.cmu.cs.eyetrack.analysis.struct.tobii.TobiiData;
import edu.cmu.cs.eyetrack.analysis.struct.tobii.TobiiEventMap;
import edu.cmu.cs.eyetrack.analysis.struct.tobii.TobiiEventMap.TobiiEventType;
import edu.cmu.cs.eyetrack.analysis.struct.tobii.TobiiFrame;
import edu.cmu.cs.eyetrack.analysis.struct.tobii.TobiiHeader;
import edu.cmu.cs.eyetrack.analysis.struct.trackit.Experiment;
import edu.cmu.cs.eyetrack.analysis.struct.trackit.Experiment.TrialType;
import edu.cmu.cs.eyetrack.analysis.struct.trackit.TrackItFrame;
import edu.cmu.cs.eyetrack.analysis.struct.trackit.Trial;
import edu.cmu.cs.eyetrack.helper.Coordinate;
public class Loader {
/**
* Reads and stores header information for a Tobii file (either the Events
* or full Data file). The CSVReader parameter will be advanced past the
* the location of the header upon completion
* @param reader a CSVReader with an open connection to a Tobii file
* @return TobiiHeader containing header info for reader's Tobii file
* @throws IOException
*/
private static TobiiHeader readHeader(CSVReader reader) throws IOException {
TobiiHeader header = new TobiiHeader();
//
// SYSTEM PROPERTIES section
String line[] = reader.readNext();
assert(line[0].equalsIgnoreCase("System Properties:"));
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Operating System:"));
header.setSysOS(line[1]);
line = reader.readNext();
assert(line[0].equalsIgnoreCase("System User Name:"));
header.setSysUser(line[1]);
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Machine Name:"));
header.setSysMachine(line[1]);
//
// DATA PROPERTIES section
reader.readNext();
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Data Properties:"));
//
// RECORDING section
reader.readNext();
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Recording name:"));
if(!line[1].isEmpty()) {
header.setRecName(line[1]);
} else if(!line[2].isEmpty()) {
header.setRecName(line[2]);
}
// Date and Time of the recording in Tobii. We combine these into one Date
// Formatted as, e.g., "6/24/2011" and "11:42:32 AM"
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Recording date:"));
String recDateStr = line[1].trim();
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Recording time:"));
String recTimeStr = line[1].trim();
DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a");
try {
Date recDateTime = dateFormat.parse(recDateStr + " " + recTimeStr);
header.setRecDateTime(recDateTime);
header.setRecDateTimestamp(recDateTime.getTime());
} catch(ParseException e) {
System.err.println("Had trouble parsing the date/times in a Tobii header file. Non-fatal.");
System.err.println("Date string: " + recDateStr + "\t\tTime string: " + recTimeStr);
}
// Resolution at which the Tobii experiment was recorded
// Formatted as string, e.g., "1024 x 768"
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Recording resolution:"));
String[] recResolutionStr = line[1].trim().split(" ");
int width = Double.valueOf(recResolutionStr[0]).intValue();
int height = Double.valueOf(recResolutionStr[2]).intValue();
header.setRecResolution(new Dimension(width, height));
// Newer version of Tobii also reports screen size in millimeters;
// older versions skip straight to the export section (added Feb. 2012)
line = reader.readNext();
if(line.length > 1) {
assert(line[0].equalsIgnoreCase("Recording screen size in millimeter:"));
String[] screenSizeStr = line[1].trim().split(" ");
double widthMM = Double.valueOf(screenSizeStr[0]).doubleValue();
double heightMM = Double.valueOf(screenSizeStr[2]).doubleValue();
header.setScreenSizeMM(new Rectangle2D.Double(0,0,widthMM, heightMM));
reader.readNext();
}
//
// EXPORT section
// Date and Time of the recording in Tobii. We combine these into one Date
// Formatted as, e.g., "6/24/2011" and "11:42:32 AM"
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Export date:"));
String exportDateStr = line[1].trim();
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Export time:"));
String exportTimeStr = line[1].trim();
try {
Date exportDateTime = dateFormat.parse(exportDateStr + " " + exportTimeStr);
header.setExportDateTime(exportDateTime);
} catch(ParseException e) {
System.err.println("Had trouble parsing the date/times in a Tobii header file. Non-fatal.");
System.err.println("Date string: " + exportDateStr + "\t\tTime string: " + exportTimeStr);
}
//
// PARTICIPANT section
reader.readNext();
line = reader.readNext();
assert(line[0].equalsIgnoreCase("Participant:"));
header.setParticipant(line[1]);
return header;
}
/**
* Loads Event Data map for a Tobii v2.x file (back when we did Track-It with videos, not real-time)
* @param path
* @return
* @throws IOException
*/
public static TobiiEventMap loadTobiiEvent2(File file) throws IOException {
CSVReader reader = new CSVReader(new FileReader(file), '\t');
String[] line;
// Get header data for this file
TobiiHeader header = readHeader(reader);
TobiiEventMap eventMap = new TobiiEventMap(header);
// Work our way down to the info we care about---timestamped data
while( (line = reader.readNext()) != null && !line[0].trim().equalsIgnoreCase("timestamp") );
if(!line[0].trim().equalsIgnoreCase("timestamp")) {
throw new IOException("Couldn't find the Timestamp column in Tobii file " + file.getAbsolutePath());
}
// Read data in about events and their timestamps, until the end of the file
while( (line = reader.readNext()) != null ) {
long timestamp = Double.valueOf(line[0]).longValue();
String eventStr = line[1].trim();
String descriptor = line[5].trim();
// Transform Tobii event string into an enum
TobiiEventType type = TobiiEventType.parseTobii(eventStr);
if(type == null) {
System.err.println("Could not understand event type " + eventStr + " in Tobii file " + file.getAbsolutePath());
continue;
}
// Hacky way to grab the corresponding Trial ID # for Track-It from the filename
// of the movie used in Tobii. Need to get Psych people to formalize naming schema
// Assume Trial ID is the first integer found in the filename
int trialId = -1;
if(type == TobiiEventType.MOVIE_START || type == TobiiEventType.MOVIE_END) {
Pattern findInt = Pattern.compile("\\d+");
Matcher matcher = findInt.matcher(descriptor);
if(!matcher.find()) {
throw new IOException("Couldn't identify the Trial ID in " + descriptor + " in file " + file.getAbsolutePath());
}
String firstInteger = matcher.group();
trialId = Integer.parseInt(firstInteger);
System.out.println(descriptor + " found Trial " + trialId);
}
eventMap.addEvent(type, timestamp, trialId);
}
reader.close();
return eventMap;
}
/**
* Loads Event Data map for a Tobii v3.x file (real-time Track-It and Tobii)
* @param path
* @return
* @throws IOException
*/
public static TobiiEventMap loadTobiiEvent3(File file) throws IOException {
CSVReader reader = new CSVReader(new FileReader(file), '\t');
String[] line;
// Get header data for this file
TobiiHeader header = readHeader(reader);
TobiiEventMap eventMap = new TobiiEventMap(header);
// Work our way down to the info we care about---timestamped data
while( (line = reader.readNext()) != null && !line[0].trim().equalsIgnoreCase("timestamp") );
if(!line[0].trim().equalsIgnoreCase("timestamp")) {
throw new IOException("Couldn't find the Timestamp column in Tobii file " + file.getAbsolutePath());
}
long initialTimestamp = header.getRecDateTimestamp();
// Read data in about events and their timestamps, until the end of the file
int trialId = 0;
while( (line = reader.readNext()) != null ) {
// Timestamp is reported relative to absolute wallclock timestamp; must add this on
long timestamp = Double.valueOf(line[0]).longValue();
timestamp += initialTimestamp;
String eventStr = line[1].trim();
String descriptor = line[5].trim();
// Transform Tobii event string into an enum
TobiiEventType type = TobiiEventType.parseTobii(eventStr);
if(type == null) {
System.err.println("Could not understand event type " + eventStr + " in Tobii file " + file.getAbsolutePath());
continue;
}
// Hacky way to grab the corresponding Trial ID # for Track-It from the filename
// of the movie used in Tobii. Basically, every time the experimenter presses the Spacebar,
// we assume that a new movie has started (corresponding to Frame 0)
if(type == TobiiEventType.KEYPRESS && descriptor.equalsIgnoreCase("Space")) {
trialId += 1;
System.out.println(descriptor + " found Trial " + trialId);
}
eventMap.addEvent(type, timestamp, trialId, descriptor);
}
reader.close();
return eventMap;
}
/**
* Loads data from the raw Tobii 2.x-generated experiment file (back when we did Track-It with videos, not real-time)
* @param path Relative or absolute path to the .csv/.tsv file exported from Tobii
* @param eventMap
* @throws IOException If we have any sort of I/O issue (from not finding the file
* to an actual issue parsing the data), throw an IOException with error message.
*/
public static TobiiData loadTobiiData2(File file, TobiiEventMap eventMap) throws IOException {
CSVReader reader = new CSVReader(new FileReader(file), '\t');
// Get header data for this file
TobiiHeader header = readHeader(reader);
// Make sure this header matches with the header of the EventMap
if(!header.isCompatible(eventMap.getHeader())) {
throw new IOException("Headers don't match in file " + file.getAbsolutePath());
}
TobiiData data = new TobiiData(eventMap);
String line[];
// Grab the rest of the header info specific to a Tobii data dump
//line[] = reader.readNext();
// Work our way down to the info we care about---timestamped data
while( (line = reader.readNext()) != null && !line[0].trim().equalsIgnoreCase("timestamp") );
if(!line[0].trim().equalsIgnoreCase("timestamp")) {
throw new IOException("Couldn't find the Timestamp column in Tobii file " + file.getAbsolutePath());
}
// Only care about the eye tracking data for when we are watching a movie
for(int eIdx=0; eIdx<eventMap.getEvents(TobiiEventType.MOVIE_START).size(); eIdx++) {
long startTime = eventMap.getEvents(TobiiEventType.MOVIE_START).get(eIdx).getTimestamp();
long stopTime = eventMap.getEvents(TobiiEventType.MOVIE_END).get(eIdx).getTimestamp();
int trialId = eventMap.getEvents(TobiiEventType.MOVIE_START).get(eIdx).getTrialId();
// Find the start of this event
while( (line = reader.readNext()) != null && Long.valueOf(line[0]) != startTime );
if(line == null) { break; }
// Parse data in (start of event, end of event) non-inclusive
Trajectory<TobiiFrame> trajectory = new Trajectory<TobiiFrame>();
while( (line = reader.readNext()) != null && Long.valueOf(line[0]) != stopTime) {
// Skip any events that weren't eye tracking
if(line[1].isEmpty()) {
continue;
}
// Adjust timestamp so that the first frame of each trial is timestamp = 0000
long timestamp = Long.valueOf(line[0]) - startTime;
TobiiFrame f = new TobiiFrame();
f.setGazeLeftPt(new Coordinate<Double>( Double.valueOf(line[4]), Double.valueOf(line[5])));
f.setValidityLeft(Integer.valueOf(line[10]));
f.setGazeRightPt(new Coordinate<Double>( Double.valueOf(line[11]), Double.valueOf(line[12])));
f.setValidityRight(Integer.valueOf(line[17]));
f.setGazePt(new Coordinate<Double>( Double.valueOf(line[38]), Double.valueOf(line[39]) ));
f.setFixationIndex(Integer.valueOf(line[18]));
f.setFixationPt(new Coordinate<Double>( Double.valueOf(line[32]), Double.valueOf(line[33])));
f.setFixationDuration(Integer.valueOf(line[34]));
f.setTimestamp(timestamp);
trajectory.registerFrame(f);
}
data.registerTrajectory(trialId, trajectory);
}
reader.close();
return data;
}
/**
* Loads data from the raw Tobii 3.x-generated experiment file (real-time Track-It and Tobii)
* @param path Relative or absolute path to the .csv/.tsv file exported from Tobii
* @param eventMap
* @throws IOException If we have any sort of I/O issue (from not finding the file
* to an actual issue parsing the data), throw an IOException with error message.
*/
public static TobiiData loadTobiiData3(File file, TobiiEventMap eventMap) throws IOException {
CSVReader reader = new CSVReader(new FileReader(file), '\t');
// Get header data for this file
TobiiHeader header = readHeader(reader);
// Make sure this header matches with the header of the EventMap
if(!header.isCompatible(eventMap.getHeader())) {
throw new IOException("Headers don't match in file " + file.getAbsolutePath());
}
TobiiData data = new TobiiData(eventMap);
String line[];
// Grab the rest of the header info specific to a Tobii data dump
//line[] = reader.readNext();
// Work our way down to the info we care about---timestamped data
while( (line = reader.readNext()) != null && !line[0].trim().equalsIgnoreCase("timestamp") );
if(!line[0].trim().equalsIgnoreCase("timestamp")) {
throw new IOException("Couldn't find the Timestamp column in Tobii file " + file.getAbsolutePath());
}
long recStartTime = header.getRecDateTimestamp();
// Only care about the eye tracking data for when Track-It is started. This is always right after the Space bar is hit
for(int eIdx=0; eIdx<eventMap.getEvents(TobiiEventType.KEYPRESS).size(); eIdx++) {
// We only care about KeyPresses that are "Space", signalling the beginning of a Track-It run
if( !eventMap.getEvents(TobiiEventType.KEYPRESS).get(eIdx).getDescriptor().equalsIgnoreCase("Space") ) {
continue;
}
long startTime = eventMap.getEvents(TobiiEventType.KEYPRESS).get(eIdx).getTimestamp();
int trialId = eventMap.getEvents(TobiiEventType.KEYPRESS).get(eIdx).getTrialId();
// Find next spacebar press to figure out end of this trial
long stopTime = Long.MAX_VALUE;
for(int sbIdx=eIdx+1; sbIdx<eventMap.getEvents(TobiiEventType.KEYPRESS).size(); sbIdx++) {
if(eventMap.getEvents(TobiiEventType.KEYPRESS).get(eIdx).getDescriptor().equalsIgnoreCase("Space")) {
stopTime = eventMap.getEvents(TobiiEventType.KEYPRESS).get(sbIdx).getTimestamp();
break;
}
}
// In the event that we stop recording before pressing other keys, get a hard upper bound on stop time
if(stopTime == Long.MAX_VALUE) {
stopTime = eventMap.getEvents(TobiiEventType.SCREEN_REC_STOPPED).get(0).getTimestamp();
}
System.out.println("Start: " + (startTime-recStartTime) + "[" + startTime + "], Stop: " + (stopTime-recStartTime) + "[" + stopTime + "]");
// Find the start of this event
while( (line = reader.readNext()) != null && Long.valueOf(line[0]) <= (startTime-recStartTime) );
if(line == null) { break; }
// Parse data in (start of event, end of event) non-inclusive
// SOME of this data (at the end) won't pertain to the actual tracking portion of the Track-It; rather,
// it'll show the grid selection or memory check. We handle this by comparing against the gold standard only
Trajectory<TobiiFrame> trajectory = new Trajectory<TobiiFrame>(startTime);
trajectory.setLengthMS(stopTime - startTime);
Map<Integer, Coordinate<Double>> fixationPts = new HashMap<Integer, Coordinate<Double>>();
while( (line = reader.readNext()) != null && ( Long.valueOf(line[0]) < stopTime-recStartTime) ) {
// Skip any events that weren't eye tracking
if(line[1].isEmpty() || line[3].isEmpty()) {
continue;
}
// Translate relative timestamp into absolute wall-clock timestamp
long timestamp = Long.valueOf(line[0]) + recStartTime;
TobiiFrame f = new TobiiFrame();
f.setGazeLeftPt(new Coordinate<Double>( Double.valueOf(line[4]), Double.valueOf(line[5])));
f.setValidityLeft(Integer.valueOf(line[10]));
f.setGazeRightPt(new Coordinate<Double>( Double.valueOf(line[11]), Double.valueOf(line[12])));
f.setValidityRight(Integer.valueOf(line[17]));
f.setGazePt(new Coordinate<Double>( Double.valueOf(line[38]), Double.valueOf(line[39]) ));
// Only some gaze points are part of a fixation point
if(!line[18].trim().isEmpty()) {
f.setFixationIndex(Integer.valueOf(line[18]));
f.setFixationPt(new Coordinate<Double>( Double.valueOf(line[32]), Double.valueOf(line[33])));
f.setFixationDuration(Integer.valueOf(line[34]));
fixationPts.put(f.getFixationIndex(), f.getFixationPt());
}
f.setTimestamp(timestamp);
trajectory.registerFrame(f);
}
data.registerTrajectory(trialId, trajectory);
}
reader.close();
return data;
}
/**
* Loads data from a Track-It-generated .csv file; returns an Experiment representing
* all the information stored in this file.
* @param path Relative or absolute path to the .csv file containing the data
* from a gold standard run of Track-It
* @return An Experiment representing all necessary data (headers, positions of
* stimuli, etc) to reconstruct the full Track-It experiment
* @throws IOException If we have any sort of I/O issue (from not finding the file
* to an actual issue parsing the data), throw an IOException with error message.
*/
public static Experiment loadGoldStandard(File file) throws IOException {
CSVReader reader = new CSVReader(new FileReader(file), ',');
String[] headers;
String[] data;
Experiment exp = new Experiment();
// Get full-experiment information (# distractors, lengths, etc)
if((headers = reader.readNext()) != null && (data = reader.readNext()) != null) {
for(int idx=0; idx<headers.length; idx++) {
// Grab the header and its associated data (e.g., "Object Speed" and "500")
String header = headers[idx].trim();
String datum = data[idx].trim();
// Store the information associated with headers we care about
if(header.equalsIgnoreCase("number of distractors")) {
exp.setNumDistractors( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("object speed")) {
exp.setObjectSpeed( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("trial type")) {
if( datum.equalsIgnoreCase("all same") ) {
exp.setType(TrialType.ALL_SAME);
} else if( datum.equalsIgnoreCase("all different") ) {
exp.setType(TrialType.ALL_SAME);
} else if( datum.equalsIgnoreCase("same as target") ) {
exp.setType(TrialType.SAME_AS_TARGET);
}
} else if(header.equalsIgnoreCase("trial count")) {
exp.setNumTrials( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("trial length")) {
exp.setBaseTrialLength( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("uses random target")) {
exp.setUsesRandomTarget(Boolean.valueOf(datum));
} else if(header.equalsIgnoreCase("fps")) {
exp.setFPS( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("seed")) {
exp.setSeed( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("grid x size")) {
exp.setGridX( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("grid y size")) {
exp.setGridY( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("uses background images")) {
exp.setUsesBackgroundImages( Boolean.valueOf(datum) );
} else if(header.equalsIgnoreCase("uses memory check")) {
exp.setUsesMemCheck(Boolean.valueOf(datum) );
}
}
exp.setParsedCorrectly(true);
} else {
throw new IOException("Error while reading the header lines in file " + file.getAbsolutePath());
}
// One of the lines was unparseable or null; error out immediately
if(!exp.isParsedCorrectly()) {
throw new IOException("We had trouble parsing your input file: " + file.getAbsolutePath());
}
// When we infer timestamps, we add frame_increment on every tick
long frame_increment = (long) (1000.0 / exp.getFPS());
//
// Parse each trial in the experiment
while( (data=reader.readNext()) != null) {
String datum=data[0].trim();
// Start a new individual trial
if(datum.equalsIgnoreCase("begin new trial")) {
Trial trial = new Trial();
// Grab the details for this trial (header+data line)
if((headers = reader.readNext()) != null && (data = reader.readNext()) != null) {
for(int idx=0; idx<headers.length; idx++) {
// Grab the header and its associated data (e.g., "Object Speed" and "500")
String header = headers[idx].trim();
datum = data[idx].trim();
if(header.equalsIgnoreCase("id")) {
trial.setID( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("length")) {
trial.setLength( Double.valueOf(datum).intValue() );
} else if(header.equalsIgnoreCase("target")) {
trial.setTarget( datum );
} else if(header.equalsIgnoreCase("startTime")) {
trial.setStartTime( Long.valueOf(datum) );
} else if(header.equalsIgnoreCase("targetX")) {
double x = Double.valueOf(datum);
datum = data[idx++].trim();
double y = Double.valueOf(datum);
trial.setTargetFinalPos(new Coordinate<Double>(x,y));
} else if(header.equalsIgnoreCase("targetGridX")) {
int x = Double.valueOf(datum).intValue();
datum = data[idx++].trim();
int y = Double.valueOf(datum).intValue();
trial.setTargetFinalGridPos(new Coordinate<Integer>(x,y));
} else if(header.equalsIgnoreCase("userX")) {
double x = Double.valueOf(datum);
datum = data[idx++].trim();
double y = Double.valueOf(datum);
trial.setUserClickPos(new Coordinate<Double>(x,y));
} else if(header.equalsIgnoreCase("userGridX")) {
int x = Double.valueOf(datum).intValue();
datum = data[idx++].trim();
int y = Double.valueOf(datum).intValue();
trial.setUserClickGridPos(new Coordinate<Integer>(x,y));
} else if(header.equalsIgnoreCase("gridClickCorrect")) {
trial.setGridClickCorrect( Boolean.valueOf(datum) );
} else if(header.equalsIgnoreCase("gridClickRT")) {
trial.setGridClickRT( Long.valueOf(datum) );
} else if(header.equalsIgnoreCase("lineupStimulus")) {
trial.setLineupStimulus( datum );
} else if(header.equalsIgnoreCase("lineupClickCorrect")) {
trial.setLineupClickCorrect( Boolean.valueOf(datum) );
} else if(header.equalsIgnoreCase("lineupClickRT")) {
trial.setLineupClickRT( Long.valueOf(datum) );
} else {
throw new IOException("Errored while reading headers for trial " + trial.getID() + " in file " + file.getAbsolutePath());
}
}
}
// Grab the target's column number---since we have two columns per
// distractor, divide by two
int targetIdx = -1;
if( (data = reader.readNext()) != null) {
for(int idx=0; idx<data.length; idx++) {
datum = data[idx].trim().toLowerCase();
if(datum.equalsIgnoreCase("target")) {
targetIdx = idx / 2;
break;
}
}
} else {
throw new IOException("Errored while reading the target specifier line in trial " + trial.getID() + " in file " + file.getAbsolutePath());
}
// If we can't find any stimulus marked as the target, fail immediately
if(targetIdx < 0) {
throw new IOException("Could not find the target column for trial id " + trial.getID() + " in file " + file.getAbsolutePath());
} else {
trial.setTargetIdx(targetIdx);
}
// Grab column mappings for distractors' <x,y> coordinates
// Newer versions have the first col set to "Frame Timestamp"---check for this
boolean usesTimestamp = false;
List<String> stimuliNames = new ArrayList<String>();
if( (data = reader.readNext()) != null) {
for(int idx=0; idx<data.length && !data[idx].isEmpty(); idx++) {
// Grab the title of the stimulus, minus the _x or _y
datum = data[idx].trim();
// If this column pertains to the timestamp, note it and move on
if(datum.equalsIgnoreCase("frame timestamp")) {
usesTimestamp = true;
continue;
}
if(datum.endsWith("_x") || datum.endsWith("_y")) {
datum = datum.substring(0,datum.length()-2);
}
stimuliNames.add(datum);
idx++;
}
trial.setStimuliNames(stimuliNames);
} else {
throw new IOException("Couldn't find the stimulus names in trial " + trial.getID() + " in file " + file.getAbsolutePath());
}
// Tell the parser whether or not we need to read timestamps or infer them
long timestamp = 0;
long timestampStart = 0;
int colOffset = 0;
if(usesTimestamp) {
colOffset = 1;
timestampStart = trial.getStartTime();
}
// Maps Frame -> { <Stim.x,Stim.y> , <Stim.x, Stim.y> , ... }
// Encode how long the trajectory is in milliseconds (regardless of refresh rate)
List<Trajectory<TrackItFrame>> stimMoveList = new ArrayList<Trajectory<TrackItFrame>>();
for(int idx=0; idx<stimuliNames.size(); idx++) {
stimMoveList.add(new Trajectory<TrackItFrame>(timestampStart));
stimMoveList.get(idx).setLengthMS(trial.getLength());
}
// Parse the movement data for each frame/object in the trial
while( (data=reader.readNext()) != null) {
datum = data[0].trim().toLowerCase();
// Triggers the end of this trial
if(datum.startsWith("end new trial")) {
trial.setTrajectories(stimMoveList);
exp.addTrial(trial.getID(), trial);
break;
}
if(usesTimestamp) {
// If we're tracking timestamps, make sure to offset columns correctly
timestamp = Long.valueOf( datum );
// Add on the start-time to get the real timestamp (corresponding to wallclock)
timestamp += timestampStart;
}
// A single row of <x,y>'s for each stimulus at a certain point in time
for(int idx=0; idx<stimuliNames.size(); idx++) {
// Grab the <x,y> coords at columns <idx, idx+1>
// and store in the right stimulus' column
datum = data[colOffset + (2*idx)].trim();
double x = Double.valueOf(datum);
datum = data[colOffset + (2*idx+1)].trim();
double y = Double.valueOf(datum);
stimMoveList.get(idx).registerFrame( new TrackItFrame(new Coordinate<Double>(x,y),timestamp) );
}
if(!usesTimestamp) {
// For older versions of Track-It, we don't explicitly mark a timestamp;
// we estimate our own. This won't be used in Track-It v2.0.1+.
timestamp += frame_increment;
}
}
}
}
reader.close();
return exp;
}
}