      1 define([
      2     'jquery',
      3     'underscore',
      4     'view',
      5     'viewcontroller',
      6     'animationdirector',
      7     'draw',
      8     'color-editor',
      9     'colorviewcontroller'
     10 ], function($, _, DecompositionView, ViewControllers, AnimationDirector,
     11             draw, Color, ColorViewController) {
     12   var EmperorViewController = ViewControllers.EmperorViewController;
     13   var drawTrajectoryLineStatic = draw.drawTrajectoryLineStatic;
     14   var drawTrajectoryLineDynamic = draw.drawTrajectoryLineDynamic;
     15   var disposeTrajectoryLineStatic = draw.disposeTrajectoryLineStatic;
     16   var disposeTrajectoryLineDynamic = draw.disposeTrajectoryLineDynamic;
     17   var updateStaticTrajectoryDrawRange = draw.updateStaticTrajectoryDrawRange;
     18   var ColorEditor = Color.ColorEditor, ColorFormatter = Color.ColorFormatter;
     19
     20   /**
     21    * @class AnimationsController
     22    *
     23    * Controls the axes that are displayed on screen as well as their
     24    * orientation.
     25    *
     26    * @param {UIState} uiState The shared state
     27    * @param {Node} container Container node to create the controller in.
     28    * @param {Object} decompViewDict This is object is keyed by unique
     29    * identifiers and the values are DecompositionView objects referring to a
     30    * set of objects presented on screen. This dictionary will usually be shared
     31    * by all the tabs in the application. This argument is passed by reference.
     32    *
     33    * @return {AnimationsController}
     34    * @constructs AnimationsController
     35    * @extends EmperorViewController
     36    */
     37   function AnimationsController(uiState, container, decompViewDict) {
     38     var helpmenu = 'Animate trajectories connecting samples in your data';
     39     var title = 'Animations';
     40     var scope = this, dm, label, gradientTooltip, trajectoryTooltip;
     41     EmperorViewController.call(this, uiState, container, title, helpmenu,
     42                                decompViewDict);
     43
     44     trajectoryTooltip = 'Category to group samples';
     45     gradientTooltip = 'Category to sort samples';
     46
     47     dm = this.getView().decomp;
     48
     49     this.$gradientSelect = $("<select class='emperor-tab-drop-down'>");
     50     this.$trajectorySelect = $("<select class='emperor-tab-drop-down'>");
     51
     52     // http://stackoverflow.com/a/6602002
     53     // prepend an empty string so dropdown shows the "tooltip string"
     54     _.each([''].concat(dm.md_headers), function(header) {
     55       scope.$gradientSelect.append(
     56           $('<option>').attr('value', header).text(header));
     57       scope.$trajectorySelect.append(
     58           $('<option>').attr('value', header).text(header));
     59     });
     60
     61     // add a label to the chosen drop downs
     62     label = $('<label>').text('Gradient').append(this.$gradientSelect);
     63     label.attr('title', gradientTooltip);
     64     this.$header.append(label);
     65
     66     label = $('<label>').text('Trajectory').append(this.$trajectorySelect);
     67     label.attr('title', trajectoryTooltip);
     68     this.$header.append(label);
     69
     70     // container of the sliders and buttons
     71     this._$mediaContainer = $('<div name="media-controls-container"></div>');
     72     this._$mediaContainer.css({'padding-top': '10px',
     73                                'width': 'inherit',
     74                                'text-align': 'center'});
     75     this.$body.append(this._$mediaContainer);
     76
     77     this.$rewind = $('<button></button>');
     78     this._$mediaContainer.append(this.$rewind);
     79
     80     this.$play = $('<button></button>');
     81     this._$mediaContainer.append(this.$play);
     82
     83     this.$pause = $('<button></button>');
     84     this._$mediaContainer.append(this.$pause);
     85
     86     this._colors = {};
     87     this._currentFrame = 0;
     88
     89     // make the buttons squared
     90     this._$mediaContainer.find('button').css({'width': '30px',
     91                                               'height': '30px',
     92                                               'margin': '0 auto',
     93                                               'margin-left': '10px',
     94                                               'margin-right': '10px'});
     95
     96     this._$mediaContainer.append($('<hr>'));
     97
     98     this._$speedLabel = $('<text name="speed">Speed: 1x</text>');
     99     this._$speedLabel.attr('title', 'Speed at which the traces animate');
    100     this._$mediaContainer.append(this._$speedLabel);
    101
    102     this.$speed = $('<div></div>').css('margin-top', '10px');
    103     this.$speed.attr('title', 'Speed at which the traces animate');
    104     this._$mediaContainer.append(this.$speed);
    105
    106     this._$radiusLabel = $('<text name="radius">Radius: 1</text>');
    107     this._$radiusLabel.attr('title', 'Radius of the traces');
    108     this._$mediaContainer.append(this._$radiusLabel);
    109
    110     this.$radius = $('<div></div>').css('margin-top', '10px');
    111     this.$radius.attr('title', 'Radius of the animated traces');
    112     this._$mediaContainer.append(this.$radius);
    113
    114     this.$gridDiv = $('<div name="emperor-grid-div"></div>');
    115     this.$gridDiv.css('margin', '0 auto');
    116     this.$gridDiv.css('width', 'inherit');
    117     this.$gridDiv.css('height', '100%');
    118     this.$gridDiv.attr('title', 'Change the color of the animated traces.');
    119     this.$body.append(this.$gridDiv);
    120
    121     this.director = null;
    122     this.playing = false;
    123
    124     /**
    125      * @type {Slick.Grid}
    126      * Container that lists the trajectories and their colors
    127      */
    128     this._grid = null;
    129
    130     // initialize interface elements here
    131     $(this).ready(function() {
    132       scope.$speed.slider({'min': 0.01,
    133                            'max': 10,
    134                            'step': 0.05,
    135                            'value': 1,
    136                            'range': 'max',
    137                            'slide': function(event, ui) {
    138                              scope._$speedLabel.text('Speed: ' + ui.value +
    139                                                      'x');
    140                            },
    141                            'change': function(event, ui) {
    142                              scope._$speedLabel.text('Speed: ' + ui.value +
    143                                                      'x');
    144                            }});
    145       scope.$speed.css('background', '#70caff');
    146
    147       scope.$radius.slider({'min': 0.01,
    148                             'max': 10,
    149                             'step': 0.05,
    150                             'value': 1,
    151                             'range': 'max',
    152                             'slide': function(event, ui) {
    153                               scope._$radiusLabel.text('Radius: ' + ui.value);
    154                             },
    155                             'change': function(event, ui) {
    156                               scope._$radiusLabel.text('Radius: ' + ui.value);
    157                             }});
    158       scope.$radius.css('background', '#70caff');
    159
    160       // once this element is ready, it is safe to execute the "ready" callback
    161       // if a subclass needs to wait on other elements, this attribute should
    162       // be changed to null so this callback is effectively cancelled, for an
    163       // example see the constructor of ColorViewController
    164       scope.$trajectorySelect.on('chosen:ready', function() {
    165         if (scope.ready !== null) {
    166           scope.ready();
    167         }
    168       });
    169
    170       // setup chosen
    171       scope.$gradientSelect.chosen({
    172         width: '100%',
    173         search_contains: true,
    174         placeholder_text_single: gradientTooltip
    175       });
    176       scope.$trajectorySelect.chosen({
    177         width: '100%',
    178         search_contains: true,
    179         placeholder_text_single: trajectoryTooltip
    180       });
    181
    182       scope.$gradientSelect.chosen().change(function(e, p) {
    183                                               scope._gradientChanged(e, p);
    184                                             });
    185       scope.$trajectorySelect.chosen().change(function(e, p) {
    186                                                 scope._trajectoryChanged(e, p);
    187                                               });
    188
    189       scope.$rewind.button({icons: {primary: 'ui-icon-seek-first'}});
    190       scope.$rewind.attr('title', 'Restart the animation');
    191       scope.$rewind.on('click', function() {
    192         scope._rewindButtonClicked();
    193       });
    194
    195       scope.$play.button({icons: {primary: 'ui-icon-play'}});
    196       scope.$play.attr('title', 'Start the animation');
    197       scope.$play.on('click', function() {
    198         scope._playButtonClicked();
    199       });
    200
    201       scope.$pause.button({icons: {primary: 'ui-icon-pause'}});
    202       scope.$pause.attr('title', 'Pause the animation');
    203       scope.$pause.on('click', function() {
    204         scope._pauseButtonClicked();
    205       });
    206
    207       scope._buildGrid();
    208
    209       scope.setEnabled(false);
    210
    211       //Note that we can't do this before the buttons are ready.
    212       scope.UIState.registerProperty('view.viewType',
    213                                scope._viewTypeChanged.bind(scope));
    214     });
    215
    216     return this;
    217   }
    218   AnimationsController.prototype = Object.create(
    219     EmperorViewController.prototype);
    220   AnimationsController.prototype.constructor = EmperorViewController;
    221
    222   /**
    223    * Get the colors for the trajectories
    224    *
    225    * @return {Object} Returns the object mapping trajectories to colors.
    226    */
    227   AnimationsController.prototype.getColors = function() {
    228     return this._colors;
    229   };
    230
    231   /**
    232    * Set the colors of the trajectories
    233    *
    234    * @param {Object} colors Mapping between trajectories and colors.
    235    */
    236   AnimationsController.prototype.setColors = function(colors) {
    237     this._colors = colors;
    238
    239     var data = [];
    240     for (var value in colors) {
    241       data.push({category: value, value: colors[value]});
    242     }
    243
    244     this._grid.setData(data);
    245     this._grid.invalidate();
    246     this._grid.render();
    247   };
    248
    249   /**
    250    * Callback when a row's color changes
    251    *
    252    * See _buildGrid for information about the arguments.
    253    *
    254    * @private
    255    */
    256   AnimationsController.prototype._colorChanged = function(e, args) {
    257     this._colors[args.item.category] = args.item.value;
    258   };
    259
    260   /**
    261    * Helper method to create a grid and set it up for the traces
    262    *
    263    * @private
    264    */
    265   AnimationsController.prototype._buildGrid = function() {
    266     var scope = this, columns, gridOptions;
    267
    268     columns = [
    269       {id: 'title', name: '', field: 'value', sortable: false, maxWidth: 25,
    270        minWidth: 25, editor: ColorEditor, formatter: ColorFormatter},
    271       {id: 'field1', name: '', field: 'category'}
    272     ];
    273
    274     // autoEdit enables one-click editor trigger on the entire grid, instead
    275     // of requiring users to click twice on a widget.
    276     gridOptions = {editable: true, enableAddRow: false, autoEdit: true,
    277                    enableCellNavigation: true, forceFitColumns: true,
    278                    enableColumnReorder: false};
    279
    280     this._grid = new Slick.Grid(this.$gridDiv, [], columns, gridOptions);
    281
    282     // hide the header row of the grid
    283     // http://stackoverflow.com/a/29827664/379593
    284     this.$body.find('.slick-header').css('display', 'none');
    285
    286     // subscribe to events when a cell is changed
    287     this._grid.onCellChange.subscribe(function(e, args) {
    288       scope._colorChanged(e, args);
    289     });
    290   };
    291
    292   /**
    293    * Sets whether or not the tab can be modified or accessed.
    294    *
    295    * @param {boolean} trulse option to enable tab.
    296    */
    297   AnimationsController.prototype.setEnabled = function(trulse) {
    298     EmperorViewController.prototype.setEnabled.call(this, trulse);
    299     this._updateButtons();
    300   };
    301
    302   /**
    303    * Resizes the container and the individual elements.
    304    *
    305    * Note, the consumer of this class, likely the main controller should call
    306    * the resize function any time a resizing event happens.
    307    *
    308    * @param {Float} width the container width.
    309    * @param {Float} height the container height.
    310    */
    311   AnimationsController.prototype.resize = function(width, height) {
    312     // call super, most of the header and body resizing logic is done there
    313     EmperorViewController.prototype.resize.call(this, width, height);
    314
    315     this.$body.height(this.$canvas.height() - this.$header.height());
    316     this.$body.width(this.$canvas.width());
    317
    318     var grid = this.$canvas.height();
    319     grid -= this.$header.height() + this._$mediaContainer.height();
    320     this.$gridDiv.height(grid);
    321
    322     // the whole code is asynchronous, so there may be situations where
    323     // _grid doesn't exist yet, so check before trying to modify the object
    324     if (this._grid !== null) {
    325       // make the columns fit the available space whenever the window resizes
    326       // http://stackoverflow.com/a/29835739
    327       this._grid.setColumns(this._grid.getColumns());
    328       // Resize the slickgrid canvas for the new body size.
    329       this._grid.resizeCanvas();
    330     }
    331   };
    332
    333   /**
    334    *
    335    * Helper method to update what media buttons should be enabled
    336    *
    337    * @private
    338    */
    339   AnimationsController.prototype._updateButtons = function() {
    340     var play, pause, speed, rewind;
    341
    342     /*
    343      *
    344      * The behavior of the media buttons is a bit complicated. It is explained
    345      * by the following truth table where the variables are "director",
    346      * "playing" and "enabled". Each output's value determines if the button
    347      * should be enabled. Note that we negate the values when we make the
    348      * assignment because jQuery only has a "disabled" method.
    349      *
    350      * ||----------|---------|---------||-------|-------|-------|--------|
    351      * || director | playing | enabled || Play  | Speed | Pause | Rewind |
    352      * ||          |         |         ||       | Radius|       |        |
    353      * ||          |         |         ||       | Colors|       |        |
    354      * ||----------|---------|---------||-------|-------|-------|--------|
    355      * || FALSE    | FALSE   | FALSE   || FALSE | FALSE | FALSE | FALSE  |
    356      * || FALSE    | FALSE   | TRUE    || TRUE  | TRUE  | FALSE | FALSE  |
    357      * || FALSE    | TRUE    | FALSE   || FALSE | FALSE | FALSE | FALSE  |
    358      * || FALSE    | TRUE    | TRUE    || FALSE | FALSE | FALSE | FALSE  |
    359      * || TRUE     | FALSE   | FALSE   || FALSE | FALSE | FALSE | FALSE  |
    360      * || TRUE     | FALSE   | TRUE    || TRUE  | FALSE | FALSE | TRUE   |
    361      * || TRUE     | TRUE    | FALSE   || FALSE | FALSE | FALSE | FALSE  |
    362      * || TRUE     | TRUE    | TRUE    || FALSE | FALSE | TRUE  | TRUE   |
    363      * ||----------|---------|---------||-------|-------|-------|--------|
    364      *
    365      */
    366     play = ((this.enabled && this.director === null && !this.playing) ||
    367             (this.enabled && this.director !== null && !this.playing));
    368
    369     pause = this.director !== null && this.enabled && this.playing;
    370
    371     speed = this.director === null && !this.playing && this.enabled;
    372
    373     rewind = this.director !== null && this.enabled;
    374
    375     this.$speed.slider('option', 'disabled', !speed);
    376     this.$radius.slider('option', 'disabled', !speed);
    377
    378     // jquery ui requires a manual refresh of to the UI after state changes
    379     this.$play.prop('disabled', !play).button('refresh');
    380     this.$pause.prop('disabled', !pause).button('refresh');
    381     this.$rewind.prop('disabled', !rewind).button('refresh');
    382
    383     this._grid.setOptions({editable: speed});
    384   };
    385
    386   /**
    387    *
    388    * Helper method to update a grid.
    389    *
    390    * @private
    391    */
    392   AnimationsController.prototype._updateGrid = function() {
    393     var category = this.getTrajectoryCategory(), colors, values;
    394
    395     values = this.getView().decomp.getUniqueValuesByCategory(category);
    396     colors = ColorViewController.getColorList(values,
    397                                               'discrete-coloring-qiime',
    398                                               true, false)[0];
    399
    400     this.setColors(colors);
    401     this.resize();
    402   };
    403
    404   /**
    405    *
    406    * Callback method executed when the Gradient menu changes.
    407    *
    408    * @private
    409    */
    410   AnimationsController.prototype._gradientChanged = function(evt, params) {
    411     if (this.getGradientCategory() !== '' &&
    412         this.getTrajectoryCategory() !== '' &&
    413         this.UIState['view.viewType'] === 'scatter') {
    414       this.setEnabled(true);
    415       this._updateGrid();
    416     }
    417     else if (this.getGradientCategory() === '' ||
    418              this.getTrajectoryCategory() === '' ||
    419              this.UIState['view.viewType'] !== 'scatter') {
    420       this.setEnabled(false);
    421     }
    422   };
    423
    424   /**
    425    *
    426    * Callback method executed when the Trajectory menu changes.
    427    *
    428    * @private
    429    */
    430   AnimationsController.prototype._trajectoryChanged = function(evt, params) {
    431     if (this.getGradientCategory() !== '' &&
    432         this.getTrajectoryCategory() !== '' &&
    433         this.UIState['view.viewType'] === 'scatter') {
    434       this.setEnabled(true);
    435       this._updateGrid();
    436     }
    437     else if (this.getGradientCategory() === '' ||
    438              this.getTrajectoryCategory() === '' ||
    439              this.UIState['view.viewType'] !== 'scatter') {
    440       this.setColors({});
    441       this.setEnabled(false);
    442     }
    443   };
    444
    445   /**
    446    *
    447    * Callback method executed when the UIState view.viewType changes.
    448    *
    449    * @private
    450    */
    451   AnimationsController.prototype._viewTypeChanged = function(evt) {
    452     if (this.getGradientCategory() !== '' &&
    453       this.getTrajectoryCategory() !== '' &&
    454       this.UIState['view.viewType'] === 'scatter') {
    455       this.setEnabled(true);
    456       this._updateGrid();
    457     }
    458     else if (this.getGradientCategory() === '' ||
    459              this.getTrajectoryCategory() === '' ||
    460              this.UIState['view.viewType'] !== 'scatter') {
    461       this.setEnabled(false);
    462     }
    463   };
    464
    465   /**
    466    *
    467    * Callback method executed when the Rewind button is clicked.
    468    *
    469    * @private
    470    */
    471   AnimationsController.prototype._rewindButtonClicked = function(evt, params) {
    472     var view = this.getView();
    473
    474     this.playing = false;
    475     this.director = null;
    476
    477     view.staticTubes.forEach(function(tube) {
    478       if (tube !== null && tube.parent !== null) {
    479         tube.parent.remove(tube);
    480         disposeTrajectoryLineStatic(tube);
    481       }
    482     });
    483     view.dynamicTubes.forEach(function(tube) {
    484       if (tube !== null && tube.parent !== null) {
    485         tube.parent.remove(tube);
    486         disposeTrajectoryLineDynamic(tube);
    487       }
    488     });
    489
    490     view.staticTubes = [];
    491     view.dynamicTubes = [];
    492
    493     view.needsUpdate = true;
    494
    495     this._updateButtons();
    496
    497     this.dispatchEvent({type: 'animation-cancelled', message: {
    498       gradient: this.getGradientCategory(),
    499       trajectory: this.getTrajectoryCategory(),
    500       controller: this
    501     }});
    502   };
    503
    504   /**
    505    *
    506    * Callback method when the Pause button is clicked.
    507    *
    508    * @private
    509    */
    510   AnimationsController.prototype._pauseButtonClicked = function(evt, params) {
    511     if (this.playing) {
    512       this.playing = false;
    513     }
    514     this._updateButtons();
    515
    516     this.dispatchEvent({type: 'animation-paused', message: {
    517       gradient: this.getGradientCategory(),
    518       trajectory: this.getTrajectoryCategory(),
    519       controller: this
    520     }});
    521
    522   };
    523
    524   /**
    525    *
    526    * Callback method when the Play button is clicked.
    527    *
    528    * @private
    529    */
    530   AnimationsController.prototype._playButtonClicked = function(evt, params) {
    531
    532     if (this.playing === false && this.director !== null) {
    533       this.playing = true;
    534       this._updateButtons();
    535       return;
    536     }
    537
    538     var headers, data = {}, positions = {}, gradient, trajectory, decomp, p;
    539     var view, marker, pos, speed;
    540
    541     view = this.getView();
    542     decomp = this.getView().decomp;
    543     headers = decomp.md_headers;
    544
    545     // get the current visible dimensions
    546     var x = view.visibleDimensions[0], y = view.visibleDimensions[1],
    547         z = view.visibleDimensions[2];
    548     var is2D = (z === null || z === undefined);
    549
    550     gradient = this.$gradientSelect.val();
    551     trajectory = this.$trajectorySelect.val();
    552
    553     speed = this.getSpeed();
    554
    555     for (var i = 0; i < decomp.plottable.length; i++) {
    556       p = decomp.plottable[i];
    557
    558       data[p.name] = p.metadata;
    559
    560       // get the view's position, not the metadata's position
    561       positions[p.name] = {
    562         'name': p.name, 'color': 0,
    563         'x': p.coordinates[x] * view.axesOrientation[0],
    564         'y': p.coordinates[y] * view.axesOrientation[1],
    565         'z': is2D ? 0 : (p.coordinates[z] * view.axesOrientation[2])
    566       };
    567     }
    568
    569     this.director = new AnimationDirector(headers, data, positions, gradient,
    570                                           trajectory, speed);
    571
    572     this.director.updateFrame();
    573     this._currentFrame = 0;
    574
    575     this.playing = true;
    576     this._updateButtons();
    577
    578     this.dispatchEvent({type: 'animation-started', message: {
    579       gradient: this.getGradientCategory(),
    580       trajectory: this.getTrajectoryCategory(),
    581       controller: this
    582     }});
    583   };
    584
    585   /**
    586    *
    587    * Update the portion of the trajectory that needs to be drawn.
    588    *
    589    * If the animation is not playing (because it was paused or it has finished)
    590    * or a director hasn't been instantiated, no action is taken. Otherwise,
    591    * trajectories are updated on screen.
    592    *
    593    */
    594   AnimationsController.prototype.drawFrame = function() {
    595     if (this.director === null || this.director.animationCycleFinished() ||
    596         !this.playing) {
    597       return;
    598     }
    599
    600     var view = this.getView(), tube, scope = this, color;
    601
    602     var radius = view.getGeometryFactor();
    603     radius *= 0.45 * this.getRadius();
    604
    605     for (var i = 0; i < this.director.trajectories.length; i++) {
    606       var trajectory = this.director.trajectories[i];
    607
    608       //Ensure static tubes are constructed
    609       if (view.staticTubes[i] === null || view.staticTubes[i] === undefined)
    610       {
    611         var color = this._colors[trajectory.metadataCategoryName] || 'red';
    612         view.staticTubes[i] = drawTrajectoryLineStatic(trajectory,
    613                                                        color,
    614                                                        radius);
    615       }
    616
    617       //Ensure static tube draw ranges are set to visible segment
    618       updateStaticTrajectoryDrawRange(trajectory, this.director.currentFrame,
    619                                       view.staticTubes[i]);
    620     }
    621
    622     //Remove any old dynamic tubes from the scene
    623     view.dynamicTubes.forEach(function(tube) {
    624       if (tube === undefined || tube === null) {
    625         return;
    626       }
    627       if (tube.parent !== null) {
    628         tube.parent.remove(tube);
    629         disposeTrajectoryLineDynamic(tube);
    630       }
    631     });
    632
    633     if (this.UIState['view.viewType'] !== 'parallel-plot') {
    634       //Construct new dynamic tubes containing necessary
    635       //interpolated segment for the current frame
    636       view.dynamicTubes = this.director.trajectories.map(function(trajectory) {
    637         var color = scope._colors[trajectory.metadataCategoryName] || 'red';
    638         var tube = drawTrajectoryLineDynamic(trajectory,
    639                                     scope.director.currentFrame,
    640                                     color,
    641                                     radius);
    642         return tube;
    643       });
    644     }
    645
    646     view.needsUpdate = true;
    647
    648     if (this.director.currentFrameIsGradientPoint()) {
    649       this.dispatchEvent({type: 'animation-new-frame-started', message: {
    650         frame: this._currentFrame,
    651         gradientPoint: this.director.gradientPoints[this._currentFrame],
    652         controller: this
    653       }});
    654
    655       this._currentFrame += 1;
    656     }
    657
    658
    659     this.director.updateFrame();
    660
    661     if (this.director.animationCycleFinished()) {
    662       this.director = null;
    663       this.playing = false;
    664
    665       // When the animation cycle finishes, update the state of the media
    666       // buttons and re-enable the rewind button so users can clear the
    667       // screen.
    668       this._updateButtons();
    669       this.$rewind.prop('disabled', false).button('refresh');
    670
    671       this.dispatchEvent({type: 'animation-ended', message: {
    672         gradient: this.getGradientCategory(),
    673         trajectory: this.getTrajectoryCategory(),
    674         controller: this
    675       }});
    676
    677     }
    678   };
    679
    680   /**
    681    *
    682    * Setter for the gradient category
    683    *
    684    * Represents how samples are ordered in each trajectory.
    685    *
    686    * @param {String} category The name of the category to set in the menu.
    687    */
    688   AnimationsController.prototype.setGradientCategory = function(category) {
    689     if (!this.hasMetadataField(category)) {
    690       category = '';
    691     }
    692
    693     this.$gradientSelect.val(category);
    694     this.$gradientSelect.trigger('chosen:updated');
    695     this.$gradientSelect.change();
    696   };
    697
    698   /**
    699    *
    700    * Getter for the gradient category
    701    *
    702    * Represents how samples are ordered in each trajectory.
    703    *
    704    * @return {String} The name of the gradient category in the menu.
    705    */
    706   AnimationsController.prototype.getGradientCategory = function() {
    707     return this.$gradientSelect.val();
    708   };
    709
    710   /**
    711    *
    712    * Setter for the trajectory category
    713    *
    714    * Represents how samples are grouped together.
    715    *
    716    * @param {String} category The name of the category to set in the menu.
    717    */
    718   AnimationsController.prototype.setTrajectoryCategory = function(category) {
    719     if (!this.hasMetadataField(category)) {
    720       category = '';
    721     }
    722
    723     this.$trajectorySelect.val(category);
    724     this.$trajectorySelect.trigger('chosen:updated');
    725     this.$trajectorySelect.change();
    726   };
    727
    728   /**
    729    *
    730    * Getter for the trajectory category
    731    *
    732    * Represents how samples are grouped together.
    733    *
    734    * @return {String} The name of the trajectory category in the menu.
    735    */
    736   AnimationsController.prototype.getTrajectoryCategory = function() {
    737     return this.$trajectorySelect.val();
    738   };
    739
    740   /**
    741    *
    742    * Setter for the speed of the animation.
    743    *
    744    * @param {Float} speed Speed at which the animation is played.
    745    * @throws {Error} If the radius value is lesser than or equal to 0 or
    746    * greater than 10.
    747    */
    748   AnimationsController.prototype.setSpeed = function(speed) {
    749     if (speed <= 0 || speed > 10) {
    750       throw new Error('The speed must be greater than 0 and lesser than 10');
    751     }
    752     this.$speed.slider('option', 'value', speed);
    753   };
    754
    755   /**
    756    *
    757    * Getter for the speed of the animation.
    758    *
    759    * @return {Float} Speed at which the animation is played.
    760    */
    761   AnimationsController.prototype.getSpeed = function() {
    762     return this.$speed.slider('option', 'value');
    763   };
    764
    765   /**
    766    *
    767    * Setter for the radius of the animation.
    768    *
    769    * @param {Float} radius Radius of the traces in the animations.
    770    * @throws {Error} If the radius value is lesser than or equal to 0 or
    771    * greater than 10.
    772    */
    773   AnimationsController.prototype.setRadius = function(radius) {
    774     if (radius <= 0 || radius > 10) {
    775       throw new Error('The radius must be greater than 0 and lesser than 10');
    776     }
    777     this.$radius.slider('option', 'value', radius);
    778   };
    779
    780   /**
    781    *
    782    * Getter for the radius of the traces in the animation.
    783    *
    784    * @return {Float} Radius of the traces in the animation
    785    */
    786   AnimationsController.prototype.getRadius = function() {
    787     return this.$radius.slider('option', 'value');
    788   };
    789
    790   /**
    791    * Converts the current instance into a JSON string.
    792    *
    793    * @return {Object} JSON ready representation of self.
    794    */
    795   AnimationsController.prototype.toJSON = function() {
    796     var json = {};
    797
    798     json.gradientCategory = this.getGradientCategory();
    799     json.trajectoryCategory = this.getTrajectoryCategory();
    800     json.speed = this.getSpeed();
    801     json.radius = this.getRadius();
    802     json.colors = this.getColors();
    803
    804     return json;
    805   };
    806
    807   /**
    808    * Decodes JSON string and modifies its own instance variables accordingly.
    809    *
    810    * @param {Object} Parsed JSON string representation of self.
    811    */
    812   AnimationsController.prototype.fromJSON = function(json) {
    813     this._rewindButtonClicked();
    814
    815     this.setGradientCategory(json.gradientCategory);
    816     this.setTrajectoryCategory(json.trajectoryCategory);
    817
    818     this.setSpeed(json.speed);
    819     this.setRadius(json.radius);
    820
    821     this.setColors(json.colors);
    822   };
    823
    824   return AnimationsController;
    825 });
    826