define([
'underscore',
'trajectory'
],
function(_, trajectory) {
var getSampleNamesAndDataForSortedTrajectories =
trajectory.getSampleNamesAndDataForSortedTrajectories;
var getMinimumDelta = trajectory.getMinimumDelta;
var TrajectoryOfSamples = trajectory.TrajectoryOfSamples;
/**
*
* @class AnimationDirector
*
* This object represents an animation director, as the name implies,
* is an object that manages an animation. Takes the for a plot (mapping file
* and coordinates) as well as the metadata categories we want to animate
* over. This object gets called in the main emperor module when an
* animation starts and an instance will only be alive for one animation
* cycle i. e. until the cycle hits the final frame of the animation.
*
* @param {String[]} mappingFileHeaders an Array of strings containing
* metadata mapping file headers.
* @param {Object[]} mappingFileData an Array where the indices are sample
* identifiers and each of the contained elements is an Array of strings where
* the first element corresponds to the first data for the first column in the
* mapping file (mappingFileHeaders).
* @param {Object[]} coordinatesData an Array of Objects where the indices are
* the sample identifiers and each of the objects has the following
* properties: x, y, z, name, color, P1, P2, P3, ... PN where N is the number
* of dimensions in this dataset.
* @param {String} gradientCategory a string with the name of the mapping file
* header where the data that spreads the samples over a gradient is
* contained, usually time or days_since_epoch. Note that this should be an
* all numeric category.
* @param {String} trajectoryCategory a string with the name of the mapping
* file header where the data that groups the samples is contained, this will
* usually be BODY_SITE, HOST_SUBJECT_ID, etc..
* @param {speed} Positive real number determining the speed of an animation,
* this is reflected in the number of frames produced for each time interval.
*
* @return {AnimationDirector} returns an animation director if the parameters
* passed in were all valid.
*
* @throws {Error} Note that this class will raise an Error in any of the
* following cases:
* - One of the input arguments is undefined.
* - If gradientCategory is not in the mappingFileHeaders.
* - If trajectoryCategory is not in the mappingFileHeaders.
* @constructs AnimationDirector
*/
function AnimationDirector(mappingFileHeaders, mappingFileData,
coordinatesData, gradientCategory,
trajectoryCategory, speed) {
// all arguments are required
if (mappingFileHeaders === undefined || mappingFileData === undefined ||
coordinatesData === undefined || gradientCategory === undefined ||
trajectoryCategory === undefined || speed === undefined) {
throw new Error('All arguments are required');
}
var index;
index = mappingFileHeaders.indexOf(gradientCategory);
if (index == -1) {
throw new Error('Could not find the gradient category in the mapping' +
' file');
}
index = mappingFileHeaders.indexOf(trajectoryCategory);
if (index == -1) {
throw new Error('Could not find the trajectory category in the mapping' +
' file');
}
// guard against logical problems with the trajectory object
if (speed <= 0) {
throw new Error('The animation speed cannot be less than or equal to ' +
'zero');
}
/**
* @type {String[]}
mappingFileHeaders an Array of strings containing metadata mapping file
headers.
*/
this.mappingFileHeaders = mappingFileHeaders;
/**
* @type {Object[]}
*an Array where the indices are sample identifiers
* and each of the contained elements is an Array of strings where the first
* element corresponds to the first data for the first column in the mapping
* file (mappingFileHeaders).
*/
this.mappingFileData = mappingFileData;
/**
* @type {Object[]}
* an Array of Objects where the indices are the
* sample identifiers and each of the objects has the following properties:
* x, y, z, name, color, P1, P2, P3, ... PN where N is the number of
* dimensions in this dataset.
*/
this.coordinatesData = coordinatesData;
/**
* @type {String}
*a string with the name of the mapping file
* header where the data that spreads the samples over a gradient is
* contained, usually time or days_since_epoch. Note that this should be an
* all numeric category
*/
this.gradientCategory = gradientCategory;
/**
* @type {String}
* a string with the name of the mapping file
* header where the data that groups the samples is contained, this will
* usually be BODY_SITE, HOST_SUBJECT_ID, etc..
*/
this.trajectoryCategory = trajectoryCategory;
/**
* @type {Float}
* A floating point value determining what the minimum separation between
* samples along the gradients is. Will be null until it is initialized to
* the values according to the input data.
* @default null
*/
this.minimumDelta = null;
/**
* @type {Integer}
* Maximum length the groups of samples have along a gradient.
* @default null
*/
this.maximumTrajectoryLength = null;
/*
* @type {Integer}
* The current frame being served by the director
* @default -1
*/
this.currentFrame = -1;
/**
* @type {Integer}
* The previous frame served by the director
*/
this.previousFrame = -1;
/**
* @type {Array}
* Array where each element in the trajectory is a trajectory with the
* interpolated points in it.
*/
this.trajectories = [];
/**
* @type {Float}
* How fast should the animation run, has to be a postive non-zero value.
*/
this.speed = speed;
this.initializeTrajectories();
this.getMaximumTrajectoryLength();
return this;
}
/**
*
* Initializes the trajectories that the director manages.
*
*/
AnimationDirector.prototype.initializeTrajectories = function() {
var chewedData = null, trajectoryBuffer = null, minimumDelta;
var sampleNamesBuffer = [], gradientPointsBuffer = [];
var coordinatesBuffer = [];
var chewedDataBuffer = null;
// frames we want projected in the trajectory's interval
var n = Math.floor((1 / (this.speed)) * 10);
// compute a dictionary from where we will extract the germane data
chewedData = getSampleNamesAndDataForSortedTrajectories(
this.mappingFileHeaders, this.mappingFileData, this.coordinatesData,
this.trajectoryCategory, this.gradientCategory);
if (chewedData === null) {
throw new Error('Error initializing the trajectories, could not ' +
'compute the data');
}
// calculate the minimum delta per step
this.minimumDelta = getMinimumDelta(chewedData);
// we have to iterate over the keys because chewedData is a dictionary-like
// object, if possible this should be changed in the future to be an Array
for (var key in chewedData) {
// re-initalize the arrays, essentially dropping all the previously
// existing information
sampleNamesBuffer = [];
gradientPointsBuffer = [];
coordinatesBuffer = [];
// buffer this to avoid the multiple look-ups below
chewedDataBuffer = chewedData[key];
// each of the keys is a trajectory name i. e. CONTROL, TREATMENT, etc
// we are going to generate buffers so we can initialize the trajectory
for (var index = 0; index < chewedDataBuffer.length; index++) {
// list of sample identifiers
sampleNamesBuffer.push(chewedDataBuffer[index]['name']);
// list of the value each sample has in the gradient
gradientPointsBuffer.push(chewedDataBuffer[index]['value']);
// x, y and z values for the coordinates data
coordinatesBuffer.push({'x': chewedDataBuffer[index]['x'],
'y': chewedDataBuffer[index]['y'],
'z': chewedDataBuffer[index]['z']});
}
// Don't add a trajectory unless it has more than one sample in the
// gradient. For example, there's no reason why we should animate a
// trajectory that has 3 samples at timepoint 0 ([0, 0, 0]) or a
// trajectory that has just one sample at timepoint 0 ([0])
if (sampleNamesBuffer.length <= 1 ||
_.uniq(gradientPointsBuffer).length <= 1) {
continue;
}
// create the trajectory object, we use Infinity to draw as many frames
// as they may be needed
trajectoryBuffer = new TrajectoryOfSamples(sampleNamesBuffer, key,
gradientPointsBuffer, coordinatesBuffer, this.minimumDelta, n,
Infinity);
this.trajectories.push(trajectoryBuffer);
}
return;
};
/**
*
* Retrieves the lengths of all the trajectories and figures out which of
* them is the longest one, then assigns that value to the
* maximumTrajectoryLength property.
* @return {Integer} Maximum trajectory length
*
*/
AnimationDirector.prototype.getMaximumTrajectoryLength = function() {
if (this.maximumTrajectoryLength === null) {
this._computeN();
}
return this.maximumTrajectoryLength;
};
/**
*
* Helper function to compute the maximum length of the trajectories that the
* director is in charge of.
* @private
*
*/
AnimationDirector.prototype._computeN = function() {
var arrayOfLengths = [];
// retrieve the length of all the trajectories
for (var index = 0; index < this.trajectories.length; index++) {
arrayOfLengths.push(
this.trajectories[index].interpolatedCoordinates.length);
}
// assign the value of the maximum value for these lengths
this.maximumTrajectoryLength = _.max(arrayOfLengths);
};
/**
*
* Helper method to update the value of the currentFrame property.
*
*/
AnimationDirector.prototype.updateFrame = function() {
if (this.animationCycleFinished() === false) {
this.previousFrame = this.currentFrame;
this.currentFrame = this.currentFrame + 1;
}
};
/**
*
* Check whether or not the animation cycle has finished for this object.
* @return {bool} True if the animation has reached it's end and False if the
* animation still has frames to go.
*
*/
AnimationDirector.prototype.animationCycleFinished = function() {
return this.currentFrame > this.getMaximumTrajectoryLength();
};
return AnimationDirector;
});