//***********************************************************
// Filename: StudyDataParser.cs
// Author: Marco Fendrich, Moritz Kolvenbach
// Last changes: Thursday, 9th of August 2018
// Content: A parser to read the data from a given study; containers to implement a structure for the data from the study; further processes the data and writes it into a csv file
//***********************************************************
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using System.Runtime.CompilerServices;
using Debug = UnityEngine.Debug;
///
/// A class containing multiple container classes that represent a structure for the recorded data, and further process the raw data.
/// Reads a given file of study data and and sorts it into the data structure. Writes the processed data into a new file.
///
public class StudyDataParser : MonoBehaviour
{
//Information about the current study parameters
private const int NumberOfConditions = 5;
private const int SampleSize = 50; // Amount of trials for each condition
private const int NumberOfSubjects = 20;
private const int NumberResultValues = 8; // Amount of values being processed from the raw data
// Delimiter for writing and reading the csv file
private string del = ",";
// Use this for initialization
void Start()
{
List parsedSubjectList = ScanFiles();
ProcessData(parsedSubjectList);
WriteToFile(parsedSubjectList);
}
///
/// Writes the final processed list of data into a csv file. Grouped after study subjects
///
/// A list containing all the study subjects and their respective processed data
void WriteToFile(List parsedSubjectList)
{
// The string for the name of the csv file
var filepath = string.Format(@"{0:yyyy-MM-dd_HH-mm-ss}-analysis.csv", System.DateTime.Now);
// start writing the csv file
using (StreamWriter writer = new StreamWriter(new FileStream(filepath, FileMode.Create, FileAccess.Write)))
{
// write first row with two empty fields (being filled later only)
string subjectIDs = " " + del + " " + del;
for (int i = 0; i < NumberOfSubjects; i++)
{
subjectIDs = subjectIDs + parsedSubjectList[i].SubjectIdentifier() + del;
}
writer.WriteLine(subjectIDs);
// loop for different values to be calculated
for (int i = 1; i <= NumberResultValues; i++)
{
string resultValue;
switch (i)
{
case 1:
resultValue = "distanceOffset";
break;
case 2:
resultValue = "distanceOffsetAfterTeleport";
break;
case 3:
resultValue = "rotationOffset";
break;
case 4:
resultValue = "rotationOffsetAfterTeleport";
break;
case 5:
resultValue = "timeUntilConfirmation";
break;
case 6:
resultValue = "timeForTeleports";
break;
case 7:
resultValue = "timeForCorrection";
break;
case 8:
resultValue = "teleportsNeeded";
break;
default:
resultValue = "somethingWentIncrediblyWrong";
break;
}
// loop for teleportConditions
for (int j = 0; j < NumberOfConditions; j++)
{
string condition;
switch (j)
{
case 0:
condition = "linear";
break;
case 1:
condition = "parabolic";
break;
case 2:
condition = "angleSelect";
break;
case 3:
condition = "curve";
break;
case 4:
condition = "distorted";
break;
default:
condition = "dafuqHappendHere?!";
break;
}
string row = resultValue + del + condition + del;
// write all calculated average values for each study subject
foreach (StudySubjectSample studySubject in parsedSubjectList)
{
switch (i)
{
case 1:
row = row + studySubject.AvgDistanceOffset[j] + del;
break;
case 2:
row = row + studySubject.AvgDistanceOffsetAfterTeleport[j] + del;
break;
case 3:
row = row + studySubject.AvgRotationOffset[j] + del;
break;
case 4:
row = row + studySubject.AvgRotationOffsetAfterTeleport[j] + del;
break;
case 5:
row = row + studySubject.AvgTimeUntilConfirmation[j] + del;
break;
case 6:
row = row + studySubject.AvgTimeForTeleports[j] + del;
break;
case 7:
row = row + studySubject.AvgTimeForCorrection[j] + del;
break;
case 8:
row = row + studySubject.AvgTeleportsNeeded[j] + del;
break;
}
}
// write specific values
writer.WriteLine(row);
}
}
}
}
///
/// Averages the required values over the whole study sample
///
/// A list containing all the study subjects for a given study
private void ProcessData(List parsedSubjectList)
{
//*** all arrays per teleportation [linear, parabolic, angleSelect, curve, distorted] ***//
float[] avgDistanceOffset = new float[NumberOfConditions]; // average offset after confirmation
float[] avgDistanceOffsetAfterTeleport = new float[NumberOfConditions]; // average offset after last teleportation
float[] avgRotationOffset = new float[NumberOfConditions]; // average rotation after confirmation
float[] avgRotationOffsetAfterTeleport = new float[NumberOfConditions]; // average rotation after last teleportation
float[] avgTimeUntilConfirmation = new float[NumberOfConditions]; // average time needed per target from first teleport to confirmation
float[] avgTimeForTeleports = new float[NumberOfConditions]; // average time needed for all teleportation between two targets
float[] avgTimeForCorrection = new float[NumberOfConditions]; // average time needed for correcting one's position after last teleport until confirmation
float[] avgTeleportsNeeded = new float[NumberOfConditions]; // average number of teleportations needed to reach target
// average the values for each teleportCondition separately
for (int i = 0; i < NumberOfConditions; i++)
{
// add up the values from each ubject for each condition
foreach (StudySubjectSample studySubject in parsedSubjectList)
{
avgDistanceOffset[i] += studySubject.AvgDistanceOffset[i];
avgDistanceOffsetAfterTeleport[i] += studySubject.AvgDistanceOffsetAfterTeleport[i];
avgRotationOffset[i] += studySubject.AvgRotationOffset[i];
avgRotationOffsetAfterTeleport[i] += studySubject.AvgRotationOffsetAfterTeleport[i];
avgTimeUntilConfirmation[i] += studySubject.AvgTimeUntilConfirmation[i];
avgTimeForTeleports[i] += studySubject.AvgTimeForTeleports[i];
avgTimeForCorrection[i] += studySubject.AvgTimeForCorrection[i];
avgTeleportsNeeded[i] += studySubject.AvgTeleportsNeeded[i];
}
// average the values per condition
avgDistanceOffset[i] = avgDistanceOffset[i] / NumberOfSubjects;
avgDistanceOffsetAfterTeleport[i] = avgDistanceOffsetAfterTeleport[i] / NumberOfSubjects;
avgRotationOffset[i] = avgRotationOffset[i] / NumberOfSubjects;
avgRotationOffsetAfterTeleport[i] = avgRotationOffsetAfterTeleport[i] / NumberOfSubjects;
avgTimeUntilConfirmation[i] = avgTimeUntilConfirmation[i] / NumberOfSubjects;
avgTimeForTeleports[i] = avgTimeForTeleports[i] / NumberOfSubjects;
avgTimeForCorrection[i] = avgTimeForCorrection[i] / NumberOfSubjects;
avgTeleportsNeeded[i] = avgTeleportsNeeded[i] / NumberOfSubjects;
}
}
///
/// Iterates through all folders of a given directory and uses to read each file. Creates a list with all confirmed targets per condition per subject.
///
/// A list of all the studySubjects and their respective targtes per condition
private List ScanFiles()
{
// A list containing one entry for each studySubject
List returnList = new List();
// Iterate through the different folders
for (int i = 1; i <= NumberOfSubjects; i++)
{
// path to the directory
string path = ("Assets/UserStudy/VP" + i);
// create new lists for each condition
List linearTeleport = new List();
List parabolicTeleport = new List();
List angleSelectTeleport = new List();
List curveTeleport = new List();
List distortedTeleport = new List();
// ID for the current studySubject
string subjectIdentifier = "userNotFound";
// Get the filePath for each separate csv file of data
string[] filePathList = Directory.GetFiles(path);
// Read all the files from the current directory into the lists
foreach (string filePath in filePathList)
{
if (Path.GetExtension(filePath) == ".csv")
switch (Path.GetFileName(filePath).Substring(0, 4))
{
case "line":
linearTeleport = ReadFile(filePath);
subjectIdentifier = Path.GetFileName(filePath).Substring(15, 4);
break;
case "para":
parabolicTeleport = ReadFile(filePath);
break;
case "turn":
angleSelectTeleport = ReadFile(filePath);
break;
case "curv":
curveTeleport = ReadFile(filePath);
break;
case "dist":
distortedTeleport = ReadFile(filePath);
break;
}
}
// Write all the condition lists for the current studySubject into a new StudySubjectSample object and add the subject to the return list
returnList.Add(new StudySubjectSample(subjectIdentifier, linearTeleport, parabolicTeleport, angleSelectTeleport, curveTeleport, distortedTeleport));
}
return returnList;
}
///
/// Reads a given csv file of studyData and sorts the data into the container classes
///
/// The path to the file that's supposed to be read
/// A list of TargetDataSample each containing a list of TeleportDataSample
private static List ReadFile(string path)
{
// raw input from the csv file
string fileData = System.IO.File.ReadAllText(path);
// raw input splitted
string[] temp = fileData.Split("\n"[0]);
// skip first two rows (header and initial target) and last row (empty)
string[] rows = new string[temp.Length - 3];
for (int i = 0; i < temp.Length - 3; i++)
{
rows[i] = temp[i + 2];
}
List parsedTargetList = new List();
List currentTeleportList = new List();
bool newTarget = false;
// get time until first target was triggered
string[] temporaryContent = (temp[1].Trim()).Split(","[0]);
float timeSinceLastTarget = float.Parse(temporaryContent[15]);
// iterate through each row of the rawData
foreach (string row in rows)
{
string[] content = (row.Trim()).Split(","[0]);
// skip last empty row
if (content.Length >= 2)
{
int typeIdentificator;
if (int.TryParse(content[1], out typeIdentificator))
{
// if content is of type target
if (typeIdentificator == -1)
{
newTarget = true;
timeSinceLastTarget += float.Parse(content[15]);
// create a new TargetDataSample from the data
TargetDataSample target = new TargetDataSample(
int.Parse(content[0]),
new Vector2(float.Parse(content[2]), float.Parse(content[3])),
float.Parse(content[4]),
new Vector2(float.Parse(content[9]), float.Parse(content[10])),
float.Parse(content[11]),
timeSinceLastTarget,
currentTeleportList
);
parsedTargetList.Add(target);
}
// If content is of type teleport
else
{
if (newTarget)
{
// Create new list of teleports for the next TargetDataSample
currentTeleportList = new List();
}
// Create new TeleportDataSample from the data and add it to the list of teleports needed for the current TargetDataSample
TeleportDataSample teleport = new TeleportDataSample(
int.Parse(content[1]),
new Vector2(float.Parse(content[5]), float.Parse(content[6])),
new Vector2(float.Parse(content[9]), float.Parse(content[10])),
float.Parse(content[7]),
float.Parse(content[11]),
float.Parse(content[8]),
float.Parse(content[12])
);
currentTeleportList.Add(teleport);
newTarget = false;
}
}
}
}
return parsedTargetList;
}
///
/// A container class representing the studyData for one studySubject in a given study. Infers required values from raw inputData and averages them over all trials of the subject
///
public class StudySubjectSample
{
// values being given on construction
private readonly string subjectIdentifier;
private readonly List[] teleportDataSamples = new List[NumberOfConditions]; // the TeleportDataSample between this target and the last one
private readonly List linearTeleport;
private readonly List parabolicTeleport;
private readonly List angleSelectTeleport;
private readonly List curveTeleport;
private readonly List distortedTeleport;
// values inferred from above data as average; see TargetDataSample for details
//*** all arrays per teleportation [linear, parabolic, angleSelect, curve, distorted] ***//
private readonly float[] avgDistanceOffset = new float[NumberOfConditions];
private readonly float[] avgDistanceOffsetAfterTeleport = new float[NumberOfConditions];
private readonly float[] avgRotationOffset = new float[NumberOfConditions];
private readonly float[] avgRotationOffsetAfterTeleport = new float[NumberOfConditions];
private readonly float[] avgTimeUntilConfirmation = new float[NumberOfConditions];
private readonly float[] avgTimeForTeleports = new float[NumberOfConditions];
private readonly float[] avgTimeForCorrection = new float[NumberOfConditions];
private readonly float[] avgTeleportsNeeded = new float[NumberOfConditions];
public StudySubjectSample(string subjectIdentifier, List linearTeleport,
List parabolicTeleport, List angleSelectTeleport,
List curveTeleport, List distortedTeleport)
{
this.subjectIdentifier = subjectIdentifier;
this.linearTeleport = linearTeleport;
this.parabolicTeleport = parabolicTeleport;
this.angleSelectTeleport = angleSelectTeleport;
this.curveTeleport = curveTeleport;
this.distortedTeleport = distortedTeleport;
teleportDataSamples[0] = linearTeleport;
teleportDataSamples[1] = parabolicTeleport;
teleportDataSamples[2] = angleSelectTeleport;
teleportDataSamples[3] = curveTeleport;
teleportDataSamples[4] = distortedTeleport;
// check if all data samples have the same size and throw exception if not
if (new[] { parabolicTeleport.Count, angleSelectTeleport.Count, curveTeleport.Count, distortedTeleport.Count }
.All(trialSize => trialSize != linearTeleport.Count)) throw new System.FormatException();
// average the values for each condition
for (int j = 0; j < teleportDataSamples.Length; j++)
{
for (int i = 0; i < SampleSize; i++)
{
// sum up single values
avgDistanceOffset[j] += (teleportDataSamples[j])[i].DistanceOffset;
avgDistanceOffsetAfterTeleport[j] += (teleportDataSamples[j])[i].DistanceOffsetAfterTeleport;
avgRotationOffset[j] += (teleportDataSamples[j])[i].RotationOffset;
avgRotationOffsetAfterTeleport[j] += (teleportDataSamples[j])[i].RotationOffsetAfterTeleport;
avgTimeUntilConfirmation[j] += (teleportDataSamples[j])[i].TimeUntilConfirmation;
avgTimeForTeleports[j] += (teleportDataSamples[j])[i].TimeForTeleports;
avgTimeForCorrection[j] += (teleportDataSamples[j])[i].TimeForCorrection;
avgTeleportsNeeded[j] += (teleportDataSamples[j])[i].TeleportsNeeded;
}
// average single values
avgDistanceOffset[j] = avgDistanceOffset[j] / SampleSize;
avgDistanceOffsetAfterTeleport[j] = avgDistanceOffsetAfterTeleport[j] / SampleSize;
avgRotationOffset[j] = avgRotationOffset[j] / SampleSize;
avgRotationOffsetAfterTeleport[j] = avgRotationOffsetAfterTeleport[j] / SampleSize;
avgTimeUntilConfirmation[j] = avgTimeUntilConfirmation[j] / SampleSize;
avgTimeForTeleports[j] = avgTimeForTeleports[j] / SampleSize;
avgTimeForCorrection[j] = avgTimeForCorrection[j] / SampleSize;
avgTeleportsNeeded[j] = avgTeleportsNeeded[j] / SampleSize;
}
}
//*** getter
public string SubjectIdentifier() { return subjectIdentifier; }
public List LinearTeleport() { return linearTeleport; }
public List ParabolicTeleport() { return parabolicTeleport; }
public List AngleSelectTeleport() { return angleSelectTeleport; }
public List CurveTeleport() { return curveTeleport; }
public List DistortedTeleport() { return distortedTeleport; }
public List[] AllTeleports() { return teleportDataSamples; }
public float[] AvgDistanceOffset { get { return avgDistanceOffset; } }
public float[] AvgDistanceOffsetAfterTeleport { get { return avgDistanceOffsetAfterTeleport; } }
public float[] AvgRotationOffset { get { return avgRotationOffset; } }
public float[] AvgRotationOffsetAfterTeleport { get { return avgRotationOffsetAfterTeleport; } }
public float[] AvgTimeUntilConfirmation { get { return avgTimeUntilConfirmation; } }
public float[] AvgTimeForTeleports { get { return avgTimeForTeleports; } }
public float[] AvgTimeForCorrection { get { return avgTimeForCorrection; } }
public float[] AvgTeleportsNeeded { get { return avgTeleportsNeeded; } }
}
///
/// A container class representing one confirmed target in a given trial of a Study. Contains a list of with information about all the teleports used between confirmation
/// of this target and the last one
///
public class TargetDataSample
{
// Values being given on construction
private readonly int targetIdentifier; // target number since trial start
private readonly Vector2 targetPosition; // position of this target
private readonly float targetRotation; // rotation to zero world of this target
private readonly Vector2 playerPositionOnTrigger; // position of player when triggering
private readonly float playerRotationOnTrigger; // rotation of player to zero world when triggering
private readonly float timeTriggered; // absolute time since trial start when triggering
private readonly List teleports; // list of teleports needed since last target
// Values inferred from above data
private readonly float distanceOffset; // distance from player to target on confirmation
private readonly float distanceOffsetAfterTeleport; // distance from player to target after last teleportation
private readonly float rotationOffset; // rotation from player to target on confirmation
private readonly float rotationOffsetAfterTeleport; // rotation from player to target on confirmation after last teleportation
private readonly float timeUntilConfirmation; // time from first teleport initiation to confirmation of target
private readonly float timeForTeleports; // time needed for all teleports (first teleport initiation until completion of last teleport)
private readonly float timeForCorrection; // time needed for correcting one's position after last teleport
private readonly int teleportsNeeded; // number of teleports needed to get to target
public TargetDataSample(int targetIdentifier, Vector2 targetPosition, float targetRotation, Vector2 playerPositionOnTrigger, float playerRotationOnTrigger, float timeTriggered, List teleports)
{
this.targetIdentifier = targetIdentifier;
this.targetPosition = targetPosition;
this.targetRotation = targetRotation;
this.playerPositionOnTrigger = playerPositionOnTrigger;
this.playerRotationOnTrigger = playerRotationOnTrigger;
this.timeTriggered = timeTriggered;
this.teleports = teleports;
// Infer required values from rawData
distanceOffset = Mathf.Sqrt(Mathf.Pow(playerPositionOnTrigger.x - targetPosition.x, 2) + Mathf.Pow(playerPositionOnTrigger.y - targetPosition.y, 2));
distanceOffsetAfterTeleport = Mathf.Sqrt(Mathf.Pow(teleports[teleports.Count - 1].PlayerEndPosition.x - targetPosition.x, 2) + Mathf.Pow(teleports[teleports.Count - 1].PlayerEndPosition.y - targetPosition.y, 2));
float tempRot = (playerRotationOnTrigger - targetRotation + 180) % 360;
rotationOffset = Mathf.Abs(tempRot >= 180 ? tempRot - 360 : tempRot);
tempRot = (teleports[teleports.Count - 1].PlayerEndRotation - targetRotation + 180) % 360;
rotationOffsetAfterTeleport = Mathf.Abs(tempRot >= 180 ? tempRot - 360 : tempRot);
timeUntilConfirmation = timeTriggered - teleports[0].TeleportStartTime;
timeForTeleports = teleports[teleports.Count - 1].TeleportEndTime - teleports[0].TeleportStartTime;
timeForCorrection = timeTriggered - teleports[teleports.Count - 1].TeleportEndTime;
teleportsNeeded = teleports.Count;
}
//*** getter
public int TargetIdentifier { get { return targetIdentifier; } }
public Vector2 TargetPosition { get { return targetPosition; } }
public float TargetRotation { get { return targetRotation; } }
public Vector2 PlayerPositionOnTrigger { get { return playerPositionOnTrigger; } }
public float PlayerRotationOnTrigger { get { return playerRotationOnTrigger; } }
public float TimeTriggered { get { return timeTriggered; } }
public List Teleports { get { return teleports; } }
public float DistanceOffset { get { return distanceOffset; } }
public float DistanceOffsetAfterTeleport { get { return distanceOffsetAfterTeleport; } }
public float RotationOffset { get { return rotationOffset; } }
public float RotationOffsetAfterTeleport { get { return rotationOffsetAfterTeleport; } }
public float TimeUntilConfirmation { get { return timeUntilConfirmation; } }
public float TimeForTeleports { get { return timeForTeleports; } }
public float TimeForCorrection { get { return timeForCorrection; } }
public int TeleportsNeeded { get { return teleportsNeeded; } }
}
///
/// A container class representing a single teleport event.
///
public class TeleportDataSample
{
private readonly int teleportIdentifier; // teleports since last target triggered
private readonly Vector2 playerStartPosition; // player position when pressing down teleport button
private readonly Vector2 playerEndPosition; // player position after teleport
private readonly float playerStartRotation; // player rotation when pressing down teleport button
private readonly float playerEndRotation; // player rotation after teleport
private readonly float teleportStartTime; // absolute time since trial start when player pressed down teleport button
private readonly float teleportEndTime; // absolute time since trial start of teleport
public TeleportDataSample(int teleportIdentifier, Vector2 playerStartPosition, Vector2 playerEndPosition, float playerStartRotation, float playerEndRotation, float teleportStartTime, float teleportEndTime)
{
this.teleportIdentifier = teleportIdentifier;
this.playerStartPosition = playerStartPosition;
this.playerEndPosition = playerEndPosition;
this.playerStartRotation = playerStartRotation;
this.playerEndRotation = playerEndRotation;
this.teleportStartTime = teleportStartTime;
this.teleportEndTime = teleportEndTime;
}
//*** getter
public int TeleportIdentifier { get { return teleportIdentifier; } }
public Vector2 PlayerStartPosition { get { return playerStartPosition; } }
public Vector2 PlayerEndPosition { get { return playerEndPosition; } }
public float PlayerStartRotation { get { return playerStartRotation; } }
public float PlayerEndRotation { get { return playerEndRotation; } }
public float TeleportStartTime { get { return teleportStartTime; } }
public float TeleportEndTime { get { return teleportEndTime; } }
}
}