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