1 define([
  2     'jquery',
  3     'underscore',
  4     'contextmenu',
  5     'three',
  6     'view',
  7     'scene3d',
  8     'colorviewcontroller',
  9     'visibilitycontroller',
 10     'opacityviewcontroller',
 11     'shapecontroller',
 12     'axescontroller',
 13     'scaleviewcontroller',
 14     'animationscontroller',
 15     'filesaver',
 16     'viewcontroller',
 17     'svgrenderer',
 18     'draw',
 19     'canvasrenderer',
 20     'canvastoblob',
 21     'multi-model',
 22     'uistate'
 23 ], function($, _, contextMenu, THREE, DecompositionView, ScenePlotView3D,
 24             ColorViewController, VisibilityController, OpacityViewController,
 25             ShapeController, AxesController, ScaleViewController,
 26             AnimationsController, FileSaver, viewcontroller, SVGRenderer, Draw,
 27             CanvasRenderer, canvasToBlob, MultiModel, UIStateInit) {
 28 
 29   var EmperorAttributeABC = viewcontroller.EmperorAttributeABC;
 30 
 31   var TAB_ORDER = ['color', 'visibility', 'opacity', 'scale',
 32                    'shape', 'axes', 'animations'];
 33 
 34   var controllerConstructors = {
 35       'color': ColorViewController,
 36       'visibility': VisibilityController,
 37       'opacity': OpacityViewController,
 38       'scale': ScaleViewController,
 39       'shape': ShapeController,
 40       'axes': AxesController,
 41       'animations': AnimationsController
 42     };
 43 
 44   /**
 45    *
 46    * @class EmperorController
 47    *       This is the application controller
 48    *
 49    * The application controller, contains all the information on how the model
 50    * is being presented to the user.
 51    *
 52    * @param {DecompositionModel} scatter A decomposition object that represents
 53    * the scatter-represented objects.
 54    * @param {DecompositionModel} biplot An optional decomposition object that
 55    * represents the arrow-represented objects. Can be null or undefined.
 56    * @param {string} divId The element id where the controller should
 57    * instantiate itself.
 58    * @param {node} [webglcanvas = undefined] the canvas to use to render the
 59    * information. This parameter is optional, and should rarely be set. But is
 60    * useful for external applications like SAGE2.
 61    *
 62    * @return {EmperorController}
 63    * @constructs EmperorController
 64    *
 65    */
 66   function EmperorController(scatter, biplot, divId, webglcanvas, info) {
 67 
 68     /**
 69      * The state shared across one instance of the UI
 70      * @type {UIState}
 71      */
 72     this.UIState = new UIStateInit();
 73     this.UIState.setProperty('view.usesPointCloud', scatter.length > 20000);
 74 
 75     var scope = this;
 76     /**
 77      * Scaling constant for grid dimensions (read only).
 78      * @type {float}
 79      */
 80     this.GRID_SCALE = 0.97;
 81 
 82     /**
 83      * Scaling constant for scene plot view dimensions
 84      * @type {float}
 85      */
 86     this.SCENE_VIEW_SCALE = 0.5;
 87     /**
 88      * jQuery object where the object lives in.
 89      * @type {node}
 90      */
 91     this.$divId = $('#' + divId);
 92     /**
 93      * Width of the object.
 94      * @type {float}
 95      */
 96     this.width = this.$divId.width();
 97     /**
 98      * Height of the object.
 99      * @type {float}
100      */
101     this.height = this.$divId.height();
102 
103     /**
104      * Information to be displayed in the plot banner.
105      * @type {string}
106      */
107     this.info = info;
108 
109     var decModelMap = {'scatter': scatter};
110     if (biplot)
111       decModelMap['biplot'] = biplot;
112 
113     /**
114      * MultiModel object containing all DecompositionModels
115      *
116      * @type {MultiModel}
117      */
118     this.decModels = new MultiModel(decModelMap);
119 
120     /**
121      * Object with all the available decomposition views.
122      *
123      * @type {object}
124      */
125     this.decViews = {'scatter':
126                         new DecompositionView(this.decModels,
127                                               'scatter',
128                                               this.UIState)};
129 
130     if (biplot) {
131       this.decViews.biplot = new DecompositionView(this.decModels,
132                                                    'biplot',
133                                                    this.UIState);
134     }
135 
136     /**
137      * Keep track of whether or not the biplot labels should be hidden.
138      *
139      * @type {Bool}
140      * @private
141      */
142     this._hideBiplotLabels = false;
143 
144     /**
145      * List of the scene plots views being rendered.
146      * @type {ScenePlotView3D[]}
147      */
148     this.sceneViews = [];
149 
150     /**
151      * Internal div where the menus live in (jQuery object).
152      * @type {node}
153      */
154     this.$plotSpace = $("<div class='emperor-plot-wrapper'></div>");
155 
156     /**
157      * Div with the number of visible samples
158      * @type {node}
159      */
160     this.$plotBanner = $('<label>Loading ...</label>');
161     this.$plotBanner.css({'padding': '2px',
162                           'font-style': '9pt helvetica',
163                           'color': 'white',
164                           'border': '1px solid',
165                           'border-color': 'white',
166                           'position': 'absolute',
167                           '-webkit-user-select': 'none',
168                           '-moz-user-select': 'none',
169                           '-ms-user-select': 'none',
170                           'user-select': 'none'});
171 
172     // add the sample count to the plot space
173     this.$plotSpace.append(this.$plotBanner);
174 
175     /**
176      * Internal div where the plots live in (jQuery object).
177      * @type {node}
178      */
179     this.$plotMenu = $("<div class='emperor-plot-menu'></div>");
180     this.$plotMenu.attr('title', 'Right click on the plot for more options, ' +
181                         ' click on a sample to reveal its name, or ' +
182                         'double-click on a sample to copy its name to the ' +
183                         'clipboard');
184 
185     this.$divId.append(this.$plotSpace);
186     this.$divId.append(this.$plotMenu);
187 
188     /**
189      * @type {Function}
190      * Callback to execute when all the view controllers have been successfully
191      * loaded.
192      */
193     this.ready = null;
194 
195     /**
196      * Holds a reference to all the tabs (view controllers) in the `$plotMenu`.
197      * @type {object}
198      */
199     this.controllers = {};
200 
201     /**
202      * Object in charge of doing the rendering of the scenes.
203      * @type {THREE.Renderer}
204      */
205     this.renderer = null;
206     if (webglcanvas !== undefined) {
207         this.renderer = new THREE.WebGLRenderer({canvas: webglcanvas,
208                                                  antialias: true});
209     }
210     else {
211         this.renderer = new THREE.WebGLRenderer({antialias: true});
212     }
213 
214     this.renderer.setSize(this.width, this.height);
215     this.renderer.autoClear = false;
216     this.renderer.sortObjects = true;
217     this.$plotSpace.append(this.renderer.domElement);
218 
219 
220     /**
221      * The number of tabs that we expect to see. This attribute is updated by
222      * the addTab method, and is only releveant during the initialization
223      * process.
224      * @private
225      */
226     this._expected = 0;
227 
228     /**
229      * The number of tabs that have finished initalization. This attribute is
230      * only relevant during the initialization process.
231      * @private
232      */
233     this._seen = 0;
234 
235     /**
236      * Menu tabs containers, note that we need them in this format to have
237      * jQuery's UI tabs work properly. All the view controllers will be added
238      * to this container, see the addTab method
239      * @see EmperorController.addTab
240      * @type {node}
241      * @private
242      */
243     this._$tabsContainer = $("<div name='emperor-tabs-container'></div>");
244     this._$tabsContainer.css('background-color', '#EEEEEE');
245     this._$tabsContainer.addClass('unselectable');
246     /**
247      * List of available tabs, lives inside `_$tabsContainer`.
248      * @type {node}
249      * @private
250      */
251     this._$tabsList = $("<ul name='emperor-tabs-list'></ul>");
252 
253     // These will both live in the menu space. As of the writing of this code
254     // there's nothing else but tabs on the menu, but this may change in the
255     // future, that's why we are creating the extra "tabsContainer" div
256     this.$plotMenu.append(this._$tabsContainer);
257     this._$tabsContainer.append(this._$tabsList);
258 
259     /**
260      * @type {Node}
261      * jQuery object To show the context menu (as an alternative to
262      * right-clicking on the plot).
263      *
264      * The context menu that this button shows is created in the _buildUI
265      * method.
266      */
267     this.$optionsButton = $('<button name="options-button"> </button>');
268     this.$optionsButton.css({
269       'position': 'absolute',
270       'z-index': '3',
271       'top': '5px',
272       'right': '5px'
273     }).attr('title', 'More Options').on('click', function(event) {
274       // add offset to avoid overlapping the button with the menu
275       scope.$plotSpace.contextMenu({x: event.pageX, y: event.pageY + 5});
276     });
277     this.$plotSpace.append(this.$optionsButton);
278 
279     // default decomposition view uses the full window
280     this.addSceneView();
281 
282     $(function() {
283       // setup the jquery properties of the button
284       scope.$optionsButton.button({text: false,
285                                    icons: {primary: ' ui-icon-gear'}});
286 
287       scope._buildUI();
288       // Hide the loading splashscreen
289       scope.$divId.find('.loading').hide();
290 
291       // The next few lines setup the space/menu resizing logic. Specifically,
292       // we only enable the "west' handle, set double-click toggle behaviour
293       // and add a tooltip to the handle.
294       scope.$plotMenu.resizable({
295         handles: 'w',
296         helper: 'plot-space-resizable-helper',
297         stop: function(event, ui) {
298           var percent = (ui.size.width / scope.width) * 100;
299 
300           scope.$plotSpace.width((100 - percent) + '%');
301           scope.$plotMenu.css({'width': percent + '%', 'left': 0});
302 
303           // The scrollbars randomly appear on the window while showing the
304           // helper, with this small delay we give them enough time to
305           // disappear.
306           setTimeout(function() {
307             scope.resize(scope.width, scope.height);
308           }, 50);
309         }
310       });
311 
312       scope.$plotMenu.find('.ui-resizable-handle').dblclick(function() {
313         var percent = (scope.$plotSpace.width() / scope.width) * 100;
314 
315         // allow for a bit of leeway
316         if (percent >= 98) {
317           scope.$plotSpace.css({'width': '73%'});
318           scope.$plotMenu.css({'width': '27%', 'left': 0});
319         }
320         else {
321           scope.$plotSpace.css({'width': '99%'});
322           scope.$plotMenu.css({'width': '1%', 'left': 0});
323         }
324         scope.resize(scope.width, scope.height);
325       }).attr('title', 'Drag to resize or double click to toggle visibility');
326 
327     });
328 
329     // once the object finishes loading, resize the contents so everything fits
330     // nicely
331     $(this).ready(function() {
332       scope.resize(scope.$divId.width(), scope.$divId.height());
333     });
334 
335     this.UIState.registerProperty('view.viewType', function(evt) {
336       toDisable = ['scale', 'shape', 'animations'];
337 
338       for (controllerName in scope.controllers) {
339         var c = scope.controllers[controllerName];
340         selector = "li[aria-controls='" + c.identifier + "']";
341         //jquery effects are less jarring, but also remind me of people who add
342         //effects to slide transitions in powerpoint.  I'd still prefer css
343         //to gray out the tab...
344         //effects list at https://api.jqueryui.com/category/effects/
345 
346         if (toDisable.includes(controllerName)) {
347           if (evt.newVal === 'parallel-plot')
348             $(selector).hide('blind');
349           else if (evt.newVal === 'scatter')
350             $(selector).show('blind');
351         }
352       }
353     });
354   };
355 
356   /**
357    *
358    * Add a new decomposition view
359    *
360    * @param {String} key New name for the decomposition view.
361    * @param {DecompositionView} value The decomposition view that will be
362    * added.
363    *
364    * @throws Error if `key` already exists, or if `value` is not a
365    * decomposition view.
366    *
367    */
368   EmperorController.prototype.addDecompositionView = function(key, value) {
369     if (!(value instanceof DecompositionView)) {
370       console.error('The value is not a decomposition view');
371     }
372 
373     if (_.contains(_.keys(this.decViews), key)) {
374       throw Error('A decomposition view named "' + key + '" already exists,' +
375                   'cannot add an already existing decomposition.');
376     }
377 
378     this.decViews[key] = value;
379 
380     _.each(this.controllers, function(controller) {
381       if (controller instanceof EmperorAttributeABC) {
382         controller.refreshMetadata();
383       }
384     });
385 
386     _.each(this.sceneViews, function(sv) {
387       sv.addDecompositionsToScene();
388     });
389   };
390 
391   /**
392    *
393    * Helper method to add additional ScenePlotViews (i.e. another plot)
394    *
395    */
396   EmperorController.prototype.addSceneView = function() {
397     if (this.sceneViews.length > 4) {
398       throw Error('Cannot add another scene plot view');
399     }
400 
401     var spv = new ScenePlotView3D(this.UIState,
402                                   this.renderer,
403                                   this.decViews,
404                                   this.decModels,
405                                   this.$plotSpace, 0, 0,
406                                   this.width, this.height);
407     this.sceneViews.push(spv);
408 
409     // this will setup the appropriate sizes and widths
410     this.resize(this.width, this.height);
411   };
412 
413   /**
414    *
415    * Helper method to resize the plots
416    *
417    * @param {width} the width of the entire plotting space
418    * @param {height} the height of the entire plotting space
419    *
420    */
421   EmperorController.prototype.resize = function(width, height) {
422     // update the available space we have
423     this.width = width;
424     this.height = height;
425 
426     this.$plotSpace.height(height);
427     this.$plotMenu.height(height);
428 
429     this._$tabsContainer.height(height);
430 
431     // the area we have to present the plot is smaller than the total
432     var plotWidth = this.$plotSpace.width();
433 
434     // TODO: The below will need refactoring
435     // This is addressed in issue #405
436     if (this.sceneViews.length === 1) {
437       this.sceneViews[0].resize(0, 0, plotWidth, this.height);
438     }
439     else if (this.sceneViews.length === 2) {
440       this.sceneViews[0].resize(0, 0, this.SCENE_VIEW_SCALE * plotWidth,
441           this.height);
442       this.sceneViews[1].resize(this.SCENE_VIEW_SCALE * plotWidth, 0,
443           this.SCENE_VIEW_SCALE * plotWidth, this.height);
444     }
445     else if (this.sceneViews.length === 3) {
446       this.sceneViews[0].resize(0, 0,
447           this.SCENE_VIEW_SCALE * plotWidth,
448           this.SCENE_VIEW_SCALE * this.height);
449       this.sceneViews[1].resize(this.SCENE_VIEW_SCALE * plotWidth, 0,
450           this.SCENE_VIEW_SCALE * plotWidth,
451           this.SCENE_VIEW_SCALE * this.height);
452       this.sceneViews[2].resize(0, this.SCENE_VIEW_SCALE * this.height,
453           plotWidth, this.SCENE_VIEW_SCALE * this.height);
454     }
455     else if (this.sceneViews.length === 4) {
456       this.sceneViews[0].resize(0, 0, this.SCENE_VIEW_SCALE * plotWidth,
457           this.SCENE_VIEW_SCALE * this.height);
458       this.sceneViews[1].resize(this.SCENE_VIEW_SCALE * plotWidth, 0,
459           this.SCENE_VIEW_SCALE * plotWidth,
460           this.SCENE_VIEW_SCALE * this.height);
461       this.sceneViews[2].resize(0, this.SCENE_VIEW_SCALE * this.height,
462           this.SCENE_VIEW_SCALE * plotWidth,
463           this.SCENE_VIEW_SCALE * this.height);
464       this.sceneViews[3].resize(this.SCENE_VIEW_SCALE * plotWidth,
465           this.SCENE_VIEW_SCALE * this.height,
466           this.SCENE_VIEW_SCALE * plotWidth,
467           this.SCENE_VIEW_SCALE * this.height);
468     }
469     else {
470       throw Error('More than four views are currently not supported');
471     }
472 
473     this.renderer.setSize(plotWidth, this.height);
474 
475     /* Resizing the tabs (view controllers) */
476 
477     // resize the grid according to the size of the container, since we are
478     // inside the tabs we have to account for that lost space.
479     var tabHeight = this.$plotMenu.height() * this.GRID_SCALE;
480 
481     // the tab list at the top takes up a variable amount of space and
482     // without this, the table displayed below will have an odd scrolling
483     // behaviour
484     tabHeight -= this._$tabsList.height();
485 
486     // for each controller, we need to (1) trigger the resize method, and (2)
487     // resize the height of the containing DIV tag (we don't need to resize the
488     // width as this is already taken care of since it just has to fit the
489     // available space).
490     _.each(this.controllers, function(controller, index) {
491       if (controller !== undefined) {
492         $('#' + controller.identifier).height(tabHeight);
493 
494         var w = $('#' + controller.identifier).width(),
495             h = $('#' + controller.identifier).height();
496 
497         controller.resize(w, h);
498       }
499     });
500 
501     //Set all scenes to needing update
502     for (var i = 0; i < this.sceneViews.length; i++) {
503       this.sceneViews[i].needsUpdate = true;
504     }
505   };
506 
507   /**
508    *
509    * Helper method to render sceneViews, gets called every time the browser
510    * indicates we can render a new frame, however it only triggers the
511    * appropriate rendering functions if something has changed since the last
512    * frame.
513    *
514    */
515   EmperorController.prototype.render = function() {
516     var scope = this;
517 
518     if (this.controllers.animations !== undefined) {
519       this.controllers.animations.drawFrame();
520     }
521 
522     $.each(this.sceneViews, function(i, sv) {
523       requiredActions = sv.checkUpdate();
524       if (requiredActions &
525           ScenePlotView3D.prototype.NEEDS_CONTROLLER_REFRESH) {
526         //loop over controllers and update
527         for (controllerKey in scope.controllers) {
528           scope.controllers[controllerKey].forceRefresh();
529         }
530       }
531       if (requiredActions &
532           ScenePlotView3D.prototype.NEEDS_RENDER) {
533         scope.renderer.setViewport(0, 0, scope.width, scope.height);
534         scope.renderer.clear();
535         sv.render();
536 
537         // if there's a change for the scene view update the counts
538         scope.updatePlotBanner();
539       }
540     });
541 
542   };
543 
544   /**
545    *
546    * Updates the plot banner based on the number of visible elements and the
547    * scene's background color.
548    *
549    */
550   EmperorController.prototype.updatePlotBanner = function() {
551     var color = this.sceneViews[0].scene.background.clone(), visible = 0,
552         total = 0, message = '';
553 
554     // invert the color so it's visible regardless of the background
555     color.setRGB((Math.floor(color.r * 255) ^ 0xFF) / 255,
556                  (Math.floor(color.g * 255) ^ 0xFF) / 255,
557                  (Math.floor(color.b * 255) ^ 0xFF) / 255);
558     color = color.getStyle();
559 
560     _.each(this.decViews, function(decomposition) {
561       // computing this with every update requires traversin all elements,
562       // however it seems as the only reliable way to get this number right
563       // without depending on the view controllers (an anti-pattern)
564       visible += decomposition.getVisibleCount();
565       total += decomposition.count;
566     });
567 
568     this.$plotBanner.css({'color': color, 'border-color': color});
569     if (this.info) {
570         message += '<br>' + this.info;
571     }
572     if (visible !== total) {
573       message += ' <br> WARNING: hiding samples in an ordination can be ' +
574                 'misleading';
575     }
576 
577     this.$plotBanner.html(visible.toLocaleString() + ' / ' +
578                           total.toLocaleString() + ' visible' + message);
579   };
580 
581   EmperorController.prototype.getPlotBanner = function(text) {
582     return this.$plotBanner.text();
583   };
584 
585   /**
586    *
587    * Helper method to check if all the view controllers have finished loading.
588    * Relies on the fact that each view controller announces when it is ready.
589    *
590    * @private
591    *
592    */
593   EmperorController.prototype._controllerHasFinishedLoading = function() {
594     this._seen += 1;
595 
596     if (this._seen >= this._expected) {
597       if (this.ready !== null) {
598         this.ready();
599       }
600     }
601   };
602 
603   /**
604    *
605    * Helper method to assemble UI, completely independent of HTML template.
606    * This method is called when the object is constructed.
607    *
608    * @private
609    *
610    */
611   EmperorController.prototype._buildUI = function() {
612     var scope = this, isLargeDataset = this.UIState['view.usesPointCloud'];
613 
614     for (var index in TAB_ORDER) {
615       var item = TAB_ORDER[index];
616       if (item === 'shape' && isLargeDataset)
617         continue;
618       scope.controllers[item] = scope.addTab(scope.sceneViews[0].decViews,
619                                              controllerConstructors[item]);
620     }
621 
622     // We are tabifying this div, I don't know man.
623     this._$tabsContainer.tabs({heightStyle: 'fill',
624                                // The tabs on the plot space only get resized
625                                // when they are visible, thus we subscribe to
626                                // the event that's fired after a user selects a
627                                // tab.  If you don't do this, the width and
628                                // height of each of the view controllers will
629                                // be wrong.  We also found that subscribing to
630                                // document.ready() wouldn't work either as the
631                                // resize callback couldn't be executed on a tab
632                                // that didn't exist yet.
633                                activate: function(event, ui) {
634                                  scope.resize(scope.$divId.width(),
635                                               scope.$divId.height());
636                                }});
637 
638     // Set up the context menu
639     this.$contextMenu = $.contextMenu({
640       // only tie this selector to our own container div, otherwise with
641       // multiple plots on the same screen, this callback gets confused
642       selector: '#' + scope.$divId.attr('id') + ' .emperor-plot-wrapper',
643       trigger: 'none',
644       items: {
645         'recenterCamera': {
646           name: 'Recenter camera',
647           icon: 'home',
648           callback: function(key, opts) {
649             _.each(scope.sceneViews, function(scene) {
650               scene.recenterCamera();
651             });
652           }
653         },
654         'toggleAutorotate': {
655           name: 'Toggle autorotation',
656           icon: 'rotate-left',
657           callback: function(key, opts) {
658             _.each(scope.sceneViews, function(scene) {
659               scene.control.autoRotate = scene.control.autoRotate ^ true;
660             });
661           },
662           disabled: function(key, opts) {
663             return scope.UIState['view.viewType'] === 'parallel-plot';
664           }
665         },
666         'labels' : {
667           name: 'Toggle label visibility',
668           visible: scope.decViews.biplot !== undefined,
669           icon: 'font',
670           callback: function() {
671             scope._hideBiplotLabels = Boolean(scope._hideBiplotLabels ^ true);
672             scope.decViews.biplot.toggleLabelVisibility();
673           }
674         },
675         'sep0': '----------------',
676         'saveState': {
677           name: 'Save current settings',
678           icon: 'save',
679           callback: function(key, opts) {
680             scope.saveConfig();
681           }
682         },
683         'loadState': {
684           name: 'Load saved settings',
685           icon: 'folder-open-o',
686           callback: function(key, opts) {
687             if (!FileReader) {
688               alert('Your browser does not support file loading. We ' +
689                     'recommend using Google Chrome for full functionality.');
690               return;
691             }
692             var file = $('<input type="file">');
693             file.on('change', function(evt) {
694               var f = evt.target.files[0];
695               // With help from
696               // http://www.htmlgoodies.com/beyond/javascript/read-text-files-using-the-javascript-filereader.html
697               var r = new FileReader();
698               r.onload = function(e) {
699                 try {
700                   var json = JSON.parse(e.target.result);
701                 } catch (err) {
702                   alert('File given is not a JSON parsable file.');
703                   return;
704                 }
705                 try {
706                   scope.loadConfig(json);
707                 } catch (err) {
708                   alert('Error loading settings from file: ' + err.message);
709                   return;
710                 }
711               };
712               r.readAsText(f);
713             });
714             file.click();
715           }
716         },
717         'sep1': '---------',
718         // With large datasets we can't save to SVG. The PNG file will not be
719         // high resolution.
720         'fold1': {
721             'name': 'Save Image',
722             icon: 'file-picture-o',
723             'items': {
724               'saveImagePNG': {
725                 name: 'PNG' + (isLargeDataset ? '' : ' (high resolution)'),
726                 callback: function(key, opts) {
727                   scope.screenshot('png');
728                 }
729               },
730               'saveImageSVG': {
731                 name: 'SVG + labels' + (isLargeDataset ?
732                       ' (not supported for large datasets)' : '') ,
733                 callback: function(key, opts) {
734                   scope.screenshot('svg');
735                 },
736                 disabled: function(key, opt) {
737                   return isLargeDataset ||
738                          (scope.UIState['view.viewType'] === 'parallel-plot');
739                 }
740               }
741             }
742         },
743         fold2: {
744           name: 'Experimental',
745           disabled: function(key, opt) {
746             // Only enable if this is a "vanilla" plot
747             if (scope.UIState['view.viewType'] === 'scatter' &&
748                 scope.decViews.scatter.lines.left === null &&
749                 scope.decViews.scatter.lines.right === null &&
750                 scope.decViews.biplot === undefined) {
751               return false;
752             }
753             return true;
754           },
755           icon: 'warning',
756           items: {
757             openInVegaEditor: {
758               name: 'Open in Vega Editor',
759               callback: function(key, opts) {
760                 scope.exportToVega();
761               }
762             }
763           }
764         }
765       }
766     });
767 
768     // The context menu is only shown if there's a single right click. We
769     // intercept the clicking event and if it's followed by mouseup event then
770     // the context menu is shown, otherwise the event is sent to the THREE.js
771     // orbit controls callback. See: http://stackoverflow.com/a/20831728
772     this.$plotSpace.on('mousedown', function(evt) {
773       scope.$plotSpace.on('mouseup mousemove', function handler(evt) {
774         if (evt.type === 'mouseup') {
775           // 3 is the right click
776           if (evt.which === 3) {
777             var contextDiv = $('#' + scope.$divId.attr('id') +
778                                ' .emperor-plot-wrapper');
779             contextDiv.contextMenu({x: evt.pageX, y: evt.pageY});
780           }
781         }
782         scope.$plotSpace.off('mouseup mousemove', handler);
783       });
784     });
785   };
786 
787   /**
788    *
789    * Save the current canvas view to a new window
790    *
791    * @param {string} [type = png] Format to save the file as: ('png', 'svg')
792    *
793    */
794   EmperorController.prototype.screenshot = function(type) {
795     var img, renderer, factor = 5;
796     type = type || 'png';
797 
798     if (type === 'png') {
799       var pngRenderer;
800 
801       // Point clouds can't be rendered by the CanvasRenderer, therefore we
802       // have to use the WebGLRenderer and can't increase the image size.
803       if (this.UIState['view.usesPointCloud'] ||
804           this.UIState['view.viewType'] === 'parallel-plot') {
805         pngRenderer = this.sceneViews[0].renderer;
806       }
807       else {
808         pngRenderer = new THREE.CanvasRenderer({
809           antialias: true,
810           preserveDrawingBuffer: true
811         });
812 
813         pngRenderer.autoClear = true;
814         pngRenderer.sortObjects = true;
815         pngRenderer.setSize(this.$plotSpace.width() * factor,
816                             this.$plotSpace.height() * factor);
817         pngRenderer.setPixelRatio(window.devicePixelRatio);
818       }
819       pngRenderer.render(this.sceneViews[0].scene, this.sceneViews[0].camera);
820 
821       // toBlob is only available in some browsers, that's why we use
822       // canvas-toBlob
823       pngRenderer.domElement.toBlob(function(blob) {
824         saveAs(blob, 'emperor.png');
825       });
826     }
827     else if (type === 'svg') {
828       // confirm box based on number of samples: better safe than sorry
829       if (this.decViews.scatter.decomp.length >= 9000) {
830         if (confirm('This number of samples could take a long time and in ' +
831            'some computers the browser will crash. If this happens we ' +
832            'suggest to use the png implementation. Do you want to ' +
833            'continue?') === false) {
834           return;
835         }
836       }
837 
838       // generating SVG image
839       var svgRenderer = new THREE.SVGRenderer({antialias: true,
840                                                preserveDrawingBuffer: true});
841       svgRenderer.setSize(this.$plotSpace.width(), this.$plotSpace.height());
842       svgRenderer.render(this.sceneViews[0].scene, this.sceneViews[0].camera);
843       svgRenderer.sortObjects = true;
844 
845       // converting svgRenderer to string: http://stackoverflow.com/a/17415624
846       var XMLS = new XMLSerializer();
847       var svgfile = XMLS.serializeToString(svgRenderer.domElement);
848 
849       // some browsers (Chrome) will add the namespace, some won't. Make sure
850       // that if it's not there, you add it to make sure the file can be opened
851       // in tools like Adobe Illustrator or in browsers like Safari or FireFox
852       if (svgfile.indexOf('xmlns="http://www.w3.org/2000/svg"') === -1) {
853         // adding xmlns header to open in the browser
854         svgfile = svgfile.replace('viewBox=',
855                                   'xmlns="http://www.w3.org/2000/svg" ' +
856                                   'viewBox=');
857       }
858 
859       // hacking the background color by adding a rectangle
860       var index = svgfile.indexOf('viewBox="') + 9;
861       var viewBox = svgfile.substring(index,
862                                       svgfile.indexOf('"', index)).split(' ');
863       var background = '<rect id="background" height="' + viewBox[3] +
864                        '" width="' + viewBox[2] + '" y="' + viewBox[1] +
865                        '" x="' + viewBox[0] +
866                        '" stroke-width="0" stroke="#000000" fill="#' +
867                        this.sceneViews[0].scene.background.getHexString() +
868                        '"/>';
869       index = svgfile.indexOf('>', index) + 1;
870       svgfile = svgfile.substr(0, index) + background + svgfile.substr(index);
871 
872       var blob = new Blob([svgfile], {type: 'image/svg+xml'});
873       saveAs(blob, 'emperor-image.svg');
874 
875       // generating legend
876       var names = [], colors = [], legend;
877 
878       if (this.controllers.color.isColoringContinuous()) {
879         legend = XMLS.serializeToString(this.controllers.color.$colorScale[0]);
880       }
881       else {
882         _.each(this.controllers.color.getSlickGridDataset(), function(element) {
883           names.push(element.category);
884           colors.push(element.value);
885         });
886 
887         legend = Draw.formatSVGLegend(names, colors);
888       }
889       blob = new Blob([legend], {type: 'image/svg+xml'});
890       saveAs(blob, 'emperor-image-labels.svg');
891     } else {
892       console.error('Screenshot type not implemented');
893     }
894 
895     // re-render everything, sometimes after saving objects, the colors change
896     this.sceneViews.forEach(function(view) {
897       view.needsUpdate = true;
898     });
899   };
900 
901   /**
902    *
903    * Write settings file for the current controller settings
904    *
905    * The format is as follows: a javascript object with the camera position
906    * stored in the 'cameraPosition' key and the quaternion in the
907    * 'cameraQuaternion' key. Each controller in this.controllers is then saved
908    * by calling toJSON on them, and the resulting object saved under the same
909    * key as the controllers object.
910    *
911    */
912   EmperorController.prototype.saveConfig = function() {
913     var saveinfo = {};
914     // Assuming single sceneview for now
915     sceneview = this.sceneViews[0];
916     saveinfo.cameraPosition = sceneview.camera.position;
917     saveinfo.cameraQuaternion = sceneview.camera.quaternion;
918     saveinfo.hideBiplotLabels = this._hideBiplotLabels;
919 
920     // Save settings for each controller in the view
921      _.each(this.controllers, function(controller, index) {
922       if (controller !== undefined) {
923         saveinfo[index] = controller.toJSON();
924       }
925     });
926 
927     // Save the file
928     var blob = new Blob([JSON.stringify(saveinfo)], {type: 'text/json'});
929     saveAs(blob, 'emperor-settings.json');
930    };
931 
932   /**
933    *
934    * Load a settings file and set all controller variables.
935    *
936    * This method will trigger a rendering callback.
937    *
938    * @param {object} json Information about the emperor session to load.
939    *
940    */
941   EmperorController.prototype.loadConfig = function(json) {
942     //still assuming one sceneview for now
943     var sceneview = this.sceneViews[0];
944 
945     if (json.cameraPosition !== undefined) {
946       sceneview.camera.position.set(json.cameraPosition.x,
947                                     json.cameraPosition.y,
948                                     json.cameraPosition.z);
949     }
950     if (json.cameraQuaternion !== undefined) {
951       sceneview.camera.quaternion.set(json.cameraQuaternion._x,
952                                       json.cameraQuaternion._y,
953                                       json.cameraQuaternion._z,
954                                       json.cameraQuaternion._w);
955     }
956     if (json.hideBiplotLabels !== undefined) {
957       /*
958        * The controller only needs to toggle the visibility if the saved state
959        * is different from the current state.
960        *
961        * saved | current || result
962        * =========================
963        * false | false   || no-op
964        * false | true    || toggle
965        * true  | false   || toggle
966        * true  | true    || no-op
967        *
968        * The table above represents a logical XOR.
969        */
970       if (json.hideBiplotLabels ^ this._hideBiplotLabels) {
971         this.decViews.biplot.toggleLabelVisibility();
972       }
973       this._hideBiplotLabels = json.hideBiplotLabels;
974     }
975 
976     //must call updates to reset for camera move
977     sceneview.camera.updateProjectionMatrix();
978     sceneview.control.update();
979 
980     //load the rest of the controller settings
981     _.each(this.controllers, function(controller, index) {
982       if (controller !== undefined && json[index] !== undefined) {
983         // wrap everything inside this "ready" call to prevent problems with
984         // the jQuery elements not being loaded yet
985         $(function() {
986           controller.fromJSON(json[index]);
987         });
988       }
989     });
990 
991     sceneview.needsUpdate = true;
992    };
993 
994   /**
995    *
996    * Helper method to add tabs to the controller.
997    *
998    * @param {DecompositionView[]} dvdict Dictionary of DecompositionViews.
999    * @param {EmperorViewControllerABC} viewConstructor Constructor of the view
1000    * controller.
1001    *
1002    */
1003   EmperorController.prototype.addTab = function(dvdict, viewConstructor) {
1004     var scope = this;
1005     this._expected += 1;
1006 
1007     // nothing but a temporary id
1008     var id = (Math.round(1000000 * Math.random())).toString(), $li;
1009 
1010     this._$tabsContainer.append("<div id='" + id +
1011                                 "' class='emperor-tab-div' ></div>");
1012     $('#' + id).height(this.$plotMenu.height() - this._$tabsList.height());
1013 
1014     // dynamically instantiate the controller, see:
1015     // http://stackoverflow.com/a/8843181
1016     var params = [null, this.UIState, '#' + id, dvdict];
1017     var obj = new (Function.prototype.bind.apply(viewConstructor, params));
1018 
1019     obj.ready = function() {
1020       scope._controllerHasFinishedLoading();
1021     };
1022 
1023     // set the identifier of the div to the one defined by the object
1024     $('#' + id).attr('id', obj.identifier);
1025 
1026     // now add the list element linking to the container div with the proper
1027     // title
1028     $li = $("<li><a href='#" + obj.identifier + "'>" + obj.title + '</a></li>');
1029     $li.attr('title', obj.description);
1030     this._$tabsList.append($li);
1031 
1032     return obj;
1033   };
1034 
1035   /**
1036    *
1037    * Helper that posts messages between browser tabs
1038    *
1039    * @private
1040    *
1041    */
1042   _postMessage = function(url, payload) {
1043     // Shamelessly pulled from https://github.com/vega/vega-embed/
1044     var editor = window.open(url);
1045     var wait = 10000;
1046     var step = 250;
1047     var count = ~~(wait / step);
1048 
1049     function listen(e) {
1050       if (e.source === editor) {
1051         count = 0;
1052         window.removeEventListener('message', listen, false);
1053       }
1054     }
1055 
1056     window.addEventListener('message', listen, false);
1057 
1058     function send() {
1059       if (count <= 0) {
1060         return;
1061       }
1062       editor.postMessage(payload, '*');
1063       setTimeout(send, step);
1064       count -= 1;
1065     }
1066     setTimeout(send, step);
1067   };
1068 
1069   /**
1070    *
1071    * Open in Vega editor
1072    *
1073    */
1074   EmperorController.prototype.exportToVega = function() {
1075     var url = 'https://vega.github.io/editor/';
1076     var spec = this.decViews.scatter._buildVegaSpec();
1077     var payload = {
1078       mode: 'vega',
1079       renderer: 'canvas',
1080       spec: JSON.stringify(spec)
1081     };
1082     _postMessage(url, payload);
1083   };
1084 
1085   return EmperorController;
1086 });
1087