      1 define([
      2     'underscore',
      3     'trajectory'
      4 ],
      5 function(_, trajectory) {
      6   var getSampleNamesAndDataForSortedTrajectories =
      7     trajectory.getSampleNamesAndDataForSortedTrajectories;
      8   var getMinimumDelta = trajectory.getMinimumDelta;
      9   var TrajectoryOfSamples = trajectory.TrajectoryOfSamples;
     10
     11   /**
     12    *
     13    * @class AnimationDirector
     14    *
     15    * This object represents an animation director, as the name implies,
     16    * is an object that manages an animation. Takes the for a plot (mapping file
     17    * and coordinates) as well as the metadata categories we want to animate
     18    * over.  This object gets called in the main emperor module when an
     19    * animation starts and an instance will only be alive for one animation
     20    * cycle i. e. until the cycle hits the final frame of the animation.
     21    *
     22    * @param {String[]} mappingFileHeaders an Array of strings containing
     23    * metadata mapping file headers.
     24    * @param {Object[]} mappingFileData an Array where the indices are sample
     25    * identifiers and each of the contained elements is an Array of strings where
     26    * the first element corresponds to the first data for the first column in the
     27    * mapping file (mappingFileHeaders).
     28    * @param {Object[]} coordinatesData an Array of Objects where the indices are
     29    * the sample identifiers and each of the objects has the following
     30    * properties: x, y, z, name, color, P1, P2, P3, ... PN where N is the number
     31    * of dimensions in this dataset.
     32    * @param {String} gradientCategory a string with the name of the mapping file
     33    * header where the data that spreads the samples over a gradient is
     34    * contained, usually time or days_since_epoch. Note that this should be an
     35    * all numeric category.
     36    * @param {String} trajectoryCategory a string with the name of the mapping
     37    * file header where the data that groups the samples is contained, this will
     38    * usually be BODY_SITE, HOST_SUBJECT_ID, etc..
     39    * @param {speed} Positive real number determining the speed of an animation,
     40    * this is reflected in the number of frames produced for each time interval.
     41    *
     42    * @return {AnimationDirector} returns an animation director if the parameters
     43    * passed in were all valid.
     44    *
     45    * @throws {Error} Note that this class will raise an Error in any of the
     46    * following cases:
     47    * - One of the input arguments is undefined.
     48    * - If gradientCategory is not in the mappingFileHeaders.
     49    * - If trajectoryCategory is not in the mappingFileHeaders.
     50    * @constructs AnimationDirector
     51    */
     52   function AnimationDirector(mappingFileHeaders, mappingFileData,
     53                              coordinatesData, gradientCategory,
     54                              trajectoryCategory, speed) {
     55
     56     // all arguments are required
     57     if (mappingFileHeaders === undefined || mappingFileData === undefined ||
     58       coordinatesData === undefined || gradientCategory === undefined ||
     59       trajectoryCategory === undefined || speed === undefined) {
     60       throw new Error('All arguments are required');
     61     }
     62
     63     var index;
     64
     65     index = mappingFileHeaders.indexOf(gradientCategory);
     66     if (index == -1) {
     67       throw new Error('Could not find the gradient category in the mapping' +
     68                       ' file');
     69     }
     70     index = mappingFileHeaders.indexOf(trajectoryCategory);
     71     if (index == -1) {
     72       throw new Error('Could not find the trajectory category in the mapping' +
     73                       ' file');
     74     }
     75
     76     // guard against logical problems with the trajectory object
     77     if (speed <= 0) {
     78       throw new Error('The animation speed cannot be less than or equal to ' +
     79                       'zero');
     80     }
     81
     82     /**
     83      * @type {String[]}
     84      mappingFileHeaders an Array of strings containing metadata mapping file
     85      headers.
     86      */
     87     this.mappingFileHeaders = mappingFileHeaders;
     88     /**
     89      * @type {Object[]}
     90      *an Array where the indices are sample identifiers
     91      * and each of the contained elements is an Array of strings where the first
     92      * element corresponds to the first data for the first column in the mapping
     93      * file (mappingFileHeaders).
     94      */
     95     this.mappingFileData = mappingFileData;
     96     /**
     97      * @type {Object[]}
     98      * an Array of Objects where the indices are the
     99      * sample identifiers and each of the objects has the following properties:
    100      * x, y, z, name, color, P1, P2, P3, ... PN where N is the number of
    101      * dimensions in this dataset.
    102      */
    103     this.coordinatesData = coordinatesData;
    104     /**
    105      * @type {String}
    106      *a string with the name of the mapping file
    107      * header where the data that spreads the samples over a gradient is
    108      * contained, usually time or days_since_epoch. Note that this should be an
    109      * all numeric category
    110      */
    111     this.gradientCategory = gradientCategory;
    112     /**
    113      * @type {String}
    114      * a string with the name of the mapping file
    115      * header where the data that groups the samples is contained, this will
    116      * usually be BODY_SITE, HOST_SUBJECT_ID, etc..
    117      */
    118     this.trajectoryCategory = trajectoryCategory;
    119
    120     /**
    121      * @type {Float}
    122      * A floating point value determining what the minimum separation between
    123      * samples along the gradients is. Will be null until it is initialized to
    124      * the values according to the input data.
    125      * @default null
    126      */
    127     this.minimumDelta = null;
    128     /**
    129      * @type {Integer}
    130      * Maximum length the groups of samples have along a gradient.
    131      * @default null
    132      */
    133     this.maximumTrajectoryLength = null;
    134     /*
    135      * @type {Integer}
    136      * The current frame being served by the director
    137      * @default -1
    138      */
    139     this.currentFrame = -1;
    140
    141     /**
    142      * @type {Integer}
    143      * The previous frame served by the director
    144      */
    145     this.previousFrame = -1;
    146
    147     /**
    148      * @type {Array}
    149      * Array where each element in the trajectory is a trajectory with the
    150      * interpolated points in it.
    151      */
    152     this.trajectories = [];
    153
    154     /**
    155      * @type {Float}
    156      * How fast should the animation run, has to be a postive non-zero value.
    157      */
    158     this.speed = speed;
    159
    160     /**
    161      * @type {Array}
    162      * Sorted array of values in the gradient that all trajectories go through.
    163      */
    164     this.gradientPoints = [];
    165
    166     this._frameIndices = null;
    167
    168     // frames we want projected in the trajectory's interval
    169     this._n = Math.floor((1 / this.speed) * 10);
    170
    171     this.initializeTrajectories();
    172     this.getMaximumTrajectoryLength();
    173
    174     return this;
    175   }
    176
    177   /**
    178    *
    179    * Initializes the trajectories that the director manages.
    180    *
    181    */
    182   AnimationDirector.prototype.initializeTrajectories = function() {
    183
    184     var chewedData = null, trajectoryBuffer = null, minimumDelta;
    185     var sampleNamesBuffer = [], gradientPointsBuffer = [];
    186     var coordinatesBuffer = [];
    187     var chewedDataBuffer = null;
    188
    189     // compute a dictionary from where we will extract the germane data
    190     chewedData = getSampleNamesAndDataForSortedTrajectories(
    191         this.mappingFileHeaders, this.mappingFileData, this.coordinatesData,
    192         this.trajectoryCategory, this.gradientCategory);
    193
    194     if (chewedData === null) {
    195       throw new Error('Error initializing the trajectories, could not ' +
    196                       'compute the data');
    197     }
    198
    199     // calculate the minimum delta per step
    200     this.minimumDelta = getMinimumDelta(chewedData);
    201
    202     // we have to iterate over the keys because chewedData is a dictionary-like
    203     // object, if possible this should be changed in the future to be an Array
    204     for (var key in chewedData) {
    205
    206       // re-initalize the arrays, essentially dropping all the previously
    207       // existing information
    208       sampleNamesBuffer = [];
    209       gradientPointsBuffer = [];
    210       coordinatesBuffer = [];
    211
    212       // buffer this to avoid the multiple look-ups below
    213       chewedDataBuffer = chewedData[key];
    214
    215       // each of the keys is a trajectory name i. e. CONTROL, TREATMENT, etc
    216       // we are going to generate buffers so we can initialize the trajectory
    217       for (var index = 0; index < chewedDataBuffer.length; index++) {
    218         // list of sample identifiers
    219         sampleNamesBuffer.push(chewedDataBuffer[index]['name']);
    220
    221         // list of the value each sample has in the gradient
    222         gradientPointsBuffer.push(chewedDataBuffer[index]['value']);
    223
    224         // x, y and z values for the coordinates data
    225         coordinatesBuffer.push({'x': chewedDataBuffer[index]['x'],
    226                                 'y': chewedDataBuffer[index]['y'],
    227                                 'z': chewedDataBuffer[index]['z']});
    228       }
    229
    230       // Don't add a trajectory unless it has more than one sample in the
    231       // gradient. For example, there's no reason why we should animate a
    232       // trajectory that has 3 samples at timepoint 0 ([0, 0, 0]) or a
    233       // trajectory that has just one sample at timepoint 0 ([0])
    234       if (sampleNamesBuffer.length <= 1 ||
    235           _.uniq(gradientPointsBuffer).length <= 1) {
    236         continue;
    237       }
    238
    239       // create the trajectory object, we use Infinity to draw as many frames
    240       // as they may be needed
    241       trajectoryBuffer = new TrajectoryOfSamples(sampleNamesBuffer, key,
    242           gradientPointsBuffer, coordinatesBuffer, this.minimumDelta, this._n,
    243           Infinity);
    244
    245       this.trajectories.push(trajectoryBuffer);
    246
    247       // keep track of all gradient points so we can track uninterpolated
    248       // frames - only for trajectories that are added into the animation
    249       this.gradientPoints = this.gradientPoints.concat(gradientPointsBuffer);
    250     }
    251
    252     // javascript sorting is a hot mess, we need to convert to float first
    253     this.gradientPoints = _.map(_.uniq(this.gradientPoints), parseFloat);
    254     this.gradientPoints = _.sortBy(this.gradientPoints);
    255
    256     this._frameIndices = this._computeFrameIndices();
    257
    258     return;
    259   };
    260
    261   /**
    262    * Check if the current frame represents one of the gradient points.
    263    *
    264    * This is useful to keep track of when a new segment of the gradient has
    265    * started.
    266    * @return {boolean} True if the currentFrame represents a point in the
    267    * animation's gradient. False if it represents an interpolated frame.
    268    */
    269   AnimationDirector.prototype.currentFrameIsGradientPoint = function() {
    270     // use _.sortedIndex instead of .indexOf to do a binary search because the
    271     // array is guaranteed to be sorted
    272     var i = _.sortedIndex(this._frameIndices, this.currentFrame);
    273     return this._frameIndices[i] === this.currentFrame;
    274   };
    275
    276
    277   /**
    278    * Compute the indices where a gradient point is found
    279    * @return {Array} Array of index values where the gradient points fall.
    280    * @private
    281    */
    282   AnimationDirector.prototype._computeFrameIndices = function() {
    283     // 1 represents the first frame
    284     var delta = 0, out = [1];
    285
    286     for (var i = 0; i < this.gradientPoints.length - 1; i++) {
    287       delta = Math.abs(Math.abs(this.gradientPoints[i]) -
    288                        Math.abs(this.gradientPoints[i + 1]));
    289
    290       // no need to truncate since we use Infinity when creating the trajectory
    291       pointsPerStep = Math.floor((delta * this._n) / this.minimumDelta);
    292
    293       out.push(out[i] + pointsPerStep);
    294     }
    295
    296     return out;
    297   };
    298
    299   /**
    300    *
    301    * Retrieves the lengths of all the trajectories and figures out which of
    302    * them is the longest one, then assigns that value to the
    303    * maximumTrajectoryLength property.
    304    * @return {Integer} Maximum trajectory length
    305    *
    306    */
    307   AnimationDirector.prototype.getMaximumTrajectoryLength = function() {
    308     if (this.maximumTrajectoryLength === null) {
    309       this._computeN();
    310     }
    311
    312     return this.maximumTrajectoryLength;
    313   };
    314
    315   /**
    316    *
    317    * Helper function to compute the maximum length of the trajectories that the
    318    * director is in charge of.
    319    * @private
    320    *
    321    */
    322   AnimationDirector.prototype._computeN = function() {
    323     var arrayOfLengths = [];
    324
    325     // retrieve the length of all the trajectories
    326     for (var index = 0; index < this.trajectories.length; index++) {
    327       arrayOfLengths.push(
    328           this.trajectories[index].interpolatedCoordinates.length);
    329     }
    330
    331     // assign the value of the maximum value for these lengths
    332     this.maximumTrajectoryLength = _.max(arrayOfLengths);
    333   };
    334
    335   /**
    336    *
    337    * Helper method to update the value of the currentFrame property.
    338    *
    339    */
    340   AnimationDirector.prototype.updateFrame = function() {
    341     if (this.animationCycleFinished() === false) {
    342       this.previousFrame = this.currentFrame;
    343       this.currentFrame = this.currentFrame + 1;
    344     }
    345   };
    346
    347   /**
    348    *
    349    * Check whether or not the animation cycle has finished for this object.
    350    * @return {boolean} True if the animation has reached it's end and False if
    351    * the animation still has frames to go.
    352    *
    353    */
    354   AnimationDirector.prototype.animationCycleFinished = function() {
    355     return this.currentFrame > this.getMaximumTrajectoryLength();
    356   };
    357
    358   return AnimationDirector;
    359 });
    360