//*********************************************************** // 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; } } } }