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