/**
* Experiment Handler
*
* @author Alain Pitiot
* @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
import {PsychObject} from '../util/PsychObject';
import {MonotonicClock} from '../util/Clock';
import * as util from '../util/Util';
/**
*
An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful
* for generating a single data file from an experiment with many different loops (e.g. interleaved
* staircases or loops within loops.
*
* @name module:data.ExperimentHandler
* @class
* @extends PsychObject
* @param {Object} options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
* @param {string} options.name - name of the experiment
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
*/
export class ExperimentHandler extends PsychObject
{
/**
* Getter for experimentEnded.
*
* @name module:core.Window#experimentEnded
* @function
* @public
*/
get experimentEnded()
{
return this._experimentEnded;
}
/**
* Setter for experimentEnded.
*
* @name module:core.Window#experimentEnded
* @function
* @public
*/
set experimentEnded(ended)
{
this._experimentEnded = ended;
}
/**
* Legacy experiment getters.
*/
get _thisEntry()
{
return this._currentTrialData;
}
get _entries()
{
return this._trialsData;
}
constructor({
psychoJS,
name,
extraInfo
} = {})
{
super(psychoJS, name);
this._addAttributes(ExperimentHandler, extraInfo);
// loop handlers:
this._loops = [];
this._unfinishedLoops = [];
// data dictionaries (one per trial) and current data dictionary:
this._trialsKeys = [];
this._trialsData = [];
this._currentTrialData = {};
this._experimentEnded = false;
}
/**
* Whether or not the current entry (i.e. trial data) is empty.
*
Note: this is mostly useful at the end of an experiment, in order to ensure that the last entry is saved.
*
* @name module:data.ExperimentHandler#isEntryEmpty
* @function
* @public
* @returns {boolean} whether or not the current entry is empty
*/
isEntryEmpty()
{
return (Object.keys(this._currentTrialData).length > 0);
}
isEntryEmtpy()
{
return (Object.keys(this._currentTrialData).length > 0);
}
/**
* Add a loop.
*
*
The loop might be a {@link TrialHandler}, for instance.
*
Data from this loop will be included in the resulting data files.
*
* @name module:data.ExperimentHandler#addLoop
* @function
* @public
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
*/
addLoop(loop)
{
this._loops.push(loop);
this._unfinishedLoops.push(loop);
loop.experimentHandler = this;
}
/**
* Remove the given loop from the list of unfinished loops, e.g. when it has completed.
*
* @name module:data.ExperimentHandler#removeLoop
* @function
* @public
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
*/
removeLoop(loop)
{
const index = this._unfinishedLoops.indexOf(loop);
if (index !== -1)
{
this._unfinishedLoops.splice(index, 1);
}
}
/**
* Add the key/value pair.
*
*
Multiple key/value pairs can be added to any given entry of the data file. There are
* considered part of the same entry until a call to {@link nextEntry} is made.
*
* @name module:data.ExperimentHandler#addData
* @function
* @public
* @param {Object} key - the key
* @param {Object} value - the value
*/
addData(key, value)
{
if (this._trialsKeys.indexOf(key) === -1)
{
this._trialsKeys.push(key);
}
// turn arrays into their json equivalent:
if (Array.isArray(value))
{
value = JSON.stringify(value);
}
this._currentTrialData[key] = value;
}
/**
* Inform this ExperimentHandler that the current trial has ended. Further calls to {@link addData}
* will be associated with the next trial.
*
* @name module:data.ExperimentHandler#nextEntry
* @function
* @public
* @param {Object[]} snapshots - array of loop snapshots
*/
nextEntry(snapshots)
{
if (typeof snapshots !== 'undefined')
{
// turn single snapshot into a one-element array:
if (!Array.isArray(snapshots))
{
snapshots = [snapshots];
}
for (const snapshot of snapshots)
{
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
for (let a in attributes)
{
if (attributes.hasOwnProperty(a))
{
this._currentTrialData[a] = attributes[a];
}
}
}
}
// this is to support legacy generated JavaScript code and does not properly handle
// loops within loops:
else
{
for (const loop of this._unfinishedLoops)
{
const attributes = ExperimentHandler._getLoopAttributes(loop);
for (const a in attributes)
{
if (attributes.hasOwnProperty(a))
{
this._currentTrialData[a] = attributes[a];
}
}
}
}
// add the extraInfo dict to the data:
for (let a in this.extraInfo)
{
if (this.extraInfo.hasOwnProperty(a))
{
this._currentTrialData[a] = this.extraInfo[a];
}
}
this._trialsData.push(this._currentTrialData);
this._currentTrialData = {};
}
/**
* Save the results of the experiment.
*
*
*
For an experiment running locally, the results are offered for immediate download.
*
For an experiment running on the server, the results are uploaded to the server.