1 define(['underscore'],
  2 function(_) {
  3   /**
  4    * @class MultiModel
  5    *
  6    * The MultiModel wraps a dictionary of DecompositionModel objects and
  7    * supplies methods to compute aggregate metrics over all such model objects
  8    *
  9    * This is used to provide the 'global' dimension ranges of the scene, rather
 10    * than the 'local' dimension ranges of any individual scatter or biplot.
 11    */
 12   function MultiModel(underlyingModelsDict) {
 13     this.models = underlyingModelsDict;
 14 
 15     var first = true;
 16     var firstModel = null;
 17 
 18     for (var key in underlyingModelsDict) {
 19       if (first) {
 20         first = false;
 21         firstModel = underlyingModelsDict[key];
 22       }
 23       else {
 24         if (underlyingModelsDict[key].dimensions != firstModel.dimensions)
 25           throw 'Underlying models must have same number of dimensions';
 26       }
 27     }
 28 
 29     this.dimensionRanges = {'max': [], 'min': []};
 30     this._unionRanges();
 31   }
 32 
 33   /**
 34    *
 35    * Utility method to find the union of the ranges in the DecompositionModels
 36    * this method will populate the dimensionRanges attributes.
 37    * @private
 38    *
 39    */
 40   MultiModel.prototype._unionRanges = function() {
 41     var scope = this, computeRanges;
 42 
 43     // first check if there's any range data, if there isn't, then we need
 44     // to compute it by looking at all the decompositions
 45     computeRanges = scope.dimensionRanges.max.length === 0;
 46 
 47     // if there's range data then check it lies within the global ranges
 48     if (computeRanges === false) {
 49       _.each(this.models, function(decomp, unused) {
 50         for (var i = 0; i < decomp.dimensionRanges.max.length; i++) {
 51           // global
 52           var gMax = scope.dimensionRanges.max[i];
 53           var gMin = scope.dimensionRanges.min[i];
 54 
 55           // local
 56           var lMax = decomp.dimensionRanges.max[i];
 57           var lMin = decomp.dimensionRanges.min[i];
 58 
 59           // when we detect a point outside the global ranges we break and
 60           // recompute them
 61           if (!(gMin <= lMin && lMin <= gMax) ||
 62               !(gMin <= lMax && lMax <= gMax)) {
 63             computeRanges = true;
 64             break;
 65           }
 66         }
 67       });
 68     }
 69 
 70     if (computeRanges === false) {
 71       // If at this point we still don't need to compute the data, it is safe
 72       // to exit because all data still exists within the expected ranges
 73       return;
 74     }
 75     else {
 76       // TODO: If this entire function ever becomes a bottleneck we should only
 77       // update the dimensions that changed.
 78       // See: https://github.com/biocore/emperor/issues/526
 79 
 80       // if we have to compute the data, clean up the previously known ranges
 81       this.dimensionRanges.max = [];
 82       this.dimensionRanges.max.length = 0;
 83       this.dimensionRanges.min = [];
 84       this.dimensionRanges.min.length = 0;
 85     }
 86 
 87     _.each(this.models, function(decomp, unused) {
 88 
 89       if (scope.dimensionRanges.max.length === 0) {
 90         scope.dimensionRanges.max = decomp.dimensionRanges.max.slice();
 91         scope.dimensionRanges.min = decomp.dimensionRanges.min.slice();
 92       }
 93       else {
 94         // when we have more than one decomposition view we need to figure out
 95         // the absolute largest range that views span over
 96         _.each(decomp.dimensionRanges.max, function(value, index) {
 97           var vMax = decomp.dimensionRanges.max[index],
 98               vMin = decomp.dimensionRanges.min[index];
 99 
100           if (vMax > scope.dimensionRanges.max[index]) {
101             scope.dimensionRanges.max[index] = vMax;
102           }
103           if (vMin < scope.dimensionRanges.min[index]) {
104             scope.dimensionRanges.min[index] = vMin;
105           }
106         });
107       }
108     });
109   };
110 
111   return MultiModel;
112 });
113