diff options
Diffstat (limited to 'chromium/third_party/skia/gm/rebaseline_server/static/loader.js')
-rw-r--r-- | chromium/third_party/skia/gm/rebaseline_server/static/loader.js | 932 |
1 files changed, 932 insertions, 0 deletions
diff --git a/chromium/third_party/skia/gm/rebaseline_server/static/loader.js b/chromium/third_party/skia/gm/rebaseline_server/static/loader.js new file mode 100644 index 00000000000..296689bde23 --- /dev/null +++ b/chromium/third_party/skia/gm/rebaseline_server/static/loader.js @@ -0,0 +1,932 @@ +/* + * 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'] +); + +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, hiddenResultTypes, hiddenConfigs, + builderSubstring, testSubstring, viewingTab) { + var filteredImagePairs = []; + for (var i = 0; i < unfilteredImagePairs.length; i++) { + var imagePair = unfilteredImagePairs[i]; + var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS]; + // For performance, we examine the "set" objects directly rather + // than calling $scope.isValueInSet(). + // Besides, I don't think we have access to $scope in here... + if (!(true == hiddenResultTypes[extraColumnValues[ + constants.KEY__EXTRACOLUMNS__RESULT_TYPE]]) && + !(true == hiddenConfigs[extraColumnValues[ + constants.KEY__EXTRACOLUMNS__CONFIG]]) && + !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__BUILDER] + .indexOf(builderSubstring)) && + !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__TEST] + .indexOf(testSubstring)) && + (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.constants = constants; + $scope.windowTitle = "Loading GM Results..."; + $scope.resultsToLoad = $location.search().resultsToLoad; + $scope.loadingMessage = "please wait..."; + + /** + * 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. + */ + $http.get($scope.resultsToLoad).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.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS]; + $scope.imageSets = data[constants.KEY__ROOT__IMAGESETS]; + $scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES; + $scope.sortColumnKey = constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF; + + $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 = []; + + // Sets within which the user can toggle individual elements. + $scope.hiddenResultTypes = {}; + $scope.hiddenResultTypes[ + constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true; + $scope.hiddenResultTypes[ + constants.KEY__RESULT_TYPE__NOCOMPARISON] = true; + $scope.hiddenResultTypes[ + constants.KEY__RESULT_TYPE__SUCCEEDED] = true; + $scope.allResultTypes = $scope.columnSliceOf2DArray( + $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] + [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS], + 0); + $scope.hiddenConfigs = {}; + $scope.allConfigs = $scope.columnSliceOf2DArray( + $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__CONFIG] + [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS], + 0); + + // Associative array of partial string matches per category. + $scope.categoryValueMatch = {}; + $scope.categoryValueMatch.builder = ""; + $scope.categoryValueMatch.test = ""; + + // 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] = $scope.resultsToLoad + '/../' + baseUrl; + } + } + ); + + $scope.updateResults(); + $scope.loadingMessage = ""; + $scope.windowTitle = "Current GM Results"; + } + } + ).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]; + } + }, + + 'categoryValueMatch': { + 'load': function(nameValuePairs, name) { + var value = nameValuePairs[name]; + if (value) { + $scope.categoryValueMatch[name] = value; + } + }, + 'save': function(nameValuePairs, name) { + nameValuePairs[name] = $scope.categoryValueMatch[name]; + } + }, + + 'set': { + 'load': function(nameValuePairs, name) { + var value = nameValuePairs[name]; + if (value) { + var valueArray = value.split(','); + $scope[name] = {}; + $scope.toggleValuesInSet(valueArray, $scope[name]); + } + }, + 'save': function(nameValuePairs, name) { + nameValuePairs[name] = Object.keys($scope[name]).join(','); + } + }, + + }; + + // parameter name -> copier objects to load/save parameter value + $scope.queryParameters.map = { + 'resultsToLoad': $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, + + 'hiddenResultTypes': $scope.queryParameters.copiers.set, + 'hiddenConfigs': $scope.queryParameters.copiers.set, + }; + $scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__BUILDER] = + $scope.queryParameters.copiers.categoryValueMatch; + $scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__TEST] = + $scope.queryParameters.copiers.categoryValueMatch; + + // 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(); + 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 = {}; + angular.forEach($scope.queryParameters.map, + function(copier, paramName) { + copier.save(nameValuePairs, paramName); + } + ); + $location.search(nameValuePairs); + }; + + + // + // 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; + // 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) { + + // TODO(epoger): Until we allow the user to reverse sort order, + // there are certain columns we want to sort in a different order. + var doReverse = ( + ($scope.sortColumnKey == + constants.KEY__DIFFERENCES__PERCENT_DIFF_PIXELS) || + ($scope.sortColumnKey == + constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF)); + + $scope.filteredImagePairs = + $filter("orderBy")( + $filter("removeHiddenImagePairs")( + $scope.imagePairs, + $scope.hiddenResultTypes, + $scope.hiddenConfigs, + $scope.categoryValueMatch.builder, + $scope.categoryValueMatch.test, + $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) { + $scope.sortColumnSubdict = subdict; + $scope.sortColumnKey = key; + $scope.updateResults(); + } + + /** + * 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.categoryValueMatch[name] = value, and update results. + * + * @param name + * @param value + */ + $scope.setCategoryValueMatch = function(name, value) { + $scope.categoryValueMatch[name] = value; + $scope.updateResults(); + } + + /** + * Update $scope.hiddenResultTypes so that ONLY this resultType is showing, + * and update the visible results. + * + * @param resultType + */ + $scope.showOnlyResultType = function(resultType) { + $scope.hiddenResultTypes = {}; + // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like + // $scope.hiddenResultTypes (rather than an array), so this operation is + // simpler (just assign or add allResultTypes to hiddenResultTypes). + $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes); + $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes); + $scope.updateResults(); + } + + /** + * Update $scope.hiddenResultTypes so that ALL resultTypes are showing, + * and update the visible results. + */ + $scope.showAllResultTypes = function() { + $scope.hiddenResultTypes = {}; + $scope.updateResults(); + } + + /** + * Update $scope.hiddenConfigs so that ONLY this config is showing, + * and update the visible results. + * + * @param config + */ + $scope.showOnlyConfig = function(config) { + $scope.hiddenConfigs = {}; + $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs); + $scope.toggleValueInSet(config, $scope.hiddenConfigs); + $scope.updateResults(); + } + + /** + * Update $scope.hiddenConfigs so that ALL configs are showing, + * and update the visible results. + */ + $scope.showAllConfigs = function() { + $scope.hiddenConfigs = {}; + $scope.updateResults(); + } + + + // + // Operations for sending info back to the server. + // + + /** + * Tell the server that the actual results of these particular tests + * are acceptable. + * + * TODO(epoger): This assumes that the original expectations are in + * imageSetA, and the actuals are in imageSetB. + * + * @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; + + // Convert bug text field to null or 1-item array. + var bugs = null; + var bugNumber = parseInt($scope.submitAdvancedSettings['bug']); + if (!isNaN(bugNumber)) { + bugs = [bugNumber]; + } + + // TODO(epoger): This is a suboptimal way to prevent users from + // rebaselining failures in alternative renderModes, but it does work. + // For a better solution, see + // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new + // result type, RenderModeMismatch') + var encounteredComparisonConfig = false; + + 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]; + // 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]; + if (0 == updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS] + [constants.KEY__EXTRACOLUMNS__CONFIG] + .indexOf('comparison-')) { + encounteredComparisonConfig = true; + } + + // 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); + } + if (encounteredComparisonConfig) { + alert("Approval failed -- you cannot approve results with config " + + "type comparison-*"); + $scope.submitPending = false; + return; + } + var modificationData = {}; + modificationData[constants.KEY__EDITS__MODIFICATIONS] = + updatedExpectations; + modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] = + $scope.header[constants.KEY__HEADER__DATAHASH]; + modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] = + $scope.header[constants.KEY__HEADER__TYPE]; + $http({ + method: "POST", + url: "/edits", + data: modificationData + }).success(function(data, status, headers, config) { + var imagePairIndicesToMove = []; + for (var i = 0; i < imagePairsSubset.length; i++) { + imagePairIndicesToMove.push(imagePairsSubset[i].index); + } + $scope.moveImagePairsToTab(imagePairIndicesToMove, + "HackToMakeSureThisImagePairDisappears"); + $scope.updateResults(); + alert("New baselines submitted successfully!\n\n" + + "You still need to commit the updated expectations files on " + + "the server side to the Skia repo.\n\n" + + "When you click OK, your web UI will reload; after that " + + "completes, you will see the updated data (once the server has " + + "finished loading the update results into memory!) and you can " + + "submit more baselines if you want."); + // I don't know why, but if I just call reload() here it doesn't work. + // Making a timer call it fixes the problem. + $timeout(function(){location.reload();}, 1); + }).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); + } + + /** + * Returns the last path component of image diff URL for a given ImagePair. + * + * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this + * will be relative to different base URLs. + * + * We must keep this function in sync with _get_difference_locator() in + * ../imagediffdb.py + * + * @param imagePair: ImagePair to generate image diff URL for + */ + $scope.getImageDiffRelativeUrl = function(imagePair) { + var before = + imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" + + imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL]; + return before.replace(/[^\w\-]/g, "_") + ".png"; + } + + } +); |