1 define([
  2     'jquery',
  3     'underscore',
  4     'three',
  5     'shapes',
  6     'draw',
  7     'multi-model',
  8     'util'
  9 ], function($, _, THREE, shapes, draw, multiModel, util) {
 10   var makeArrow = draw.makeArrow;
 11   var makeLineCollection = draw.makeLineCollection;
 12 /**
 13  *
 14  * @class DecompositionView
 15  *
 16  * Contains all the information on how the model is being presented to the
 17  * user.
 18  *
 19  * @param {MultiModel} multiModel - A multi model object with all models
 20  * @param {string} modelKey - The key referencing the target model
 21  *                            within the multiModel
 22  *
 23  * @return {DecompositionView}
 24  * @constructs DecompositionView
 25  *
 26  */
 27 function DecompositionView(multiModel, modelKey, uiState) {
 28   /**
 29    * The decomposition model that the view represents.
 30    * @type {DecompositionModel}
 31    */
 32   this.decomp = multiModel.models[modelKey];
 33 
 34   /**
 35    * All models in the current scene and global metrics about them
 36    * @type {MultiModel}
 37    */
 38   this.allModels = multiModel;
 39 
 40   /**
 41    * Number of samples represented in the view.
 42    * @type {integer}
 43    */
 44   this.count = this.decomp.length;
 45   /**
 46    * Top visible dimensions
 47    * @type {integer[]}
 48    */
 49   // make sure we only use at most 3 elements for scatter and arrow plots
 50   this.visibleDimensions = _.range(this.decomp.dimensions).slice(0, 3);
 51   /**
 52    * Orientation of the axes, `-1` means the axis is flipped, `1` means the
 53    * axis is not flipped.
 54    * @type {integer[]}
 55    */
 56   this.axesOrientation = _.map(this.visibleDimensions, function() {
 57     // by default values are not flipped i.e. all elements are equal to 1
 58     return 1;
 59   });
 60 
 61   /**
 62    * Axes color.
 63    * @type {integer}
 64    * @default '#FFFFFF' (white)
 65    */
 66   this.axesColor = '#FFFFFF';
 67   /**
 68    * Background color.
 69    * @type {integer}
 70    * @default '#000000' (black)
 71    */
 72   this.backgroundColor = '#000000';
 73   /**
 74    * Static tubes objects covering an entire trajectory.
 75    * Can use setDrawRange on the underlying geometry to display
 76    * just part of the trajectory.
 77    * @type {THREE.Mesh[]}
 78    */
 79   this.staticTubes = [];
 80   /**
 81    * Dynamic tubes covering the final tube segment of a trajectory
 82    * Must be rebuilt each frame by the animations controller
 83    * @type {THREE.Mesh[]}
 84    */
 85   this.dynamicTubes = [];
 86   /**
 87    * Array of THREE.Mesh objects on screen (represent samples).
 88    * @type {THREE.Mesh[]}
 89    */
 90   this.markers = [];
 91 
 92   /**
 93    * Meshes to be swapped out of scene when markers are modified.
 94    * @type {THREE.Mesh[]}
 95    */
 96   this.oldMarkers = [];
 97 
 98   /**
 99    * Flag indicating old markers must be removed from the scene tree.
100    * @type {boolean}
101    */
102   this.needsSwapMarkers = false;
103 
104   /**
105    * Array of THREE.Mesh objects on screen (represent confidence intervals).
106    * @type {THREE.Mesh[]}
107    */
108   this.ellipsoids = [];
109   /**
110    * Object with THREE.LineSegments for the procrustes edges. Has a left and
111    * a right attribute.
112    * @type {Object}
113    */
114   this.lines = {'left': null, 'right': null};
115 
116   /**
117    * The shared state for the UI
118    * @type {UIState}
119    */
120   this.UIState = uiState;
121 
122   //Register property changes
123   //Note that declaring var scope at the local scope is absolutely critical
124   //or callbacks will call into the wrong scope!
125   var scope = this;
126   this.UIState.registerProperty('view.viewType', function(evt) {
127     scope._initGeometry();
128   });
129 }
130 
131 DecompositionView.prototype._initGeometry = function() {
132   this.oldMarkers = this.markers;
133   if (this.oldMarkers.length > 0)
134     this.needsSwapMarkers = true;
135   this.markers = [];
136 
137   //TODO FIXME HACK:  Do we need to swap lines as well?
138   this.lines = {'left': null, 'right': null};
139 
140   if (this.decomp.isScatterType() &&
141       (this.UIState['view.viewType'] === 'parallel-plot')) {
142     this._fastInitParallelPlot();
143   }
144   else if (this.UIState['view.usesPointCloud']) {
145     this._fastInit();
146   }
147   else {
148     this._initBaseView();
149   }
150   this.needsUpdate = true;
151 };
152 
153 /**
154  * Calculate the appropriate size for a geometry based on the first dimension's
155  * range.
156  */
157 DecompositionView.prototype.getGeometryFactor = function() {
158   // this is a heuristic tested on numerous plots since 2013, based off of
159   // the old implementation of emperor. We select the dimensions of all the
160   // geometries based on this factor.
161   return (this.decomp.dimensionRanges.max[0] -
162           this.decomp.dimensionRanges.min[0]) * 0.012;
163 };
164 
165 /**
166  * Retrieve a shallow copy of concatenated static and dynamic tube arrays
167  * @type {THREE.Mesh[]}
168  */
169 DecompositionView.prototype.getTubes = function() {
170   return this.staticTubes.concat(this.dynamicTubes);
171 };
172 
173 /**
174  *
175  * Helper method to initialize the base THREE.js objects.
176  * @private
177  *
178  */
179 DecompositionView.prototype._initBaseView = function() {
180   var mesh, x = this.visibleDimensions[0], y = this.visibleDimensions[1],
181       z = this.visibleDimensions[2];
182   var scope = this;
183 
184   // get the correctly sized geometry
185   var radius = this.getGeometryFactor(), hasConfidenceIntervals;
186   var geometry = shapes.getGeometry('Sphere', radius);
187 
188   hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();
189 
190   if (this.decomp.isScatterType()) {
191     this.decomp.apply(function(plottable) {
192       mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial());
193       mesh.name = plottable.name;
194 
195       mesh.material.color = new THREE.Color(0xff0000);
196       mesh.material.transparent = false;
197       mesh.material.depthWrite = true;
198       mesh.material.opacity = 1;
199       mesh.matrixAutoUpdate = true;
200 
201       mesh.position.set(plottable.coordinates[x], plottable.coordinates[y],
202                         plottable.coordinates[z] || 0);
203 
204       mesh.userData.shape = 'Sphere';
205 
206       scope.markers.push(mesh);
207 
208       if (hasConfidenceIntervals) {
209         // copy the current sphere and make it an ellipsoid
210         mesh = mesh.clone();
211 
212         mesh.name = plottable.name + '_ci';
213         mesh.material.transparent = true;
214         mesh.material.opacity = 0.5;
215 
216         mesh.scale.set(plottable.ci[x] / geometry.parameters.radius,
217                        plottable.ci[y] / geometry.parameters.radius,
218                        plottable.ci[z] / geometry.parameters.radius);
219 
220         scope.ellipsoids.push(mesh);
221       }
222     });
223   }
224   else if (this.decomp.isArrowType()) {
225     var arrow, zero = [0, 0, 0], point;
226 
227     this.decomp.apply(function(plottable) {
228       point = [plottable.coordinates[x],
229                plottable.coordinates[y],
230                plottable.coordinates[z] || 0];
231       arrow = makeArrow(zero, point, 0xc0c0c0, plottable.name);
232 
233       scope.markers.push(arrow);
234     });
235   }
236   else {
237     throw new Error('Unsupported decomposition type');
238   }
239 
240   if (this.decomp.edges.length) {
241     var left, center, right, u, v, verticesLeft = [], verticesRight = [];
242     this.decomp.edges.forEach(function(edge) {
243       u = edge[0];
244       v = edge[1];
245 
246       // remember x, y and z
247       center = [(u.coordinates[x] + v.coordinates[x]) / 2,
248                 (u.coordinates[y] + v.coordinates[y]) / 2,
249                 ((u.coordinates[z] + v.coordinates[z]) / 2) || 0];
250 
251       left = [u.coordinates[x], u.coordinates[y], u.coordinates[z] || 0];
252       right = [v.coordinates[x], v.coordinates[y], v.coordinates[z] || 0];
253 
254       verticesLeft.push(left, center);
255       verticesRight.push(right, center);
256     });
257 
258     this.lines.left = makeLineCollection(verticesLeft, 0xffffff);
259     this.lines.right = makeLineCollection(verticesRight, 0xff0000);
260   }
261 };
262 
263 DecompositionView.prototype._fastInit = function() {
264   if (this.decomp.hasConfidenceIntervals()) {
265     throw new Error('Ellipsoids are not supported in fast mode');
266   }
267   if (this.decomp.isArrowType()) {
268     throw new Error('Only scatter type is supported in fast mode');
269   }
270 
271   var positions, colors, scales, opacities, visibilities, emissives, geometry,
272       cloud;
273 
274   var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
275       z = this.visibleDimensions[2];
276 
277   /**
278    * In order to draw large numbers of samples we can't use full-blown
279    * geometries like spheres. Instead we will use shaders to draw each sample
280    * as a circle. Note that since these are programs that need to be compiled
281    * for the GPU, they need to be stored as strings.
282    *
283    * The "vertexShader" determines the location and size of each vertex in the
284    * geometry. And the "fragmentShader" determines the shape, opacity,
285    * visibility and color. In addition there's some logic to smooth the circles
286    * and add antialiasing.
287    *
288    * The source for the shaders was inspired and or modified from:
289    *
290    * https://www.desultoryquest.com/blog/drawing-anti-aliased-circular-points-using-opengl-slash-webgl/
291    * http://jsfiddle.net/callum/x7y72k1e/10/
292    * http://math.hws.edu/eck/cs424/s12/lab4/lab4-files/points.html
293    * https://stackoverflow.com/q/33695202/379593
294    *
295    */
296   var vertexShader = [
297     'attribute float scale;',
298 
299     'attribute vec3 color;',
300     'attribute float opacity;',
301     'attribute float visible;',
302     'attribute float emissive;',
303 
304     'varying vec3 vColor;',
305     'varying float vOpacity;',
306     'varying float vVisible;',
307     'varying float vEmissive;',
308 
309     'void main() {',
310       'vColor = color;',
311       'vOpacity = opacity;',
312       'vVisible = visible;',
313       'vEmissive = emissive;',
314 
315       'vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);',
316       'gl_Position = projectionMatrix * mvPosition; ',
317       'gl_PointSize = kSIZE * scale * (800.0 / length(mvPosition.xyz));',
318     '}'].join('\n');
319 
320   var fragmentShader = [
321     'precision mediump float;',
322     'varying vec3 vColor;',
323     'varying float vOpacity;',
324     'varying float vVisible;',
325     'varying float vEmissive;',
326 
327     'void main() {',
328       // remove objects when they might be "visible" but completely transparent
329       'if (vVisible > 0.0 && vOpacity > 0.0) {',
330         'vec2 cxy = 2.0 * gl_PointCoord - 1.0;',
331         'float delta = 0.0, alpha = 1.0, r = dot(cxy, cxy);',
332 
333         // get rid of the frame around the points
334         'if(r > 1.1) discard;',
335 
336         // antialiasing smoothing
337         'delta = fwidth(r);',
338         'alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);',
339 
340         // if the object is selected make it white
341         'if (vEmissive > 0.0) {',
342         '  gl_FragColor = vec4(1, 1, 1, vOpacity) * alpha;',
343         '}',
344         'else {',
345         '  gl_FragColor = vec4(vColor, vOpacity) * alpha;',
346         '}',
347       '}',
348       'else {',
349         'discard;',
350       '}',
351     '}'].join('\n');
352 
353   positions = new Float32Array(this.decomp.length * 3);
354   colors = new Float32Array(this.decomp.length * 3);
355   scales = new Float32Array(this.decomp.length);
356   opacities = new Float32Array(this.decomp.length);
357   visibilities = new Float32Array(this.decomp.length);
358   emissives = new Float32Array(this.decomp.length);
359 
360   var material = new THREE.ShaderMaterial({
361     vertexShader: vertexShader,
362     fragmentShader: fragmentShader,
363     transparent: true
364   });
365 
366   // we need to define a baseline size for markers so we can control the scale
367   material.defines.kSIZE = this.getGeometryFactor();
368 
369   // needed for the shader's smoothstep and fwidth functions
370   material.extensions.derivatives = true;
371 
372   geometry = new THREE.BufferGeometry();
373   geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
374   geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
375   geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
376   geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
377   geometry.setAttribute('visible', new THREE.BufferAttribute(visibilities, 1));
378   geometry.setAttribute('emissive', new THREE.BufferAttribute(emissives, 1));
379 
380   cloud = new THREE.Points(geometry, material);
381 
382   this.decomp.apply(function(plottable) {
383     geometry.attributes.position.setXYZ(plottable.idx,
384                                         plottable.coordinates[x],
385                                         plottable.coordinates[y],
386                                         plottable.coordinates[z] || 0);
387 
388     // set default to red, visible, full opacity and of scale 1
389     geometry.attributes.color.setXYZ(plottable.idx, 1, 0, 0);
390     geometry.attributes.visible.setX(plottable.idx, 1);
391     geometry.attributes.opacity.setX(plottable.idx, 1);
392     geometry.attributes.emissive.setX(plottable.idx, 0);
393     geometry.attributes.scale.setX(plottable.idx, 1);
394   });
395 
396   geometry.attributes.position.needsUpdate = true;
397   geometry.attributes.color.needsUpdate = true;
398   geometry.attributes.visible.needsUpdate = true;
399   geometry.attributes.opacity.needsUpdate = true;
400   geometry.attributes.scale.needsUpdate = true;
401   geometry.attributes.emissive.needsUpdate = true;
402 
403   this.markers.push(cloud);
404 };
405 
406 /**
407  * Parallel plots closely mirroring the shader enabled _fastInit calls
408  */
409 DecompositionView.prototype._fastInitParallelPlot = function()
410 {
411   var positions, colors, opacities, visibilities, geometry, cloud;
412 
413   // We're really just drawing a bunch of line strips...
414   // highly doubt shaders are necessary for this...
415   var vertexShader = [
416     'attribute vec3 color;',
417     'attribute float opacity;',
418     'attribute float visible;',
419     'attribute float emissive;',
420 
421     'varying vec3 vColor;',
422     'varying float vOpacity;',
423     'varying float vVisible;',
424     'varying float vEmissive;',
425 
426     'void main() {',
427     '  vColor = color;',
428     '  vOpacity = opacity;',
429     '  vVisible = visible;',
430     '  vEmissive = emissive;',
431 
432     '  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
433     '}'].join('\n');
434 
435   var fragmentShader = [
436     'precision mediump float;',
437     'varying vec3 vColor;',
438     'varying float vOpacity;',
439     'varying float vVisible;',
440     'varying float vEmissive;',
441 
442     'void main() {',
443     ' if (vVisible <= 0.0 || vOpacity <= 0.0)',
444     '   discard;',
445 
446     // if the object is selected make it white
447     ' if (vEmissive > 0.0) {',
448     '   gl_FragColor = vec4(1, 1, 1, vOpacity);',
449     ' }',
450     ' else {',
451     '   gl_FragColor = vec4(vColor, vOpacity);',
452     ' }',
453     '}'].join('\n');
454 
455   var allDimensions = _.range(this.decomp.dimensions);
456 
457   // We'll build the line strips as GL_LINES for simplicity, at least for now,
458   // by doubling up vertex positions at each of the intermediate axes.
459   var numPoints = (allDimensions.length * 2 - 2) * (this.decomp.length);
460   positions = new Float32Array(numPoints * 3);
461   colors = new Float32Array(numPoints * 3);
462   opacities = new Float32Array(numPoints);
463   visibilities = new Float32Array(numPoints);
464   emissives = new Float32Array(numPoints);
465 
466   var material = new THREE.ShaderMaterial({
467     vertexShader: vertexShader,
468     fragmentShader: fragmentShader,
469     transparent: true
470   });
471 
472   geometry = new THREE.BufferGeometry();
473   geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
474   geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
475   geometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
476   geometry.setAttribute('visible', new THREE.BufferAttribute(visibilities, 1));
477   geometry.setAttribute('emissive', new THREE.BufferAttribute(emissives, 1));
478 
479   lines = new THREE.LineSegments(geometry, material);
480 
481   var attributeIndex = 0;
482 
483   for (var i = 0; i < this.decomp.length; i++)
484   {
485     var plottable = this.decomp.plottable[i];
486     // Each point in the model maps to (allDimensions.length * 2 - 2)
487     // positions due to the use of lines rather than line strips.
488     for (var j = 0; j < allDimensions.length; j++)
489     {
490       //normalize by global range bounds
491       var globalMin = this.allModels.dimensionRanges.min[allDimensions[j]];
492       var globalMax = this.allModels.dimensionRanges.max[allDimensions[j]];
493       var maxMinusMin = globalMax - globalMin;
494       var interpVal = (plottable.coordinates[j] - globalMin) / (maxMinusMin);
495       geometry.attributes.position.setXYZ(attributeIndex,
496                                         j,
497                                         interpVal,
498                                         0);
499 
500       geometry.attributes.color.setXYZ(attributeIndex, 1, 0, 0);
501       geometry.attributes.visible.setX(attributeIndex, 1);
502       geometry.attributes.opacity.setX(attributeIndex, 1);
503       attributeIndex++;
504 
505       //Because we are drawing all line strips at once using GL_LINES
506       //(which seemed easier than multiple line strip calls)
507       //it is necessary to duplicate the end points of each line.  But the
508       //duplicate points are only necessary for points in the middle of the
509       //line strip: the first point and last point of the strip are added once
510       //all of the points in the middle of the line strip must be duplicated.
511       if (j == 0 || j == allDimensions.length - 1)
512         continue;
513 
514       geometry.attributes.position.setXYZ(attributeIndex,
515                                         j,
516                                         interpVal,
517                                         0);
518       geometry.attributes.color.setXYZ(attributeIndex, 1, 0, 0);
519       geometry.attributes.visible.setX(attributeIndex, 1);
520       geometry.attributes.opacity.setX(attributeIndex, 1);
521       attributeIndex++;
522     }
523   }
524 
525   geometry.attributes.position.needsUpdate = true;
526   geometry.attributes.color.needsUpdate = true;
527   geometry.attributes.visible.needsUpdate = true;
528   geometry.attributes.opacity.needsUpdate = true;
529 
530   this.markers.push(lines);
531 };
532 
533 DecompositionView.prototype.getModelPointIndex = function(raytraceIndex,
534                                                           viewType)
535 {
536   var allDimensions = _.range(this.decomp.dimensions);
537   var numPointsPerScatterPoint = (allDimensions.length * 2 - 2);
538 
539   if (viewType === 'scatter') {
540     //Each point in the model maps to a single point in the mesh in scatter
541     return raytraceIndex;
542   }
543   else if (viewType === 'parallel-plot') {
544     return Math.floor(raytraceIndex / numPointsPerScatterPoint);
545   }
546 };
547 /**
548  *
549  * Get the number of visible elements
550  *
551  * @return {Number} The number of visible elements in this view.
552  *
553  */
554 DecompositionView.prototype.getVisibleCount = function() {
555   var visible = 0, attrVisible, numPoints = 0, scope = this;
556 
557   visible = _.reduce(this.markers, function(acc, marker) {
558     var perMarkerCount = 0;
559 
560     // shader objects need to be counted different from meshes
561     if (marker.isLineSegments || marker.isPoints) {
562       attrVisible = marker.geometry.attributes.visible;
563 
564       // for line segments we need to go in jumps of dimensions*2
565       if (marker.isLineSegments) {
566         numPoints = (scope.decomp.dimensions * 2 - 2);
567       }
568       else {
569         numPoints = 1;
570       }
571 
572       for (var i = 0; i < attrVisible.count; i += numPoints) {
573         perMarkerCount += (attrVisible.getX(i) + 0);
574       }
575     }
576     else {
577       // +0 cast bool to int
578       perMarkerCount += (marker.visible + 0);
579     }
580 
581     return acc + perMarkerCount;
582   }, 0);
583 
584   return visible;
585 };
586 
587 /**
588  *
589  * Update the position of the markers, arrows and lines.
590  *
591  * This method is called by flipVisibleDimension and by changeVisibleDimensions
592  * and will naively change the positions even if they haven't changed.
593  *
594  */
595 DecompositionView.prototype.updatePositions = function() {
596   var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
597       z = this.visibleDimensions[2], scope = this, hasConfidenceIntervals,
598       radius = 0, is2D = (z === null || z === undefined);
599 
600   hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();
601 
602   // we need the original radius to scale confidence intervals (if they exist)
603   if (hasConfidenceIntervals) {
604     radius = this.getGeometryFactor();
605   }
606 
607   if (this.UIState['view.usesPointCloud'] &&
608       (this.UIState['view.viewType'] === 'scatter')) {
609     var cloud = this.markers[0];
610 
611     this.decomp.apply(function(plottable) {
612       cloud.geometry.attributes.position.setXYZ(
613         plottable.idx,
614         plottable.coordinates[x] * scope.axesOrientation[0],
615         plottable.coordinates[y] * scope.axesOrientation[1],
616         is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);
617     });
618     cloud.geometry.attributes.position.needsUpdate = true;
619   }
620   else if (this.decomp.isScatterType() &&
621            (this.UIState['view.viewType'] === 'parallel-plot')) {
622     //TODO:  Do we need to do anything when axes are changed in parallel plots?
623   }
624   else if (this.decomp.isScatterType()) {
625     this.decomp.apply(function(plottable) {
626       mesh = scope.markers[plottable.idx];
627 
628       // always use the original data plus the axis orientation
629       mesh.position.set(
630         plottable.coordinates[x] * scope.axesOrientation[0],
631         plottable.coordinates[y] * scope.axesOrientation[1],
632         is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);
633       mesh.updateMatrix();
634 
635       if (hasConfidenceIntervals) {
636         mesh = scope.ellipsoids[plottable.idx];
637 
638         mesh.position.set(
639           plottable.coordinates[x] * scope.axesOrientation[0],
640           plottable.coordinates[y] * scope.axesOrientation[1],
641           is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);
642 
643         // flatten the ellipsoids ever so slightly
644         mesh.scale.set(plottable.ci[x] / radius, plottable.ci[y] / radius,
645                        is2D ? 0.01 : plottable.ci[z] / radius);
646 
647         mesh.updateMatrix();
648       }
649     });
650   }
651   else if (this.decomp.isArrowType()) {
652     var target, arrow;
653 
654     this.decomp.apply(function(plottable) {
655       arrow = scope.markers[plottable.idx];
656 
657       target = new THREE.Vector3(
658         plottable.coordinates[x] * scope.axesOrientation[0],
659         plottable.coordinates[y] * scope.axesOrientation[1],
660         is2D ? 0 : plottable.coordinates[z] * scope.axesOrientation[2]);
661 
662       arrow.setPointsTo(target);
663     });
664   }
665 
666   // edges are made using THREE.LineSegments and a buffer geometry so updating
667   // the position takes a bit more work but these objects will render faster
668   if (this.decomp.edges.length) {
669     this._redrawEdges();
670   }
671   this.needsUpdate = true;
672 };
673 
674 
675 /**
676  *
677  * Internal method to draw edges for plottables
678  *
679  * @param {Plottable[]} plottables An array of plottables for which the edges
680  * should be redrawn. If this object is not supplied, all the edges are drawn.
681  */
682 DecompositionView.prototype._redrawEdges = function(plottables) {
683   var u, v, j = 0, left = [], right = [];
684   var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
685       z = this.visibleDimensions[2], scope = this,
686       is2D = (z === null), drawAll = (plottables === undefined);
687 
688   this.decomp.edges.forEach(function(edge) {
689     u = edge[0];
690     v = edge[1];
691 
692     if (drawAll ||
693         (plottables.indexOf(u) !== -1 || plottables.indexOf(v) !== -1)) {
694 
695       center = [(u.coordinates[x] + v.coordinates[x]) / 2,
696                 (u.coordinates[y] + v.coordinates[y]) / 2,
697                 is2D ? 0 : (u.coordinates[z] + v.coordinates[z]) / 2];
698 
699       left = [u.coordinates[x], u.coordinates[y],
700               is2D ? 0 : u.coordinates[z]];
701       right = [v.coordinates[x], v.coordinates[y],
702                is2D ? 0 : v.coordinates[z]];
703 
704       scope.lines.left.setLineAtIndex(j, left, center);
705       scope.lines.right.setLineAtIndex(j, right, center);
706     }
707 
708     j++;
709   });
710 
711   // otherwise the geometry will remain unchanged
712   this.lines.left.geometry.attributes.position.needsUpdate = true;
713   this.lines.right.geometry.attributes.position.needsUpdate = true;
714 
715   this.needsUpdate = true;
716 };
717 
718 /**
719  *
720  * Change the visible coordinates
721  *
722  * @param {integer[]} newDims An Array of integers in which each integer is the
723  * index to the principal coordinate to show
724  *
725  */
726 DecompositionView.prototype.changeVisibleDimensions = function(newDims) {
727   if (newDims.length < 2 || newDims.length > 3) {
728     throw new Error('Only three dimensions can be shown at the same time');
729   }
730 
731   // one by one, find and update the dimensions that are changing
732   for (var i = 0; i < newDims.length; i++) {
733     if (this.visibleDimensions[i] !== newDims[i]) {
734       // index represents the global position of the dimension
735       var index = this.visibleDimensions[i],
736           orientation = this.axesOrientation[i];
737 
738       // 1.- Correct the limits of the ranges for the dimension that we are
739       // moving out of the scene i.e. the old dimension
740       if (this.axesOrientation[i] === -1) {
741         var max = this.decomp.dimensionRanges.max[index];
742         var min = this.decomp.dimensionRanges.min[index];
743         this.decomp.dimensionRanges.max[index] = min * (-1);
744         this.decomp.dimensionRanges.min[index] = max * (-1);
745       }
746 
747       // 2.- Set the orientation of the new dimension to be 1
748       this.axesOrientation[i] = 1;
749 
750       // 3.- Update the visible dimensions to include the new value
751       this.visibleDimensions[i] = newDims[i];
752     }
753   }
754 
755   this.updatePositions();
756 };
757 
758 /**
759  *
760  * Reorient one of the visible dimensions.
761  *
762  * @param {integer} index The index of the dimension to re-orient, if this
763  * dimension is not visible i.e. not in `this.visibleDimensions`, then the
764  * method will return right away.
765  *
766  */
767 DecompositionView.prototype.flipVisibleDimension = function(index) {
768   var scope = this, newMin, newMax;
769 
770   // the index in the visible dimensions
771   var localIndex = this.visibleDimensions.indexOf(index);
772 
773   if (localIndex !== -1) {
774     // update the ranges for this decomposition
775     var max = this.decomp.dimensionRanges.max[index];
776     var min = this.decomp.dimensionRanges.min[index];
777     this.decomp.dimensionRanges.max[index] = min * (-1);
778     this.decomp.dimensionRanges.min[index] = max * (-1);
779 
780     // and update the state of the orientation
781     this.axesOrientation[localIndex] *= -1;
782 
783     this.updatePositions();
784   }
785 };
786 
787 /**
788  * Change the plottables attributes based on the metadata category using the
789  * provided setPlottableAttributes function
790  *
791  * @param {object} attributes Key:value pairs of elements and values to change
792  * in plottables.
793  * @param {function} setPlottableAttributes Helper function to change the
794  * values of plottables, in general this should be implemented in the
795  * controller but it can be nullable if not needed. setPlottableAttributes
796  * should receive: the scope where the plottables exist, the value to be
797  * applied to the plottables and the plotables to change. For more info
798  * see ColorViewController.setPlottableAttribute
799  * @see ColorViewController.setPlottableAttribute
800  * @param {string} category The category/column in the mapping file
801  *
802  * @return {object[]} Array of objects to be consumed by Slick grid.
803  *
804  */
805 DecompositionView.prototype.setCategory = function(attributes,
806                                                    setPlottableAttributes,
807                                                    category) {
808   var scope = this, dataView = [], plottables;
809 
810   var fieldValues = util.naturalSort(_.keys(attributes));
811 
812   _.each(fieldValues, function(fieldVal, index) {
813     /*
814      *
815      * WARNING: This is mixing attributes of the view with the model ...
816      * it's a bit of a gray area though.
817      *
818      **/
819     plottables = scope.decomp.getPlottablesByMetadataCategoryValue(category,
820                                                                    fieldVal);
821     if (setPlottableAttributes !== null) {
822       setPlottableAttributes(scope, attributes[fieldVal], plottables);
823     }
824 
825     dataView.push({id: index, category: fieldVal, value: attributes[fieldVal],
826                    plottables: plottables});
827   });
828   this.needsUpdate = true;
829 
830   return dataView;
831 };
832 
833 /**
834  *
835  * Hide edges where plottables are present.
836  *
837  * @param {Plottable[]} plottables An array of plottables for which the edges
838  * should be hidden. If this object is not supplied, all the edges are hidden.
839  */
840 DecompositionView.prototype.hideEdgesForPlottables = function(plottables) {
841   // no edges to hide
842   if (this.decomp.edges.length === 0) {
843     return;
844   }
845 
846   var u, v, j = 0, hideAll, scope = this;
847 
848   hideAll = plottables === undefined;
849 
850   this.decomp.edges.forEach(function(edge) {
851     u = edge[0];
852     v = edge[1];
853 
854     if (hideAll ||
855         (plottables.indexOf(u) !== -1 || plottables.indexOf(v) !== -1)) {
856 
857       scope.lines.left.setLineAtIndex(j, [0, 0, 0], [0, 0, 0]);
858       scope.lines.right.setLineAtIndex(j, [0, 0, 0], [0, 0, 0]);
859     }
860     j++;
861   });
862 
863   // otherwise the geometry will remain unchanged
864   this.lines.left.geometry.attributes.position.needsUpdate = true;
865   this.lines.right.geometry.attributes.position.needsUpdate = true;
866 };
867 
868 /**
869  *
870  * Hide edges where plottables are present.
871  *
872  * @param {Plottable[]} plottables An array of plottables for which the edges
873  * should be hidden. If this object is not supplied, all the edges are hidden.
874  */
875 DecompositionView.prototype.showEdgesForPlottables = function(plottables) {
876   // no edges to show
877   if (this.decomp.edges.length === 0) {
878     return;
879   }
880 
881   this._redrawEdges(plottables);
882 };
883 
884 /**
885  * Set the color for a group of plottables.
886  *
887  * @param {Object} color An object that can be interpreted as a color by the
888  * THREE.Color class. Can be either a string like '#ff0000' or a number like
889  * 0xff0000, or a CSS color name like 'red', etc.
890  * @param {Plottable[]} group An array of plottables for which the color should
891  * be set. If this object is not provided, all the plottables in the view will
892  * have the color set.
893  */
894 DecompositionView.prototype.setColor = function(color, group) {
895   var idx, hasConfidenceIntervals, scope = this;
896 
897   group = group || this.decomp.plottable;
898   hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();
899 
900   if (this.UIState['view.usesPointCloud'] &&
901       (this.UIState['view.viewType'] === 'scatter')) {
902     var cloud = this.markers[0];
903     color = new THREE.Color(color);
904 
905     group.forEach(function(plottable) {
906       cloud.geometry.attributes.color.setXYZ(plottable.idx,
907                                              color.r, color.g, color.b);
908     });
909     cloud.geometry.attributes.color.needsUpdate = true;
910   }
911   else if (this.UIState['view.viewType'] == 'parallel-plot' &&
912            this.decomp.isScatterType()) {
913     var lines = this.markers[0];
914     color = new THREE.Color(color);
915     var numPoints = (this.decomp.dimensions * 2 - 2);
916     group.forEach(function(plottable) {
917       var startIndex = plottable.idx * numPoints;
918       var endIndex = (plottable.idx + 1) * numPoints;
919       for (var i = startIndex; i < endIndex; i++)
920         lines.geometry.attributes.color.setXYZ(i, color.r, color.g, color.b);
921     });
922     lines.geometry.attributes.color.needsUpdate = true;
923   }
924   else if (this.decomp.isScatterType()) {
925     group.forEach(function(plottable) {
926       idx = plottable.idx;
927       scope.markers[idx].material.color = new THREE.Color(color);
928 
929       if (hasConfidenceIntervals) {
930         scope.ellipsoids[idx].material.color = new THREE.Color(color);
931       }
932     });
933   }
934   else if (this.decomp.isArrowType()) {
935     group.forEach(function(plottable) {
936       scope.markers[plottable.idx].setColor(new THREE.Color(color));
937     });
938   }
939   this.needsUpdate = true;
940 };
941 
942 /**
943  * Set the visibility for a group of plottables.
944  *
945  * @param {Bool} visible Whether or not the objects should be visible.
946  * @param {Plottable[]} group An array of plottables for which the visibility
947  * should be set. If this object is not provided, all the plottables in the
948  * view will be have the visibility set.
949  */
950 DecompositionView.prototype.setVisibility = function(visible, group) {
951   var hasConfidenceIntervals, scope = this;
952 
953   group = group || this.decomp.plottable;
954 
955   hasConfidenceIntervals = this.decomp.hasConfidenceIntervals();
956 
957   if (this.UIState['view.usesPointCloud'] &&
958       (this.UIState['view.viewType'] === 'scatter')) {
959     var cloud = this.markers[0];
960 
961     _.each(group, function(plottable) {
962       cloud.geometry.attributes.visible.setX(plottable.idx, visible * 1);
963     });
964     cloud.geometry.attributes.visible.needsUpdate = true;
965   }
966   else if (this.UIState['view.viewType'] == 'parallel-plot' &&
967            this.decomp.isScatterType()) {
968     var lines = this.markers[0];
969     var numPoints = (this.decomp.dimensions * 2 - 2);
970     _.each(group, function(plottable) {
971       var startIndex = plottable.idx * numPoints;
972       var endIndex = (plottable.idx + 1) * (numPoints);
973       for (i = startIndex; i < endIndex; i++)
974         lines.geometry.attributes.visible.setX(i, visible * 1);
975     });
976     lines.geometry.attributes.visible.needsUpdate = true;
977   }
978   else {
979     _.each(group, function(plottable) {
980       scope.markers[plottable.idx].visible = visible;
981 
982       if (hasConfidenceIntervals) {
983         scope.ellipsoids[plottable.idx].visible = visible;
984       }
985     });
986   }
987 
988   if (visible === true) {
989     this.showEdgesForPlottables(group);
990   }
991   else {
992     this.hideEdgesForPlottables(group);
993   }
994 
995   this.needsUpdate = true;
996 };
997 
998 /**
999  * Set the scale for a group of plottables.
1000  *
1001  * @param {Float} scale The scale to set for the objects, relative to the
1002  * original size. Should be a positive and non-zero value.
1003  * @param {Plottable[]} group An array of plottables for which the scale
1004  * should be set. If this object is not provided, all the plottables in the
1005  * view will be have the scale set.
1006  */
1007 DecompositionView.prototype.setScale = function(scale, group) {
1008   var scope = this;
1009 
1010   if (this.decomp.isArrowType()) {
1011     throw Error('Cannot change the scale of an arrow.');
1012   }
1013 
1014   group = group || this.decomp.plottable;
1015 
1016   if (this.UIState['view.usesPointCloud'] &&
1017       (this.UIState['view.viewType'] === 'scatter')) {
1018     var cloud = this.markers[0];
1019 
1020     _.each(group, function(plottable) {
1021       cloud.geometry.attributes.scale.setX(plottable.idx, scale);
1022     });
1023     cloud.geometry.attributes.scale.needsUpdate = true;
1024   }
1025   else if (this.UIState['view.viewType'] == 'parallel-plot' &&
1026            this.decomp.isScatterType()) {
1027     //Nothing to do for parallel plots.
1028   }
1029   else {
1030     _.each(group, function(element) {
1031       scope.markers[element.idx].scale.set(scale, scale, scale);
1032     });
1033   }
1034   this.needsUpdate = true;
1035 };
1036 
1037 /**
1038  * Set the opacity for a group of plottables.
1039  *
1040  * @param {Float} opacity The opacity value (from 0 to 1) for the selected
1041  * objects.
1042  * @param {Plottable[]} group An array of plottables for which the opacity
1043  * should be set. If this object is not provided, all the plottables in the
1044  * view will be have the opacity set.
1045  */
1046 DecompositionView.prototype.setOpacity = function(opacity, group) {
1047   // webgl acts up with transparent objects, so we only set them to be
1048   // explicitly transparent if the opacity is not at full
1049   var transparent = opacity !== 1, funk, scope = this;
1050 
1051   group = group || this.decomp.plottable;
1052 
1053   if (this.UIState['view.usesPointCloud'] &&
1054       (this.UIState['view.viewType'] === 'scatter')) {
1055     var cloud = this.markers[0];
1056 
1057     _.each(group, function(plottable) {
1058       cloud.geometry.attributes.opacity.setX(plottable.idx, opacity);
1059     });
1060     cloud.geometry.attributes.opacity.needsUpdate = true;
1061   }
1062   else if (this.UIState['view.viewType'] == 'parallel-plot' &&
1063            this.decomp.isScatterType()) {
1064     var lines = this.markers[0];
1065     var numPoints = (this.decomp.dimensions * 2 - 2);
1066     _.each(group, function(plottable) {
1067       var startIndex = plottable.idx * numPoints;
1068       var endIndex = (plottable.idx + 1) * (numPoints);
1069       for (var i = startIndex; i < endIndex; i++)
1070         lines.geometry.attributes.opacity.setX(i, opacity);
1071     });
1072     lines.geometry.attributes.opacity.needsUpdate = true;
1073   }
1074   else {
1075     if (this.decomp.isScatterType()) {
1076       funk = _changeMeshOpacity;
1077     }
1078     else if (this.decomp.isArrowType()) {
1079       funk = _changeArrowOpacity;
1080     }
1081 
1082     _.each(group, function(plottable) {
1083       funk(scope.markers[plottable.idx], opacity, transparent);
1084     });
1085   }
1086   this.needsUpdate = true;
1087 };
1088 
1089 /**
1090  * Toggles the visibility of arrow labels
1091  *
1092  * @throws {Error} if this method is called on a scatter type.
1093  */
1094 DecompositionView.prototype.toggleLabelVisibility = function() {
1095   if (this.decomp.isScatterType()) {
1096     throw new Error('Cannot hide labels of scatter types');
1097   }
1098   var scope = this;
1099 
1100   this.decomp.apply(function(plottable) {
1101     arrow = scope.markers[plottable.idx];
1102     arrow.label.visible = Boolean(arrow.label.visible ^ true);
1103   });
1104   this.needsUpdate = true;
1105 };
1106 
1107 
1108 /**
1109  * Set the emissive attribute of the markers
1110  *
1111  * @param {Bool} emissive Whether the object should be emissive.
1112  * @param {Plottable[]} group An array of plottables for which the emissive
1113  * attribute will be set. If this object is not provided, all the plottables in
1114  * the view will be have the scale set.
1115  */
1116 DecompositionView.prototype.setEmissive = function(emissive, group) {
1117   group = group || this.decomp.plottable;
1118 
1119   if (this.decomp.isArrowType()) {
1120     throw new Error('Cannot set emissive attribute of arrows');
1121   }
1122 
1123   var i = 0, j = 0;
1124 
1125   if (this.UIState.getProperty('view.usesPointCloud') ||
1126       this.UIState.getProperty('view.viewType') === 'parallel-plot') {
1127     var emissives = this.markers[0].geometry.attributes.emissive;
1128 
1129     // the emissive attribute is a boolean one
1130     emissive = (emissive > 0) * 1;
1131 
1132     if (this.markers[0].isPoints) {
1133       for (i = 0; i < group.length; i++) {
1134         emissives.setX(group[i].idx, emissive);
1135       }
1136     }
1137     else if (this.markers[0].isLineSegments) {
1138       // line segments need to be repeated one per dimension
1139       for (i = 0; i < group.length; i++) {
1140         var numPoints = (this.decomp.dimensions * 2 - 2);
1141         var startIndex = group[i].idx * numPoints;
1142         var endIndex = (group[i].idx + 1) * (numPoints);
1143 
1144         for (j = startIndex; j < endIndex; j++) {
1145           emissives.setX(j, emissive);
1146         }
1147       }
1148     }
1149     emissives.needsUpdate = true;
1150   }
1151   else {
1152     for (i = 0; i < group.length; i++) {
1153       var material = this.markers[group[i].idx].material;
1154       material.emissive.set(emissive);
1155     }
1156   }
1157 
1158   this.needsUpdate = true;
1159 };
1160 
1161 /**
1162  * Group by color
1163  *
1164  * @param {Array} names An array of strings with the sample names.
1165  * @return {Object} Mapping of colors to objects.
1166  */
1167 DecompositionView.prototype.groupByColor = function(names) {
1168 
1169   var colorGroups = {}, groupping, markers = this.markers;
1170   var plottables = this.decomp.getPlottableByIDs(names);
1171 
1172   // we need to retrieve colors in a very different way
1173   if (this.UIState['view.viewType'] === 'parallel-plot' ||
1174       this.UIState['view.usesPointCloud']) {
1175     var colors = this.markers[0].geometry.attributes.color;
1176     var numPoints = 1;
1177 
1178     if (this.markers[0].isLineSegments) {
1179         numPoints = (this.decomp.dimensions * 2 - 2);
1180     }
1181 
1182     groupping = function(plottable) {
1183       // taken from Color.getHexString in THREE.js
1184       r = (colors.getX(plottable.idx * numPoints) * 255) << 16;
1185       g = (colors.getY(plottable.idx * numPoints) * 255) << 8;
1186       b = (colors.getZ(plottable.idx * numPoints) * 255) << 0;
1187       return ('000000' + (r ^ g ^ b).toString(16)).slice(-6);
1188     };
1189   }
1190   else {
1191     if (this.decomp.isScatterType()) {
1192       groupping = function(plottable) {
1193         return markers[plottable.idx].material.color.getHexString();
1194       };
1195     }
1196     else {
1197       // check that this getColor method works
1198       groupping = function(plottable) {
1199         return markers[plottable.idx].getColor().getHexString();
1200       };
1201     }
1202   }
1203 
1204   return _.groupBy(plottables, groupping);
1205 };
1206 
1207 /**
1208  *
1209  * Helper that builds a vega specification off of the current view state
1210  *
1211  * @private
1212  */
1213 DecompositionView.prototype._buildVegaSpec = function() {
1214   function rgbColor(colorObj) {
1215     var r = colorObj.r * 255;
1216     var g = colorObj.g * 255;
1217     var b = colorObj.b * 255;
1218     return 'rgb(' + r + ',' + g + ',' + b + ')';
1219   }
1220 
1221   // Maps THREE.js geometries to vega shapes
1222   var getShape = {
1223     Sphere: 'circle',
1224     Diamond: 'diamond',
1225     Cone: 'triangle-down',
1226     Cylinder: 'square',
1227     Ring: 'circle',
1228     Square: 'square',
1229     Icosahedron: 'cross',
1230     Star: 'cross'
1231   };
1232 
1233   function viewMarkersAsVegaDataset(markers) {
1234     var points = [], marker, i;
1235     for (i = 0; i < markers.length; i++) {
1236       marker = markers[i];
1237       if (marker.visible) {
1238         points.push({
1239           id: marker.name,
1240           x: marker.position.x,
1241           y: marker.position.y,
1242           color: rgbColor(marker.material.color),
1243           originalShape: marker.userData.shape,
1244           shape: getShape[marker.userData.shape],
1245           scale: { x: marker.scale.x, y: marker.scale.y },
1246           opacity: marker.material.opacity
1247         });
1248       }
1249     }
1250     return points;
1251   };
1252 
1253   // This is probably horribly slow on QIITA-scale MD files, probably needs
1254   // some attention
1255   function plottablesAsMetadata(points, header) {
1256     var md = [], point, row, i, j;
1257     for (i = 0; i < points.length; i++) {
1258       point = points[i];
1259       row = {};
1260       for (j = 0; j < header.length; j++) {
1261         row[header[j]] = point.metadata[j];
1262       }
1263       md.push(row);
1264     }
1265     return md;
1266   }
1267 
1268   var scope = this;
1269   var model = scope.decomp;
1270 
1271   var axisX = scope.visibleDimensions[0];
1272   var axisY = scope.visibleDimensions[1];
1273 
1274   var dimRanges = model.dimensionRanges;
1275   var rangeX = [dimRanges.min[axisX], dimRanges.max[axisX]];
1276   var rangeY = [dimRanges.min[axisY], dimRanges.max[axisY]];
1277 
1278   var baseWidth = 800;
1279 
1280   return {
1281     '$schema': 'https://vega.github.io/schema/vega/v5.json',
1282     padding: 5,
1283     background: scope.backgroundColor,
1284     config: {
1285       axis: { labelColor: scope.axesColor, titleColor: scope.axesColor },
1286       title: { color: scope.axesColor }
1287     },
1288     title: 'Emperor PCoA',
1289     data: [
1290       {
1291         name: 'metadata',
1292         values: plottablesAsMetadata(model.plottable, model.md_headers)
1293       },
1294       {
1295         name: 'points', values: viewMarkersAsVegaDataset(scope.markers),
1296         transform: [
1297           {
1298             type: 'lookup',
1299             from: 'metadata',
1300             key: model.md_headers[0],
1301             fields: ['id'],
1302             as: ['metadata']
1303           }
1304         ]
1305       }
1306     ],
1307     signals: [
1308       {
1309         name: 'width',
1310         update: baseWidth + ' * ((' + rangeX[1] + ') - (' + rangeX[0] + '))'
1311       },
1312       {
1313         name: 'height',
1314         update: baseWidth + ' * ((' + rangeY[1] + ') - (' + rangeY[0] + '))'
1315       }
1316     ],
1317     scales: [
1318       { name: 'xScale', range: 'width', domain: [rangeX[0], rangeX[1]] },
1319       { name: 'yScale', range: 'height', domain: [rangeY[0], rangeY[1]] }
1320     ],
1321     axes: [
1322       { orient: 'bottom', scale: 'xScale', title: model.axesLabels[axisX] },
1323       { orient: 'left', scale: 'yScale', title: model.axesLabels[axisY] }
1324     ],
1325     marks: [
1326       {
1327         type: 'symbol',
1328         from: {data: 'points'},
1329         encode: {
1330           enter: {
1331             fill: { field: 'color' },
1332             x: { scale: 'xScale', field: 'x' },
1333             y: { scale: 'yScale', field: 'y' },
1334             shape: { field: 'shape' },
1335             size: { signal: 'datum.scale.x * datum.scale.y * 100' },
1336             opacity: { field: 'opacity' }
1337           },
1338           update: {
1339             tooltip: { signal: 'datum.metadata' }
1340           }
1341         }
1342       }
1343     ]
1344   };
1345 };
1346 
1347 /**
1348  * Called as part of the swap operation to change out objects in the scene,
1349  * this function atomically clears the swap flag, clears the old markers,
1350  * and returns what the old markers were.
1351  */
1352 DecompositionView.prototype.getAndClearOldMarkers = function() {
1353   this.needsSwapMarkers = false;
1354   var oldMarkers = this.oldMarkers;
1355   this.oldMarkers = [];
1356   return oldMarkers;
1357 };
1358 
1359 /**
1360  * Helper function to change the opacity of an arrow object.
1361  *
1362  * @private
1363  */
1364 function _changeArrowOpacity(arrow, value, transparent) {
1365   arrow.line.material.transparent = transparent;
1366   arrow.line.material.opacity = value;
1367 
1368   arrow.cone.material.transparent = transparent;
1369   arrow.cone.material.opacity = value;
1370 }
1371 
1372 /**
1373  * Helper function to change the opacity of a mesh object.
1374  *
1375  * @private
1376  */
1377 function _changeMeshOpacity(mesh, value, transparent) {
1378   mesh.material.transparent = transparent;
1379   mesh.material.opacity = value;
1380 }
1381 
1382   return DecompositionView;
1383 });
1384