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