1 define([
  2     'three',
  3     'orbitcontrols',
  4     'draw',
  5     'underscore',
  6     'selectionbox',
  7     'selectionhelper'
  8 ], function(THREE, OrbitControls, draw, _, SelectionBox, SelectionHelper) {
  9   /** @private */
 10   var makeLine = draw.makeLine;
 11   /** @private */
 12   var makeLabel = draw.makeLabel;
 13 
 14   /**
 15    *
 16    * @class ScenePlotView3D
 17    *
 18    * Represents a three dimensional scene in THREE.js.
 19    *
 20    * @param {UIState} uiState shared UIState state object
 21    * @param {THREE.renderer} renderer THREE renderer object.
 22    * @param {Object} decViews dictionary of DecompositionViews shown in this
 23    * scene
 24    * @param {MultiModel} decModels MultiModel of DecompositionModels shown in
 25    * this scene (with extra global data about them)
 26    * @param {Node} container Div where the scene will be rendered.
 27    * @param {Float} xView Horizontal position of the rendered scene in the
 28    * container element.
 29    * @param {Float} yView Vertical position of the rendered scene in the
 30    * container element.
 31    * @param {Float} width The width of the renderer
 32    * @param {Float} height The height of the renderer
 33    *
 34    * @return {ScenePlotView3D} An instance of ScenePlotView3D.
 35    * @constructs ScenePlotView3D
 36    */
 37   function ScenePlotView3D(uiState, renderer, decViews, decModels, container,
 38                            xView, yView, width, height) {
 39     var scope = this;
 40 
 41     this.UIState = uiState;
 42 
 43     // convert to jquery object for consistency with the rest of the objects
 44     var $container = $(container);
 45     this.decViews = decViews;
 46     this.decModels = decModels;
 47     this.renderer = renderer;
 48     /**
 49      * Horizontal position of the scene.
 50      * @type {Float}
 51      */
 52     this.xView = xView;
 53     /**
 54      * Vertical position of the scene.
 55      * @type {Float}
 56      */
 57     this.yView = yView;
 58     /**
 59      * Width of the scene.
 60      * @type {Float}
 61      */
 62     this.width = width;
 63     /**
 64      * Height of the scene.
 65      * @type {Float}
 66      */
 67     this.height = height;
 68     /**
 69      * Axes color.
 70      * @type {String}
 71      * @default '#FFFFFF' (white)
 72      */
 73     this.axesColor = '#FFFFFF';
 74     /**
 75      * Background color.
 76      * @type {String}
 77      * @default '#000000' (black)
 78      */
 79     this.backgroundColor = '#000000';
 80     /**
 81      * True when changes have occured that require re-rendering of the canvas
 82      * @type {Boolean}
 83      */
 84     this.needsUpdate = true;
 85     /**
 86      * Array of integers indicating the index of the visible dimension at each
 87      * axis ([x, y, z]).
 88      * @type {Integer[]}
 89      */
 90     this.visibleDimensions = _.clone(this.decViews.scatter.visibleDimensions);
 91 
 92     // used to name the axis lines/labels in the scene
 93     this._axisPrefix = 'emperor-axis-line-';
 94     this._axisLabelPrefix = 'emperor-axis-label-';
 95 
 96     //need to initialize the scene
 97     this.scene = new THREE.Scene();
 98     this.scene.background = new THREE.Color(this.backgroundColor);
 99 
100     /**
101      * Camera used to display the scatter scene.
102      * @type {THREE.OrthographicCamera}
103      */
104     this.scatterCam = this.buildCamera('scatter');
105 
106     /**
107      * Object used to light the scene in scatter mode,
108      * by default is set to a light and
109      * transparent color (0x99999999).
110      * @type {THREE.DirectionalLight}
111      */
112     this.light = new THREE.DirectionalLight(0x999999, 2);
113     this.light.position.set(1, 1, 1).normalize();
114     this.scatterCam.add(this.light);
115 
116     /**
117      * Camera used to display the parallel plot scene.
118      * @type {THREE.OrthographicCamera}
119      */
120     this.parallelCam = this.buildCamera('parallel-plot');
121 
122     // use $container.get(0) to retrieve the native DOM object
123     this.scatterController = this.buildCamController('scatter',
124                                                     this.scatterCam,
125                                                     $container.get(0));
126     this.parallelController = this.buildCamController('parallel-plot',
127                                                      this.parallelCam,
128                                                      $container.get(0));
129     this.control = this.scatterController;
130 
131     this.scene.add(this.scatterCam);
132     this.scene.add(this.parallelCam);
133 
134     this._raycaster = new THREE.Raycaster();
135     this._mouse = new THREE.Vector2();
136 
137     /**
138      * Special purpose group for points that are selectable with the
139      * SelectionBox.
140      * @type {THREE.Group}
141      * @private
142      */
143     this._selectable = new THREE.Group();
144     this.scene.add(this._selectable);
145 
146     /**
147      * Object to compute bounding boxes from a selection area
148      *
149      * Selection is only enabled when the user is holding Shift.
150      *
151      * @type {THREE.SelectionBox}
152      * @private
153      */
154     this._selectionBox = new THREE.SelectionBox(this.camera,
155                                                 this._selectable);
156 
157     /**
158      * Helper to view the selection space when the user holds shift
159      *
160      * This object is disabled by default, and is only renabled when the user
161      * holds the shift key.
162      * @type {THREE.SelectionHelper}
163      * @private
164      */
165     this._selectionHelper = new THREE.SelectionHelper(this._selectionBox,
166                                                      renderer,
167                                                      'emperor-selection-area');
168     this._selectionHelper.enabled = false;
169 
170     //Swap the camera whenever the view type changes
171     this.UIState.registerProperty('view.viewType', function(evt) {
172       if (evt.newVal === 'parallel-plot') {
173         scope.camera = scope.parallelCam;
174         scope.control = scope.parallelController;
175         //Don't let the controller move around when its not the active camera
176         scope.scatterController.enabled = false;
177         scope.scatterController.autoRotate = false;
178         scope.parallelController.enabled = true;
179         scope._selectionBox.camera = scope.camera;
180         scope._selectionBox.collection = [];
181       } else {
182         scope.camera = scope.scatterCam;
183         scope.control = scope.scatterController;
184         //Don't let the controller move around when its not the active camera
185         scope.scatterController.enabled = true;
186         scope.parallelController.enabled = false;
187         scope._selectionBox.camera = scope.camera;
188         scope._selectionBox.collection = [];
189       }
190     });
191 
192     this.addDecompositionsToScene();
193 
194     this.updateCameraTarget();
195     this.control.update();
196 
197     this.scatterController.addEventListener('change', function() {
198       scope.needsUpdate = true;
199     });
200     this.parallelController.addEventListener('change', function() {
201       scope.needsUpdate = true;
202     });
203 
204     /**
205      * Object with "min" and "max" attributes each of which is an array with
206      * the ranges that covers all of the decomposition views.
207      * @type {Object}
208      */
209     this.drawAxesWithColor('#FFFFFF');
210     this.drawAxesLabelsWithColor('#FFFFFF');
211 
212     // initialize subscribers for event callbacks
213     /**
214      * Events allowed for callbacks. DO NOT EDIT.
215      * @type {String[]}
216      */
217     this.EVENTS = ['click', 'dblclick', 'select'];
218     /** @private */
219     this._subscribers = {};
220 
221     for (var i = 0; i < this.EVENTS.length; i++) {
222       this._subscribers[this.EVENTS[i]] = [];
223     }
224 
225     // Add callback call when sample is clicked
226     // Double and single click together from: http://stackoverflow.com/a/7845282
227     var DELAY = 200, clicks = 0, timer = null;
228     $container.on('mousedown', function(event) {
229         clicks++;
230         if (clicks === 1) {
231             timer = setTimeout(function() {
232                 scope._eventCallback('click', event);
233                 clicks = 0;
234             }, DELAY);
235         }
236         else {
237             clearTimeout(timer);
238             scope._eventCallback('dblclick', event);
239             clicks = 0;
240         }
241     })
242     .on('dblclick', function(event) {
243         event.preventDefault();  //cancel system double-click event
244     });
245 
246     // setup the selectionBox and selectionHelper objects and callbacks
247     this._addSelectionEvents($container);
248 
249     this.control.update();
250 
251     // register callback for populating info with clicked sample name
252     // set the timeout for fading out the info div
253     var infoDuration = 4000;
254     var infoTimeout = setTimeout(function() {
255         scope.$info.fadeOut();
256       }, infoDuration);
257 
258     /**
259      *
260      * The functions showText and copyToClipboard are used in the 'click',
261      * 'dblclick', and 'select' events.
262      *
263      * When a sample is clicked we show a legend at the bottom left of the
264      * view. If this legend is clicked, we copy the sample name to the
265      * clipboard. When a sample is double-clicked we directly copy the sample
266      * name to the clipboard and add the legend at the bottom left of the view.
267      *
268      * When samples are selected we show a message on the bottom left of the
269      * view, and copy a comma-separated list of samples to the clipboard.
270      *
271      */
272 
273     function showText(n, i) {
274       clearTimeout(infoTimeout);
275       scope.$info.text(n);
276       scope.$info.show();
277 
278       // reset the timeout for fading out the info div
279       infoTimeout = setTimeout(function() {
280         scope.$info.fadeOut();
281         scope.$info.text('');
282       }, infoDuration);
283     }
284 
285     function copyToClipboard(text) {
286       var $temp = $('<input>');
287 
288       // we need an input element to be able to copy to clipboard, taken from
289       // https://codepen.io/shaikmaqsood/pen/XmydxJ/
290       $('body').append($temp);
291       $temp.val(text).select();
292       document.execCommand('copy');
293       $temp.remove();
294     }
295 
296     //Add info div as bottom of canvas
297     this.$info = $('<div>').attr('title', 'Click to copy to clipboard');
298     this.$info.css({'position': 'absolute',
299                     'bottom': 0,
300                     'height': 16,
301                     'width': '50%',
302                     'padding-left': 10,
303                     'padding-right': 10,
304                     'font-size': 12,
305                     'z-index': 10000,
306                     'background-color': 'rgb(238, 238, 238)',
307                     'border': '1px solid black',
308                     'font-family': 'Verdana,Arial,sans-serif'}).hide();
309     this.$info.click(function() {
310       var text = scope.$info.text();
311 
312       // handle the case where multiple clicks are received
313       text = text.replace(/\(copied to clipboard\) /g, '');
314       copyToClipboard(text);
315 
316       scope.$info.effect('highlight', {}, 500);
317       scope.$info.text('(copied to clipboard) ' + text);
318     });
319     $(this.renderer.domElement).parent().append(this.$info);
320 
321     // UI callbacks specific to emperor, not to be confused with DOM events
322     this.on('click', showText);
323     this.on('dblclick', function(n, i) {
324       copyToClipboard(n);
325       showText('(copied to clipboard) ' + n, i);
326     });
327     this.on('select', function(names, view) {
328       if (names.length) {
329         showText(names.length + ' samples copied to your clipboard.');
330         copyToClipboard(names.join(','));
331       }
332     });
333 
334     // if a decomposition uses a point cloud, or
335     // if a decomposition uses a parallel plot,
336     // update the default raycasting tolerance as
337     // it is otherwise too large and error-prone
338     var updateRaycasterLinePrecision = function(evt) {
339       if (scope.UIState.getProperty('view.viewType') === 'parallel-plot')
340         scope._raycaster.params.Line.threshold = 0.01;
341       else
342         scope._raycaster.params.Line.threshold = 1;
343     };
344     var updateRaycasterPointPrecision = function(evt) {
345       if (scope.UIState.getProperty('view.usesPointCloud'))
346         scope._raycaster.params.Points.threshold = 0.01;
347       else
348         scope._raycaster.params.Points.threshold = 1;
349     };
350     this.UIState.registerProperty('view.usesPointCloud',
351                              updateRaycasterPointPrecision);
352     this.UIState.registerProperty('view.viewType',
353                              updateRaycasterLinePrecision);
354   };
355 
356   /**
357    * Builds a camera (for scatter or parallel plot)
358    */
359   ScenePlotView3D.prototype.buildCamera = function(viewType) {
360 
361     var camera;
362     if (viewType === 'scatter')
363     {
364       // Set up the camera
365       var max = _.max(this.decViews.scatter.decomp.dimensionRanges.max);
366       var frontFrust = _.min([max * 0.001, 1]);
367       var backFrust = _.max([max * 100, 100]);
368 
369       // these are placeholders that are
370       // later updated in updateCameraAspectRatio
371       camera = new THREE.OrthographicCamera(-50, 50, 50, -50);
372       camera.position.set(0, 0, max * 5);
373       camera.zoom = 0.7;
374     }
375     else if (viewType === 'parallel-plot')
376     {
377       var w = this.decModels.dimensionRanges.max.length;
378 
379       // Set up the camera
380       camera = new THREE.OrthographicCamera(0, w, 1, 0);
381       camera.position.set(0, 0, 1); //Must set positive Z because near > 0
382       camera.zoom = 0.7;
383     }
384 
385     return camera;
386   };
387 
388   /**
389    * Builds a camera controller (for scatter or parallel plot)
390    */
391   ScenePlotView3D.prototype.buildCamController = function(viewType, cam, view) {
392     /**
393      * Object used to interact with the scene. By default it uses the mouse.
394      * @type {THREE.OrbitControls}
395      */
396     var control = new THREE.OrbitControls(cam, view);
397     control.enableKeys = false;
398     control.rotateSpeed = 1.0;
399     control.zoomSpeed = 1.2;
400     control.panSpeed = 0.8;
401     control.enableZoom = true;
402     control.enablePan = true;
403 
404     // don't free panning and rotation for paralle plots
405     control.screenSpacePanning = (viewType === 'scatter');
406     control.enableRotate = (viewType === 'scatter');
407 
408     return control;
409   };
410 
411   /**
412    *
413    * Adds all the decomposition views to the current scene.
414    *
415    */
416   ScenePlotView3D.prototype.addDecompositionsToScene = function() {
417     var j, marker, scaling = this.getScalingConstant();
418 
419     // Note that the internal logic of the THREE.Scene object prevents the
420     // objects from being re-added so we can simply iterate over all the
421     // decomposition views.
422 
423     // Add all the meshes to the scene, iterate through all keys in
424     // decomposition view dictionary and put points in a separate group
425     for (var decViewName in this.decViews) {
426       var isArrowType = this.decViews[decViewName].decomp.isArrowType();
427 
428       for (j = 0; j < this.decViews[decViewName].markers.length; j++) {
429         marker = this.decViews[decViewName].markers[j];
430 
431         // only arrows include text as part of their markers
432         // arrows are not selectable
433         if (isArrowType) {
434           marker.label.scale.set(marker.label.scale.x * scaling,
435                                  marker.label.scale.y * scaling, 1);
436           this.scene.add(marker);
437         }
438         else {
439           this._selectable.add(marker);
440         }
441       }
442       for (j = 0; j < this.decViews[decViewName].ellipsoids.length; j++) {
443         this.scene.add(this.decViews[decViewName].ellipsoids[j]);
444       }
445 
446       // if the left lines exist so will the right lines
447       if (this.decViews[decViewName].lines.left) {
448         this.scene.add(this.decViews[decViewName].lines.left);
449         this.scene.add(this.decViews[decViewName].lines.right);
450       }
451     }
452 
453     this.needsUpdate = true;
454   };
455 
456   /**
457    * Calculate a scaling constant for the text in the scene.
458    *
459    * It is important that this factor is calculated based on all the elements
460    * in a scene, and that it is the same for all the text elements in the
461    * scene. Otherwise, some text will be bigger than other.
462    *
463    * @return {Number} The scaling factor to use for labels.
464    */
465   ScenePlotView3D.prototype.getScalingConstant = function() {
466     return (this.decModels.dimensionRanges.max[0] -
467             this.decModels.dimensionRanges.min[0]) * 0.001;
468   };
469 
470   /**
471    *
472    * Helper method used to iterate over the ranges of the visible dimensions.
473    *
474    * This function that centralizes the pattern followed by drawAxesWithColor
475    * and drawAxesLabelsWithColor.
476    *
477    * @param {Function} action a function that can take up to three arguments
478    * "start", "end" and "index".  And for each visible dimension the function
479    * will get the "start" and "end" of the range, and the current "index" of the
480    * visible dimension.
481    * @private
482    *
483    */
484   ScenePlotView3D.prototype._dimensionsIterator = function(action) {
485 
486     this.decModels._unionRanges();
487 
488     if (this.UIState['view.viewType'] === 'scatter')
489     {
490       // shortcut to the index of the visible dimension and the range object
491       var x = this.visibleDimensions[0], y = this.visibleDimensions[1],
492           z = this.visibleDimensions[2], range = this.decModels.dimensionRanges,
493           is2D = (z === null || z === undefined);
494 
495       // Adds a padding to all dimensions such that samples don't overlap
496       // with the axes lines. Determined based on the default sphere radius
497       var axesPadding = 1.07;
498 
499       /*
500        * We special case Z when it is a 2D plot, whenever that's the case we set
501        * the range to be zero so no lines are shown on screen.
502        */
503 
504       // this is the "origin" of our ordination
505       var start = [range.min[x] * axesPadding,
506                    range.min[y] * axesPadding,
507                    is2D ? 0 : range.min[z] * axesPadding];
508 
509       var ends = [
510         [range.max[x] * axesPadding,
511          range.min[y] * axesPadding,
512          is2D ? 0 : range.min[z] * axesPadding],
513         [range.min[x] * axesPadding,
514          range.max[y] * axesPadding,
515          is2D ? 0 : range.min[z] * axesPadding],
516         [range.min[x] * axesPadding,
517          range.min[y] * axesPadding,
518          is2D ? 0 : range.max[z] * axesPadding]
519       ];
520 
521       action(start, ends[0], x);
522       action(start, ends[1], y);
523 
524       // when transitioning to 2D disable rotation to avoid awkward angles
525       if (is2D) {
526         this.control.enableRotate = false;
527       }
528       else {
529         action(start, ends[2], z);
530         this.control.enableRotate = true;
531       }
532     }
533     else {
534       //Parallel Plots show all axes
535       for (var i = 0; i < this.decViews['scatter'].decomp.dimensions; i++)
536       {
537         action([i, 0, 0], [i, 1, 0], i);
538       }
539     }
540   };
541 
542   /**
543    *
544    * Draw the axes lines in the plot
545    *
546    * @param {String} color A CSS-compatible value that specifies the color
547    * of each of the axes lines, the length of these lines is determined by the
548    * global dimensionRanges property computed in decModels.
549    * If the color value is null the lines will be removed.
550    *
551    */
552   ScenePlotView3D.prototype.drawAxesWithColor = function(color) {
553     var scope = this, axisLine;
554 
555     // axes lines are removed if the color is null
556     this.removeAxes();
557     if (color === null) {
558       return;
559     }
560 
561     this._dimensionsIterator(function(start, end, index) {
562       axisLine = makeLine(start, end, color, 3, false);
563       axisLine.name = scope._axisPrefix + index;
564 
565       scope.scene.add(axisLine);
566     });
567   };
568 
569   /**
570    *
571    * Draw the axes labels for each visible dimension.
572    *
573    * The text in the labels is determined using the percentage explained by
574    * each dimension and the abbreviated name of a single decomposition object.
575    * Note that we arbitrarily use the first one, as all decomposition objects
576    * presented in the same scene should have the same percentages explained by
577    * each axis.
578    *
579    * @param {String} color A CSS-compatible value that specifies the color
580    * of the labels, these labels will be positioned at the end of the axes
581    * line. If the color value is null the labels will be removed.
582    *
583    */
584   ScenePlotView3D.prototype.drawAxesLabelsWithColor = function(color) {
585     var scope = this, axisLabel, decomp, firstKey, text, scaling;
586     scaling = this.getScalingConstant();
587 
588     // the labels are only removed if the color is null
589     this.removeAxesLabels();
590     if (color === null) {
591       return;
592     }
593 
594     // get the first decomposition object, it doesn't really matter which one
595     // we look at though, as all of them should have the same percentage
596     // explained on each axis
597     firstKey = _.keys(this.decViews)[0];
598     decomp = this.decViews[firstKey].decomp;
599 
600     this._dimensionsIterator(function(start, end, index) {
601       text = decomp.axesLabels[index];
602       axisLabel = makeLabel(end, text, color);
603 
604       if (scope.UIState['view.viewType'] === 'scatter') {
605         //Scatter has a 1 to 1 aspect ratio and labels in world size
606         axisLabel.scale.set(axisLabel.scale.x * scaling,
607                             axisLabel.scale.y * scaling,
608                             1);
609       }
610       else if (scope.UIState['view.viewType'] === 'parallel-plot') {
611         //Parallel plot aspect ratio depends on number of dimensions
612         //We have to correct label size to account for this.
613         //But we also have to fix label width so that it fits between
614         //axes, which are exactly 1 apart in world space
615         var cam = scope.camera;
616         var labelWPix = axisLabel.scale.x;
617         var labelHPix = axisLabel.scale.y;
618         var viewWPix = scope.width;
619         var viewHPix = scope.height;
620 
621         //Assuming a camera zoom of 1:
622         var viewWUnits = cam.right - cam.left;
623         var viewHUnits = cam.top - cam.bottom;
624 
625         //These are world sizes of label for a camera zoom of 1
626         var labelWUnits = labelWPix * viewWUnits / viewWPix;
627         var labelHUnits = labelHPix * viewHUnits / viewHPix;
628 
629         //TODO FIXME HACK:  Note that our options here are to scale each
630         //label to fit in its area, or to scale all labels by the same amount
631         //We choose to scale all labels by the same amount based on an
632         //empirical 'nice' label length of ~300
633         //We could replace this with a max of all label widths, but must note
634         //that label widths are always powers of 2 in the current version
635 
636         //Resize to fit labels of width 300 between axes
637         var scalingFudge = 0.9 / (300 * viewWUnits / viewWPix);
638 
639         axisLabel.scale.set(labelWUnits * scalingFudge,
640                             labelHUnits * scalingFudge,
641                             1);
642       }
643 
644       axisLabel.name = scope._axisLabelPrefix + index;
645       scope.scene.add(axisLabel);
646     });
647   };
648 
649   /**
650    *
651    * Helper method to remove objects with some prefix from the view's scene
652    *
653    * @param {String} prefix The prefix of object names to remove
654    *
655    */
656   ScenePlotView3D.prototype._removeObjectsWithPrefix = function(prefix) {
657     var scope = this;
658     var recursiveRemove = function(rootObj) {
659       if (rootObj.name != null && rootObj.name.startsWith(prefix)) {
660         scope.scene.remove(rootObj);
661       }
662       else {
663         // We can't iterate the children array while removing from it,
664         // So we make a shallow copy.
665         var childCopy = Array.from(rootObj.children);
666         for (var child in childCopy) {
667           recursiveRemove(childCopy[child]);
668         }
669       }
670     };
671     recursiveRemove(this.scene);
672   };
673 
674   /**
675    *
676    * Helper method to remove the axis lines from the scene
677    *
678    */
679   ScenePlotView3D.prototype.removeAxes = function() {
680     this._removeObjectsWithPrefix(this._axisPrefix);
681   };
682 
683   /**
684    *
685    * Helper method to remove the axis labels from the scene
686    *
687    */
688   ScenePlotView3D.prototype.removeAxesLabels = function() {
689     this._removeObjectsWithPrefix(this._axisLabelPrefix);
690   };
691 
692   /**
693    *
694    * Resizes and relocates the scene.
695    *
696    * @param {Float} xView New horizontal location.
697    * @param {Float} yView New vertical location.
698    * @param {Float} width New scene width.
699    * @param {Float} height New scene height.
700    *
701    */
702   ScenePlotView3D.prototype.resize = function(xView, yView, width, height) {
703     this.xView = xView;
704     this.yView = yView;
705     this.width = width;
706     this.height = height;
707 
708     this.updateCameraAspectRatio();
709     this.control.update();
710 
711     //Since parallel plot labels have to correct for aspect ratio, we need
712     //to redraw when width/height of view is modified.
713     this.drawAxesLabelsWithColor(this.axesColor);
714 
715     this.needsUpdate = true;
716   };
717 
718   /**
719    *
720    * Resets the aspect ratio of the camera according to the current size of the
721    * plot space.
722    *
723    */
724   ScenePlotView3D.prototype.updateCameraAspectRatio = function() {
725     if (this.UIState['view.viewType'] === 'scatter')
726     {
727       var x = this.visibleDimensions[0], y = this.visibleDimensions[1];
728 
729       // orthographic cameras operate in space units not in pixel units i.e.
730       // the width and height of the view is based on the objects not the window
731       var owidth = this.decModels.dimensionRanges.max[x] -
732                       this.decModels.dimensionRanges.min[x];
733       var oheight = this.decModels.dimensionRanges.max[y] -
734                       this.decModels.dimensionRanges.min[y];
735 
736       var aspect = this.width / this.height;
737 
738       // ensure that the camera's aspect ratio is equal to the window's
739       owidth = oheight * aspect;
740 
741       this.camera.left = -owidth / 2;
742       this.camera.right = owidth / 2;
743       this.camera.top = oheight / 2;
744       this.camera.bottom = -oheight / 2;
745 
746       this.camera.aspect = aspect;
747       this.camera.updateProjectionMatrix();
748     }
749     else if (this.UIState['view.viewType'] === 'parallel-plot')
750     {
751       var w = this.decModels.dimensionRanges.max.length;
752       this.camera.left = 0;
753       this.camera.right = w;
754       this.camera.top = 1;
755       this.camera.bottom = 0;
756       this.camera.updateProjectionMatrix();
757     }
758   };
759 
760   /**
761    * Updates the target and dimensions of the camera and control
762    *
763    * The target of the scene depends on the coordinate space of the data, by
764    * default it is set to zero, but we need to make sure that the target is
765    * reasonable for the data.
766    */
767   ScenePlotView3D.prototype.updateCameraTarget = function() {
768     if (this.UIState['view.viewType'] === 'scatter')
769     {
770       var x = this.visibleDimensions[0], y = this.visibleDimensions[1];
771 
772       var owidth = this.decModels.dimensionRanges.max[x] -
773                       this.decModels.dimensionRanges.min[x];
774       var oheight = this.decModels.dimensionRanges.max[y] -
775                       this.decModels.dimensionRanges.min[y];
776       var xcenter = this.decModels.dimensionRanges.max[x] - (owidth / 2);
777       var ycenter = this.decModels.dimensionRanges.max[y] - (oheight / 2);
778 
779       var max = _.max(this.decViews.scatter.decomp.dimensionRanges.max);
780 
781       this.control.target.set(xcenter, ycenter, 0);
782       this.camera.position.set(xcenter, ycenter, max * 5);
783       this.camera.updateProjectionMatrix();
784 
785       this.light.position.set(xcenter, ycenter, max * 5);
786 
787       this.updateCameraAspectRatio();
788 
789       this.control.saveState();
790 
791       this.needsUpdate = true;
792     }
793     else if (this.UIState['view.viewType'] === 'parallel-plot') {
794       this.control.target.set(0, 0, 1); //Must set positive Z because near > 0
795       this.camera.position.set(0, 0, 1); //Must set positive Z because near > 0
796       this.camera.updateProjectionMatrix();
797       this.updateCameraAspectRatio();
798       this.control.saveState();
799       this.needsUpdate = true;
800     }
801   };
802 
803   ScenePlotView3D.prototype.NEEDS_RENDER = 1;
804   ScenePlotView3D.prototype.NEEDS_CONTROLLER_REFRESH = 2;
805 
806   /**
807    *
808    * Convenience method to check if this or any of the decViews under this need
809    * rendering
810    *
811    */
812    ScenePlotView3D.prototype.checkUpdate = function() {
813     var updateDimensions = false, updateColors = false,
814         currentDimensions, backgroundColor, axesColor, scope = this;
815 
816     //Check if the view type changed and swap the markers in/out of the scene
817     //tree.
818     var anyMarkersSwapped = false, isArrowType;
819 
820     _.each(this.decViews, function(view) {
821       if (view.needsSwapMarkers) {
822         isArrowType = view.decomp.isArrowType();
823         anyMarkersSwapped = true;
824 
825         // arrows are in the scene whereas points/markers are in a different
826         // group used for brush selection
827         var group = isArrowType ? scope.scene : scope._selectable;
828         var oldMarkers = view.getAndClearOldMarkers(), marker;
829 
830         for (var i = 0; i < oldMarkers.length; i++) {
831           marker = oldMarkers[i];
832 
833           group.remove(marker);
834 
835           if (isArrowType) {
836             marker.dispose();
837           }
838           else {
839             marker.material.dispose();
840             marker.geometry.dispose();
841           }
842         }
843 
844         // do not show arrows in a parallel plot
845         var newMarkers = view.markers;
846         if (isArrowType && scope.UIState['view.viewType'] === 'scatter' ||
847             view.decomp.isScatterType()) {
848           var scaling = scope.getScalingConstant();
849 
850           for (i = 0; i < newMarkers.length; i++) {
851             marker = newMarkers[i];
852 
853             // when we re-add arrows we need to re-scale the labels
854             if (isArrowType) {
855               marker.label.scale.set(marker.label.scale.x * scaling,
856                                      marker.label.scale.y * scaling, 1);
857             }
858             group.add(marker);
859           }
860         }
861 
862         var lines = view.lines;
863         var ellipsoids = view.ellipsoids;
864 
865         if (scope.UIState['view.viewType'] == 'parallel-plot') {
866           for (i = 0; i < lines.length; i++)
867             scope.scene.remove(lines[i]);
868           for (i = 0; i < ellipsoids.length; i++)
869             scope.scene.remove(ellipsoids[i]);
870         }
871         if (scope.UIState['view.viewType'] == 'scatter') {
872           for (i = 0; i < lines.length; i++)
873             scope.scene.add(lines[i]);
874           for (i = 0; i < ellipsoids.length; i++)
875             scope.scene.add(ellipsoids[i]);
876         }
877     }});
878 
879     if (anyMarkersSwapped) {
880       this.updateCameraTarget();
881       this.control.update();
882     }
883 
884 
885     // check if any of the decomposition views have changed
886     var updateData = _.any(this.decViews, function(dv) {
887       // note that we may be overwriting these variables, but we have a
888       // guarantee that if one of them changes for one of decomposition views,
889       // all of them will have changed, so grabbing one should be sufficient to
890       // perform the comparisons below
891       currentDimensions = dv.visibleDimensions;
892       backgroundColor = dv.backgroundColor;
893       axesColor = dv.axesColor;
894 
895       return dv.needsUpdate;
896     });
897 
898     _.each(this.decViews, function(view) {
899       view.getTubes().forEach(function(tube) {
900         if (tube !== null)
901           scope.scene.add(tube);
902       });
903     });
904 
905     // check if the visible dimensions have changed
906     if (!_.isEqual(currentDimensions, this.visibleDimensions)) {
907       // remove the current axes
908       this.removeAxes();
909       this.removeAxesLabels();
910 
911       // get the new dimensions and re-display the data
912       this.visibleDimensions = _.clone(currentDimensions);
913       this.drawAxesWithColor(this.axesColor);
914       this.drawAxesLabelsWithColor(this.axesColor);
915 
916       this.updateCameraTarget();
917       this.control.update();
918 
919       updateDimensions = true;
920     }
921 
922     // check if we should change the axes color
923     if (axesColor !== this.axesColor) {
924       this.drawAxesWithColor(axesColor);
925       this.drawAxesLabelsWithColor(axesColor);
926 
927       this.axesColor = _.clone(axesColor);
928 
929       updateColors = true;
930     }
931 
932     // check if we should change the background color
933     if (backgroundColor !== this.backgroundColor) {
934       this.backgroundColor = _.clone(backgroundColor);
935       this.scene.background = new THREE.Color(this.backgroundColor);
936 
937       updateColors = true;
938     }
939 
940     if (updateData) {
941       this.drawAxesWithColor(this.axesColor);
942       this.drawAxesLabelsWithColor(this.axesColor);
943     }
944 
945     var retVal = 0;
946     if (anyMarkersSwapped)
947       retVal |= ScenePlotView3D.prototype.NEEDS_CONTROLLER_REFRESH;
948     if (anyMarkersSwapped || this.needsUpdate || updateData ||
949         updateDimensions || updateColors || this.control.autoRotate)
950       retVal |= ScenePlotView3D.prototype.NEEDS_RENDER;
951 
952     // if anything has changed, then trigger an update
953     return retVal;
954    };
955 
956   /**
957    *
958    * Convenience method to re-render the contents of the scene.
959    *
960    */
961   ScenePlotView3D.prototype.render = function() {
962     this.renderer.setViewport(this.xView, this.yView, this.width, this.height);
963     this.renderer.render(this.scene, this.camera);
964     var camera = this.camera;
965 
966     // if autorotation is enabled, then update the controls
967     if (this.control.autoRotate) {
968       this.control.update();
969     }
970 
971     // Only scatter plots that are not using a point cloud should be pointed
972     // towards the camera. For arrow types and point clouds doing this will
973     // results in odd visual effects
974     if (!this.UIState.getProperty('view.usesPointCloud') &&
975         this.decViews.scatter.decomp.isScatterType()) {
976       _.each(this.decViews.scatter.markers, function(element) {
977         element.quaternion.copy(camera.quaternion);
978       });
979     }
980 
981     this.needsUpdate = false;
982     $.each(this.decViews, function(key, val) {
983       val.needsUpdate = false;
984     });
985   };
986 
987   /**
988    * Helper method to highlight and return selected objects.
989    *
990    * This is mostly necessary because depending on the rendering type we will
991    * have a slightly different way to set and return the highlighting
992    * attributes. For large plots we return the points geometry together with
993    * a userData.selected attribute with the selected indices.
994    *
995    * Note that we created a group of selectable objects in the constructor so
996    * we don't have to check for geometry types, etc.
997    *
998    * @param {Array} collection An array of objects to highlight
999    * @param {Integer} color A hexadecimal-encoded color. For shaders we only
1000    * use the first bit to decide if the marker is rendered in white or rendered
1001    * with the original color.
1002    *
1003    * @return {Array} selected objects (after checking for visibility and
1004    * opacity).
1005    *
1006    * @private
1007    */
1008   ScenePlotView3D.prototype._highlightSelected = function(collection, color) {
1009     var i = 0, j = 0, selected = [];
1010 
1011     if (this.UIState.getProperty('view.usesPointCloud') ||
1012         this.UIState.getProperty('view.viewType') === 'parallel-plot') {
1013       for (i = 0; i < collection.length; i++) {
1014         // for shaders the emissive attribute is an int
1015         var indices, emissiveColor = (color > 0) * 1;
1016 
1017         // if there's no selection then update all the points
1018         if (collection[i].userData.selected === undefined) {
1019           indices = _.range(collection[i].geometry.attributes.emissive.count);
1020         }
1021         else {
1022           indices = collection[i].userData.selected;
1023         }
1024 
1025         for (j = 0; j < indices.length; j++) {
1026           if (collection[i].geometry.attributes.visible.getX(indices[j]) &&
1027               collection[i].geometry.attributes.opacity.getX(indices[j])) {
1028             collection[i].geometry.attributes.emissive.setX(indices[j],
1029                                                             emissiveColor);
1030           }
1031         }
1032 
1033         collection[i].geometry.attributes.emissive.needsUpdate = true;
1034         selected.push(collection[i]);
1035       }
1036     }
1037     else {
1038       for (i = 0; i < collection.length; i++) {
1039         var material = collection[i].material;
1040 
1041         if (material.visible && material.opacity && material.emissive) {
1042           collection[i].material.emissive.set(color);
1043           selected.push(collection[i]);
1044         }
1045       }
1046     }
1047 
1048     return selected;
1049   };
1050 
1051   /**
1052    *
1053    * Adds the mouse selection events to the current view
1054    *
1055    * @param {node} $container The container to add the events to.
1056    * @private
1057    */
1058   ScenePlotView3D.prototype._addSelectionEvents = function($container) {
1059     var scope = this;
1060 
1061     // There're three stages to the mouse selection:
1062     //  mousedown -> mousemove -> mouseup
1063     //
1064     // The mousdown event is ignored unless the user is holding Shift. Once
1065     // selection has started the rotation controls are disabled. The mousemove
1066     // event continues until the user releases the mouse. Once this happens
1067     // rotation is re-enabled and the selection box disappears. Selected
1068     // markers are highlighted by changing the light they emit.
1069     //
1070     $container.on('mousedown', function(event) {
1071       // ignore the selection event if shift is not being held or if parallel
1072       // plots are being visualized at the moment
1073       if (!event.shiftKey) {
1074         return;
1075       }
1076 
1077       scope.control.enabled = false;
1078       scope.scatterController.enabled = false;
1079       scope.parallelController.enabled = false;
1080       scope._selectionHelper.enabled = true;
1081       scope._selectionHelper.onSelectStart(event);
1082 
1083       // clear up any color setting
1084       scope._highlightSelected(scope._selectionBox.collection, 0x000000);
1085 
1086       var element = scope.renderer.domElement;
1087       var offset = $(element).offset(), i = 0;
1088 
1089       scope._selectionBox.startPoint.set(
1090         ((event.clientX - offset.left) / element.width) * 2 - 1,
1091         -((event.clientY - offset.top) / element.height) * 2 + 1,
1092         0.5);
1093     })
1094     .on('mousemove', function(event) {
1095       // ignore if the user is not holding the shift key or the orbit control
1096       // is enabled and he selection disabled
1097       if (!event.shiftKey ||
1098           (scope.control.enabled && !scope._selectionHelper.enabled)) {
1099         return;
1100       }
1101 
1102       var element = scope.renderer.domElement, selected;
1103       var offset = $(element).offset(), i = 0;
1104 
1105       scope._selectionBox.endPoint.set(
1106         ((event.clientX - offset.left) / element.width) * 2 - 1,
1107         - ((event.clientY - offset.top) / element.height) * 2 + 1,
1108         0.5);
1109 
1110       // reset everything before updating the selected color
1111       scope._highlightSelected(scope._selectionBox.collection, 0x000000);
1112       scope._highlightSelected(scope._selectionBox.select(), 0x8c8c8f);
1113 
1114       scope.needsUpdate = true;
1115     })
1116     .on('mouseup', function(event) {
1117       // if the user is not already selecting data then ignore
1118       if (!scope._selectionHelper.enabled || scope.control.enabled) {
1119         return;
1120       }
1121 
1122       // otherwise if shift is being held then keep selecting, otherwise ignore
1123       if (event.shiftKey) {
1124         var element = scope.renderer.domElement;
1125         var offset = $(element).offset(), indices = [], names = [];
1126         scope._selectionBox.endPoint.set(
1127           ((event.clientX - offset.left) / element.width) * 2 - 1,
1128           - ((event.clientY - offset.top) / element.height) * 2 + 1,
1129           0.5);
1130 
1131         selected = scope._highlightSelected(scope._selectionBox.select(),
1132                                             0x8c8c8f);
1133 
1134         // get the list of sample names from the views
1135         for (var i = 0; i < selected.length; i++) {
1136           if (selected[i].isPoints) {
1137             // this is a list of indices of the selected samples
1138             indices = selected[i].userData.selected;
1139 
1140             for (var j = 0; j < indices.length; j++) {
1141               names.push(scope.decViews.scatter.decomp.ids[indices[j]]);
1142             }
1143           }
1144           else if (selected[i].isLineSegments) {
1145             var index, viewType, view;
1146 
1147             view = scope.decViews.scatter;
1148             viewType = scope.UIState['view.viewType'];
1149 
1150             // this is a list of indices of the selected samples
1151             indices = selected[i].userData.selected;
1152 
1153             for (var k = 0; k < indices.length; k++) {
1154               index = view.getModelPointIndex(indices[k], viewType);
1155               names.push(view.decomp.ids[index]);
1156             }
1157 
1158             // every segment is labeled the same for each sample
1159             names = _.unique(names);
1160           }
1161           else {
1162             names.push(selected[i].name);
1163           }
1164         }
1165 
1166         scope._selectCallback(names, scope.decViews.scatter);
1167       }
1168 
1169       scope.control.enabled = true;
1170       scope.scatterController.enabled = true;
1171       scope.parallelController.enabled = true;
1172       scope._selectionHelper.enabled = false;
1173       scope.needsUpdate = true;
1174     });
1175   };
1176 
1177 
1178   /**
1179    * Handle selection events.
1180    * @private
1181    */
1182   ScenePlotView3D.prototype._selectCallback = function(names, view) {
1183     var eventType = 'select';
1184 
1185     for (var i = 0; i < this._subscribers[eventType].length; i++) {
1186       // keep going if one of the callbacks fails
1187       try {
1188         this._subscribers[eventType][i](names, view);
1189       } catch (e) {
1190         console.error(e);
1191       }
1192       this.needsUpdate = true;
1193     }
1194   };
1195 
1196   /**
1197    *
1198    * Helper method that runs functions subscribed to the container's callbacks.
1199    * @param {String} eventType Event type being called
1200    * @param {event} event The event from jQuery, with x and y click coords
1201    * @private
1202    *
1203    */
1204   ScenePlotView3D.prototype._eventCallback = function(eventType, event) {
1205     event.preventDefault();
1206     // don't do anything if no subscribers
1207     if (this._subscribers[eventType].length === 0) {
1208       return;
1209     }
1210 
1211     var element = this.renderer.domElement, scope = this;
1212     var offset = $(element).offset();
1213     this._mouse.x = ((event.clientX - offset.left) / element.width) * 2 - 1;
1214     this._mouse.y = -((event.clientY - offset.top) / element.height) * 2 + 1;
1215 
1216     this._raycaster.setFromCamera(this._mouse, this.camera);
1217 
1218     // get a flattened array of markers
1219     var objects = _.map(this.decViews, function(decomp) {
1220       return decomp.markers;
1221     });
1222     objects = _.reduce(objects, function(memo, value) {
1223       return memo.concat(value);
1224     }, []);
1225     var intersects = this._raycaster.intersectObjects(objects);
1226 
1227     // Get first intersected item and call callback with it.
1228     if (intersects && intersects.length > 0) {
1229       var firstObj = intersects[0].object, intersect;
1230       /*
1231        * When the intersect object is a Points object, the raycasting method
1232        * won't intersect individual mesh objects. Instead it intersects a point
1233        * and we get the index of the point. This index can then be used to
1234        * trace the original Plottable object.
1235        */
1236       if (firstObj.isPoints || firstObj.isLineSegments) {
1237         // don't search over invisible things
1238         intersects = _.filter(intersects, function(marker) {
1239            return firstObj.geometry.attributes.visible.getX(marker.index) &&
1240                   firstObj.geometry.attributes.opacity.getX(marker.index);
1241         });
1242 
1243         // if there's no hits then finish the execution
1244         if (intersects.length === 0) {
1245           return;
1246         }
1247 
1248         var meshIndex = intersects[0].index;
1249         var modelIndex = this.decViews.scatter.getModelPointIndex(meshIndex,
1250                                                 this.UIState['view.viewType']);
1251         intersect = this.decViews.scatter.decomp.plottable[modelIndex];
1252       }
1253       else {
1254         intersects = _.filter(intersects, function(marker) {
1255           return marker.object.visible && marker.object.material.opacity;
1256         });
1257 
1258         // if there's no hits then finish the execution
1259         if (intersects.length === 0) {
1260           return;
1261         }
1262 
1263         intersect = intersects[0].object;
1264       }
1265 
1266       for (var i = 0; i < this._subscribers[eventType].length; i++) {
1267         // keep going if one of the callbacks fails
1268         try {
1269           this._subscribers[eventType][i](intersect.name, intersect);
1270         } catch (e) {
1271           console.error(e);
1272         }
1273         this.needsUpdate = true;
1274       }
1275     }
1276   };
1277 
1278   /**
1279    *
1280    * Interface to subscribe to event types in the canvas, see the EVENTS
1281    * property.
1282    *
1283    * @param {String} eventType The type of event to subscribe to.
1284    * @param {Function} handler Function to call when `eventType` is triggered,
1285    * receives two parameters, a string with the name of the object, and the
1286    * object itself i.e. f(objectName, object).
1287    *
1288    * @throws {Error} If the given eventType is unknown.
1289    *
1290    */
1291   ScenePlotView3D.prototype.on = function(eventType, handler) {
1292     if (this.EVENTS.indexOf(eventType) === -1) {
1293       throw new Error('Unknown event ' + eventType + '. Known events are: ' +
1294                       this.EVENTS.join(', '));
1295     }
1296 
1297     this._subscribers[eventType].push(handler);
1298   };
1299 
1300   /**
1301    *
1302    * Interface to unsubscribe a function from an event type, see the EVENTS
1303    * property.
1304    *
1305    * @param {String} eventType The type of event to unsubscribe from.
1306    * @param {Function} handler Function to remove from the subscribers list.
1307    *
1308    * @throws {Error} If the given eventType is unknown.
1309    *
1310    */
1311   ScenePlotView3D.prototype.off = function(eventType, handler) {
1312     if (this.EVENTS.indexOf(eventType) === -1) {
1313       throw new Error('Unknown event ' + eventType + '. Known events are ' +
1314                       this.EVENTS.join(', '));
1315     }
1316 
1317     var pos = this._subscribers[eventType].indexOf(handler);
1318     if (pos !== -1) {
1319       this._subscribers[eventType].splice(pos, 1);
1320     }
1321   };
1322 
1323   /**
1324    *
1325    * Recenter the position of the camera to the initial default.
1326    *
1327    */
1328   ScenePlotView3D.prototype.recenterCamera = function() {
1329     this.control.reset();
1330     this.control.update();
1331 
1332     this.needsUpdate = true;
1333   };
1334 
1335   return ScenePlotView3D;
1336 });
1337