      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