Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
controllers.js 58.70 KiB
/******************************************************************************
 * Vipra Application
 * Controllers
 ******************************************************************************/
/* globals angular, Vipra, moment, vis, prompt, randomColor, Highcharts, $ */
(function() {

  "use strict";

  var app = angular.module('vipra.controllers', []);

  app.controller('RootController', ['$scope', '$rootScope', '$state', '$window', 'hotkeys', 'TopicModelFactory',
    function($scope, $rootScope, $state, $window, hotkeys, TopicModelFactory) {

      $scope.rootModels = {
        topicModel: null,
        search: null
      };

      var prevTopicModelLoading = false;
      if (localStorage.tm) {
        prevTopicModelLoading = true;
        $rootScope.loadingScreen = true;

        TopicModelFactory.get({
          id: localStorage.tm
        }, function(data) {
          $scope.rootModels.topicModel = data;
          prevTopicModelLoading = false;
          $scope.loadingScreen = false;
        }, function() {
          delete localStorage.tm;
          prevTopicModelLoading = false;
          $scope.loadingScreen = false;
          $scope.chooseTopicModel();
        });
      }

      $scope.queryTopicModels = function() {
        TopicModelFactory.query({
          fields: '_all'
        }, function(data) {
          $scope.topicModels = data;
        });
      };

      $scope.chooseTopicModel = function() {
        if (prevTopicModelLoading)
          return;
        $scope.queryTopicModels();
        $scope.rootModels.topicModelModalOpen = true;
        $('#topicModelModal').modal();
        if ($scope.rootModels.topicModel)
          $('.selected-model').focus();
        else
          $('.topic-model').first().focus();
      };

      $scope.changeTopicModel = function(topicModel) {
        $scope.rootModels.topicModel = topicModel;
        $('#topicModelModal').modal('hide');
        localStorage.tm = topicModel.id;
      };

      $scope.showFeedbackModal = function() {
        $('#feedbackModal').modal();
        $('#feedbackIframe').attr('src', 'https://docs.google.com/forms/d/1RjXyGgw8F3v7QsfgOQKz0Koa2Dkr1wDNx3veRZr9I3o/viewform?embedded=true');
      };

      $scope.menubarSearch = function(query) {
        $state.transitionTo('index', {
          q: query
        });
      };

      $scope.goBack = function() {
        $window.history.back();
      };

      $scope.$on('$stateChangeSuccess', function() {
        $scope.rootModels.search = null;
      });

      hotkeys.add({
        combo: 's',
        description: 'Search for articles',
        callback: function($event) {
          if ($event.stopPropagation) $event.stopPropagation();
          if ($event.preventDefault) $event.preventDefault();
          if ($state.current.name === 'index')
            $('#searchBox').focus();
          else
            $('#menuSearchBox').focus();
        }
      });

      hotkeys.add({
        combo: 'i',
        description: 'Go to index',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'index')
            $state.transitionTo('index');
        }
      });

      hotkeys.add({
        combo: 'e',
        description: 'Go to explorer',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'explorer')
            $state.transitionTo('explorer');
        }
      });

      hotkeys.add({
        combo: 'n',
        description: 'Go to network',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'network')
            $state.transitionTo('network');
        }
      });

      hotkeys.add({
        combo: 'a',
        description: 'Go to articles',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'articles')
            $state.transitionTo('articles');
        }
      });

      hotkeys.add({
        combo: 't',
        description: 'Go to topics',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'topics')
            $state.transitionTo('topics');
        }
      });

      hotkeys.add({
        combo: 'i',
        description: 'Go to entities',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'entities')
            $state.transitionTo('entities');
        }
      });

      hotkeys.add({
        combo: 'w',
        description: 'Go to words',
        callback: function() {
          if ($scope.rootModels.topicModel && $state.current.name !== 'words')
            $state.transitionTo('words');
        }
      });

      hotkeys.add({
        combo: 'm',
        description: 'Choose a topic model',
        callback: function() {
          $scope.chooseTopicModel();
        }
      });

      $scope.showCheatSheet = hotkeys.toggleCheatSheet;
    }
  ]);

  /**
   * Index controller
   */
  app.controller('IndexController', ['$scope', '$stateParams', '$location', '$timeout', 'ArticleFactory', 'TopicFactory', 'SearchFactory',
    function($scope, $stateParams, $location, $timeout, ArticleFactory, TopicFactory, SearchFactory) {

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel)
        $scope.chooseTopicModel();

      $scope.searchResultsStep = 10;
      $scope.advancedSearchEnabled = false;
      $scope.search = $stateParams.q || $scope.search;

      $scope.$watch('rootModels.topicModel', function() {
        if (!$scope.rootModels.topicModel) return;

        ArticleFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          limit: 3,
          sort: '-created'
        }, function(data) {
          $scope.latestArticles = data;
        });

        TopicFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          limit: 3,
          sort: '-created'
        }, function(data) {
          $scope.latestTopics = data;
        });
      });

      $scope.$watchGroup(['search', 'rootModels.topicModel', 'rootModels.advFromDate', 'rootModels.advToDate'], function() {
        if ($scope.search && $scope.rootModels.topicModel) {
          $location.search('q', $scope.search);
          $scope.goSearch();
        } else {
          $location.search('q', null);
          $scope.searchResults = [];
        }
      });

      $scope.goSearch = function() {
        $scope.searching = true;
        $scope.skip = 0;
        $scope.searchResults = [];
        $scope.loadMoreResults();
      };

      $scope.loadMoreResults = function() {
        SearchFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          skip: $scope.skip,
          limit: $scope.searchResultsStep,
          query: $scope.search,
          from: $scope.rootModels.advFromDate ? $scope.rootModels.advFromDate.getTime() : null,
          to: $scope.rootModels.advToDate ? $scope.rootModels.advToDate.getTime() : null
        }, function(data, headers) {
          $scope.searching = false;
          $scope.searchResults.push.apply($scope.searchResults, data);
          $scope.skip += $scope.searchResultsStep;
          $scope.totalResults = headers("V-Total");
          $scope.noMoreResults = data.length < $scope.searchResultsStep;
        }, function() {
          $scope.searching = false;
        });
      };
    }
  ]);

  /**
   * About controller
   */
  app.controller('AboutController', ['$scope', 'InfoFactory',
    function($scope, InfoFactory) {

      InfoFactory.get(function(data) {
        $scope.info = data;
        $scope.buildDate = Vipra.formatDateTime(moment($scope.info.app.builddate, 'YYMMDD_HHmm').toDate());
        $scope.startTime = Vipra.formatDateTime(moment($scope.info.vm.starttime, 'x').toDate());
        $scope.upTime = moment.duration($scope.info.vm.uptime).humanize();
      });
    }
  ]);

  /**
   * Network controller
   */
  app.controller('NetworkController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory', 'TopicFactory', 'WordFactory', 'WindowFactory',
    function($scope, $state, $stateParams, $timeout, ArticleFactory, TopicFactory, WordFactory, WindowFactory) {

      var id = 0,
        ids = {},
        edges = {};

      $scope.colors = {
        articles: '#BBC9D2',
        topics: '#DBB234',
        words: '#FFFFFF'
      };

      $scope.nodes = new vis.DataSet();
      $scope.edges = new vis.DataSet();
      $scope.data = {
        nodes: $scope.nodes,
        edges: $scope.edges
      };

      $scope.options = {
        nodes: {
          font: {
            size: 14
          },
          shape: 'dot',
          borderWidth: 1
        },
        edges: {
          color: {
            highlight: '#f00'
          }
        },
        layout: {
          randomSeed: 1
        },
        physics: {
          barnesHut: {
            springConstant: 0.008,
            gravitationalConstant: -3500
          }
        },
        interaction: {
          tooltipDelay: 200,
          hideEdgesOnDrag: true
        }
      };

      $scope.shown = {
        articles: true,
        similararticles: false,
        topics: true,
        words: false
      };

      var newNode = function(title, type, show, dbid, color, shape, loader, x, y) {
        ids[dbid] = ++id;
        return {
          id: id,
          x: x,
          y: y,
          title: title,
          label: title.multiline(5).ellipsize(50),
          type: type,
          show: show,
          dbid: dbid,
          shape: shape || 'dot',
          loader: loader,
          borderWidth: 1,
          origColor: {
            background: color,
            highlight: {
              background: color,
            }
          },
          color: {
            border: '#000',
            background: color,
            highlight: {
              background: color,
            }
          },
          font: {
            color: '#000'
          },
          shadow: {
            enabled: true
          }
        };
      };

      var newEdge = function(from, to) {
        return {
          from: from,
          to: to,
          selectionWidth: 1,
          color: {
            color: '#333',
            highlight: '#f00'
          }
        };
      };

      var topicNode = function(topic, x, y) {
        topic = topic.topic || topic;
        return newNode(topic.name, 'topic', 'topics.show', topic.id, $scope.colors.topics, 'triangle', $scope.loadTopic, x, y);
      };

      var articleNode = function(article, x, y) {
        return newNode(article.title, 'article', 'articles.show', article.id, $scope.colors.articles, 'square', $scope.loadArticle, x, y);
      };

      var wordNode = function(word, x, y) {
        return newNode(word.id, 'word', 'words.show', word.id, $scope.colors.words, 'box', $scope.loadWord, x, y);
      };

      var edgeExists = function(idA, idB) {
        if (idB < idA) {
          var tmp = idA;
          idA = idB;
          idB = tmp;
        }
        return edges.hasOwnProperty(idA + '-' + idB);
      };

      var addEdge = function(idA, idB) {
        if (idB < idA) {
          var tmp = idA;
          idA = idB;
          idB = tmp;
        }
        edges[idA + '-' + idB] = 1;
      };

      // construct new nodes
      var constructor = function(result, nodeFunction, node, props) {
        if (result) {
          var newNodes = [],
            newEdges = [];
          for (var i = 0; i < result.length; i++) {
            var current = result[i];
            if (ids.hasOwnProperty(current.id)) {
              if (node && edgeExists(ids[current.id], node.id))
                continue;
              if(node) {
                newEdges.push(newEdge(ids[current.id], node.id));
                addEdge(ids[current.id], node.id);
              }
            } else {
              if(node) {
                if(props) {
                  newNodes.push(nodeFunction(current, props.pointer.canvas.x, props.pointer.canvas.y));
                } else {
                  newNodes.push(nodeFunction(current));
                }
                newEdges.push(newEdge(id, node.id));
                addEdge(id, node.id);
              } else {
                newNodes.push(nodeFunction(current));
              }
            }
          }
          if (newNodes.length)
            $scope.nodes.add(newNodes);
          if (newEdges.length)
            $scope.edges.add(newEdges);
        }
      };

      // on node select
      $scope.select = function(props) {
        var node = $scope.nodes.get(props.nodes[0]);
        if (node && !node.loaded) {
          node.loader(node, props);
          node.loaded = true;
          $scope.nodes.update(node);
        }
      };

      $scope.deselect = function() {
        $scope.$apply(function() {
          $scope.currentArticle = null;
        });
      };

      $scope.loadArticle = function(node, props) {
        ArticleFactory.get({
          id: node.dbid,
          fields: '_all'
        }, function(data) {
          $scope.currentArticle = data;
          var i;
          if (data.topics) {
            for (i = 0; i < data.topics.length; i++)
              data.topics[i] = data.topics[i].topic;
            constructor(data.topics, topicNode, node, props);
          }
          if (data.similarArticles && $scope.shown.similararticles) {
            var articles = [];
            for (i = 0; i < data.similarArticles.length; i++)
              articles.push(data.similarArticles[i].article);
            constructor(articles, articleNode, node, props);
          }
        });
      };

      $scope.loadTopic = function(node, props) {
        if ($scope.shown.articles) {
          TopicFactory.articles({
            id: node.dbid,
            limit: 50
          }, function(data) {
            constructor(data, articleNode, node, props);
          });
        }
        if ($scope.shown.words) {
          TopicFactory.get({
            id: node.dbid,
            limit: 50
          }, function(data) {
            constructor(data.words, wordNode, node, props);
          });
        }
      };

      $scope.loadWord = function(node, props) {
        if ($scope.shown.articles) {
          ArticleFactory.query({
            word: node.dbid,
            topicModel: $scope.rootModels.topicModel.id,
            limit: 50
          }, function(data) {
            constructor(data, articleNode, node, props);
          });
        }
        if ($scope.shown.topics) {
          TopicFactory.query({
            word: node.dbid,
            topicModel: $scope.rootModels.topicModel.id,
            limit: 50
          }, function(data) {
            constructor(data, topicNode, node, props);
          });
        }
      };

      // on node open
      $scope.open = function(props) {
        var node = $scope.nodes.get(props.nodes[0]);
        $state.transitionTo(node.show, {
          id: node.dbid
        });
      };

      $scope.reset = function() {
        $state.go($state.current, {}, {
          reload: true
        });
      };

      $scope.fit = function() {
        $scope.graph.fit({
          animation: {
            offset: {x: 0, y: 0},
            duration: 1000,
            easingFunction: 'easeInOutQuad'
          }
        });
      };

      $scope.completeNetwork = function() {
        var nodes = $scope.nodes.get(),
          updates = [];
        for(var i = 0; i < nodes.length; i++)
          nodes[i].loader(nodes[i]);
        $scope.nodes.update(updates);
      };

      $scope.$watch('rootModels.topicModel', function(newVal) {
        if ($scope.rootNode && $scope.rootNode.topicModel.id !== newVal.id)
          $state.transitionTo('index');
      });

      $scope.$watch('searchNodes', function() {
        var nodes = $scope.nodes.get();
        var i;
        var updates = [];
        if($scope.searchNodes) {
          for(i = 0; i < nodes.length; i++) {
            if(nodes[i].title.indexOf($scope.searchNodes) != -1) {
              updates.push({
                id: nodes[i].id,
                color: nodes[i].origColor,
                font: {
                  color: '#000'
                }
              });
            } else {
              updates.push({
                id: nodes[i].id,
                color: {
                  background: '#eee'
                },
                font: {
                  color: '#ccc'
                }
              });
            }
          }
        } else {
          for(i = 0; i < nodes.length; i++) {
            updates.push({
              id: nodes[i].id,
              color: nodes[i].origColor,
              font: {
                color: '#000'
              }
            });
          }
        }

        $scope.data.nodes.update(updates);
      });

      // create graph
      var container = document.getElementById("visgraph");
      $scope.graph = new vis.Network(container, $scope.data, $scope.options);
      $scope.graph.on('selectNode', $scope.select);
      $scope.graph.on('deselectNode', $scope.deselect);
      $scope.graph.on('doubleClick', $scope.open);

      if($stateParams.type) {
        // if the topic model is not set, the page was refreshed
        // set to true, id of current node decides topic model
        if (!$scope.rootModels.topicModel)
          $scope.rootModels.topicModel = true;

        // type is given, load node
        var factory;
        if ($stateParams.type === 'articles')
          factory = ArticleFactory;
        else if ($stateParams.type === 'topics')
          factory = TopicFactory;
        else if ($stateParams.type === 'word')
          factory = WordFactory;

        // get root node
        factory.get({
          id: $stateParams.id
        }, function(data) {
          $scope.rootNode = data;

          // add root node
          if ($stateParams.type === 'articles')
            $scope.nodes.add([articleNode(data)]);
          else if ($stateParams.type === 'topics')
            $scope.nodes.add([topicNode(data)]);
          else if ($stateParams.type === 'words')
            $scope.nodes.add([wordNode(data)]);
          ids[data.id] = id;

          // take topic model from node
          if (!angular.isObject($scope.rootModels.topicModel))
            $scope.rootModels.topicModel = data.topicModel;
        });
      } else {
        $scope.queryTopics = function() {
          if ($scope.shown.topics) {
            TopicFactory.query({
              topicModel: $scope.rootModels.topicModel.id
            }, function(data) {
              constructor(data, topicNode);
            });
          }
        };

        $scope.$watch('rootModels.topicModel', function() {
          $scope.queryTopics();
        });

        // page was reloaded, choose topic model
        if (!$scope.rootModels.topicModel)
          $scope.chooseTopicModel();
        else
          $scope.queryTopics();
      }

      $scope.reloadWindows = function() {
        if(!$scope.rootModels.topicModel) return;

        WindowFactory.query({
          topicModel: $scope.rootModels.topicModel.id
        }, function(data) {
          $scope.windows = data;
        });
      };

      $scope.$watch('rootModels.topicModel', function() {
        $scope.reloadWindows();
      });
    }
  ]);

  app.controller('ExplorerController', ['$scope', '$templateCache', '$timeout', 'TopicFactory',
    function($scope, $templateCache, $timeout, TopicFactory) {

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel)
        $scope.chooseTopicModel();

      $scope.reflowCharts = function() {
        var topicHC = $('#topicRelChart').highcharts();
        if (topicHC) topicHC.reflow();
        var wordHC = $('#wordEvoChart').highcharts();
        if (wordHC) wordHC.reflow();
      };

      $timeout(function() {
        $('#outer').layout({
          applyDefaultStyles: true,
          south__size: '50%'
        });
        $('#upper').layout({
          applyDefaultStyles: true,
          center__onresize: function() {
            $scope.reflowCharts();
          },
          west: {
            size: 250
          }
        });
        $('#lower').layout({
          applyDefaultStyles: true,
          center__onresize: function() {
            $scope.reflowCharts();
          },
          west: {
            size: 250
          }
        });
      });

      var avgMax = 0,
        varMax = 0,
        fallingMax = 0,
        risingMax = 0,
        risingDecayMax = 0,
        risingDecayMin = 0;

      $scope.explorerModels = {
        sorttopics: 'name',
        sortdir: false,
        seqstyle: 'absolute',
        chartstyle: 'areaspline',
        chartstack: 'none',
        articlesSort: 'title'
      };

      $scope.$watch('rootModels.topicModel', function() {
        if (!$scope.rootModels.topicModel) return;

        TopicFactory.query({
          fields: 'name,sequences,avgRelevance,varRelevance,risingRelevance,fallingRelevance,risingDecayRelevance,words',
          topicModel: $scope.rootModels.topicModel.id
        }, function(data) {
          $scope.topics = data;
          var colors = randomColor({
            count: $scope.topics.length
          });
          for (var i = 0, t; i < $scope.topics.length; i++) {
            t = $scope.topics[i];
            t.color = colors[i];
            avgMax = Math.max(t.avgRelevance, avgMax);
            varMax = Math.max(t.varRelevance, varMax);
            fallingMax = Math.max(t.fallingRelevance, fallingMax);
            risingMax = Math.max(t.risingRelevance, risingMax);
            risingDecayMax = Math.max(t.risingDecayRelevance, risingDecayMax);
            risingDecayMin = Math.max(t.risingDecayRelevance, risingDecayMin);
          }
          risingDecayMin = Math.abs(risingDecayMin);
          risingDecayMax = risingDecayMin + Math.abs(risingDecayMax);

          $scope.redrawGraph();
        });
      });

      $scope.checkTopics = function(to) {
        var toggle = typeof to === 'undefined';
        for (var i = 0, t; i < $scope.topics.length; i++) {
          t = $scope.topics[i];
          t.selected = toggle ? !t.selected : to;
        }
        $scope.redrawGraph();
        $scope.changeSelectedTopics();
      };

      $scope.changeSelectedTopics = function() {
        if($scope.explorerModels.activeTopic && !$scope.explorerModels.activeTopic.selected) {
          delete $scope.explorerModels.activeTopic;
          delete $scope.explorerModels.activeSequence;
        }
        $scope.redrawGraph();
      };

      $scope.redrawGraph = function() {
        if (!$scope.topics) return;
        var series = [];

        // create series of selected topics
        for (var i = 0; i < $scope.topics.length; i++) {
          if ($scope.topics[i].selected) {
            var topic = $scope.topics[i],
              relevances = [];

            // data array with relevances and min/max
            for (var j = 0, sequence, relevance; j < topic.sequences.length; j++) {
              sequence = topic.sequences[j];
              relevance = $scope.explorerModels.seqstyle === 'relative' ? sequence.relevanceChange : sequence.relevance;
              relevances.push({
                x: new Date(sequence.window.startDate).getTime(),
                y: relevance,
                sequence: sequence,
                topic: topic
              });
            }

            series.push({
              id: topic.id,
              name: topic.name,
              data: relevances,
              color: topic.color,
              topic: topic,
              zIndex: i + 1
            });
          }
        }

        // highcharts configuration
        $scope.topicSeq = areaRelevanceChart(series, 'Topic Relevance', $scope.explorerModels.chartstyle,
          $scope.explorerModels.chartstack, $scope.pointSelected);
        $scope.topicsSelected = series.length;
      };

      $scope.topicCurrValue = function(topic) {
        var percent = 0;
        switch ($scope.explorerModels.sorttopics) {
          case 'avgRelevance':
            percent = topic.avgRelevance / avgMax;
            break;
          case 'varRelevance':
            percent = topic.varRelevance / varMax;
            break;
          case 'fallingRelevance':
            percent = topic.fallingRelevance / fallingMax;
            break;
          case 'risingRelevance':
            percent = topic.risingRelevance / risingMax;
            break;
          case 'risingDecayRelevance':
            percent = (topic.risingDecayRelevance + risingDecayMin) / risingDecayMax;
            break;
        }
        return (percent * 100) + '%';
      };

      $scope.highlightSeries = function(id, toggle) {
        if (!$scope.topicsSelected) return;
        var highcharts = $('#topicRelChart').highcharts();
        if (!highcharts) return;
        var series = highcharts.get(id);
        if (!series) return;

        if (toggle) {
          series.onMouseOver();
          series.group.zIndexOrig = series.group.zIndex;
          series.group.zIndexSetter(9999, 'zIndex');
          series.graph.attr('stroke', '#000000').attr('stroke-dasharray', '5,5');
        } else {
          series.onMouseOut();
          series.group.zIndexSetter(series.group.zIndexOrig, 'zIndex');
          series.graph.attr('stroke', series.color).attr('stroke-dasharray', '');
        }
      };

      $scope.resetZoom = function() {
        if (!$scope.topicsSelected) return;
        var highcharts = $('#topicRelChart').highcharts();
        if (!highcharts) return;

        highcharts.xAxis[0].setExtremes(null, null);
      };

      $scope.pointSelected = function(e) {
        $scope.$apply(function() {
          $scope.activateTopic(e.point.topic);
          $scope.activateSequence(e.point.sequence);
        });
      };

      $scope.activateTopic = function(topic) {
        $scope.explorerModels.activeTopic = topic;
      };

      $scope.activateSequence = function(seq) {
        $scope.explorerModels.activeSequence = seq;
      };

      $scope.clearSequence = function() {
        if(!$scope.explorerModels.activeSequence) return;

        delete $scope.explorerModels.activeSequence;
        delete $scope.articles;
        var selectedPoints = $('#topicRelChart').highcharts().getSelectedPoints();
        for(var i = 0; i < selectedPoints.length; i++) {
          selectedPoints[i].select(false);
        }
      };

      $scope.sequenceChanged = function() {
        if(!$scope.explorerModels.activeTopic || !$scope.explorerModels.activeSequence) return;

        $scope.articles = [];
        $scope.showMoreArticles();
      };

      $scope.showMoreArticles = function(limit) {
        $scope.loadingArticles = true;
        TopicFactory.articles({
          skip: $scope.articles ? $scope.articles.length : 0,
          limit: typeof limit === 'undefined' ? 20 : limit,
          id: $scope.explorerModels.activeTopic.id,
          from: new Date($scope.explorerModels.activeSequence.window.startDate).getTime(),
          to: new Date($scope.explorerModels.activeSequence.window.endDate).getTime()
        }, function(data, headers) {
          $scope.articles.push.apply($scope.articles, data);
          $scope.articlesTotal = headers("V-Total");
          $scope.loadingArticles = false;
        }, function() {
          $scope.loadingArticles = false;
        });
      };

      $scope.showAllArticles = function() {
        $scope.showMoreArticles(0);
      };

      $scope.$watchGroup(['explorerModels.seqstyle', 'explorerModels.chartstyle', 'explorerModels.chartstack'], $scope.redrawGraph);

      $scope.$watch('explorerModels.sorttopics', function() {
        if (!$scope.topics) return;

        if ($scope.explorerModels.sorttopics === 'name') {
          $scope.explorerModels.sortdir = false;
        } else {
          $scope.explorerModels.sortdir = true;
        }

        $timeout(function() {
          for (var i = 0; i < $scope.topics.length; i++)
            $scope.topics[i].topicCurrValue = $scope.topicCurrValue($scope.topics[i]);
        }, 0);
      });

      $scope.$watch('explorerModels.activeTopic', function() {
        if(!$scope.explorerModels.activeTopic) return;

        // preselect some words
        if ($scope.explorerModels.activeTopic.words) {
          for (var i = 0; i < Math.min(3, $scope.explorerModels.activeTopic.words.length); i++)
            $scope.explorerModels.activeTopic.words[i].selected = true;
        }
      });

      $scope.$watch('explorerModels.activeSequence', function() {
        $scope.sequenceChanged();
      });
    }
  ]);

  /****************************************************************************
   * Article Controllers
   ****************************************************************************/

  /**
   * Article Index route
   */
  app.controller('ArticlesIndexController', ['$scope', '$state', 'ArticleFactory',
    function($scope, $state, ArticleFactory) {

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'articles')
        $scope.chooseTopicModel();

      $scope.articlesIndexModels = {
        sortkey: 'date',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.reloadArticles = function() {
        if (!$scope.rootModels.topicModel) return;

        $scope.loadingArticles = true;
        ArticleFactory.query({
          skip: ($scope.articlesIndexModels.page - 1) * $scope.articlesIndexModels.limit,
          limit: $scope.articlesIndexModels.limit,
          sort: ($scope.articlesIndexModels.sortdir ? '' : '-') + $scope.articlesIndexModels.sortkey,
          topicModel: $scope.rootModels.topicModel.id,
          char: $scope.articlesIndexModels.startChar,
          contains: $scope.articlesIndexModels.contains
        }, function(data, headers) {
          $scope.articles = data;
          $scope.articlesTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.articlesIndexModels.limit);
          $scope.loadingArticles = false;
        }, function() {
          $scope.loadingArticles = false;
        });
      };

      $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'rootModels.topicModel'], function() {
        $scope.reloadArticles();
      });

      $scope.$watchGroup(['articlesIndexModels.startChar', 'articlesIndexModels.contains'], function() {
        if($scope.articlesIndexModels.page !== 1)
          $scope.articlesIndexModels.page = 1;
        else
          $scope.reloadArticles();
      });
    }
  ]);

  /**
   * Article Show route
   */
  app.controller('ArticlesShowController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory',
    function($scope, $state, $stateParams, $timeout, ArticleFactory) {

      $scope.articlesShowModels = {
        topicsSort: '-share',
        similarSort: '-share',
        wordsSort: '-count',
        entitiesSort: '-count'
      };

      ArticleFactory.get({
        id: $stateParams.id
      }, function(data) {
        $scope.article = data;
        $scope.articleDate = Vipra.formatDate($scope.article.date);
        $scope.articleCreated = Vipra.formatDateTime($scope.article.created);
        $scope.articleModified = Vipra.formatDateTime($scope.article.modified);

        // calculate share from divergence
        if ($scope.article.similarArticles) {
          for (var articleIndex = 0; articleIndex < $scope.article.similarArticles.length; articleIndex++)
            $scope.article.similarArticles[articleIndex].share = Math.round(((1 - $scope.article.similarArticles[articleIndex].divergence) * 100));
        }

        // take topic model from article
        if (!angular.isObject($scope.rootModels.topicModel))
          $scope.rootModels.topicModel = data.topicModel;

        // calculate percentage share
        var topicShareSeries = [];

        if ($scope.article.topics) {
          var topics = $scope.article.topics,
            colors = randomColor({
              count: $scope.article.topics.length
            });

          for (var topicIndex = 0, d; topicIndex < topics.length; topicIndex++) {
            d = {
              name: topics[topicIndex].topic.name,
              y: topics[topicIndex].share,
              color: colors[topicIndex],
              id: topics[topicIndex].topic.id
            };

            topicShareSeries.push(d);
            $scope.article.topics[topicIndex].color = colors[topicIndex];
          }
        }

        $timeout(function() {
          // highcharts data
          $scope.topicShare = topicShareChart([{
            name: 'Topic Share',
            colorByPoint: true,
            data: topicShareSeries
          }]);
        }, 0);
      });

      $scope.$watch('rootModels.topicModel', function(newVal) {
        if ($scope.article && $scope.article.topicModel.id !== newVal.id)
          $state.transitionTo('index');
      });

      $scope.openTabWords = function() {
        if ($scope.words) return;

        ArticleFactory.get({
          id: $stateParams.id,
          fields: 'words'
        }, function(data) {
          $scope.allWords = data.words;
          $scope.showMoreWords();
        });
      };

      var wordsCount = 0;
      $scope.showMoreWords = function() {
        wordsCount += 20;
        $scope.words = $scope.allWords.slice(0, wordsCount);
      };

      $scope.showAllWords = function() {
        wordsCount = $scope.allWords.length;
        $scope.words = $scope.allWords;
      };

      $scope.openTabEntities = function() {
        if ($scope.entities) return;

        ArticleFactory.get({
          id: $stateParams.id,
          fields: 'entities'
        }, function(data) {
          $scope.allEntities = data.entities;
          $scope.showMoreEntities();
        });
      };

      var entitiesCount = 0;
      $scope.showMoreEntities = function() {
        entitiesCount += 20;
        $scope.entities = $scope.allEntities.slice(0, entitiesCount);
      };

      $scope.showAllEntities = function() {
        entitiesCount = $scope.allEntities.length;
        $scope.entities = $scope.allEntities;
      };

      var topicShareChartElement = $('#topic-share');
      $scope.highlightSlice = function(id, toggle) {
        var highcharts = topicShareChartElement.highcharts();
        if (!highcharts) return;
        var point = highcharts.get(id);
        if (!point) return;

        if (toggle) {
          point.onMouseOver();
        } else {
          point.onMouseOut();
          highcharts.tooltip.hide();
        }
      };
    }
  ]);

  /****************************************************************************
   * Topic Controllers
   ****************************************************************************/

  /**
   * Topic Index route
   */
  app.controller('TopicsIndexController', ['$scope', '$state', 'TopicFactory',
    function($scope, $state, TopicFactory) {

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'topics')
        $scope.chooseTopicModel();

      $scope.topicsIndexModels = {
        sortkey: 'name',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.reloadTopics = function() {
        if (!$scope.rootModels.topicModel) return;

        $scope.loadingTopics = true;
        TopicFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          skip: ($scope.topicsIndexModels.page - 1) * $scope.topicsIndexModels.limit,
          limit: $scope.topicsIndexModels.limit,
          sort: ($scope.topicsIndexModels.sortdir ? '' : '-') + $scope.topicsIndexModels.sortkey,
          char: $scope.topicsIndexModels.startChar,
          contains: $scope.topicsIndexModels.contains
        }, function(data, headers) {
          $scope.topics = data;
          $scope.topicsTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.topicsTotal / $scope.topicsIndexModels.limit);
          $scope.loadingTopics = false;
        }, function() {
          $scope.loadingTopics = false;
        });
      };

      $scope.$watchGroup(['topicsIndexModels.page', 'topicsIndexModels.sortkey', 'topicsIndexModels.sortdir', 'rootModels.topicModel'], function() {
        $scope.reloadTopics();
      });

      $scope.$watchGroup(['topicsIndexModels.startChar', 'topicsIndexModels.contains'], function() {
        if($scope.topicsIndexModels.page !== 1)
          $scope.topicsIndexModels.page = 1;
        else
          $scope.reloadTopics();
      });
    }
  ]);

  /**
   * Topic Show route
   */
  app.controller('TopicsShowController', ['$scope', '$state', '$stateParams', '$timeout', 'TopicFactory', 'SequenceFactory',
    function($scope, $state, $stateParams, $timeout, TopicFactory, SequenceFactory) {

      $scope.topicsShowModels = {
        relSeqstyle: 'absolute',
        relChartstyle: 'areaspline',
        wordSeqstyle: 'absolute',
        wordChartstyle: 'spline',
        seqSortWords: '-probability'
      };

      TopicFactory.get({
        id: $stateParams.id
      }, function(data) {
        $scope.topic = data;
        $scope.topicCreated = Vipra.formatDateTime($scope.topic.created);
        $scope.topicModified = Vipra.formatDateTime($scope.topic.modified);

        // take topic model from topic
        if (!angular.isObject($scope.rootModels.topicModel))
          $scope.rootModels.topicModel = data.topicModel;

        // preselect some words
        if ($scope.topic.words) {
          for (var i = 0; i < Math.min(3, $scope.topic.words.length); i++)
            $scope.topic.words[i].selected = true;
        }

        // preselect first sequence
        if ($scope.topic.sequences && $scope.topic.sequences.length)
          $scope.topicsShowModels.sequence = $scope.topic.sequences[0];
        
        $scope.redrawRelevanceGraph();
      });

      $scope.redrawRelevanceGraph = function() {
        if (!$scope.topic || !$scope.topic.sequences) return;
        var relevances = [];

        // create series
        for (var i = 0, sequence, relevance; i < $scope.topic.sequences.length; i++) {
          sequence = $scope.topic.sequences[i];
          relevance = $scope.topicsShowModels.relSeqstyle === 'relative' ? sequence.relevanceChange : sequence.relevance;
          relevances.push([new Date(sequence.window.startDate).getTime(), relevance]);
        }

        // highcharts configuration
        $scope.topicSeq = areaRelevanceChart([{
          name: $scope.topic.name,
          data: relevances
        }], 'Topic Relevance', $scope.topicsShowModels.relChartstyle);
      };

      $scope.resetRelZoom = function() {
        if (!$scope.topic) return;
        var highcharts = $('#topicRelChart').highcharts();
        if (!highcharts) return;

        highcharts.xAxis[0].setExtremes(null, null);
      };

      $scope.startRename = function() {
        $scope.origName = $scope.topic.name;
        $scope.isRename = true;
        $timeout(function() {
          document.getElementById('topicName').select();
        }, 0);
      };

      $scope.endRename = function(save) {
        delete $scope.renameErrors;
        if (save) {
          TopicFactory.update({
            id: $scope.topic.id
          }, $scope.topic, function(data) {
            $scope.topic = data;
            $scope.isRename = false;

            // if topic list of parent view is loaded, replace name by new name
            if ($scope.$parent.topics) {
              for (var i = 0, topic; i < $scope.$parent.topics.length; i++) {
                topic = $scope.$parent.topics[i];
                if (topic.id === data.id) {
                  break;
                }
              }
            }
          });
        } else {
          $scope.isRename = false;
          $scope.topic.name = $scope.origName;
        }
      };

      $scope.keyup = function($event) {
        if ($event.which === 13 || $event.which === 27) {
          $scope.endRename($event.which === 13);
          $event.preventDefault();
        }
      };

      $scope.recalcSeqChange = function() {
        if (!$scope.sequence || !$scope.sequenceCompare) return;
        wordLoop:
          for (var i = 0, word; i < $scope.sequence.words.length; i++) {
            word = $scope.sequence.words[i];
            for (var j = 0, word2; j < $scope.sequenceCompare.words.length; j++) {
              word2 = $scope.sequenceCompare.words[j];
              if (word.id === word2.id) {
                word.change = word2.change = j - i;
                continue wordLoop;
              }
            }
            word.change = '-';
          }
      };

      $scope.closeCompare = function() {
        delete $scope.sequenceCompare;
        delete $scope.topicsShowModels.sequenceCompare;
      };

      $scope.$watch('topicsShowModels.relSeqstyle', $scope.redrawRelevanceGraph);
      $scope.$watch('topicsShowModels.relChartstyle', $scope.redrawRelevanceGraph);

      $scope.$watch('topicsShowModels.sequence', function() {
        if (!$scope.topicsShowModels.sequence) return;

        SequenceFactory.get({
          id: $scope.topicsShowModels.sequence.id
        }, function(data) {
          $scope.sequence = data;
          $scope.recalcSeqChange();
        });
      });

      $scope.$watch('topicsShowModels.sequenceCompare', function() {
        if (!$scope.topicsShowModels.sequenceCompare) return;

        SequenceFactory.get({
          id: $scope.topicsShowModels.sequenceCompare.id
        }, function(data) {
          $scope.sequenceCompare = data;
          $scope.recalcSeqChange();
        });
      });

      $scope.$watch('rootModels.topicModel', function(newVal) {
        if ($scope.topic && $scope.topic.topicModel.id !== newVal.id)
          $state.transitionTo('index');
      });

      $('.tab-sequences').on('mouseleave', '.compare-row', function() {
        $('[word-id=' + $(this).attr('word-id') + ']').removeClass('highlight');
      });

      $('.tab-sequences').on('mouseenter', '.compare-row', function() {
        $('[word-id=' + $(this).attr('word-id') + ']').addClass('highlight');
      });
    }
  ]);

  /**
   * Topic Show Articles route
   */
  app.controller('TopicsArticlesController', ['$scope', '$stateParams', 'TopicFactory',
    function($scope, $stateParams, TopicFactory) {

      $scope.topicsArticlesModels = {
        sortkey: 'title',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.$watchGroup(['topicsArticlesModels.page', 'topicsArticlesModels.sortkey', 'topicsArticlesModels.sortdir'], function() {
        TopicFactory.articles({
          id: $stateParams.id,
          skip: ($scope.topicsArticlesModels.page - 1) * $scope.topicsArticlesModels.limit,
          limit: $scope.topicsArticlesModels.limit,
          sort: ($scope.topicsArticlesModels.sortdir ? '' : '-') + $scope.topicsArticlesModels.sortkey
        }, function(data, headers) {
          $scope.articles = data;
          $scope.articlesTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.topicsArticlesModels.limit);
        });
      });

    }
  ]);

  /****************************************************************************
   * Entity Controllers
   ****************************************************************************/

  app.controller('EntitiesIndexController', ['$scope', '$state', 'EntityFactory',
    function($scope, $state, EntityFactory) {

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'entities')
        $scope.chooseTopicModel();

      $scope.entitiesIndexModels = {
        sortkey: 'id',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.reloadEntities = function() {
        if (!$scope.rootModels.topicModel) return;

        $scope.loadingEntities = true;
        EntityFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          skip: ($scope.entitiesIndexModels.page - 1) * $scope.entitiesIndexModels.limit,
          limit: $scope.entitiesIndexModels.limit,
          sort: ($scope.entitiesIndexModels.sortdir ? '' : '-') + $scope.entitiesIndexModels.sortkey,
          char: $scope.entitiesIndexModels.startChar,
          contains: $scope.entitiesIndexModels.contains
        }, function(data, headers) {
          $scope.entities = data;
          $scope.entitiesTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.entitiesTotal / $scope.entitiesIndexModels.limit);
          $scope.loadingEntities = false;
        }, function() {
          $scope.loadingEntities = false;
        });
      };

      $scope.$watchGroup(['entitiesIndexModels.page', 'entitiesIndexModels.sortkey', 'entitiesIndexModels.sortdir', 'rootModels.topicModel'], function() {
        $scope.reloadEntities();
      });

      $scope.$watchGroup(['entitiesIndexModels.startChar', 'entitiesIndexModels.contains'], function() {
        if($scope.entitiesIndexModels.page !== 1)
          $scope.entitiesIndexModels.page = 1;
        else
          $scope.reloadEntities();
      });
    }
  ]);

  app.controller('EntitiesShowController', ['$scope', '$stateParams', 'EntityFactory',
    function($scope, $stateParams, EntityFactory) {

      EntityFactory.get({
        id: $stateParams.id
      }, function(data) {
        $scope.entity = data;
        $scope.entityCreated = Vipra.formatDateTime($scope.entity.created);
        $scope.entityModified = Vipra.formatDateTime($scope.entity.modified);

        // take entity model from entity
        if (!angular.isObject($scope.rootModels.topicModel))
          $scope.rootModels.topicModel = data.topicModel;
      });
    }
  ]);

  app.controller('EntitiesArticlesController', ['$scope', '$state', '$stateParams', 'ArticleFactory',
    function($scope, $state, $stateParams, ArticleFactory) {

      $scope.entity = $stateParams.id;

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'entities.articles')
        $scope.chooseTopicModel();

      $scope.entitiesArticlesModels = {
        sortkey: 'date',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.$watchGroup(['entitiesArticlesModels.page', 'entitiesArticlesModels.sortkey', 'entitiesArticlesModels.sortdir', 'rootModels.topicModel'], function() {
        if (!$scope.rootModels.topicModel) return;

        ArticleFactory.query({
          skip: ($scope.entitiesArticlesModels.page - 1) * $scope.entitiesArticlesModels.limit,
          limit: $scope.entitiesArticlesModels.limit,
          sort: ($scope.entitiesArticlesModels.sortdir ? '' : '-') + $scope.entitiesArticlesModels.sortkey,
          topicModel: $scope.rootModels.topicModel.id,
          entity: $scope.entity
        }, function(data, headers) {
          $scope.articles = data;
          $scope.articlesTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.entitiesArticlesModels.limit);
        });
      });

    }
  ]);

  /****************************************************************************
   * Word Controllers
   ****************************************************************************/

  app.controller('WordsIndexController', ['$scope', '$state', 'WordFactory',
    function($scope, $state, WordFactory) {

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'words')
        $scope.chooseTopicModel();

      $scope.wordsIndexModels = {
        sortkey: 'id',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.reloadWords = function() {
        if (!$scope.rootModels.topicModel) return;
        
        $scope.loadingWords = true;
        WordFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          skip: ($scope.wordsIndexModels.page - 1) * $scope.wordsIndexModels.limit,
          limit: $scope.wordsIndexModels.limit,
          sort: ($scope.wordsIndexModels.sortdir ? '' : '-') + $scope.wordsIndexModels.sortkey,
          char: $scope.wordsIndexModels.startChar,
          contains: $scope.wordsIndexModels.contains
        }, function(data, headers) {
          $scope.words = data;
          $scope.wordsTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.wordsTotal / $scope.wordsIndexModels.limit);
          $scope.loadingWords = false;
        }, function() {
          $scope.loadingWords = false;
        });
      };

      $scope.$watchGroup(['wordsIndexModels.page', 'wordsIndexModels.sortkey', 'wordsIndexModels.sortdir', 'rootModels.topicModel'], function() {
        $scope.reloadWords();
      });

      $scope.$watchGroup(['wordsIndexModels.startChar', 'wordsIndexModels.contains'], function() {
        if($scope.wordsIndexModels.page !== 1)
          $scope.wordsIndexModels.page = 1;
        else
          $scope.reloadWords();
      });
    }
  ]);

  app.controller('WordsShowController', ['$scope', '$stateParams', 'WordFactory',
    function($scope, $stateParams, WordFactory) {

      WordFactory.get({
        id: $stateParams.id
      }, function(data) {
        $scope.word = data;

        // take word model from word
        if (!angular.isObject($scope.rootModels.topicModel))
          $scope.rootModels.topicModel = data.topicModel;
      });
    }
  ]);

  app.controller('WordsTopicsController', ['$scope', '$state', '$stateParams', 'TopicFactory',
    function($scope, $state, $stateParams, TopicFactory) {

      $scope.word = $stateParams.id;

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'words.topics')
        $scope.chooseTopicModel();

      $scope.wordsTopicsModels = {
        sortkey: 'name',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.$watchGroup(['wordsTopicsModels.page', 'wordsTopicsModels.sortkey', 'wordsTopicsModels.sortdir', 'rootModels.topicModel'], function() {
        if (!$scope.rootModels.topicModel) return;

        TopicFactory.query({
          topicModel: $scope.rootModels.topicModel.id,
          skip: ($scope.wordsTopicsModels.page - 1) * $scope.wordsTopicsModels.limit,
          limit: $scope.wordsTopicsModels.limit,
          sort: ($scope.wordsTopicsModels.sortdir ? '' : '-') + $scope.wordsTopicsModels.sortkey,
          word: $scope.word
        }, function(data, headers) {
          $scope.topics = data;
          $scope.topicsTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.topicsTotal / $scope.wordsTopicsModels.limit);
        });
      });

    }
  ]);

  app.controller('WordsArticlesController', ['$scope', '$state', '$stateParams', 'ArticleFactory',
    function($scope, $state, $stateParams, ArticleFactory) {

      $scope.word = $stateParams.id;

      // page was reloaded, choose topic model
      if (!$scope.rootModels.topicModel && $state.current.name === 'words.articles')
        $scope.chooseTopicModel();

      $scope.wordsArticlesModels = {
        sortkey: 'date',
        sortdir: true,
        page: 1,
        limit: 100
      };

      $scope.$watchGroup(['wordsArticlesModels.page', 'wordsArticlesModels.sortkey', 'wordsArticlesModels.sortdir', 'rootModels.topicModel'], function() {
        if (!$scope.rootModels.topicModel) return;

        ArticleFactory.query({
          skip: ($scope.wordsArticlesModels.page - 1) * $scope.wordsArticlesModels.limit,
          limit: $scope.wordsArticlesModels.limit,
          sort: ($scope.wordsArticlesModels.sortdir ? '' : '-') + $scope.wordsArticlesModels.sortkey,
          topicModel: $scope.rootModels.topicModel.id,
          word: $scope.word
        }, function(data, headers) {
          $scope.articles = data;
          $scope.articlesTotal = headers("V-Total");
          $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.wordsArticlesModels.limit);
        });
      });

    }
  ]);

  /****************************************************************************
   * Slides Controllers
   ****************************************************************************/

  app.controller('SlidesController', ['$scope', function($scope) {

      $scope.current = 1;

      var folder = '//ftp.cochu.io/vipra/slides/';
      var prefix = 'Folie';
      var suffix = '.PNG';
      var slides = $('.slides');

      slides.css('background-image', 'url(' + folder + prefix + $scope.current + suffix + ')');

      $scope.go = function(next) {
        $('<img/>').attr('src', folder + prefix + next + suffix).load(function() {
          $scope.$apply(function() {
            $scope.current = next;
            slides.css('background-image', 'url(' + folder + prefix + $scope.current + suffix + ')');
          });
        });
      };
    }
  ]);

  /****************************************************************************
   * Error Controllers
   ****************************************************************************/

  app.controller('ErrorsController', ['$scope', '$state', '$stateParams',
    function($scope, $state, $stateParams) {
      $scope.code = $stateParams.code;
      $scope.title = Vipra.statusMsg($scope.code);
    }
  ]);

  /****************************************************************************
   * Directive Controllers
   ****************************************************************************/

  /**
   * Pagination
   */
  app.controller('PaginationController', ['$scope',
    function($scope) {

      $scope.calculatePages = function() {
        var pages = [],
          max = Math.ceil($scope.total / $scope.limit * 1.0),
          pad = 4,
          start = Math.max($scope.page - pad, 1),
          end = Math.min(Math.max($scope.page + pad, start + pad * 2), max);
        for (var i = start; i <= end; i++) {
          pages.push(i);
        }
        $scope.pages = pages;
        $scope.maxPage = max;
      };

      $scope.$watchGroup(['total', 'page', 'limit'], function(newVal, oldVal) {
        if (!angular.equals(newVal, oldVal)) {
          $scope.calculatePages();
        }
      });

      $scope.calculatePages();

      $scope.changePage = function(page) {
        $scope.page = parseInt(page, 10);
      };

      $scope.toPage = function() {
        var page = prompt("Enter a page number (between 1 and " + $scope.maxPage + ")");
        if (page > 0 && page <= $scope.maxPage)
          $scope.changePage(page);
      };
    }
  ]);

  app.controller('WordEvolutionController', ['$scope',
    function($scope) {

      $scope.chartId = $scope.chartId || Vipra.randomId();
      $scope.wordSeqstyle = 'absolute';
      $scope.wordChartstyle = 'spline';

      $scope.resetWordZoom = function() {
        if (!$scope.wordsSelected) return;
        var highcharts = $('#' + $scope.chartId).highcharts();
        if (!highcharts) return;

        highcharts.xAxis[0].setExtremes(null, null);
      };

      $scope.redrawWordEvolutionChart = function() {
        var evolutions = [];
        if ($scope.topic && $scope.topic.words && $scope.topic.sequences) {

          // create series
          for (var i = 0, word, probs; i < $scope.topic.words.length; i++) {
            word = $scope.topic.words[i];
            if (!word.selected) continue;
            probs = [];
            for (var j = 0, prob; j < word.sequenceProbabilities.length; j++) {
              prob = $scope.wordSeqstyle === 'relative' ? word.sequenceProbabilitiesChange[j] : word.sequenceProbabilities[j];
              probs.push([new Date($scope.topic.sequences[j].window.startDate).getTime(), prob]);
            }
            evolutions.push({
              id: word.id,
              name: word.id,
              color: word.color,
              data: probs
            });
          }
        }

        $scope.wordEvolution = areaRelevanceChart(evolutions, 'Word Evolution', $scope.wordChartstyle);
        $scope.wordsSelected = evolutions.length;
      };

      $scope.highlightSeries = function(id, toggle) {
        if (!$scope.wordsSelected) return;
        var highcharts = $('#' + $scope.chartId).highcharts();
        if (!highcharts) return;
        var series = highcharts.get(id);
        if (!series) return;

        if (toggle) {
          series.onMouseOver();
          series.group.zIndexOrig = series.group.zIndex;
          series.group.zIndexSetter(9999, 'zIndex');
          series.graph.attr('stroke', '#000000').attr('stroke-dasharray', '5,5');
        } else {
          series.onMouseOut();
          series.group.zIndexSetter(series.group.zIndexOrig, 'zIndex');
          series.graph.attr('stroke', series.color).attr('stroke-dasharray', '');
        }
      };

      $scope.$watchGroup(['wordSeqstyle', 'wordChartstyle', 'topic'], $scope.redrawWordEvolutionChart);

      $scope.$watch('topic', function() {
        if($scope.topic) {
          var colors = randomColor({
            count: $scope.topic.words.length
          });

          for (var i = 0; i < $scope.topic.words.length; i++) {
            $scope.topic.words[i].color = colors[i];
          }
        }

        $scope.redrawWordEvolutionChart();
      });
    }
  ]);

  /****************************************************************************
   * Shared Highcharts configurations
   ****************************************************************************/

  function areaRelevanceChart(series, title, chartType, chartStack, clickCallback) {
    return {
      chart: {
        type: (chartType || 'areaspline'),
        zoomType: 'x',
        spacingLeft: 0,
        spacingRight: 0
      },
      title: {
        text: title
      },
      xAxis: {
        type: 'datetime',
        title: {
          text: 'Sequence'
        }
      },
      yAxis: {
        title: {
          text: 'Relevance'
        }
      },
      tooltip: {
        headerFormat: '<b>{series.name}</b><br>',
        pointFormat: '{point.x:%Y}: {point.y:.4f}'
      },
      legend: {
        enabled: false
      },
      credits: {
        enabled: false
      },
      navigator: {
        enabled: true
      },
      plotOptions: {
        areaspline: {
          fillOpacity: 0.5,
        },
        series: {
          stacking: (chartStack === 'none' ? null : chartStack),
          animation: false,
          cursor: 'pointer',
          allowPointSelect: true,
          states: {
            hover: {
              color: '#ff0000',
              lineWidth: 4,
              halo: {
                size: 9,
                attributes: {
                  fill: Highcharts.getOptions().colors[2],
                  'stroke-width': 2,
                  stroke: Highcharts.getOptions().colors[1]
                }
              }
            }
          },
          point: {
            events: {
              click: clickCallback
            }
          }
        }
      },
      noData: {
        style: {
          fontSize: "14px",
          color: "#777",
          fontWeight: "normal"
        }
      },
      series: series
    };
  }

  function topicShareChart(series) {
    return {
      chart: {
        type: 'pie',
        spacingBottom: 0,
        spacingTop: 0,
        spacingLeft: 0,
        spacingRight: 0,
      },
      credits: {
        enabled: false
      },
      plotOptions: {
        pie: {
          size: '100%',
          dataLabels: {
            enabled: false
          },
          allowPointSelect: true
        }
      },
      title: {
        text: ''
      },
      tooltip: {
        pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>'
      },
      noData: {
        style: {
          fontSize: "14px",
          color: "#777",
          fontWeight: "normal"
        }
      },
      series: series
    };
  }

})();