| Index: gm/rebaseline_server/static/new/js/app.js
|
| diff --git a/gm/rebaseline_server/static/new/js/app.js b/gm/rebaseline_server/static/new/js/app.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0a1fac0a450f848083742c51e9ab3a159cab6ed9
|
| --- /dev/null
|
| +++ b/gm/rebaseline_server/static/new/js/app.js
|
| @@ -0,0 +1,1130 @@
|
| +'use strict';
|
| +
|
| +/**
|
| + * TODO (stephana@): This is still work in progress.
|
| + * It does not offer the same functionality as the current version, but
|
| + * will serve as the starting point for a new backend.
|
| + * It works with the current backend, but does not support rebaselining.
|
| + */
|
| +
|
| +/*
|
| + * Wrap everything into an IIFE to not polute the global namespace.
|
| + */
|
| +(function () {
|
| +
|
| + // Declare app level module which contains everything of the current app.
|
| + // ui.bootstrap refers to directives defined in the AngularJS Bootstrap
|
| + // UI package (http://angular-ui.github.io/bootstrap/).
|
| + var app = angular.module('rbtApp', ['ngRoute', 'ui.bootstrap']);
|
| +
|
| + // Configure the different within app views.
|
| + app.config(['$routeProvider', function($routeProvider) {
|
| + $routeProvider.when('/', {templateUrl: 'partials/index-view.html',
|
| + controller: 'IndexCtrl'});
|
| + $routeProvider.when('/view', {templateUrl: 'partials/rebaseline-view.html',
|
| + controller: 'RebaselineCrtrl'});
|
| + $routeProvider.otherwise({redirectTo: '/'});
|
| + }]);
|
| +
|
| +
|
| + // TODO (stephana): Some of these constants are 'gm' specific. In the
|
| + // next iteration we need to remove those as we move the more generic
|
| + // 'dm' testing tool.
|
| + //
|
| + // Shared constants used here and in the markup. These are exported when
|
| + // when used by a controller.
|
| + var c = {
|
| + // Define different view states as we load the data.
|
| + ST_LOADING: 1,
|
| + ST_STILL_LOADING: 2,
|
| + ST_READY: 3,
|
| +
|
| + // These column types are used by the Column class.
|
| + COL_T_FILTER: 'filter',
|
| + COL_T_IMAGE: 'image',
|
| + COL_T_REGULAR: 'regular',
|
| +
|
| + // Request parameters used to select between subsets of results.
|
| + RESULTS_ALL: 'all',
|
| + RESULTS_FAILURES: 'failures',
|
| +
|
| + // Filter types are used by the Column class.
|
| + FILTER_FREE_FORM: 'free_form',
|
| + FILTER_CHECK_BOX: 'checkbox',
|
| +
|
| + // Columns either provided by the backend response or added in code.
|
| + // TODO (stephana): This should go away once we switch to 'dm'.
|
| + COL_BUGS: 'bugs',
|
| + COL_IGNORE_FAILURE: 'ignore-failure',
|
| + COL_REVIEWED_BY_HUMANS: 'reviewed-by-human',
|
| +
|
| + // Defines the order in which image columns appear.
|
| + // TODO (stephana@): needs to be driven by backend data.
|
| + IMG_COL_ORDER: [
|
| + {
|
| + key: 'imageA',
|
| + urlField: ['imageAUrl']
|
| + },
|
| + {
|
| + key: 'imageB',
|
| + urlField: ['imageBUrl']
|
| + },
|
| + {
|
| + key: 'whiteDiffs',
|
| + urlField: ['differenceData', 'whiteDiffUrl'],
|
| + percentField: ['differenceData', 'percentDifferingPixels'],
|
| + valueField: ['differenceData', 'numDifferingPixels']
|
| + },
|
| + {
|
| + key: 'diffs',
|
| + urlField: ['differenceData', 'diffUrl'],
|
| + percentField: ['differenceData', 'perceptualDifference'],
|
| + valueField: ['differenceData', 'maxDiffPerChannel']
|
| + }
|
| + ],
|
| +
|
| + // Choice of availabe image size selection.
|
| + IMAGE_SIZES: [
|
| + 100,
|
| + 200,
|
| + 400
|
| + ],
|
| +
|
| + // Choice of available number of records selection.
|
| + MAX_RECORDS: [
|
| + '100',
|
| + '200',
|
| + '300'
|
| + ]
|
| + }; // end constants
|
| +
|
| + /*
|
| + * Index Controller
|
| + */
|
| + // TODO (stephana): Remove $timeout since it only simulates loading delay.
|
| + app.controller('IndexCtrl', ['$scope', '$timeout', 'dataService',
|
| + function($scope, $timeout, dataService) {
|
| + // init the scope
|
| + $scope.c = c;
|
| + $scope.state = c.ST_LOADING;
|
| + $scope.qStr = dataService.getQueryString;
|
| +
|
| + // TODO (stephana): Remove and replace with index data generated by the
|
| + // backend to reflect the current "known" image sets to compare.
|
| + $scope.allSKPs = [
|
| + {
|
| + params: {
|
| + setBSection: 'actual-results',
|
| + setASection: 'expected-results',
|
| + setBDir: 'gs://chromium-skia-skp-summaries/' +
|
| + 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug',
|
| + setADir: 'repo:expectations/skp/' +
|
| + 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
|
| + },
|
| + title: 'expected vs actuals on ' +
|
| + 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
|
| + },
|
| + {
|
| + params: {
|
| + setBSection: 'actual-results',
|
| + setASection: 'expected-results',
|
| + setBDir: 'gs://chromium-skia-skp-summaries/' +
|
| + 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
|
| + setADir: 'repo:expectations/skp/'+
|
| + 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
|
| + },
|
| + title: 'expected vs actuals on Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
|
| + },
|
| + {
|
| + params: {
|
| + setBSection: 'actual-results',
|
| + setASection: 'actual-results',
|
| + setBDir: 'gs://chromium-skia-skp-summaries/' +
|
| + 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
|
| + setADir: 'gs://chromium-skia-skp-summaries/' +
|
| + 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
|
| + },
|
| + title: 'Actuals on Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug ' +
|
| + 'vs Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
|
| + }
|
| + ];
|
| +
|
| + // TODO (stephana): Remove this once we load index data from the server.
|
| + $timeout(function () {
|
| + $scope.state = c.ST_READY;
|
| + });
|
| + }]);
|
| +
|
| + /*
|
| + * RebaselineCtrl
|
| + * Controls the main comparison view.
|
| + *
|
| + * @param {service} dataService Service that encapsulates functions to
|
| + * retrieve data from the backend.
|
| + *
|
| + */
|
| + app.controller('RebaselineCrtrl', ['$scope', '$timeout', 'dataService',
|
| + function($scope, $timeout, dataService) {
|
| + // determine which to request
|
| + // TODO (stephana): This should be extracted from the query parameters.
|
| + var target = c.TARGET_GM;
|
| +
|
| + // process the rquest arguments
|
| + // TODO (stephana): This should be determined from the query parameters.
|
| + var loadFn = dataService.loadAll;
|
| +
|
| + // controller state variables
|
| + var allData = null;
|
| + var filterFuncs = null;
|
| + var currentData = null;
|
| + var selectedData = null;
|
| +
|
| + // Index of the column that should provide the sort key
|
| + var sortByIdx = 0;
|
| +
|
| + // Sort in asending (true) or descending (false) order
|
| + var sortOrderAsc = true;
|
| +
|
| + // Array of functions for each column used for comparison during sort.
|
| + var compareFunctions = null;
|
| +
|
| + // Variables to track load and render times
|
| + var startTime;
|
| + var loadStartTime;
|
| +
|
| +
|
| + /** Load the data from the backend **/
|
| + loadStartTime = Date.now();
|
| + function loadData() {
|
| + loadFn().then(
|
| + function (serverData) {
|
| + $scope.header = serverData.header;
|
| + $scope.loadTime = (Date.now() - loadStartTime)/1000;
|
| +
|
| + // keep polling if the data are not ready yet
|
| + if ($scope.header.resultsStillLoading) {
|
| + $scope.state = c.ST_STILL_LOADING;
|
| + $timeout(loadData, 5000);
|
| + return;
|
| + }
|
| +
|
| + // get the filter colunms and an array to hold filter data by user
|
| + var fcol = getFilterColumns(serverData);
|
| + $scope.filterCols = fcol[0];
|
| + $scope.filterVals = fcol[1];
|
| +
|
| + // Add extra columns and retrieve the image columns
|
| + var otherCols = [ Column.regular(c.COL_BUGS) ];
|
| + var imageCols = getImageColumns(serverData);
|
| +
|
| + // Concat to get all columns
|
| + // NOTE: The order is important since filters are rendered first,
|
| + // followed by regular columns and images
|
| + $scope.allCols = $scope.filterCols.concat(otherCols, imageCols);
|
| +
|
| + // Pre-process the data and get the filter functions.
|
| + var dataFilters = getDataAndFilters(serverData, $scope.filterCols,
|
| + otherCols, imageCols);
|
| + allData = dataFilters[0];
|
| + filterFuncs = dataFilters[1];
|
| +
|
| + // Get regular columns (== not image columns)
|
| + var regularCols = $scope.filterCols.concat(otherCols);
|
| +
|
| + // Get the compare functions for regular and image columns. These
|
| + // are then used to sort by the respective columns.
|
| + compareFunctions = DataRow.getCompareFunctions(regularCols,
|
| + imageCols);
|
| +
|
| + // Filter and sort the results to get them ready for rendering
|
| + updateResults();
|
| +
|
| + // Data are ready for display
|
| + $scope.state = c.ST_READY;
|
| + },
|
| + function (httpErrResponse) {
|
| + console.log(httpErrResponse);
|
| + });
|
| + };
|
| +
|
| + /*
|
| + * updateResults
|
| + * Central render function. Everytime settings/filters/etc. changed
|
| + * this function is called to filter, sort and splice the data.
|
| + *
|
| + * NOTE (stephana): There is room for improvement here: before filtering
|
| + * and sorting we could check if this is necessary. But this has not been
|
| + * a bottleneck so far.
|
| + */
|
| + function updateResults () {
|
| + // run digest before we update the results. This allows
|
| + // updateResults to be called from functions trigger by ngChange
|
| + $scope.updating = true;
|
| + startTime = Date.now();
|
| +
|
| + // delay by one render cycle so it can be called via ng-change
|
| + $timeout(function() {
|
| + // filter data
|
| + selectedData = filterData(allData, filterFuncs, $scope.filterVals);
|
| +
|
| + // sort the selected data.
|
| + sortData(selectedData, compareFunctions, sortByIdx, sortOrderAsc);
|
| +
|
| + // only conside the elements that we really need
|
| + var nRecords = $scope.settings.nRecords;
|
| + currentData = selectedData.slice(0, parseInt(nRecords));
|
| +
|
| + DataRow.setRowspanValues(currentData, $scope.mergeIdenticalRows);
|
| +
|
| + // update the scope with relevant data for rendering.
|
| + $scope.data = currentData;
|
| + $scope.totalRecords = allData.length;
|
| + $scope.showingRecords = currentData.length;
|
| + $scope.selectedRecords = selectedData.length;
|
| + $scope.updating = false;
|
| +
|
| + // measure the filter time and total render time (via timeout).
|
| + $scope.filterTime = Date.now() - startTime;
|
| + $timeout(function() {
|
| + $scope.renderTime = Date.now() - startTime;
|
| + });
|
| + });
|
| + };
|
| +
|
| + /**
|
| + * Generate the style value to set the width of images.
|
| + *
|
| + * @param {Column} col Column that we are trying to render.
|
| + * @param {int} paddingPx Number of padding pixels.
|
| + * @param {string} defaultVal Default value if not an image column.
|
| + *
|
| + * @return {string} Value to be used in ng-style element to set the width
|
| + * of a image column.
|
| + **/
|
| + $scope.getImageWidthStyle = function (col, paddingPx, defaultVal) {
|
| + var result = (col.ctype === c.COL_T_IMAGE) ?
|
| + ($scope.imageSize + paddingPx + 'px') : defaultVal;
|
| + return result;
|
| + };
|
| +
|
| + /**
|
| + * Sets the column by which to sort the data. If called for the
|
| + * currently sorted column it will cause the sort to toggle between
|
| + * ascending and descending.
|
| + *
|
| + * @param {int} colIdx Index of the column to use for sorting.
|
| + **/
|
| + $scope.sortBy = function (colIdx) {
|
| + if (sortByIdx === colIdx) {
|
| + sortOrderAsc = !sortOrderAsc;
|
| + } else {
|
| + sortByIdx = colIdx;
|
| + sortOrderAsc = true;
|
| + }
|
| + updateResults();
|
| + };
|
| +
|
| + /**
|
| + * Helper function to generate a CSS class indicating whether this column
|
| + * is the sort key. If it is a class name with the sort direction (Asc/Desc) is
|
| + * return otherwise the default value is returned. In markup we use this
|
| + * to display (or not display) an arrow next to the column name.
|
| + *
|
| + * @param {string} prefix Prefix of the classname to be generated.
|
| + * @param {int} idx Index of the target column.
|
| + * @param {string} defaultVal Value to return if current column is not used
|
| + * for sorting.
|
| + *
|
| + * @return {string} CSS class name that a combination of the prefix and
|
| + * direction indicator ('Asc' or 'Desc') if the column is
|
| + * used for sorting. Otherwise the defaultVal is returned.
|
| + **/
|
| + $scope.getSortedClass = function (prefix, idx, defaultVal) {
|
| + if (idx === sortByIdx) {
|
| + return prefix + ((sortOrderAsc) ? 'Asc' : 'Desc');
|
| + }
|
| +
|
| + return defaultVal;
|
| + };
|
| +
|
| + /**
|
| + * Checkbox to merge identical records has change. Force an update.
|
| + **/
|
| + $scope.mergeRowsChanged = function () {
|
| + updateResults();
|
| + }
|
| +
|
| + /**
|
| + * Max number of records to display has changed. Force an update.
|
| + **/
|
| + $scope.maxRecordsChanged = function () {
|
| + updateResults();
|
| + };
|
| +
|
| + /**
|
| + * Filter settings changed. Force an update.
|
| + **/
|
| + $scope.filtersChanged = function () {
|
| + updateResults();
|
| + };
|
| +
|
| + /**
|
| + * Sets all possible values of the specified values to the given value.
|
| + * That means all checkboxes are eiter selected or unselected.
|
| + * Then force an update.
|
| + *
|
| + * @param {int} idx Index of the target filter column.
|
| + * @param {boolean} val Value to set the filter values to.
|
| + *
|
| + **/
|
| + $scope.setFilterAll = function (idx, val) {
|
| + for(var i=0, len=$scope.filterVals[idx].length; i<len; i++) {
|
| + $scope.filterVals[idx][i] = val;
|
| + }
|
| + updateResults();
|
| + };
|
| +
|
| + /**
|
| + * Toggle the values of a filter. This toggles all values in a
|
| + * filter.
|
| + *
|
| + * @param {int} idx Index of the target filter column.
|
| + **/
|
| + $scope.setFilterToggle = function (idx) {
|
| + for(var i=0, len=$scope.filterVals[idx].length; i<len; i++) {
|
| + $scope.filterVals[idx][i] = !$scope.filterVals[idx][i];
|
| + }
|
| + updateResults();
|
| + };
|
| +
|
| + // ****************************************
|
| + // Initialize the scope.
|
| + // ****************************************
|
| +
|
| + // Inject the constants into the scope and set the initial state.
|
| + $scope.c = c;
|
| + $scope.state = c.ST_LOADING;
|
| +
|
| + // Initial settings
|
| + $scope.settings = {
|
| + showThumbnails: true,
|
| + imageSize: c.IMAGE_SIZES[0],
|
| + nRecords: c.MAX_RECORDS[0],
|
| + mergeIdenticalRows: true
|
| + };
|
| +
|
| + // Initial values for filters set in loadData()
|
| + $scope.filterVals = [];
|
| +
|
| + // Information about records - set in loadData()
|
| + $scope.totalRecords = 0;
|
| + $scope.showingRecords = 0;
|
| + $scope.updating = false;
|
| +
|
| + // Trigger the data loading.
|
| + loadData();
|
| +
|
| + }]);
|
| +
|
| + // data structs to interface with markup and backend
|
| + /**
|
| + * Models a column. It aggregates attributes of all
|
| + * columns types. Some might be empty. See convenience
|
| + * factory methods below for different column types.
|
| + *
|
| + * @param {string} key Uniquely identifies this columns
|
| + * @param {string} ctype Type of columns. Use COL_* constants.
|
| + * @param {string} ctitle Human readable title of the column.
|
| + * @param {string} ftype Filter type. Use FILTER_* constants.
|
| + * @param {FilterOpt[]} foptions Filter options. For 'checkbox' filters this
|
| + is used to render all the checkboxes.
|
| + For freeform filters this is a list of all
|
| + available values.
|
| + * @param {string} baseUrl Baseurl for image columns. All URLs are relative
|
| + to this.
|
| + *
|
| + * @return {Column} Instance of the Column class.
|
| + **/
|
| + function Column(key, ctype, ctitle, ftype, foptions, baseUrl) {
|
| + this.key = key;
|
| + this.ctype = ctype;
|
| + this.ctitle = ctitle;
|
| + this.ftype = ftype;
|
| + this.foptions = foptions;
|
| + this.baseUrl = baseUrl;
|
| + this.foptionsArr = [];
|
| +
|
| + // get the array of filter options for lookup in indexOfOptVal
|
| + if (this.foptions) {
|
| + for(var i=0, len=foptions.length; i<len; i++) {
|
| + this.foptionsArr.push(this.foptions[i].value);
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Find the index of an value in a column with a fixed set
|
| + * of options.
|
| + *
|
| + * @param {string} optVal Value of the column.
|
| + *
|
| + * @return {int} Index of optVal in this column.
|
| + **/
|
| + Column.prototype.indexOfOptVal = function (optVal) {
|
| + return this.foptionsArr.indexOf(optVal);
|
| + };
|
| +
|
| + /**
|
| + * Set filter options for this column.
|
| + *
|
| + * @param {FilterOpt[]} foptions Possible values for this column.
|
| + **/
|
| + Column.prototype.setFilterOptions = function (foptions) {
|
| + this.foptions = foptions;
|
| + };
|
| +
|
| + /**
|
| + * Factory function to create a filter column. Same args as Column()
|
| + **/
|
| + Column.filter = function(key, ctitle, ftype, foptions) {
|
| + return new Column(key, c.COL_T_FILTER, ctitle || key, ftype, foptions);
|
| + }
|
| +
|
| + /**
|
| + * Factory function to create an image column. Same args as Column()
|
| + **/
|
| + Column.image = function (key, ctitle, baseUrl) {
|
| + return new Column(key, c.COL_T_IMAGE, ctitle || key, null, null, baseUrl);
|
| + };
|
| +
|
| + /**
|
| + * Factory function to create a regular column. Same args as Column()
|
| + **/
|
| + Column.regular = function (key, ctitle) {
|
| + return new Column(key, c.COL_T_REGULAR, ctitle || key);
|
| + };
|
| +
|
| + /**
|
| + * Helper class to wrap a single option in a filter.
|
| + *
|
| + * @param {string} value Option value.
|
| + * @param {int} count Number of instances of this option in the dataset.
|
| + *
|
| + * @return {} Instance of FiltertOpt
|
| + **/
|
| + function FilterOpt(value, count) {
|
| + this.value = value;
|
| + this.count = count;
|
| + }
|
| +
|
| + /**
|
| + * Container for a single row in the dataset.
|
| + *
|
| + * @param {int} rowspan Number of rows (including this and following rows)
|
| + that have identical values.
|
| + * @param {string[]} dataCols Values of the respective columns (combination
|
| + of filter and regular columns)
|
| + * @param {ImgVal[]} imageCols Image meta data for the image columns.
|
| + *
|
| + * @return {DataRow} Instance of DataRow.
|
| + **/
|
| + function DataRow(rowspan, dataCols, imageCols) {
|
| + this.rowspan = rowspan;
|
| + this.dataCols = dataCols;
|
| + this.imageCols = imageCols;
|
| + }
|
| +
|
| + /**
|
| + * Gets the comparator functions for the columns in this dataset.
|
| + * The comparators are then used to sort the dataset by the respective
|
| + * column.
|
| + *
|
| + * @param {Column[]} dataCols Data columns (= non-image columns)
|
| + * @param {Column[]} imgCols Image columns.
|
| + *
|
| + * @return {Function[]} Array of functions that can be used to sort by the
|
| + * respective column.
|
| + **/
|
| + DataRow.getCompareFunctions = function (dataCols, imgCols) {
|
| + var result = [];
|
| + for(var i=0, len=dataCols.length; i<len; i++) {
|
| + result.push(( function (col, idx) {
|
| + return function (a, b) {
|
| + return (a.dataCols[idx] < b.dataCols[idx]) ? -1 :
|
| + ((a.dataCols[idx] === b.dataCols[idx]) ? 0 : 1);
|
| + };
|
| + }(dataCols[i], i) ));
|
| + }
|
| +
|
| + for(var i=0, len=imgCols.length; i<len; i++) {
|
| + result.push((function (col, idx) {
|
| + return function (a,b) {
|
| + var aVal = a.imageCols[idx].percent;
|
| + var bVal = b.imageCols[idx].percent;
|
| +
|
| + return (aVal < bVal) ? -1 : ((aVal === bVal) ? 0 : 1);
|
| + };
|
| + }(imgCols[i], i) ));
|
| + }
|
| +
|
| + return result;
|
| + };
|
| +
|
| + /**
|
| + * Set the rowspan values of a given array of DataRow instances.
|
| + *
|
| + * @param {DataRow[]} data Dataset in desired order (after sorting).
|
| + * @param {mergeRows} mergeRows Indicate whether to sort
|
| + **/
|
| + DataRow.setRowspanValues = function (data, mergeRows) {
|
| + var curIdx, rowspan, cur;
|
| + if (mergeRows) {
|
| + for(var i=0, len=data.length; i<len;) {
|
| + curIdx = i;
|
| + cur = data[i];
|
| + rowspan = 1;
|
| + for(i++; ((i<len) && (data[i].dataCols === cur.dataCols)); i++) {
|
| + rowspan++;
|
| + data[i].rowspan=0;
|
| + }
|
| + data[curIdx].rowspan = rowspan;
|
| + }
|
| + } else {
|
| + for(var i=0, len=data.length; i<len; i++) {
|
| + data[i].rowspan = 1;
|
| + }
|
| + }
|
| + };
|
| +
|
| + /**
|
| + * Wrapper class for image related data.
|
| + *
|
| + * @param {string} url Relative Url of the image or null if not available.
|
| + * @param {float} percent Percent of pixels that are differing.
|
| + * @param {int} value Absolute number of pixes differing.
|
| + *
|
| + * @return {ImgVal} Instance of ImgVal.
|
| + **/
|
| + function ImgVal(url, percent, value) {
|
| + this.url = url;
|
| + this.percent = percent;
|
| + this.value = value;
|
| + }
|
| +
|
| + /**
|
| + * Extracts the filter columns from the JSON response of the server.
|
| + *
|
| + * @param {object} data Server response.
|
| + *
|
| + * @return {Column[]} List of filter columns as described in 'header' field.
|
| + **/
|
| + function getFilterColumns(data) {
|
| + var result = [];
|
| + var vals = [];
|
| + var colOrder = data.extraColumnOrder;
|
| + var colHeaders = data.extraColumnHeaders;
|
| + var fopts, optVals, val;
|
| +
|
| + for(var i=0, len=colOrder.length; i<len; i++) {
|
| + if (colHeaders[colOrder[i]].isFilterable) {
|
| + if (colHeaders[colOrder[i]].useFreeformFilter) {
|
| + result.push(Column.filter(colOrder[i],
|
| + colHeaders[colOrder[i]].headerText,
|
| + c.FILTER_FREE_FORM));
|
| + vals.push('');
|
| + }
|
| + else {
|
| + fopts = [];
|
| + optVals = [];
|
| +
|
| + // extract the different options for this column
|
| + for(var j=0, jlen=colHeaders[colOrder[i]].valuesAndCounts.length;
|
| + j<jlen; j++) {
|
| + val = colHeaders[colOrder[i]].valuesAndCounts[j];
|
| + fopts.push(new FilterOpt(val[0], val[1]));
|
| + optVals.push(false);
|
| + }
|
| +
|
| + // ad the column and values
|
| + result.push(Column.filter(colOrder[i],
|
| + colHeaders[colOrder[i]].headerText,
|
| + c.FILTER_CHECK_BOX,
|
| + fopts));
|
| + vals.push(optVals);
|
| + }
|
| + }
|
| + }
|
| +
|
| + return [result, vals];
|
| + }
|
| +
|
| + /**
|
| + * Extracts the image columns from the JSON response of the server.
|
| + *
|
| + * @param {object} data Server response.
|
| + *
|
| + * @return {Column[]} List of images columns as described in 'header' field.
|
| + **/
|
| + function getImageColumns(data) {
|
| + var CO = c.IMG_COL_ORDER;
|
| + var imgSet;
|
| + var result = [];
|
| + for(var i=0, len=CO.length; i<len; i++) {
|
| + imgSet = data.imageSets[CO[i].key];
|
| + result.push(Column.image(CO[i].key,
|
| + imgSet.description,
|
| + ensureTrailingSlash(imgSet.baseUrl)));
|
| + }
|
| + return result;
|
| + }
|
| +
|
| + /**
|
| + * Make sure Url has a trailing '/'.
|
| + *
|
| + * @param {string} url Base url.
|
| + * @return {string} Same url with a trailing '/' or same as input if it
|
| + already contained '/'.
|
| + **/
|
| + function ensureTrailingSlash(url) {
|
| + var result = url.trim();
|
| +
|
| + // TODO: remove !!!
|
| + result = fixUrl(url);
|
| + if (result[result.length-1] !== '/') {
|
| + result += '/';
|
| + }
|
| + return result;
|
| + }
|
| +
|
| + // TODO: remove. The backend should provide absoute URLs
|
| + function fixUrl(url) {
|
| + url = url.trim();
|
| + if ('http' === url.substr(0, 4)) {
|
| + return url;
|
| + }
|
| +
|
| + var idx = url.indexOf('static');
|
| + if (idx != -1) {
|
| + return '/' + url.substr(idx);
|
| + }
|
| +
|
| + return url;
|
| + };
|
| +
|
| + /**
|
| + * Processes that data and returns filter functions.
|
| + *
|
| + * @param {object} Server response.
|
| + * @param {Column[]} filterCols Filter columns.
|
| + * @param {Column[]} otherCols Columns that are neither filters nor images.
|
| + * @param {Column[]} imageCols Image columns.
|
| + *
|
| + * @return {[]} Returns a pair [dataRows, filterFunctions] where:
|
| + * - dataRows is an array of DataRow instances.
|
| + * - filterFunctions is an array of functions that can be used to
|
| + * filter the column at the corresponding index.
|
| + *
|
| + **/
|
| + function getDataAndFilters(data, filterCols, otherCols, imageCols) {
|
| + var el;
|
| + var result = [];
|
| + var lookupIndices = [];
|
| + var indexerFuncs = [];
|
| + var temp;
|
| +
|
| + // initialize the lookupIndices
|
| + var filterFuncs = initIndices(filterCols, lookupIndices, indexerFuncs);
|
| +
|
| + // iterate over the data and get the rows
|
| + for(var i=0, len=data.imagePairs.length; i<len; i++) {
|
| + el = data.imagePairs[i];
|
| + temp = new DataRow(1, getColValues(el, filterCols, otherCols),
|
| + getImageValues(el, imageCols));
|
| + result.push(temp);
|
| +
|
| + // index the row
|
| + for(var j=0, jlen=filterCols.length; j < jlen; j++) {
|
| + indexerFuncs[j](lookupIndices[j], filterCols[j], temp.dataCols[j], i);
|
| + }
|
| + }
|
| +
|
| + setFreeFormFilterOptions(filterCols, lookupIndices);
|
| + return [result, filterFuncs];
|
| + }
|
| +
|
| + /**
|
| + * Initiazile the lookup indices and indexer functions for the filter
|
| + * columns.
|
| + *
|
| + * @param {Column} filterCols Filter columns
|
| + * @param {[]} lookupIndices Will be filled with datastructures for
|
| + fast lookup (output parameter)
|
| + * @param {[]} lookupIndices Will be filled with functions to index data
|
| + of the column with the corresponding column.
|
| + *
|
| + * @return {[]} Returns an array of filter functions that can be used to
|
| + filter the respective column.
|
| + **/
|
| + function initIndices(filterCols, lookupIndices, indexerFuncs) {
|
| + var filterFuncs = [];
|
| + var temp;
|
| +
|
| + for(var i=0, len=filterCols.length; i<len; i++) {
|
| + if (filterCols[i].ftype === c.FILTER_FREE_FORM) {
|
| + lookupIndices.push({});
|
| + indexerFuncs.push(indexFreeFormValue);
|
| + filterFuncs.push(
|
| + getFreeFormFilterFunc(lookupIndices[lookupIndices.length-1]));
|
| + }
|
| + else if (filterCols[i].ftype === c.FILTER_CHECK_BOX) {
|
| + temp = [];
|
| + for(var j=0, jlen=filterCols[i].foptions.length; j<jlen; j++) {
|
| + temp.push([]);
|
| + }
|
| + lookupIndices.push(temp);
|
| + indexerFuncs.push(indexDiscreteValue);
|
| + filterFuncs.push(
|
| + getDiscreteFilterFunc(lookupIndices[lookupIndices.length-1]));
|
| + }
|
| + }
|
| +
|
| + return filterFuncs;
|
| + }
|
| +
|
| + /**
|
| + * Helper function that extracts the values of free form columns from
|
| + * the lookupIndex and injects them into the Column object as FilterOpt
|
| + * objects.
|
| + **/
|
| + function setFreeFormFilterOptions(filterCols, lookupIndices) {
|
| + var temp, k;
|
| + for(var i=0, len=filterCols.length; i<len; i++) {
|
| + if (filterCols[i].ftype === c.FILTER_FREE_FORM) {
|
| + temp = []
|
| + for(k in lookupIndices[i]) {
|
| + if (lookupIndices[i].hasOwnProperty(k)) {
|
| + temp.push(new FilterOpt(k, lookupIndices[i][k].length));
|
| + }
|
| + }
|
| + filterCols[i].setFilterOptions(temp);
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Index a discrete column (column with fixed number of values).
|
| + *
|
| + **/
|
| + function indexDiscreteValue(lookupIndex, col, dataVal, dataRowIndex) {
|
| + var i = col.indexOfOptVal(dataVal);
|
| + lookupIndex[i].push(dataRowIndex);
|
| + }
|
| +
|
| + /**
|
| + * Index a column with free form text (= not fixed upfront)
|
| + *
|
| + **/
|
| + function indexFreeFormValue(lookupIndex, col, dataVal, dataRowIndex) {
|
| + if (!lookupIndex[dataVal]) {
|
| + lookupIndex[dataVal] = [];
|
| + }
|
| + lookupIndex[dataVal].push(dataRowIndex);
|
| + }
|
| +
|
| +
|
| + /**
|
| + * Get the function to filter a column with the given lookup index
|
| + * for discrete (fixed upfront) values.
|
| + *
|
| + **/
|
| + function getDiscreteFilterFunc(lookupIndex) {
|
| + return function(filterVal) {
|
| + var result = [];
|
| + for(var i=0, len=lookupIndex.length; i < len; i++) {
|
| + if (filterVal[i]) {
|
| + // append the indices to the current array
|
| + result.push.apply(result, lookupIndex[i]);
|
| + }
|
| + }
|
| + return { nofilter: false, records: result };
|
| + };
|
| + }
|
| +
|
| + /**
|
| + * Get the function to filter a column with the given lookup index
|
| + * for free form values.
|
| + *
|
| + **/
|
| + function getFreeFormFilterFunc(lookupIndex) {
|
| + return function(filterVal) {
|
| + filterVal = filterVal.trim();
|
| + if (filterVal === '') {
|
| + return { nofilter: true };
|
| + }
|
| + return {
|
| + nofilter: false,
|
| + records: lookupIndex[filterVal] || []
|
| + };
|
| + };
|
| + }
|
| +
|
| + /**
|
| + * Filters the data based on the given filterColumns and
|
| + * corresponding filter values.
|
| + *
|
| + * @return {[]} Subset of the input dataset based on the
|
| + * filter values.
|
| + **/
|
| + function filterData(data, filterFuncs, filterVals) {
|
| + var recordSets = [];
|
| + var filterResult;
|
| +
|
| + // run through all the filters
|
| + for(var i=0, len=filterFuncs.length; i<len; i++) {
|
| + filterResult = filterFuncs[i](filterVals[i]);
|
| + if (!filterResult.nofilter) {
|
| + recordSets.push(filterResult.records);
|
| + }
|
| + }
|
| +
|
| + // If there are no restrictions then return the whole dataset.
|
| + if (recordSets.length === 0) {
|
| + return data;
|
| + }
|
| +
|
| + // intersect the records returned by filters.
|
| + var targets = intersectArrs(recordSets);
|
| + var result = [];
|
| + for(var i=0, len=targets.length; i<len; i++) {
|
| + result.push(data[targets[i]]);
|
| + }
|
| +
|
| + return result;
|
| + }
|
| +
|
| + /**
|
| + * Creates an object where the keys are the elements of the input array
|
| + * and the values are true. To be used for set operations with integer.
|
| + **/
|
| + function arrToObj(arr) {
|
| + var o = {};
|
| + var i,len;
|
| + for(i=0, len=arr.length; i<len; i++) {
|
| + o[arr[i]] = true;
|
| + }
|
| + return o;
|
| + }
|
| +
|
| + /**
|
| + * Converts the keys of an object to an array after converting
|
| + * each key to integer. To be used for set operations with integers.
|
| + **/
|
| + function objToArr(obj) {
|
| + var result = [];
|
| + for(var k in obj) {
|
| + if (obj.hasOwnProperty(k)) {
|
| + result.push(parseInt(k));
|
| + }
|
| + }
|
| + return result;
|
| + }
|
| +
|
| + /**
|
| + * Find the intersection of a set of arrays.
|
| + **/
|
| + function intersectArrs(sets) {
|
| + var temp, obj;
|
| +
|
| + if (sets.length === 1) {
|
| + return sets[0];
|
| + }
|
| +
|
| + // sort by size and load the smallest into the object
|
| + sets.sort(function(a,b) { return a.length - b.length; });
|
| + obj = arrToObj(sets[0]);
|
| +
|
| + // shrink the hash as we fail to find elements in the other sets
|
| + for(var i=1, len=sets.length; i<len; i++) {
|
| + temp = arrToObj(sets[i]);
|
| + for(var k in obj) {
|
| + if (obj.hasOwnProperty(k) && !temp[k]) {
|
| + delete obj[k];
|
| + }
|
| + }
|
| + }
|
| +
|
| + return objToArr(obj);
|
| + }
|
| +
|
| + /**
|
| + * Extract the column values from an ImagePair (contained in the server
|
| + * response) into filter and data columns.
|
| + *
|
| + * @return {[]} Array of data contained in one data row.
|
| + **/
|
| + function getColValues(imagePair, filterCols, otherCols) {
|
| + var result = [];
|
| + for(var i=0, len=filterCols.length; i<len; i++) {
|
| + result.push(imagePair.extraColumns[filterCols[i].key]);
|
| + }
|
| +
|
| + for(var i=0, len=otherCols.length; i<len; i++) {
|
| + result.push(get_robust(imagePair, ['expectations', otherCols[i].key]));
|
| + }
|
| +
|
| + return result;
|
| + }
|
| +
|
| + /**
|
| + * Extract the image meta data from an Image pair returned by the server.
|
| + **/
|
| + function getImageValues(imagePair, imageCols) {
|
| + var result=[];
|
| + var url, value, percent, diff;
|
| + var CO = c.IMG_COL_ORDER;
|
| +
|
| + for(var i=0, len=imageCols.length; i<len; i++) {
|
| + percent = get_robust(imagePair, CO[i].percentField);
|
| + value = get_robust(imagePair, CO[i].valueField);
|
| + url = get_robust(imagePair, CO[i].urlField);
|
| + if (url) {
|
| + url = imageCols[i].baseUrl + url;
|
| + }
|
| + result.push(new ImgVal(url, percent, value));
|
| + }
|
| +
|
| + return result;
|
| + }
|
| +
|
| + /**
|
| + * Given an object find sub objects for the given index without
|
| + * throwing an error if any of the sub objects do not exist.
|
| + **/
|
| + function get_robust(obj, idx) {
|
| + if (!idx) {
|
| + return;
|
| + }
|
| +
|
| + for(var i=0, len=idx.length; i<len; i++) {
|
| + if ((typeof obj === 'undefined') || (!idx[i])) {
|
| + return; // returns 'undefined'
|
| + }
|
| +
|
| + obj = obj[idx[i]];
|
| + }
|
| +
|
| + return obj;
|
| + }
|
| +
|
| + /**
|
| + * Set all elements in the array to the given value.
|
| + **/
|
| + function setArrVals(arr, newVal) {
|
| + for(var i=0, len=arr.length; i<len; i++) {
|
| + arr[i] = newVal;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Toggle the elements of a boolean array.
|
| + *
|
| + **/
|
| + function toggleArrVals(arr) {
|
| + for(var i=0, len=arr.length; i<len; i++) {
|
| + arr[i] = !arr[i];
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Sort the array of DataRow instances with the given compare functions
|
| + * and the column at the given index either in ascending or descending order.
|
| + **/
|
| + function sortData (allData, compareFunctions, sortByIdx, sortOrderAsc) {
|
| + var cmpFn = compareFunctions[sortByIdx];
|
| + var useCmp = cmpFn;
|
| + if (!sortOrderAsc) {
|
| + useCmp = function ( _ ) {
|
| + return -cmpFn.apply(this, arguments);
|
| + };
|
| + }
|
| + allData.sort(useCmp);
|
| + }
|
| +
|
| +
|
| + // ***************************** Services *********************************
|
| +
|
| + /**
|
| + * Encapsulates all interactions with the backend by handling
|
| + * Urls and HTTP requests. Also exposes some utility functions
|
| + * related to processing Urls.
|
| + */
|
| + app.factory('dataService', [ '$http', function ($http) {
|
| + /** Backend related constants **/
|
| + var c = {
|
| + /** Url to retrieve failures */
|
| + FAILURES: '/results/failures',
|
| +
|
| + /** Url to retrieve all GM results */
|
| + ALL: '/results/all'
|
| + };
|
| +
|
| + /**
|
| + * Convenience function to retrieve all results.
|
| + *
|
| + * @return {Promise} Will resolve to either the data (success) or to
|
| + * the HTTP response (error).
|
| + **/
|
| + function loadAll() {
|
| + return httpGetData(c.ALL);
|
| + }
|
| +
|
| + /**
|
| + * Make a HTTP get request with the given query parameters.
|
| + *
|
| + * @param {}
|
| + * @param {}
|
| + *
|
| + * @return {}
|
| + **/
|
| + function httpGetData(url, queryParams) {
|
| + var reqConfig = {
|
| + method: 'GET',
|
| + url: url,
|
| + params: queryParams
|
| + };
|
| +
|
| + return $http(reqConfig).then(
|
| + function(successResp) {
|
| + return successResp.data;
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Takes an arbitrary number of objects and generates a Url encoded
|
| + * query string.
|
| + *
|
| + **/
|
| + function getQueryString( _params_ ) {
|
| + var result = [];
|
| + for(var i=0, len=arguments.length; i < len; i++) {
|
| + if (arguments[i]) {
|
| + for(var k in arguments[i]) {
|
| + if (arguments[i].hasOwnProperty(k)) {
|
| + result.push(encodeURIComponent(k) + '=' +
|
| + encodeURIComponent(arguments[i][k]));
|
| + }
|
| + }
|
| + }
|
| + }
|
| + return result.join("&");
|
| + }
|
| +
|
| + // Interface of the service:
|
| + return {
|
| + getQueryString: getQueryString,
|
| + loadAll: loadAll
|
| + };
|
| +
|
| + }]);
|
| +
|
| +})();
|
|
|