1'use strict';
2
3/**
4 * TODO (stephana@): This is still work in progress.
5 * It does not offer the same functionality as the current version, but
6 * will serve as the starting point for a new backend.
7 * It works with the current backend, but does not support rebaselining.
8 */
9
10/*
11 * Wrap everything into an IIFE to not polute the global namespace.
12 */
13(function () {
14
15  // Declare app level module which contains everything of the current app.
16  // ui.bootstrap refers to directives defined in the AngularJS Bootstrap
17  // UI package (http://angular-ui.github.io/bootstrap/).
18  var app = angular.module('rbtApp', ['ngRoute', 'ui.bootstrap']);
19
20  // Configure the different within app views.
21  app.config(['$routeProvider', function($routeProvider) {
22    $routeProvider.when('/', {templateUrl: 'partials/index-view.html',
23                              controller: 'IndexCtrl'});
24    $routeProvider.when('/view', {templateUrl: 'partials/rebaseline-view.html',
25                                  controller: 'RebaselineCrtrl'});
26    $routeProvider.otherwise({redirectTo: '/'});
27  }]);
28
29
30  // TODO (stephana): Some of these constants are 'gm' specific. In the
31  // next iteration we need to remove those as we move the more generic
32  // 'dm' testing tool.
33  //
34  // Shared constants used here and in the markup. These are exported when
35  // when used by a controller.
36  var c = {
37    // Define different view states as we load the data.
38    ST_LOADING: 1,
39    ST_STILL_LOADING: 2,
40    ST_READY: 3,
41
42    // These column types are used by the Column class.
43    COL_T_FILTER: 'filter',
44    COL_T_IMAGE: 'image',
45    COL_T_REGULAR: 'regular',
46
47    // Request parameters used to select between subsets of results.
48    RESULTS_ALL: 'all',
49    RESULTS_FAILURES: 'failures',
50
51    // Filter types are used by the Column class.
52    FILTER_FREE_FORM: 'free_form',
53    FILTER_CHECK_BOX: 'checkbox',
54
55    // Columns either provided by the backend response or added in code.
56    // TODO (stephana): This should go away once we switch to 'dm'.
57    COL_BUGS: 'bugs',
58    COL_IGNORE_FAILURE: 'ignore-failure',
59    COL_REVIEWED_BY_HUMANS: 'reviewed-by-human',
60
61    // Defines the order in which image columns appear.
62    // TODO (stephana@): needs to be driven by backend data.
63    IMG_COL_ORDER: [
64       {
65        key: 'imageA',
66        urlField: ['imageAUrl']
67      },
68      {
69        key: 'imageB',
70        urlField: ['imageBUrl']
71      },
72      {
73        key: 'whiteDiffs',
74        urlField: ['differenceData', 'whiteDiffUrl'],
75        percentField: ['differenceData', 'percentDifferingPixels'],
76        valueField: ['differenceData', 'numDifferingPixels']
77      },
78      {
79        key: 'diffs',
80        urlField: ['differenceData', 'diffUrl'],
81        percentField: ['differenceData', 'perceptualDifference'],
82        valueField: ['differenceData', 'maxDiffPerChannel']
83      }
84    ],
85
86    // Choice of availabe image size selection.
87    IMAGE_SIZES: [
88      100,
89      200,
90      400
91    ],
92
93    // Choice of available number of records selection.
94    MAX_RECORDS: [
95      '100',
96      '200',
97      '300'
98    ]
99  };  // end constants
100
101  /*
102   * Index Controller
103   */
104  // TODO (stephana): Remove $timeout since it only simulates loading delay.
105  app.controller('IndexCtrl', ['$scope', '$timeout', 'dataService',
106  function($scope, $timeout, dataService) {
107    // init the scope
108    $scope.c = c;
109    $scope.state = c.ST_LOADING;
110    $scope.qStr = dataService.getQueryString;
111
112    // TODO (stephana): Remove and replace with index data generated by the
113    // backend to reflect the current "known" image sets to compare.
114    $scope.allSKPs = [
115    {
116      params: {
117        setBSection: 'actual-results',
118        setASection: 'expected-results',
119        setBDir: 'gs://chromium-skia-skp-summaries/' +
120                 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug',
121        setADir: 'repo:expectations/skp/' +
122                 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
123      },
124      title: 'expected vs actuals on ' +
125             'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
126    },
127    {
128      params: {
129        setBSection: 'actual-results',
130        setASection: 'expected-results',
131        setBDir: 'gs://chromium-skia-skp-summaries/' +
132                 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
133        setADir: 'repo:expectations/skp/'+
134                 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
135      },
136      title: 'expected vs actuals on Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
137    },
138    {
139      params: {
140        setBSection: 'actual-results',
141        setASection: 'actual-results',
142        setBDir: 'gs://chromium-skia-skp-summaries/' +
143                 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
144        setADir: 'gs://chromium-skia-skp-summaries/' +
145                 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
146      },
147      title: 'Actuals on Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug ' +
148             'vs Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
149    }
150    ];
151
152    // TODO (stephana): Remove this once we load index data from the server.
153    $timeout(function () {
154      $scope.state = c.ST_READY;
155    });
156  }]);
157
158  /*
159   *  RebaselineCtrl
160   *  Controls the main comparison view.
161   *
162   *  @param {service} dataService Service that encapsulates functions to
163   *                               retrieve data from the backend.
164   *
165   */
166  app.controller('RebaselineCrtrl', ['$scope', '$timeout', 'dataService',
167  function($scope, $timeout, dataService) {
168    // determine which to request
169    // TODO (stephana): This should be extracted from the query parameters.
170    var target = c.TARGET_GM;
171
172    // process the rquest arguments
173    // TODO (stephana): This should be determined from the query parameters.
174    var loadFn = dataService.loadAll;
175
176    // controller state variables
177    var allData = null;
178    var filterFuncs = null;
179    var currentData = null;
180    var selectedData = null;
181
182    // Index of the column that should provide the sort key
183    var sortByIdx = 0;
184
185    // Sort in asending (true) or descending (false) order
186    var sortOrderAsc = true;
187
188    // Array of functions for each column used for comparison during sort.
189    var compareFunctions = null;
190
191    // Variables to track load and render times
192    var startTime;
193    var loadStartTime;
194
195
196    /** Load the data from the backend **/
197    loadStartTime = Date.now();
198    function loadData() {
199      loadFn().then(
200        function (serverData) {
201          $scope.header = serverData.header;
202          $scope.loadTime = (Date.now() - loadStartTime)/1000;
203
204          // keep polling if the data are not ready yet
205          if ($scope.header.resultsStillLoading) {
206            $scope.state = c.ST_STILL_LOADING;
207            $timeout(loadData, 5000);
208            return;
209          }
210
211          // get the filter colunms and an array to hold filter data by user
212          var fcol = getFilterColumns(serverData);
213          $scope.filterCols = fcol[0];
214          $scope.filterVals = fcol[1];
215
216          // Add extra columns and retrieve the image columns
217          var otherCols = [ Column.regular(c.COL_BUGS) ];
218          var imageCols = getImageColumns(serverData);
219
220          // Concat to get all columns
221          // NOTE: The order is important since filters are rendered first,
222          // followed by regular columns and images
223          $scope.allCols = $scope.filterCols.concat(otherCols, imageCols);
224
225          // Pre-process the data and get the filter functions.
226          var dataFilters = getDataAndFilters(serverData, $scope.filterCols,
227                                              otherCols, imageCols);
228          allData = dataFilters[0];
229          filterFuncs = dataFilters[1];
230
231          // Get regular columns (== not image columns)
232          var regularCols = $scope.filterCols.concat(otherCols);
233
234          // Get the compare functions for regular and image columns. These
235          // are then used to sort by the respective columns.
236          compareFunctions = DataRow.getCompareFunctions(regularCols,
237                                                         imageCols);
238
239          // Filter and sort the results to get them ready for rendering
240          updateResults();
241
242          // Data are ready for display
243          $scope.state = c.ST_READY;
244        },
245        function (httpErrResponse) {
246          console.log(httpErrResponse);
247        });
248    };
249
250    /*
251     * updateResults
252     * Central render function. Everytime settings/filters/etc. changed
253     * this function is called to filter, sort and splice the data.
254     *
255     * NOTE (stephana): There is room for improvement here: before filtering
256     * and sorting we could check if this is necessary. But this has not been
257     * a bottleneck so far.
258     */
259    function updateResults () {
260      // run digest before we update the results. This allows
261      // updateResults to be called from functions trigger by ngChange
262      $scope.updating = true;
263      startTime = Date.now();
264
265      // delay by one render cycle so it can be called via ng-change
266      $timeout(function() {
267        // filter data
268        selectedData = filterData(allData, filterFuncs, $scope.filterVals);
269
270        // sort the selected data.
271        sortData(selectedData, compareFunctions, sortByIdx, sortOrderAsc);
272
273        // only conside the elements that we really need
274        var nRecords = $scope.settings.nRecords;
275        currentData = selectedData.slice(0, parseInt(nRecords));
276
277        DataRow.setRowspanValues(currentData, $scope.mergeIdenticalRows);
278
279        // update the scope with relevant data for rendering.
280        $scope.data = currentData;
281        $scope.totalRecords = allData.length;
282        $scope.showingRecords = currentData.length;
283        $scope.selectedRecords = selectedData.length;
284        $scope.updating = false;
285
286        // measure the filter time and total render time (via timeout).
287        $scope.filterTime = Date.now() - startTime;
288        $timeout(function() {
289          $scope.renderTime = Date.now() - startTime;
290        });
291      });
292    };
293
294    /**
295     * Generate the style value to set the width of images.
296     *
297     * @param {Column} col Column that we are trying to render.
298     * @param {int} paddingPx Number of padding pixels.
299     * @param {string} defaultVal Default value if not an image column.
300     *
301     * @return {string} Value to be used in ng-style element to set the width
302     *                  of a image column.
303     **/
304    $scope.getImageWidthStyle = function (col, paddingPx, defaultVal) {
305      var result = (col.ctype === c.COL_T_IMAGE) ?
306                   ($scope.imageSize + paddingPx + 'px') : defaultVal;
307      return result;
308    };
309
310    /**
311     * Sets the column by which to sort the data. If called for the
312     * currently sorted column it will cause the sort to toggle between
313     * ascending and descending.
314     *
315     * @param {int} colIdx Index of the column to use for sorting.
316     **/
317    $scope.sortBy = function (colIdx) {
318      if (sortByIdx === colIdx) {
319        sortOrderAsc = !sortOrderAsc;
320      } else {
321        sortByIdx = colIdx;
322        sortOrderAsc = true;
323      }
324      updateResults();
325    };
326
327    /**
328     * Helper function to generate a CSS class indicating whether this column
329     * is the sort key. If it is a class name with the sort direction (Asc/Desc) is
330     * return otherwise the default value is returned. In markup we use this
331     * to display (or not display) an arrow next to the column name.
332     *
333     * @param {string} prefix Prefix of the classname to be generated.
334     * @param {int} idx Index of the target column.
335     * @param {string} defaultVal Value to return if current column is not used
336     *                            for sorting.
337     *
338     * @return {string} CSS class name that a combination of the prefix and
339     *                  direction indicator ('Asc' or 'Desc') if the column is
340     *                  used for sorting. Otherwise the defaultVal is returned.
341     **/
342    $scope.getSortedClass = function (prefix, idx, defaultVal) {
343      if (idx === sortByIdx) {
344        return prefix + ((sortOrderAsc) ? 'Asc' : 'Desc');
345      }
346
347      return defaultVal;
348    };
349
350    /**
351     * Checkbox to merge identical records has change. Force an update.
352     **/
353    $scope.mergeRowsChanged = function () {
354      updateResults();
355    }
356
357    /**
358     * Max number of records to display has changed. Force an update.
359     **/
360    $scope.maxRecordsChanged = function () {
361      updateResults();
362    };
363
364    /**
365     * Filter settings changed. Force an update.
366     **/
367    $scope.filtersChanged = function () {
368      updateResults();
369    };
370
371    /**
372     * Sets all possible values of the specified values to the given value.
373     * That means all checkboxes are eiter selected or unselected.
374     * Then force an update.
375     *
376     * @param {int} idx Index of the target filter column.
377     * @param {boolean} val Value to set the filter values to.
378     *
379     **/
380    $scope.setFilterAll = function (idx, val) {
381      for(var i=0, len=$scope.filterVals[idx].length; i<len; i++) {
382        $scope.filterVals[idx][i] = val;
383      }
384      updateResults();
385    };
386
387    /**
388     * Toggle the values of a filter. This toggles all values in a
389     * filter.
390     *
391     * @param {int} idx Index of the target filter column.
392     **/
393    $scope.setFilterToggle = function (idx) {
394      for(var i=0, len=$scope.filterVals[idx].length; i<len; i++) {
395        $scope.filterVals[idx][i] = !$scope.filterVals[idx][i];
396      }
397      updateResults();
398    };
399
400    // ****************************************
401    // Initialize the scope.
402    // ****************************************
403
404    // Inject the constants into the scope and set the initial state.
405    $scope.c = c;
406    $scope.state = c.ST_LOADING;
407
408    // Initial settings
409    $scope.settings = {
410      showThumbnails: true,
411      imageSize: c.IMAGE_SIZES[0],
412      nRecords: c.MAX_RECORDS[0],
413      mergeIdenticalRows: true
414    };
415
416    // Initial values for filters set in loadData()
417    $scope.filterVals = [];
418
419    // Information about records - set in loadData()
420    $scope.totalRecords = 0;
421    $scope.showingRecords = 0;
422    $scope.updating = false;
423
424    // Trigger the data loading.
425    loadData();
426
427  }]);
428
429  // data structs to interface with markup and backend
430  /**
431   * Models a column. It aggregates attributes of all
432   * columns types. Some might be empty. See convenience
433   * factory methods below for different column types.
434   *
435   * @param {string} key Uniquely identifies this columns
436   * @param {string} ctype Type of columns. Use COL_* constants.
437   * @param {string} ctitle Human readable title of the column.
438   * @param {string} ftype Filter type. Use FILTER_* constants.
439   * @param {FilterOpt[]} foptions Filter options. For 'checkbox' filters this
440                                   is used to render all the checkboxes.
441                                   For freeform filters this is a list of all
442                                   available values.
443   * @param {string} baseUrl Baseurl for image columns. All URLs are relative
444                             to this.
445   *
446   * @return {Column} Instance of the Column class.
447   **/
448  function Column(key, ctype, ctitle, ftype, foptions, baseUrl) {
449    this.key = key;
450    this.ctype = ctype;
451    this.ctitle = ctitle;
452    this.ftype = ftype;
453    this.foptions = foptions;
454    this.baseUrl = baseUrl;
455    this.foptionsArr = [];
456
457    // get the array of filter options for lookup in indexOfOptVal
458    if (this.foptions) {
459      for(var i=0, len=foptions.length; i<len; i++) {
460        this.foptionsArr.push(this.foptions[i].value);
461      }
462    }
463  }
464
465  /**
466   * Find the index of an value in a column with a fixed set
467   * of options.
468   *
469   * @param {string} optVal Value of the column.
470   *
471   * @return {int} Index of optVal in this column.
472   **/
473  Column.prototype.indexOfOptVal = function (optVal) {
474    return this.foptionsArr.indexOf(optVal);
475  };
476
477  /**
478   * Set filter options for this column.
479   *
480   * @param {FilterOpt[]} foptions Possible values for this column.
481   **/
482  Column.prototype.setFilterOptions = function (foptions) {
483    this.foptions = foptions;
484  };
485
486  /**
487   * Factory function to create a filter column. Same args as Column()
488   **/
489  Column.filter = function(key, ctitle, ftype, foptions) {
490    return new Column(key, c.COL_T_FILTER, ctitle || key, ftype, foptions);
491  }
492
493  /**
494   * Factory function to create an image column. Same args as Column()
495   **/
496  Column.image = function (key, ctitle, baseUrl) {
497    return new Column(key, c.COL_T_IMAGE, ctitle || key, null, null, baseUrl);
498  };
499
500  /**
501   * Factory function to create a regular column. Same args as Column()
502   **/
503  Column.regular = function (key, ctitle) {
504    return new Column(key, c.COL_T_REGULAR, ctitle || key);
505  };
506
507  /**
508   * Helper class to wrap a single option in a filter.
509   *
510   * @param {string} value Option value.
511   * @param {int} count Number of instances of this option in the dataset.
512   *
513   * @return {} Instance of FiltertOpt
514   **/
515  function FilterOpt(value, count) {
516    this.value = value;
517    this.count = count;
518  }
519
520  /**
521   * Container for a single row in the dataset.
522   *
523   * @param {int} rowspan Number of rows (including this and following rows)
524                          that have identical values.
525   * @param {string[]} dataCols Values of the respective columns (combination
526                                of filter and regular columns)
527   * @param {ImgVal[]} imageCols Image meta data for the image columns.
528   *
529   * @return {DataRow} Instance of DataRow.
530   **/
531  function DataRow(rowspan, dataCols, imageCols) {
532    this.rowspan = rowspan;
533    this.dataCols = dataCols;
534    this.imageCols = imageCols;
535  }
536
537  /**
538   * Gets the comparator functions for the columns in this dataset.
539   * The comparators are then used to sort the dataset by the respective
540   * column.
541   *
542   * @param {Column[]} dataCols Data columns (= non-image columns)
543   * @param {Column[]} imgCols Image columns.
544   *
545   * @return {Function[]} Array of functions that can be used to sort by the
546   *                      respective column.
547   **/
548  DataRow.getCompareFunctions = function (dataCols, imgCols) {
549    var result = [];
550    for(var i=0, len=dataCols.length; i<len; i++) {
551      result.push(( function (col, idx) {
552        return function (a, b) {
553          return (a.dataCols[idx] < b.dataCols[idx]) ? -1 :
554                 ((a.dataCols[idx] === b.dataCols[idx]) ? 0 : 1);
555        };
556      }(dataCols[i], i) ));
557    }
558
559    for(var i=0, len=imgCols.length; i<len; i++) {
560      result.push((function (col, idx) {
561        return function (a,b) {
562          var aVal = a.imageCols[idx].percent;
563          var bVal = b.imageCols[idx].percent;
564
565          return (aVal < bVal) ? -1 : ((aVal === bVal) ? 0 : 1);
566        };
567      }(imgCols[i], i) ));
568    }
569
570    return result;
571  };
572
573  /**
574  * Set the rowspan values of a given array of DataRow instances.
575  *
576  * @param {DataRow[]} data Dataset in desired order (after sorting).
577  * @param {mergeRows} mergeRows Indicate whether to sort
578   **/
579  DataRow.setRowspanValues = function (data, mergeRows) {
580    var curIdx, rowspan, cur;
581    if (mergeRows) {
582      for(var i=0, len=data.length; i<len;) {
583        curIdx = i;
584        cur = data[i];
585        rowspan = 1;
586        for(i++; ((i<len) && (data[i].dataCols === cur.dataCols)); i++) {
587          rowspan++;
588          data[i].rowspan=0;
589        }
590        data[curIdx].rowspan = rowspan;
591      }
592    } else {
593      for(var i=0, len=data.length; i<len; i++) {
594        data[i].rowspan = 1;
595      }
596    }
597  };
598
599  /**
600   * Wrapper class for image related data.
601   *
602   * @param {string} url Relative Url of the image or null if not available.
603   * @param {float} percent Percent of pixels that are differing.
604   * @param {int} value Absolute number of pixes differing.
605   *
606   * @return {ImgVal} Instance of ImgVal.
607   **/
608  function ImgVal(url, percent, value) {
609    this.url = url;
610    this.percent = percent;
611    this.value = value;
612  }
613
614  /**
615   * Extracts the filter columns from the JSON response of the server.
616   *
617   * @param {object} data Server response.
618   *
619   * @return {Column[]} List of filter columns as described in 'header' field.
620   **/
621  function getFilterColumns(data) {
622    var result = [];
623    var vals = [];
624    var colOrder = data.extraColumnOrder;
625    var colHeaders = data.extraColumnHeaders;
626    var fopts, optVals, val;
627
628    for(var i=0, len=colOrder.length; i<len; i++) {
629      if (colHeaders[colOrder[i]].isFilterable) {
630        if (colHeaders[colOrder[i]].useFreeformFilter) {
631          result.push(Column.filter(colOrder[i],
632                                    colHeaders[colOrder[i]].headerText,
633                                    c.FILTER_FREE_FORM));
634          vals.push('');
635        }
636        else {
637          fopts = [];
638          optVals = [];
639
640          // extract the different options for this column
641          for(var j=0, jlen=colHeaders[colOrder[i]].valuesAndCounts.length;
642              j<jlen; j++) {
643                val = colHeaders[colOrder[i]].valuesAndCounts[j];
644                fopts.push(new FilterOpt(val[0], val[1]));
645                optVals.push(false);
646          }
647
648          // ad the column and values
649          result.push(Column.filter(colOrder[i],
650                                    colHeaders[colOrder[i]].headerText,
651                                    c.FILTER_CHECK_BOX,
652                                    fopts));
653          vals.push(optVals);
654        }
655      }
656    }
657
658    return [result, vals];
659  }
660
661  /**
662   * Extracts the image columns from the JSON response of the server.
663   *
664   * @param {object} data Server response.
665   *
666   * @return {Column[]} List of images columns as described in 'header' field.
667   **/
668  function getImageColumns(data) {
669    var CO = c.IMG_COL_ORDER;
670    var imgSet;
671    var result = [];
672    for(var i=0, len=CO.length; i<len; i++) {
673      imgSet = data.imageSets[CO[i].key];
674      result.push(Column.image(CO[i].key,
675                               imgSet.description,
676                               ensureTrailingSlash(imgSet.baseUrl)));
677    }
678    return result;
679  }
680
681  /**
682   * Make sure Url has a trailing '/'.
683   *
684   * @param {string} url Base url.
685   * @return {string} Same url with a trailing '/' or same as input if it
686                      already contained '/'.
687   **/
688  function ensureTrailingSlash(url) {
689    var result = url.trim();
690
691    // TODO: remove !!!
692    result = fixUrl(url);
693    if (result[result.length-1] !== '/') {
694      result += '/';
695    }
696    return result;
697  }
698
699  // TODO: remove. The backend should provide absoute URLs
700  function fixUrl(url) {
701    url = url.trim();
702    if ('http' === url.substr(0, 4)) {
703      return url;
704    }
705
706    var idx = url.indexOf('static');
707    if (idx != -1) {
708      return '/' + url.substr(idx);
709    }
710
711    return url;
712  };
713
714  /**
715   * Processes that data and returns filter functions.
716   *
717   * @param {object} Server response.
718   * @param {Column[]} filterCols Filter columns.
719   * @param {Column[]} otherCols Columns that are neither filters nor images.
720   * @param {Column[]} imageCols Image columns.
721   *
722   * @return {[]} Returns a pair [dataRows, filterFunctions] where:
723   *       - dataRows is an array of DataRow instances.
724   *       - filterFunctions is an array of functions that can be used to
725   *         filter the column at the corresponding index.
726   *
727   **/
728  function getDataAndFilters(data, filterCols, otherCols, imageCols) {
729    var el;
730    var result = [];
731    var lookupIndices = [];
732    var indexerFuncs = [];
733    var temp;
734
735    // initialize the lookupIndices
736    var filterFuncs = initIndices(filterCols, lookupIndices, indexerFuncs);
737
738    // iterate over the data and get the rows
739    for(var i=0, len=data.imagePairs.length; i<len; i++) {
740      el = data.imagePairs[i];
741      temp = new DataRow(1, getColValues(el, filterCols, otherCols),
742                                 getImageValues(el, imageCols));
743      result.push(temp);
744
745      // index the row
746      for(var j=0, jlen=filterCols.length; j < jlen; j++) {
747        indexerFuncs[j](lookupIndices[j], filterCols[j], temp.dataCols[j], i);
748      }
749    }
750
751    setFreeFormFilterOptions(filterCols, lookupIndices);
752    return [result, filterFuncs];
753  }
754
755  /**
756   * Initiazile the lookup indices and indexer functions for the filter
757   * columns.
758   *
759   * @param {Column} filterCols Filter columns
760   * @param {[]} lookupIndices Will be filled with datastructures for
761                               fast lookup (output parameter)
762   * @param {[]} lookupIndices Will be filled with functions to index data
763                               of the column with the corresponding column.
764   *
765   * @return {[]} Returns an array of filter functions that can be used to
766                  filter the respective column.
767   **/
768  function initIndices(filterCols, lookupIndices, indexerFuncs) {
769    var filterFuncs = [];
770    var temp;
771
772    for(var i=0, len=filterCols.length; i<len; i++) {
773      if (filterCols[i].ftype === c.FILTER_FREE_FORM) {
774        lookupIndices.push({});
775        indexerFuncs.push(indexFreeFormValue);
776        filterFuncs.push(
777          getFreeFormFilterFunc(lookupIndices[lookupIndices.length-1]));
778      }
779      else if (filterCols[i].ftype === c.FILTER_CHECK_BOX) {
780        temp = [];
781        for(var j=0, jlen=filterCols[i].foptions.length; j<jlen; j++) {
782          temp.push([]);
783        }
784        lookupIndices.push(temp);
785        indexerFuncs.push(indexDiscreteValue);
786        filterFuncs.push(
787          getDiscreteFilterFunc(lookupIndices[lookupIndices.length-1]));
788      }
789    }
790
791    return filterFuncs;
792  }
793
794  /**
795   * Helper function that extracts the values of free form columns from
796   * the lookupIndex and injects them into the Column object as FilterOpt
797   * objects.
798   **/
799  function setFreeFormFilterOptions(filterCols, lookupIndices) {
800    var temp, k;
801    for(var i=0, len=filterCols.length; i<len; i++) {
802      if (filterCols[i].ftype === c.FILTER_FREE_FORM) {
803        temp = []
804        for(k in lookupIndices[i]) {
805          if (lookupIndices[i].hasOwnProperty(k)) {
806            temp.push(new FilterOpt(k, lookupIndices[i][k].length));
807          }
808        }
809        filterCols[i].setFilterOptions(temp);
810      }
811    }
812  }
813
814  /**
815   * Index a discrete column (column with fixed number of values).
816   *
817   **/
818  function indexDiscreteValue(lookupIndex, col, dataVal, dataRowIndex) {
819    var i = col.indexOfOptVal(dataVal);
820    lookupIndex[i].push(dataRowIndex);
821  }
822
823  /**
824   * Index a column with free form text (= not fixed upfront)
825   *
826   **/
827  function indexFreeFormValue(lookupIndex, col, dataVal, dataRowIndex) {
828    if (!lookupIndex[dataVal]) {
829      lookupIndex[dataVal] = [];
830    }
831    lookupIndex[dataVal].push(dataRowIndex);
832  }
833
834
835  /**
836   * Get the function to filter a column with the given lookup index
837   * for discrete (fixed upfront) values.
838   *
839   **/
840  function getDiscreteFilterFunc(lookupIndex) {
841    return function(filterVal) {
842      var result = [];
843      for(var i=0, len=lookupIndex.length; i < len; i++) {
844        if (filterVal[i]) {
845          // append the indices to the current array
846          result.push.apply(result, lookupIndex[i]);
847        }
848      }
849      return { nofilter: false, records: result };
850    };
851  }
852
853  /**
854   * Get the function to filter a column with the given lookup index
855   * for free form values.
856   *
857   **/
858  function getFreeFormFilterFunc(lookupIndex) {
859    return function(filterVal) {
860      filterVal = filterVal.trim();
861      if (filterVal === '') {
862        return { nofilter: true };
863      }
864      return {
865        nofilter: false,
866        records: lookupIndex[filterVal] || []
867      };
868    };
869  }
870
871  /**
872   * Filters the data based on the given filterColumns and
873   * corresponding filter values.
874   *
875   * @return {[]} Subset of the input dataset based on the
876   *              filter values.
877   **/
878  function filterData(data, filterFuncs, filterVals) {
879    var recordSets = [];
880    var filterResult;
881
882    // run through all the filters
883    for(var i=0, len=filterFuncs.length; i<len; i++) {
884      filterResult = filterFuncs[i](filterVals[i]);
885      if (!filterResult.nofilter) {
886        recordSets.push(filterResult.records);
887      }
888    }
889
890    // If there are no restrictions then return the whole dataset.
891    if (recordSets.length === 0) {
892      return data;
893    }
894
895    // intersect the records returned by filters.
896    var targets = intersectArrs(recordSets);
897    var result = [];
898    for(var i=0, len=targets.length; i<len; i++) {
899      result.push(data[targets[i]]);
900    }
901
902    return result;
903  }
904
905  /**
906   * Creates an object where the keys are the elements of the input array
907   * and the values are true. To be used for set operations with integer.
908   **/
909  function arrToObj(arr) {
910    var o = {};
911    var i,len;
912    for(i=0, len=arr.length; i<len; i++) {
913      o[arr[i]] = true;
914    }
915    return o;
916  }
917
918  /**
919   * Converts the keys of an object to an array after converting
920   * each key to integer. To be used for set operations with integers.
921   **/
922  function objToArr(obj) {
923    var result = [];
924    for(var k in obj) {
925      if (obj.hasOwnProperty(k)) {
926        result.push(parseInt(k));
927      }
928    }
929    return result;
930  }
931
932  /**
933   * Find the intersection of a set of arrays.
934   **/
935  function intersectArrs(sets) {
936    var temp, obj;
937
938    if (sets.length === 1) {
939      return sets[0];
940    }
941
942    // sort by size and load the smallest into the object
943    sets.sort(function(a,b) { return a.length - b.length; });
944    obj = arrToObj(sets[0]);
945
946    // shrink the hash as we fail to find elements in the other sets
947    for(var i=1, len=sets.length; i<len; i++) {
948      temp = arrToObj(sets[i]);
949      for(var k in obj) {
950        if (obj.hasOwnProperty(k) && !temp[k]) {
951          delete obj[k];
952        }
953      }
954    }
955
956    return objToArr(obj);
957  }
958
959  /**
960   * Extract the column values from an ImagePair (contained in the server
961   * response) into filter and data columns.
962   *
963   * @return {[]} Array of data contained in one data row.
964   **/
965  function getColValues(imagePair, filterCols, otherCols) {
966    var result = [];
967    for(var i=0, len=filterCols.length; i<len; i++) {
968      result.push(imagePair.extraColumns[filterCols[i].key]);
969    }
970
971    for(var i=0, len=otherCols.length; i<len; i++) {
972      result.push(get_robust(imagePair, ['expectations', otherCols[i].key]));
973    }
974
975    return result;
976  }
977
978  /**
979   * Extract the image meta data from an Image pair returned by the server.
980   **/
981  function getImageValues(imagePair, imageCols) {
982    var result=[];
983    var url, value, percent, diff;
984    var CO = c.IMG_COL_ORDER;
985
986    for(var i=0, len=imageCols.length; i<len; i++) {
987      percent = get_robust(imagePair, CO[i].percentField);
988      value = get_robust(imagePair, CO[i].valueField);
989      url = get_robust(imagePair, CO[i].urlField);
990      if (url) {
991        url = imageCols[i].baseUrl + url;
992      }
993      result.push(new ImgVal(url, percent, value));
994    }
995
996    return result;
997  }
998
999  /**
1000   * Given an object find sub objects for the given index without
1001   * throwing an error if any of the sub objects do not exist.
1002   **/
1003  function get_robust(obj, idx) {
1004    if (!idx) {
1005      return;
1006    }
1007
1008    for(var i=0, len=idx.length; i<len; i++) {
1009      if ((typeof obj === 'undefined') || (!idx[i])) {
1010        return;  // returns 'undefined'
1011      }
1012
1013      obj = obj[idx[i]];
1014    }
1015
1016    return obj;
1017  }
1018
1019  /**
1020   * Set all elements in the array to the given value.
1021   **/
1022  function setArrVals(arr, newVal) {
1023    for(var i=0, len=arr.length; i<len; i++) {
1024      arr[i] = newVal;
1025    }
1026  }
1027
1028  /**
1029   * Toggle the elements of a boolean array.
1030   *
1031   **/
1032  function toggleArrVals(arr) {
1033    for(var i=0, len=arr.length; i<len; i++) {
1034      arr[i] = !arr[i];
1035    }
1036  }
1037
1038  /**
1039   * Sort the array of DataRow instances with the given compare functions
1040   * and the column at the given index either in ascending or descending order.
1041   **/
1042  function sortData (allData, compareFunctions, sortByIdx, sortOrderAsc) {
1043    var cmpFn = compareFunctions[sortByIdx];
1044    var useCmp = cmpFn;
1045    if (!sortOrderAsc) {
1046      useCmp = function ( _ ) {
1047        return -cmpFn.apply(this, arguments);
1048      };
1049    }
1050    allData.sort(useCmp);
1051  }
1052
1053
1054  // *****************************  Services *********************************
1055
1056  /**
1057   *  Encapsulates all interactions with the backend by handling
1058   *  Urls and HTTP requests. Also exposes some utility functions
1059   *  related to processing Urls.
1060   */
1061  app.factory('dataService', [ '$http', function ($http) {
1062    /** Backend related constants  **/
1063    var c = {
1064      /** Url to retrieve failures */
1065      FAILURES: '/results/failures',
1066
1067      /** Url to retrieve all GM results */
1068      ALL:      '/results/all'
1069    };
1070
1071    /**
1072     * Convenience function to retrieve all results.
1073     *
1074     * @return {Promise} Will resolve to either the data (success) or to
1075     *                   the HTTP response (error).
1076     **/
1077    function loadAll() {
1078      return httpGetData(c.ALL);
1079    }
1080
1081    /**
1082     * Make a HTTP get request with the given query parameters.
1083     *
1084     * @param {}
1085     * @param {}
1086     *
1087     * @return {}
1088     **/
1089    function httpGetData(url, queryParams) {
1090      var reqConfig = {
1091        method: 'GET',
1092        url: url,
1093        params: queryParams
1094      };
1095
1096      return $http(reqConfig).then(
1097        function(successResp) {
1098          return successResp.data;
1099        });
1100    }
1101
1102    /**
1103     * Takes an arbitrary number of objects and generates a Url encoded
1104     * query string.
1105     *
1106     **/
1107    function getQueryString( _params_ ) {
1108      var result = [];
1109      for(var i=0, len=arguments.length; i < len; i++) {
1110        if (arguments[i]) {
1111          for(var k in arguments[i]) {
1112            if (arguments[i].hasOwnProperty(k)) {
1113              result.push(encodeURIComponent(k) + '=' +
1114                          encodeURIComponent(arguments[i][k]));
1115            }
1116          }
1117        }
1118      }
1119      return result.join("&");
1120    }
1121
1122    // Interface of the service:
1123    return {
1124      getQueryString: getQueryString,
1125      loadAll: loadAll
1126    };
1127
1128  }]);
1129
1130})();
1131