1 /** @module trajectory */
  2 define([
  3     'underscore'
  4 ], function(_) {
  5   /**
  6    *
  7    * @class TrajectoryOfSamples
  8    *
  9    * Represents an ordered set of samples and their position in PCoA space.
 10    *
 11    * @param {string[]} sampleNames Array of sample identifiers.
 12    * @param {string} metadataCategoryName The name of the category in the
 13    * mapping file used to generate this trajectory.
 14    * @param {float[]} gradientPoints The position of the samples in the
 15    * gradient.
 16    * @param {Object[]} coordinates Array of objects with x, y and z properties
 17    * where each corresponds to the position of a sample in PCoA space.
 18    * @param {float} minimumDelta Minimum differential between the ordered
 19    * gradientPoints this value must be non-zero. Note that this value should be
 20    * computed taking into account all the other trajectories that will be
 21    * animated together, usually by an AnimationDirector object.
 22    * @param {integer} [suppliedN = 5] Determines how many points should be found
 23    * in the the trajectory.
 24    * @param {integer} [maxN = 10] Maximum number of samples allowed per
 25    * interpolation interval.
 26    *
 27    * @return {TrajectoryOfSamples} An instance of TrajectoryOfSamples
 28    * @constructs TrajectoryOfSamples
 29    **/
 30   function TrajectoryOfSamples(sampleNames, metadataCategoryName,
 31                                gradientPoints, coordinates, minimumDelta,
 32                                suppliedN, maxN) {
 33     /**
 34      * Sample identifiers
 35      * @type {string[]}
 36      */
 37     this.sampleNames = sampleNames;
 38     /**
 39      * The name of the category in the mapping file used to generate this
 40      * trajectory.
 41      * @type {string}
 42      */
 43     this.metadataCategoryName = metadataCategoryName;
 44 
 45     // array of the values that samples have through the gradient
 46     this.gradientPoints = gradientPoints;
 47 
 48     // the first three axes of the data points
 49     this.coordinates = coordinates;
 50 
 51     /**
 52      * Minimum differential between samples in the trajectory; the value is
 53      * computed using the gradientPoints array.
 54      * @type {float}
 55      */
 56     this.minimumDelta = minimumDelta;
 57 
 58     /**
 59      * Minimum number of frames a distance will have in the gradient.
 60      * This value determines how fast the animation will run.
 61      * For now we use 5 as a good default value; 60 was way too slow.
 62      * @type {float}
 63      * @default 5
 64      */
 65     this.suppliedN = suppliedN !== undefined ? suppliedN : 5;
 66     /**
 67      * Maximum number of samples allowed per interpolation interval.
 68      * @type {float}
 69      * @default 10
 70      */
 71     this.maxN = maxN !== undefined ? maxN : 10;
 72 
 73     if (this.coordinates.length != this.gradientPoints.length) {
 74       throw new Error('The number of coordinate points and gradient points is' +
 75           'different, make sure these values are consistent.');
 76     }
 77 
 78     // initialize as an empty array but fill it up upon request
 79     /**
 80      * Array of objects with the corresponding interpolated x, y and z values.
 81      * The interpolation operation takes place between subsequent samples.
 82      * @type {Object[]}
 83      */
 84     this.interpolatedCoordinates = null;
 85     this._generateInterpolatedCoordinates();
 86 
 87     return this;
 88   }
 89 
 90   /**
 91    *
 92    * Helper method to iterate over all the coordinates and generate
 93    * interpolated arrays.
 94    * @private
 95    *
 96    */
 97   TrajectoryOfSamples.prototype._generateInterpolatedCoordinates = function() {
 98     var pointsPerStep = 0, delta = 0;
 99     var interpolatedCoordinatesBuffer = new Array(),
100     intervalBuffer = new Array();
101     var currInterpolation;
102 
103     // iterate over the gradient points to compute the interpolated distances
104     for (var index = 0; index < this.gradientPoints.length - 1; index++) {
105 
106       // calculate the absolute difference of the current pair of points
107       delta = Math.abs(Math.abs(this.gradientPoints[index]) - Math.abs(
108             this.gradientPoints[index + 1]));
109 
110       pointsPerStep = this.calculateNumberOfPointsForDelta(delta);
111       if (pointsPerStep > this.maxN) {
112         pointsPerStep = this.maxN;
113       }
114 
115       currInterpolation = linearInterpolation(this.coordinates[index]['x'],
116           this.coordinates[index]['y'],
117           this.coordinates[index]['z'],
118           this.coordinates[index + 1]['x'],
119           this.coordinates[index + 1]['y'],
120           this.coordinates[index + 1]['z'],
121           pointsPerStep);
122 
123       // extend to include these interpolated points, do not include the last
124       // element of the array to avoid repeating the number per interval
125       interpolatedCoordinatesBuffer = interpolatedCoordinatesBuffer.concat(
126           currInterpolation.slice(0, -1));
127 
128       // extend the interval buffer
129       for (var i = 0; i < pointsPerStep; i++) {
130         intervalBuffer.push(index);
131       }
132     }
133 
134     // add the last point to make sure the trajectory is closed
135     this.interpolatedCoordinates = interpolatedCoordinatesBuffer.concat(
136         currInterpolation.slice(-1));
137     this._intervalValues = intervalBuffer;
138 
139     return;
140   };
141 
142   /**
143    *
144    * Helper method to calculate the number of points that there should be for a
145    * differential.
146    *
147    * @param {float} delta Value for which to determine the required number of
148    * points.
149    *
150    * @return {integer} The number of suggested frames for the differential
151    *
152    */
153   TrajectoryOfSamples.prototype.calculateNumberOfPointsForDelta =
154       function(delta) {
155     return Math.floor((delta * this.suppliedN) / this.minimumDelta);
156   };
157 
158   /**
159    *
160    * Retrieve the representative coordinates needed for a trajectory to be
161    * drawn.
162    *
163    ** Note that this implementation is naive and will return points that lay on
164    * a rect line if these were part of the original set of coordinates.
165    *
166    * @param {integer} idx Value for which to determine the required number of
167    * points.
168    *
169    * @return {Array[]} Array containing the representative float x, y, z
170    * coordinates needed to draw a trajectory at the given index.
171    */
172   TrajectoryOfSamples.prototype.representativeCoordinatesAtIndex =
173       function(idx) {
174 
175     if (idx === 0) {
176       return [this.coordinates[0]];
177     }
178 
179     // we only need to show the edges and none of the interpolated points
180     if (this.interpolatedCoordinates.length - 1 <= idx) {
181       return this.coordinates;
182     }
183 
184     var output = this.coordinates.slice(0, this._intervalValues[idx] + 1);
185     output = output.concat(this.interpolatedCoordinates[idx]);
186 
187     return output;
188   };
189 
190   /**
191    *
192    * Grab only the interpolated portion of representativeCoordinatesAtIndex.
193    *
194    * @param {integer} idx Value for which to determine the required number of
195    * points.
196    *
197    * @return {Array[]} Array containing the representative float x, y, z
198    * coordinates needed to draw the interpolated portion of a trajectory at the
199    * given index.
200    */
201   TrajectoryOfSamples.prototype.representativeInterpolatedCoordinatesAtIndex =
202   function(idx) {
203     if (idx === 0)
204       return null;
205     if (this.interpolatedCoordinates.length - 1 <= idx)
206       return null;
207 
208     lastStaticPoint = this.coordinates[this._intervalValues[idx]];
209     interpPoint = this.interpolatedCoordinates[idx];
210     if (lastStaticPoint.x === interpPoint.x &&
211       lastStaticPoint.y === interpPoint.y &&
212       lastStaticPoint.z === interpPoint.z) {
213       return null; //Shouldn't pass on a zero length segment
214     }
215 
216     return [lastStaticPoint, interpPoint];
217   };
218 
219   /**
220    *
221    * Function to interpolate a certain number of steps between two three
222    * dimensional points.
223    *
224    * This code is based on the function found in:
225    *     http://snipplr.com/view.php?codeview&id=47206
226    *
227    * @param {float} x_1 Initial value of a position in the first dimension
228    * @param {float} y_1 Initial value of a position in the second dimension
229    * @param {float} z_1 Initial value of a position in the third dimension
230    * @param {float} x_2 Final value of a position in the first dimension
231    * @param {float} y_2 Final value of a position in the second dimension
232    * @param {float} z_2 Final value of a position in the third dimension
233    * @param {integer} steps Number of steps that we want the interpolation to
234    * run
235    *
236    * @return {Object[]} Array of objects that have the x, y and z attributes
237    * @function linearInterpolation
238    *
239    */
240 
241   function linearInterpolation(x_1, y_1, z_1, x_2, y_2, z_2, steps) {
242     var xAbs = Math.abs(x_1 - x_2);
243     var yAbs = Math.abs(y_1 - y_2);
244     var zAbs = Math.abs(z_1 - z_2);
245     var xDiff = x_2 - x_1;
246     var yDiff = y_2 - y_1;
247     var zDiff = z_2 - z_1;
248 
249     // and apparetnly this makes takes no effect whatsoever
250     var length = Math.sqrt(xAbs * xAbs + yAbs * yAbs + zAbs * zAbs);
251     var xStep = xDiff / steps;
252     var yStep = yDiff / steps;
253     var zStep = zDiff / steps;
254 
255     var newx = 0;
256     var newy = 0;
257     var newz = 0;
258     var result = new Array();
259 
260     for (var s = 0; s <= steps; s++) {
261       newx = x_1 + (xStep * s);
262       newy = y_1 + (yStep * s);
263       newz = z_1 + (zStep * s);
264 
265       result.push({'x': newx, 'y': newy, 'z': newz});
266     }
267 
268     return result;
269   }
270 
271   /**
272    *
273    * Function to compute the distance between two three dimensional points.
274    *
275    * This code is based on the function found in:
276    *     {@link http://snipplr.com/view.php?codeview&id=47207}
277    *
278    * @param {float} x_1 Initial value of a position in the first dimension
279    * @param {float} y_1 Initial value of a position in the second dimension
280    * @param {float} z_1 Initial value of a position in the third dimension
281    * @param {float} x_2 Final value of a position in the first dimension
282    * @param {float} y_2 Final value of a position in the second dimension
283    * @param {float} z_2 Final value of a position in the third dimension
284    *
285    * @return {float} Value of the distance between the two points
286    * @function distanceBetweenPoints
287    *
288    */
289   function distanceBetweenPoints(x_1, y_1, z_1, x_2, y_2, z_2) {
290     var xs = 0;
291     var ys = 0;
292     var zs = 0;
293 
294     // Math.pow is faster than simple multiplication
295     xs = Math.pow(Math.abs(x_2 - x_1), 2);
296     ys = Math.pow(Math.abs(y_2 - y_1), 2);
297     zs = Math.pow(Math.abs(z_2 - z_1), 2);
298 
299     return Math.sqrt(xs + ys + zs);
300   }
301 
302   /**
303    *
304    * Helper data wrangling function, takes as inputs a mapping file and the
305    * coordinates to synthesize the information into an array. Mainly used by
306    * the AnimationDirector object.
307    *
308    * @param {string[]} mappingFileHeaders The metadata mapping file headers.
309    * @param {Array[]} mappingFileData An Array where the indices are sample
310    * identifiers and each of the contained elements is an Array of strings where
311    * the first element corresponds to the first data for the first column in the
312    * mapping file (`mappingFileHeaders`).
313    * @param {Object[]} coordinatesData An Array of Objects where the indices are
314    * the sample identifiers and each of the objects has the following
315    * properties: x, y, z, name, color, P1, P2, P3, ... PN where N is the number
316    * of dimensions in this dataset.
317    * @param {string} trajectoryCategory a string with the name of the mapping
318    * file header where the data that groups the samples is contained, this will
319    * usually be BODY_SITE, HOST_SUBJECT_ID, etc..
320    * @param {string} gradientCategory a string with the name of the mapping file
321    * header where the data that spreads the samples over a gradient is
322    * contained, usually time or days_since_epoch. Note that this should be an
323    * all numeric category.
324    *
325    * @return {Object[]} An Array with the contained data indexed by the sample
326    * identifiers.
327    * @throws {Error} Any of the following:
328    *  * gradientIndex === -1
329    *  * trajectoryIndex === -1
330    * @function getSampleNamesAndDataForSortedTrajectories
331    *
332    */
333   function getSampleNamesAndDataForSortedTrajectories(mappingFileHeaders,
334       mappingFileData,
335       coordinatesData,
336       trajectoryCategory,
337       gradientCategory) {
338     var gradientIndex = mappingFileHeaders.indexOf(gradientCategory);
339     var trajectoryIndex = mappingFileHeaders.indexOf(trajectoryCategory);
340 
341     var processedData = {}, out = {};
342     var trajectoryBuffer = null, gradientBuffer = null;
343 
344     // this is the most utterly annoying thing ever
345     if (gradientIndex === -1) {
346       throw new Error('Gradient category not found in mapping file header');
347     }
348     if (trajectoryIndex === -1) {
349       throw new Error('Trajectory category not found in mapping file header');
350     }
351 
352     for (var sampleId in mappingFileData) {
353 
354       trajectoryBuffer = mappingFileData[sampleId][trajectoryIndex];
355       gradientBuffer = mappingFileData[sampleId][gradientIndex];
356 
357       // check if there's already an element for this trajectory, if not
358       // initialize a new array for this element of the processed data
359       if (processedData[trajectoryBuffer] === undefined) {
360         processedData[trajectoryBuffer] = [];
361       }
362       processedData[trajectoryBuffer].push({'name': sampleId,
363         'value': gradientBuffer, 'x': coordinatesData[sampleId]['x'],
364         'y': coordinatesData[sampleId]['y'],
365         'z': coordinatesData[sampleId]['z']});
366     }
367 
368     // we need this custom sorting function to make the values be sorted in
369     // ascending order but accounting for the data structure that we just built
370     var sortingFunction = function(a, b) {
371       return parseFloat(a['value']) - parseFloat(b['value']);
372     };
373 
374     // sort all the values using the custom anonymous function
375     for (var key in processedData) {
376       processedData[key].sort(sortingFunction);
377     }
378 
379     // Don't add a trajectory unless it has more than one sample in the
380     // gradient. For example, there's no reason why we should animate a
381     // trajectory that has 3 samples at timepoint 0 ([0, 0, 0]) or a trajectory
382     // that has just one sample at timepoint 0 ([0])
383     for (key in processedData) {
384       var uniqueValues = _.uniq(processedData[key], false, function(sample) {
385         return sample.value;
386       });
387 
388       if (uniqueValues.length > 1 && processedData[key].length >= 1) {
389         out[key] = processedData[key];
390       }
391     }
392 
393     // we need a placeholder object as we filter trajectories below
394     processedData = out;
395     out = {};
396 
397     // note that min finds the trajectory with the oldest sample, once found
398     // we get the first sample and the first point in the gradient
399     var earliestSample = _.min(processedData, function(trajectory) {
400       return parseInt(trajectory[0].value);
401     })[0].value;
402 
403     // Left-pad all trajectories so they start at the same time, but they are
404     // not visibly different.
405     //
406     // Note: THREE.js won't display geometries with overlapping vertices,
407     // therefore we add a small amount of noise in the Z coordinate.
408     _.each(processedData, function(value, key) {
409       out[key] = processedData[key];
410       var first = processedData[key][0];
411       if (first.value !== earliestSample) {
412         out[key].unshift({'name': first.name, 'value': earliestSample,
413                           'x': first.x, 'y': first.y, 'z': first.z + 0.0001});
414       }
415     });
416 
417     return out;
418   }
419 
420   /**
421    *
422    * Function to calculate the minimum delta from an array of wrangled data by
423    * getSampleNamesAndDataForSortedTrajectories.
424    *
425    * This function will not take into account as a minimum delta zero values
426    * i.e. the differential between two samples that lie at the same position in
427    * the gradient.
428    *
429    * @param {Object[]} sampleData An Array as computed from mapping file data
430    * and coordinates by getSampleNamesAndDataForSortedTrajectories.
431    *
432    * @return {float} The minimum difference between two samples across the
433    * defined gradient.
434    *
435    * @throws {Error} Input data is undefined.
436    * @function getMinimumDelta
437    */
438   function getMinimumDelta(sampleData) {
439     if (sampleData === undefined) {
440       throw new Error('The sample data cannot be undefined');
441     }
442 
443     var bufferArray = new Array(), deltasArray = new Array();
444 
445     // go over all the data and compute the deltas for all trajectories
446     for (var key in sampleData) {
447       for (var index = 0; index < sampleData[key].length; index++) {
448         bufferArray.push(sampleData[key][index]['value']);
449       }
450       for (var index = 0; index < bufferArray.length - 1; index++) {
451         deltasArray.push(Math.abs(bufferArray[index + 1] - bufferArray[index]));
452       }
453 
454       // clean buffer array
455       bufferArray.length = 0;
456     }
457 
458     // remove all the deltas of zero so we don't skew our results
459     deltasArray = _.filter(deltasArray, function(num) { return num !== 0; });
460 
461     // return the minimum of these values
462     return _.min(deltasArray);
463   }
464 
465   return {'TrajectoryOfSamples': TrajectoryOfSamples,
466     'getMinimumDelta': getMinimumDelta,
467     'getSampleNamesAndDataForSortedTrajectories':
468       getSampleNamesAndDataForSortedTrajectories,
469     'distanceBetweenPoints': distanceBetweenPoints,
470     'linearInterpolation': linearInterpolation};
471 });
472