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