Index: gm/rebaseline_server/static/live-loader.js |
diff --git a/gm/rebaseline_server/static/live-loader.js b/gm/rebaseline_server/static/live-loader.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ab15aee41a984c782481f3a68e09d96579b7b4a7 |
--- /dev/null |
+++ b/gm/rebaseline_server/static/live-loader.js |
@@ -0,0 +1,1024 @@ |
+/* |
+ * Loader: |
+ * Reads GM result reports written out by results.py, and imports |
+ * them into $scope.extraColumnHeaders and $scope.imagePairs . |
+ */ |
+var Loader = angular.module( |
+ 'Loader', |
+ ['ConstantsModule'] |
+); |
+ |
+// This configuration is needed to allow downloads of the diff patch. |
+// See https://github.com/angular/angular.js/issues/3889 |
+Loader.config(['$compileProvider', function($compileProvider) { |
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|file|blob):/); |
+}]); |
+ |
+Loader.directive( |
+ 'resultsUpdatedCallbackDirective', |
+ ['$timeout', |
+ function($timeout) { |
+ return function(scope, element, attrs) { |
+ if (scope.$last) { |
+ $timeout(function() { |
+ scope.resultsUpdatedCallback(); |
+ }); |
+ } |
+ }; |
+ } |
+ ] |
+); |
+ |
+// TODO(epoger): Combine ALL of our filtering operations (including |
+// truncation) into this one filter, so that runs most efficiently? |
+// (We would have to make sure truncation still took place after |
+// sorting, though.) |
+Loader.filter( |
+ 'removeHiddenImagePairs', |
+ function(constants) { |
+ return function(unfilteredImagePairs, filterableColumnNames, showingColumnValues, |
+ viewingTab) { |
+ var filteredImagePairs = []; |
+ for (var i = 0; i < unfilteredImagePairs.length; i++) { |
+ var imagePair = unfilteredImagePairs[i]; |
+ var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS]; |
+ var allColumnValuesAreVisible = true; |
+ // Loop over all columns, and if any of them contain values not found in |
+ // showingColumnValues[columnName], don't include this imagePair. |
+ // |
+ // We use this same filtering mechanism regardless of whether each column |
+ // has USE_FREEFORM_FILTER set or not; if that flag is set, then we will |
+ // have already used the freeform text entry block to populate |
+ // showingColumnValues[columnName]. |
+ for (var j = 0; j < filterableColumnNames.length; j++) { |
+ var columnName = filterableColumnNames[j]; |
+ var columnValue = extraColumnValues[columnName]; |
+ if (!showingColumnValues[columnName][columnValue]) { |
+ allColumnValuesAreVisible = false; |
+ break; |
+ } |
+ } |
+ if (allColumnValuesAreVisible && (viewingTab == imagePair.tab)) { |
+ filteredImagePairs.push(imagePair); |
+ } |
+ } |
+ return filteredImagePairs; |
+ }; |
+ } |
+); |
+ |
+/** |
+ * Limit the input imagePairs to some max number, and merge identical rows |
+ * (adjacent rows which have the same (imageA, imageB) pair). |
+ * |
+ * @param unfilteredImagePairs imagePairs to filter |
+ * @param maxPairs maximum number of pairs to output, or <0 for no limit |
+ * @param mergeIdenticalRows if true, merge identical rows by setting |
+ * ROWSPAN>1 on the first merged row, and ROWSPAN=0 for the rest |
+ */ |
+Loader.filter( |
+ 'mergeAndLimit', |
+ function(constants) { |
+ return function(unfilteredImagePairs, maxPairs, mergeIdenticalRows) { |
+ var numPairs = unfilteredImagePairs.length; |
+ if ((maxPairs > 0) && (maxPairs < numPairs)) { |
+ numPairs = maxPairs; |
+ } |
+ var filteredImagePairs = []; |
+ if (!mergeIdenticalRows || (numPairs == 1)) { |
+ // Take a shortcut if we're not merging identical rows. |
+ // We still need to set ROWSPAN to 1 for each row, for the HTML viewer. |
+ for (var i = numPairs-1; i >= 0; i--) { |
+ var imagePair = unfilteredImagePairs[i]; |
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1; |
+ filteredImagePairs[i] = imagePair; |
+ } |
+ } else if (numPairs > 1) { |
+ // General case--there are at least 2 rows, so we may need to merge some. |
+ // Work from the bottom up, so we can keep a running total of how many |
+ // rows should be merged, and set ROWSPAN of the top row accordingly. |
+ var imagePair = unfilteredImagePairs[numPairs-1]; |
+ var nextRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL]; |
+ var nextRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL]; |
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1; |
+ filteredImagePairs[numPairs-1] = imagePair; |
+ for (var i = numPairs-2; i >= 0; i--) { |
+ imagePair = unfilteredImagePairs[i]; |
+ var thisRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL]; |
+ var thisRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL]; |
+ if ((thisRowImageAUrl == nextRowImageAUrl) && |
+ (thisRowImageBUrl == nextRowImageBUrl)) { |
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = |
+ filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] + 1; |
+ filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] = 0; |
+ } else { |
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1; |
+ nextRowImageAUrl = thisRowImageAUrl; |
+ nextRowImageBUrl = thisRowImageBUrl; |
+ } |
+ filteredImagePairs[i] = imagePair; |
+ } |
+ } else { |
+ // No results. |
+ } |
+ return filteredImagePairs; |
+ }; |
+ } |
+); |
+ |
+ |
+Loader.controller( |
+ 'Loader.Controller', |
+ function($scope, $http, $filter, $location, $log, $timeout, constants) { |
+ $scope.readyToDisplay = false; |
+ $scope.constants = constants; |
+ $scope.windowTitle = "Loading GM Results..."; |
+ $scope.setADir = $location.search().setADir; |
+ $scope.setASection = $location.search().setASection; |
+ $scope.setBDir = $location.search().setBDir; |
+ $scope.setBSection = $location.search().setBSection; |
+ $scope.loadingMessage = "please wait..."; |
+ |
+ var currSortAsc = true; |
+ |
+ |
+ /** |
+ * On initial page load, load a full dictionary of results. |
+ * Once the dictionary is loaded, unhide the page elements so they can |
+ * render the data. |
+ */ |
+ $scope.liveQueryUrl = |
+ "/live-results/setADir=" + encodeURIComponent($scope.setADir) + |
+ "&setASection=" + encodeURIComponent($scope.setASection) + |
+ "&setBDir=" + encodeURIComponent($scope.setBDir) + |
+ "&setBSection=" + encodeURIComponent($scope.setBSection); |
+ $http.get($scope.liveQueryUrl).success( |
+ function(data, status, header, config) { |
+ var dataHeader = data[constants.KEY__ROOT__HEADER]; |
+ if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] != |
+ constants.VALUE__HEADER__SCHEMA_VERSION) { |
+ $scope.loadingMessage = "ERROR: Got JSON file with schema version " |
+ + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] |
+ + " but expected schema version " |
+ + constants.VALUE__HEADER__SCHEMA_VERSION; |
+ } else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) { |
+ // Apply the server's requested reload delay to local time, |
+ // so we will wait the right number of seconds regardless of clock |
+ // skew between client and server. |
+ var reloadDelayInSeconds = |
+ dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] - |
+ dataHeader[constants.KEY__HEADER__TIME_UPDATED]; |
+ var timeNow = new Date().getTime(); |
+ var timeToReload = timeNow + reloadDelayInSeconds * 1000; |
+ $scope.loadingMessage = |
+ "server is still loading results; will retry at " + |
+ $scope.localTimeString(timeToReload / 1000); |
+ $timeout( |
+ function(){location.reload();}, |
+ timeToReload - timeNow); |
+ } else { |
+ $scope.loadingMessage = "processing data, please wait..."; |
+ |
+ $scope.header = dataHeader; |
+ $scope.extraColumnHeaders = data[constants.KEY__ROOT__EXTRACOLUMNHEADERS]; |
+ $scope.orderedColumnNames = data[constants.KEY__ROOT__EXTRACOLUMNORDER]; |
+ $scope.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS]; |
+ $scope.imageSets = data[constants.KEY__ROOT__IMAGESETS]; |
+ |
+ // set the default sort column and make it ascending. |
+ $scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES; |
+ $scope.sortColumnKey = constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF; |
+ currSortAsc = true; |
+ |
+ $scope.showSubmitAdvancedSettings = false; |
+ $scope.submitAdvancedSettings = {}; |
+ $scope.submitAdvancedSettings[ |
+ constants.KEY__EXPECTATIONS__REVIEWED] = true; |
+ $scope.submitAdvancedSettings[ |
+ constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false; |
+ $scope.submitAdvancedSettings['bug'] = ''; |
+ |
+ // Create the list of tabs (lists into which the user can file each |
+ // test). This may vary, depending on isEditable. |
+ $scope.tabs = [ |
+ 'Unfiled', 'Hidden' |
+ ]; |
+ if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) { |
+ $scope.tabs = $scope.tabs.concat( |
+ ['Pending Approval']); |
+ } |
+ $scope.defaultTab = $scope.tabs[0]; |
+ $scope.viewingTab = $scope.defaultTab; |
+ |
+ // Track the number of results on each tab. |
+ $scope.numResultsPerTab = {}; |
+ for (var i = 0; i < $scope.tabs.length; i++) { |
+ $scope.numResultsPerTab[$scope.tabs[i]] = 0; |
+ } |
+ $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length; |
+ |
+ // Add index and tab fields to all records. |
+ for (var i = 0; i < $scope.imagePairs.length; i++) { |
+ $scope.imagePairs[i].index = i; |
+ $scope.imagePairs[i].tab = $scope.defaultTab; |
+ } |
+ |
+ // Arrays within which the user can toggle individual elements. |
+ $scope.selectedImagePairs = []; |
+ |
+ // Set up filters. |
+ // |
+ // filterableColumnNames is a list of all column names we can filter on. |
+ // allColumnValues[columnName] is a list of all known values |
+ // for a given column. |
+ // showingColumnValues[columnName] is a set indicating which values |
+ // in a given column would cause us to show a row, rather than hiding it. |
+ // |
+ // columnStringMatch[columnName] is a string used as a pattern to generate |
+ // showingColumnValues[columnName] for columns we filter using free-form text. |
+ // It is ignored for any columns with USE_FREEFORM_FILTER == false. |
+ $scope.filterableColumnNames = []; |
+ $scope.allColumnValues = {}; |
+ $scope.showingColumnValues = {}; |
+ $scope.columnStringMatch = {}; |
+ |
+ angular.forEach( |
+ Object.keys($scope.extraColumnHeaders), |
+ function(columnName) { |
+ var columnHeader = $scope.extraColumnHeaders[columnName]; |
+ if (columnHeader[constants.KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE]) { |
+ $scope.filterableColumnNames.push(columnName); |
+ $scope.allColumnValues[columnName] = $scope.columnSliceOf2DArray( |
+ columnHeader[constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS], 0); |
+ $scope.showingColumnValues[columnName] = {}; |
+ $scope.toggleValuesInSet($scope.allColumnValues[columnName], |
+ $scope.showingColumnValues[columnName]); |
+ $scope.columnStringMatch[columnName] = ""; |
+ } |
+ } |
+ ); |
+ |
+ // TODO(epoger): Special handling for RESULT_TYPE column: |
+ // by default, show only KEY__RESULT_TYPE__FAILED results |
+ $scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] = {}; |
+ $scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE][ |
+ constants.KEY__RESULT_TYPE__FAILED] = true; |
+ |
+ // Set up mapping for URL parameters. |
+ // parameter name -> copier object to load/save parameter value |
+ $scope.queryParameters.map = { |
+ 'setADir': $scope.queryParameters.copiers.simple, |
+ 'setASection': $scope.queryParameters.copiers.simple, |
+ 'setBDir': $scope.queryParameters.copiers.simple, |
+ 'setBSection': $scope.queryParameters.copiers.simple, |
+ 'displayLimitPending': $scope.queryParameters.copiers.simple, |
+ 'showThumbnailsPending': $scope.queryParameters.copiers.simple, |
+ 'mergeIdenticalRowsPending': $scope.queryParameters.copiers.simple, |
+ 'imageSizePending': $scope.queryParameters.copiers.simple, |
+ 'sortColumnSubdict': $scope.queryParameters.copiers.simple, |
+ 'sortColumnKey': $scope.queryParameters.copiers.simple, |
+ }; |
+ // Some parameters are handled differently based on whether they USE_FREEFORM_FILTER. |
+ angular.forEach( |
+ $scope.filterableColumnNames, |
+ function(columnName) { |
+ if ($scope.extraColumnHeaders[columnName] |
+ [constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]) { |
+ $scope.queryParameters.map[columnName] = |
+ $scope.queryParameters.copiers.columnStringMatch; |
+ } else { |
+ $scope.queryParameters.map[columnName] = |
+ $scope.queryParameters.copiers.showingColumnValuesSet; |
+ } |
+ } |
+ ); |
+ |
+ // If any defaults were overridden in the URL, get them now. |
+ $scope.queryParameters.load(); |
+ |
+ // Any image URLs which are relative should be relative to the JSON |
+ // file's source directory; absolute URLs should be left alone. |
+ var baseUrlKey = constants.KEY__IMAGESETS__FIELD__BASE_URL; |
+ angular.forEach( |
+ $scope.imageSets, |
+ function(imageSet) { |
+ var baseUrl = imageSet[baseUrlKey]; |
+ if ((baseUrl.substring(0, 1) != '/') && |
+ (baseUrl.indexOf('://') == -1)) { |
+ imageSet[baseUrlKey] = '/' + baseUrl; |
+ } |
+ } |
+ ); |
+ |
+ $scope.readyToDisplay = true; |
+ $scope.updateResults(); |
+ $scope.loadingMessage = ""; |
+ $scope.windowTitle = "Current GM Results"; |
+ |
+ $timeout( function() { |
+ make_results_header_sticky(); |
+ }); |
+ } |
+ } |
+ ).error( |
+ function(data, status, header, config) { |
+ $scope.loadingMessage = "FAILED to load."; |
+ $scope.windowTitle = "Failed to Load GM Results"; |
+ } |
+ ); |
+ |
+ |
+ // |
+ // Select/Clear/Toggle all tests. |
+ // |
+ |
+ /** |
+ * Select all currently showing tests. |
+ */ |
+ $scope.selectAllImagePairs = function() { |
+ var numImagePairsShowing = $scope.limitedImagePairs.length; |
+ for (var i = 0; i < numImagePairsShowing; i++) { |
+ var index = $scope.limitedImagePairs[i].index; |
+ if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) { |
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs); |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Deselect all currently showing tests. |
+ */ |
+ $scope.clearAllImagePairs = function() { |
+ var numImagePairsShowing = $scope.limitedImagePairs.length; |
+ for (var i = 0; i < numImagePairsShowing; i++) { |
+ var index = $scope.limitedImagePairs[i].index; |
+ if ($scope.isValueInArray(index, $scope.selectedImagePairs)) { |
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs); |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Toggle selection of all currently showing tests. |
+ */ |
+ $scope.toggleAllImagePairs = function() { |
+ var numImagePairsShowing = $scope.limitedImagePairs.length; |
+ for (var i = 0; i < numImagePairsShowing; i++) { |
+ var index = $scope.limitedImagePairs[i].index; |
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs); |
+ } |
+ } |
+ |
+ /** |
+ * Toggle selection state of a subset of the currently showing tests. |
+ * |
+ * @param startIndex index within $scope.limitedImagePairs of the first |
+ * test to toggle selection state of |
+ * @param num number of tests (in a contiguous block) to toggle |
+ */ |
+ $scope.toggleSomeImagePairs = function(startIndex, num) { |
+ var numImagePairsShowing = $scope.limitedImagePairs.length; |
+ for (var i = startIndex; i < startIndex + num; i++) { |
+ var index = $scope.limitedImagePairs[i].index; |
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs); |
+ } |
+ } |
+ |
+ |
+ // |
+ // Tab operations. |
+ // |
+ |
+ /** |
+ * Change the selected tab. |
+ * |
+ * @param tab (string): name of the tab to select |
+ */ |
+ $scope.setViewingTab = function(tab) { |
+ $scope.viewingTab = tab; |
+ $scope.updateResults(); |
+ } |
+ |
+ /** |
+ * Move the imagePairs in $scope.selectedImagePairs to a different tab, |
+ * and then clear $scope.selectedImagePairs. |
+ * |
+ * @param newTab (string): name of the tab to move the tests to |
+ */ |
+ $scope.moveSelectedImagePairsToTab = function(newTab) { |
+ $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab); |
+ $scope.selectedImagePairs = []; |
+ $scope.updateResults(); |
+ } |
+ |
+ /** |
+ * Move a subset of $scope.imagePairs to a different tab. |
+ * |
+ * @param imagePairIndices (array of ints): indices into $scope.imagePairs |
+ * indicating which test results to move |
+ * @param newTab (string): name of the tab to move the tests to |
+ */ |
+ $scope.moveImagePairsToTab = function(imagePairIndices, newTab) { |
+ var imagePairIndex; |
+ var numImagePairs = imagePairIndices.length; |
+ for (var i = 0; i < numImagePairs; i++) { |
+ imagePairIndex = imagePairIndices[i]; |
+ $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--; |
+ $scope.imagePairs[imagePairIndex].tab = newTab; |
+ } |
+ $scope.numResultsPerTab[newTab] += numImagePairs; |
+ } |
+ |
+ |
+ // |
+ // $scope.queryParameters: |
+ // Transfer parameter values between $scope and the URL query string. |
+ // |
+ $scope.queryParameters = {}; |
+ |
+ // load and save functions for parameters of each type |
+ // (load a parameter value into $scope from nameValuePairs, |
+ // save a parameter value from $scope into nameValuePairs) |
+ $scope.queryParameters.copiers = { |
+ 'simple': { |
+ 'load': function(nameValuePairs, name) { |
+ var value = nameValuePairs[name]; |
+ if (value) { |
+ $scope[name] = value; |
+ } |
+ }, |
+ 'save': function(nameValuePairs, name) { |
+ nameValuePairs[name] = $scope[name]; |
+ } |
+ }, |
+ |
+ 'columnStringMatch': { |
+ 'load': function(nameValuePairs, name) { |
+ var value = nameValuePairs[name]; |
+ if (value) { |
+ $scope.columnStringMatch[name] = value; |
+ } |
+ }, |
+ 'save': function(nameValuePairs, name) { |
+ nameValuePairs[name] = $scope.columnStringMatch[name]; |
+ } |
+ }, |
+ |
+ 'showingColumnValuesSet': { |
+ 'load': function(nameValuePairs, name) { |
+ var value = nameValuePairs[name]; |
+ if (value) { |
+ var valueArray = value.split(','); |
+ $scope.showingColumnValues[name] = {}; |
+ $scope.toggleValuesInSet(valueArray, $scope.showingColumnValues[name]); |
+ } |
+ }, |
+ 'save': function(nameValuePairs, name) { |
+ nameValuePairs[name] = Object.keys($scope.showingColumnValues[name]).join(','); |
+ } |
+ }, |
+ |
+ }; |
+ |
+ // Loads all parameters into $scope from the URL query string; |
+ // any which are not found within the URL will keep their current value. |
+ $scope.queryParameters.load = function() { |
+ var nameValuePairs = $location.search(); |
+ |
+ // If urlSchemaVersion is not specified, we assume the current version. |
+ var urlSchemaVersion = constants.URL_VALUE__SCHEMA_VERSION__CURRENT; |
+ if (constants.URL_KEY__SCHEMA_VERSION in nameValuePairs) { |
+ urlSchemaVersion = nameValuePairs[constants.URL_KEY__SCHEMA_VERSION]; |
+ } else if ('hiddenResultTypes' in nameValuePairs) { |
+ // The combination of: |
+ // - absence of an explicit urlSchemaVersion, and |
+ // - presence of the old 'hiddenResultTypes' field |
+ // tells us that the URL is from the original urlSchemaVersion. |
+ // See https://codereview.chromium.org/367173002/ |
+ urlSchemaVersion = 0; |
+ } |
+ $scope.urlSchemaVersionLoaded = urlSchemaVersion; |
+ |
+ if (urlSchemaVersion != constants.URL_VALUE__SCHEMA_VERSION__CURRENT) { |
+ nameValuePairs = $scope.upconvertUrlNameValuePairs(nameValuePairs, urlSchemaVersion); |
+ } |
+ angular.forEach($scope.queryParameters.map, |
+ function(copier, paramName) { |
+ copier.load(nameValuePairs, paramName); |
+ } |
+ ); |
+ }; |
+ |
+ // Saves all parameters from $scope into the URL query string. |
+ $scope.queryParameters.save = function() { |
+ var nameValuePairs = {}; |
+ nameValuePairs[constants.URL_KEY__SCHEMA_VERSION] = constants.URL_VALUE__SCHEMA_VERSION__CURRENT; |
+ angular.forEach($scope.queryParameters.map, |
+ function(copier, paramName) { |
+ copier.save(nameValuePairs, paramName); |
+ } |
+ ); |
+ $location.search(nameValuePairs); |
+ }; |
+ |
+ /** |
+ * Converts URL name/value pairs that were stored by a previous urlSchemaVersion |
+ * to the currently needed format. |
+ * |
+ * @param oldNValuePairs name/value pairs found in the loaded URL |
+ * @param oldUrlSchemaVersion which version of the schema was used to generate that URL |
+ * |
+ * @returns nameValuePairs as needed by the current URL parser |
+ */ |
+ $scope.upconvertUrlNameValuePairs = function(oldNameValuePairs, oldUrlSchemaVersion) { |
+ var newNameValuePairs = {}; |
+ angular.forEach(oldNameValuePairs, |
+ function(value, name) { |
+ if (oldUrlSchemaVersion < 1) { |
+ if ('hiddenConfigs' == name) { |
+ name = 'config'; |
+ var valueSet = {}; |
+ $scope.toggleValuesInSet(value.split(','), valueSet); |
+ $scope.toggleValuesInSet( |
+ $scope.allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG], |
+ valueSet); |
+ value = Object.keys(valueSet).join(','); |
+ } else if ('hiddenResultTypes' == name) { |
+ name = 'resultType'; |
+ var valueSet = {}; |
+ $scope.toggleValuesInSet(value.split(','), valueSet); |
+ $scope.toggleValuesInSet( |
+ $scope.allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE], |
+ valueSet); |
+ value = Object.keys(valueSet).join(','); |
+ } |
+ } |
+ |
+ newNameValuePairs[name] = value; |
+ } |
+ ); |
+ return newNameValuePairs; |
+ } |
+ |
+ |
+ // |
+ // updateResults() and friends. |
+ // |
+ |
+ /** |
+ * Set $scope.areUpdatesPending (to enable/disable the Update Results |
+ * button). |
+ * |
+ * TODO(epoger): We could reduce the amount of code by just setting the |
+ * variable directly (from, e.g., a button's ng-click handler). But when |
+ * I tried that, the HTML elements depending on the variable did not get |
+ * updated. |
+ * It turns out that this is due to variable scoping within an ng-repeat |
+ * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat |
+ * |
+ * @param val boolean value to set $scope.areUpdatesPending to |
+ */ |
+ $scope.setUpdatesPending = function(val) { |
+ $scope.areUpdatesPending = val; |
+ } |
+ |
+ /** |
+ * Update the displayed results, based on filters/settings, |
+ * and call $scope.queryParameters.save() so that the new filter results |
+ * can be bookmarked. |
+ */ |
+ $scope.updateResults = function() { |
+ $scope.renderStartTime = window.performance.now(); |
+ $log.debug("renderStartTime: " + $scope.renderStartTime); |
+ $scope.displayLimit = $scope.displayLimitPending; |
+ $scope.mergeIdenticalRows = $scope.mergeIdenticalRowsPending; |
+ |
+ // For each USE_FREEFORM_FILTER column, populate showingColumnValues. |
+ // This is more efficient than applying the freeform filter within the |
+ // tight loop in removeHiddenImagePairs. |
+ angular.forEach( |
+ $scope.filterableColumnNames, |
+ function(columnName) { |
+ var columnHeader = $scope.extraColumnHeaders[columnName]; |
+ if (columnHeader[constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]) { |
+ var columnStringMatch = $scope.columnStringMatch[columnName]; |
+ var showingColumnValues = {}; |
+ angular.forEach( |
+ $scope.allColumnValues[columnName], |
+ function(columnValue) { |
+ if (-1 != columnValue.indexOf(columnStringMatch)) { |
+ showingColumnValues[columnValue] = true; |
+ } |
+ } |
+ ); |
+ $scope.showingColumnValues[columnName] = showingColumnValues; |
+ } |
+ } |
+ ); |
+ |
+ // TODO(epoger): Every time we apply a filter, AngularJS creates |
+ // another copy of the array. Is there a way we can filter out |
+ // the imagePairs as they are displayed, rather than storing multiple |
+ // array copies? (For better performance.) |
+ |
+ if ($scope.viewingTab == $scope.defaultTab) { |
+ var doReverse = !currSortAsc; |
+ |
+ $scope.filteredImagePairs = |
+ $filter("orderBy")( |
+ $filter("removeHiddenImagePairs")( |
+ $scope.imagePairs, |
+ $scope.filterableColumnNames, |
+ $scope.showingColumnValues, |
+ $scope.viewingTab |
+ ), |
+ [$scope.getSortColumnValue, $scope.getSecondOrderSortValue], |
+ doReverse); |
+ $scope.limitedImagePairs = $filter("mergeAndLimit")( |
+ $scope.filteredImagePairs, $scope.displayLimit, $scope.mergeIdenticalRows); |
+ } else { |
+ $scope.filteredImagePairs = |
+ $filter("orderBy")( |
+ $filter("filter")( |
+ $scope.imagePairs, |
+ {tab: $scope.viewingTab}, |
+ true |
+ ), |
+ [$scope.getSortColumnValue, $scope.getSecondOrderSortValue]); |
+ $scope.limitedImagePairs = $filter("mergeAndLimit")( |
+ $scope.filteredImagePairs, -1, $scope.mergeIdenticalRows); |
+ } |
+ $scope.showThumbnails = $scope.showThumbnailsPending; |
+ $scope.imageSize = $scope.imageSizePending; |
+ $scope.setUpdatesPending(false); |
+ $scope.queryParameters.save(); |
+ } |
+ |
+ /** |
+ * This function is called when the results have been completely rendered |
+ * after updateResults(). |
+ */ |
+ $scope.resultsUpdatedCallback = function() { |
+ $scope.renderEndTime = window.performance.now(); |
+ $log.debug("renderEndTime: " + $scope.renderEndTime); |
+ } |
+ |
+ /** |
+ * Re-sort the displayed results. |
+ * |
+ * @param subdict (string): which KEY__IMAGEPAIRS__* subdictionary |
+ * the sort column key is within, or 'none' if the sort column |
+ * key is one of KEY__IMAGEPAIRS__* |
+ * @param key (string): sort by value associated with this key in subdict |
+ */ |
+ $scope.sortResultsBy = function(subdict, key) { |
+ // if we are already sorting by this column then toggle between asc/desc |
+ if ((subdict === $scope.sortColumnSubdict) && ($scope.sortColumnKey === key)) { |
+ currSortAsc = !currSortAsc; |
+ } else { |
+ $scope.sortColumnSubdict = subdict; |
+ $scope.sortColumnKey = key; |
+ currSortAsc = true; |
+ } |
+ $scope.updateResults(); |
+ } |
+ |
+ /** |
+ * Returns ASC or DESC (from constants) if currently the data |
+ * is sorted by the provided column. |
+ * |
+ * @param colName: name of the column for which we need to get the class. |
+ */ |
+ |
+ $scope.sortedByColumnsCls = function (colName) { |
+ if ($scope.sortColumnKey !== colName) { |
+ return ''; |
+ } |
+ |
+ var result = (currSortAsc) ? constants.ASC : constants.DESC; |
+ console.log("sort class:", result); |
+ return result; |
+ }; |
+ |
+ /** |
+ * For a particular ImagePair, return the value of the column we are |
+ * sorting on (according to $scope.sortColumnSubdict and |
+ * $scope.sortColumnKey). |
+ * |
+ * @param imagePair: imagePair to get a column value out of. |
+ */ |
+ $scope.getSortColumnValue = function(imagePair) { |
+ if ($scope.sortColumnSubdict in imagePair) { |
+ return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey]; |
+ } else if ($scope.sortColumnKey in imagePair) { |
+ return imagePair[$scope.sortColumnKey]; |
+ } else { |
+ return undefined; |
+ } |
+ }; |
+ |
+ /** |
+ * For a particular ImagePair, return the value we use for the |
+ * second-order sort (tiebreaker when multiple rows have |
+ * the same getSortColumnValue()). |
+ * |
+ * We join the imageA and imageB urls for this value, so that we merge |
+ * adjacent rows as much as possible. |
+ * |
+ * @param imagePair: imagePair to get a column value out of. |
+ */ |
+ $scope.getSecondOrderSortValue = function(imagePair) { |
+ return imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" + |
+ imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL]; |
+ }; |
+ |
+ /** |
+ * Set $scope.columnStringMatch[name] = value, and update results. |
+ * |
+ * @param name |
+ * @param value |
+ */ |
+ $scope.setColumnStringMatch = function(name, value) { |
+ $scope.columnStringMatch[name] = value; |
+ $scope.updateResults(); |
+ }; |
+ |
+ /** |
+ * Update $scope.showingColumnValues[columnName] and $scope.columnStringMatch[columnName] |
+ * so that ONLY entries with this columnValue are showing, and update the visible results. |
+ * (We update both of those, so we cover both freeform and checkbox filtered columns.) |
+ * |
+ * @param columnName |
+ * @param columnValue |
+ */ |
+ $scope.showOnlyColumnValue = function(columnName, columnValue) { |
+ $scope.columnStringMatch[columnName] = columnValue; |
+ $scope.showingColumnValues[columnName] = {}; |
+ $scope.toggleValueInSet(columnValue, $scope.showingColumnValues[columnName]); |
+ $scope.updateResults(); |
+ }; |
+ |
+ /** |
+ * Update $scope.showingColumnValues[columnName] and $scope.columnStringMatch[columnName] |
+ * so that ALL entries are showing, and update the visible results. |
+ * (We update both of those, so we cover both freeform and checkbox filtered columns.) |
+ * |
+ * @param columnName |
+ */ |
+ $scope.showAllColumnValues = function(columnName) { |
+ $scope.columnStringMatch[columnName] = ""; |
+ $scope.showingColumnValues[columnName] = {}; |
+ $scope.toggleValuesInSet($scope.allColumnValues[columnName], |
+ $scope.showingColumnValues[columnName]); |
+ $scope.updateResults(); |
+ }; |
+ |
+ |
+ // |
+ // Operations for sending info back to the server. |
+ // |
+ |
+ /** |
+ * Tell the server that the actual results of these particular tests |
+ * are acceptable. |
+ * |
+ * This assumes that the original expectations are in imageSetA, and the |
+ * new expectations are in imageSetB. That's fine, because the server |
+ * mandates that anyway (it will swap the sets if the user requests them |
+ * in the opposite order). |
+ * |
+ * @param imagePairsSubset an array of test results, most likely a subset of |
+ * $scope.imagePairs (perhaps with some modifications) |
+ */ |
+ $scope.submitApprovals = function(imagePairsSubset) { |
+ $scope.submitPending = true; |
+ $scope.diffResults = ""; |
+ |
+ // Convert bug text field to null or 1-item array. |
+ var bugs = null; |
+ var bugNumber = parseInt($scope.submitAdvancedSettings['bug']); |
+ if (!isNaN(bugNumber)) { |
+ bugs = [bugNumber]; |
+ } |
+ |
+ var updatedExpectations = []; |
+ for (var i = 0; i < imagePairsSubset.length; i++) { |
+ var imagePair = imagePairsSubset[i]; |
+ var updatedExpectation = {}; |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] = |
+ imagePair[constants.KEY__IMAGEPAIRS__EXPECTATIONS]; |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS] = |
+ imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS]; |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__SOURCE_JSON_FILE] = |
+ imagePair[constants.KEY__IMAGEPAIRS__SOURCE_JSON_FILE]; |
+ // IMAGE_B_URL contains the actual image (which is now the expectation) |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__IMAGE_B_URL] = |
+ imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL]; |
+ |
+ // Advanced settings... |
+ if (null == updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]) { |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] = {}; |
+ } |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] |
+ [constants.KEY__EXPECTATIONS__REVIEWED] = |
+ $scope.submitAdvancedSettings[ |
+ constants.KEY__EXPECTATIONS__REVIEWED]; |
+ if (true == $scope.submitAdvancedSettings[ |
+ constants.KEY__EXPECTATIONS__IGNOREFAILURE]) { |
+ // if it's false, don't send it at all (just keep the default) |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] |
+ [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true; |
+ } |
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] |
+ [constants.KEY__EXPECTATIONS__BUGS] = bugs; |
+ |
+ updatedExpectations.push(updatedExpectation); |
+ } |
+ var modificationData = {}; |
+ modificationData[constants.KEY__LIVE_EDITS__MODIFICATIONS] = |
+ updatedExpectations; |
+ modificationData[constants.KEY__LIVE_EDITS__SET_A_DESCRIPTIONS] = |
+ $scope.header[constants.KEY__HEADER__SET_A_DESCRIPTIONS]; |
+ modificationData[constants.KEY__LIVE_EDITS__SET_B_DESCRIPTIONS] = |
+ $scope.header[constants.KEY__HEADER__SET_B_DESCRIPTIONS]; |
+ $http({ |
+ method: "POST", |
+ url: "/live-edits", |
+ data: modificationData |
+ }).success(function(data, status, headers, config) { |
+ $scope.diffResults = data; |
+ var blob = new Blob([$scope.diffResults], {type: 'text/plain'}); |
+ $scope.diffResultsBlobUrl = window.URL.createObjectURL(blob); |
+ $scope.submitPending = false; |
+ }).error(function(data, status, headers, config) { |
+ alert("There was an error submitting your baselines.\n\n" + |
+ "Please see server-side log for details."); |
+ $scope.submitPending = false; |
+ }); |
+ }; |
+ |
+ |
+ // |
+ // Operations we use to mimic Set semantics, in such a way that |
+ // checking for presence within the Set is as fast as possible. |
+ // But getting a list of all values within the Set is not necessarily |
+ // possible. |
+ // TODO(epoger): move into a separate .js file? |
+ // |
+ |
+ /** |
+ * Returns the number of values present within set "set". |
+ * |
+ * @param set an Object which we use to mimic set semantics |
+ */ |
+ $scope.setSize = function(set) { |
+ return Object.keys(set).length; |
+ }; |
+ |
+ /** |
+ * Returns true if value "value" is present within set "set". |
+ * |
+ * @param value a value of any type |
+ * @param set an Object which we use to mimic set semantics |
+ * (this should make isValueInSet faster than if we used an Array) |
+ */ |
+ $scope.isValueInSet = function(value, set) { |
+ return (true == set[value]); |
+ }; |
+ |
+ /** |
+ * If value "value" is already in set "set", remove it; otherwise, add it. |
+ * |
+ * @param value a value of any type |
+ * @param set an Object which we use to mimic set semantics |
+ */ |
+ $scope.toggleValueInSet = function(value, set) { |
+ if (true == set[value]) { |
+ delete set[value]; |
+ } else { |
+ set[value] = true; |
+ } |
+ }; |
+ |
+ /** |
+ * For each value in valueArray, call toggleValueInSet(value, set). |
+ * |
+ * @param valueArray |
+ * @param set |
+ */ |
+ $scope.toggleValuesInSet = function(valueArray, set) { |
+ var arrayLength = valueArray.length; |
+ for (var i = 0; i < arrayLength; i++) { |
+ $scope.toggleValueInSet(valueArray[i], set); |
+ } |
+ }; |
+ |
+ |
+ // |
+ // Array operations; similar to our Set operations, but operate on a |
+ // Javascript Array so we *can* easily get a list of all values in the Set. |
+ // TODO(epoger): move into a separate .js file? |
+ // |
+ |
+ /** |
+ * Returns true if value "value" is present within array "array". |
+ * |
+ * @param value a value of any type |
+ * @param array a Javascript Array |
+ */ |
+ $scope.isValueInArray = function(value, array) { |
+ return (-1 != array.indexOf(value)); |
+ }; |
+ |
+ /** |
+ * If value "value" is already in array "array", remove it; otherwise, |
+ * add it. |
+ * |
+ * @param value a value of any type |
+ * @param array a Javascript Array |
+ */ |
+ $scope.toggleValueInArray = function(value, array) { |
+ var i = array.indexOf(value); |
+ if (-1 == i) { |
+ array.push(value); |
+ } else { |
+ array.splice(i, 1); |
+ } |
+ }; |
+ |
+ |
+ // |
+ // Miscellaneous utility functions. |
+ // TODO(epoger): move into a separate .js file? |
+ // |
+ |
+ /** |
+ * Returns a single "column slice" of a 2D array. |
+ * |
+ * For example, if array is: |
+ * [[A0, A1], |
+ * [B0, B1], |
+ * [C0, C1]] |
+ * and index is 0, this this will return: |
+ * [A0, B0, C0] |
+ * |
+ * @param array a Javascript Array |
+ * @param column (numeric): index within each row array |
+ */ |
+ $scope.columnSliceOf2DArray = function(array, column) { |
+ var slice = []; |
+ var numRows = array.length; |
+ for (var row = 0; row < numRows; row++) { |
+ slice.push(array[row][column]); |
+ } |
+ return slice; |
+ }; |
+ |
+ /** |
+ * Returns a human-readable (in local time zone) time string for a |
+ * particular moment in time. |
+ * |
+ * @param secondsPastEpoch (numeric): seconds past epoch in UTC |
+ */ |
+ $scope.localTimeString = function(secondsPastEpoch) { |
+ var d = new Date(secondsPastEpoch * 1000); |
+ return d.toString(); |
+ }; |
+ |
+ /** |
+ * Returns a hex color string (such as "#aabbcc") for the given RGB values. |
+ * |
+ * @param r (numeric): red channel value, 0-255 |
+ * @param g (numeric): green channel value, 0-255 |
+ * @param b (numeric): blue channel value, 0-255 |
+ */ |
+ $scope.hexColorString = function(r, g, b) { |
+ var rString = r.toString(16); |
+ if (r < 16) { |
+ rString = "0" + rString; |
+ } |
+ var gString = g.toString(16); |
+ if (g < 16) { |
+ gString = "0" + gString; |
+ } |
+ var bString = b.toString(16); |
+ if (b < 16) { |
+ bString = "0" + bString; |
+ } |
+ return '#' + rString + gString + bString; |
+ }; |
+ |
+ /** |
+ * Returns a hex color string (such as "#aabbcc") for the given brightness. |
+ * |
+ * @param brightnessString (string): 0-255, 0 is completely black |
+ * |
+ * TODO(epoger): It might be nice to tint the color when it's not completely |
+ * black or completely white. |
+ */ |
+ $scope.brightnessStringToHexColor = function(brightnessString) { |
+ var v = parseInt(brightnessString); |
+ return $scope.hexColorString(v, v, v); |
+ }; |
+ } |
+); |