Home | History | Annotate | Download | only in js
      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