2013-09-27 15:02:44 +00:00
|
|
|
/*
|
|
|
|
* Loader:
|
2013-09-30 15:06:25 +00:00
|
|
|
* Reads GM result reports written out by results.py, and imports
|
2014-02-26 19:05:20 +00:00
|
|
|
* them into $scope.extraColumnHeaders and $scope.imagePairs .
|
2013-09-27 15:02:44 +00:00
|
|
|
*/
|
|
|
|
var Loader = angular.module(
|
|
|
|
'Loader',
|
2014-03-10 19:12:53 +00:00
|
|
|
['ConstantsModule']
|
2013-09-27 15:02:44 +00:00
|
|
|
);
|
2013-10-02 18:57:48 +00:00
|
|
|
|
2014-03-10 15:36:06 +00:00
|
|
|
Loader.directive(
|
|
|
|
'resultsUpdatedCallbackDirective',
|
|
|
|
['$timeout',
|
|
|
|
function($timeout) {
|
|
|
|
return function(scope, element, attrs) {
|
|
|
|
if (scope.$last) {
|
|
|
|
$timeout(function() {
|
|
|
|
scope.resultsUpdatedCallback();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
2013-10-02 18:57:48 +00:00
|
|
|
// 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(
|
2014-02-26 19:05:20 +00:00
|
|
|
'removeHiddenImagePairs',
|
|
|
|
function(constants) {
|
2014-07-09 13:19:20 +00:00
|
|
|
return function(unfilteredImagePairs, filterableColumnNames, showingColumnValues,
|
|
|
|
viewingTab) {
|
2014-02-26 19:05:20 +00:00
|
|
|
var filteredImagePairs = [];
|
|
|
|
for (var i = 0; i < unfilteredImagePairs.length; i++) {
|
|
|
|
var imagePair = unfilteredImagePairs[i];
|
2014-05-12 20:40:29 +00:00
|
|
|
var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
|
2014-07-09 13:19:20 +00:00
|
|
|
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)) {
|
2014-02-26 19:05:20 +00:00
|
|
|
filteredImagePairs.push(imagePair);
|
2013-10-09 18:05:58 +00:00
|
|
|
}
|
2013-10-02 18:57:48 +00:00
|
|
|
}
|
2014-02-26 19:05:20 +00:00
|
|
|
return filteredImagePairs;
|
2013-10-02 18:57:48 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2014-06-11 21:01:35 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
|
2013-09-27 15:02:44 +00:00
|
|
|
Loader.controller(
|
|
|
|
'Loader.Controller',
|
2014-03-10 15:36:06 +00:00
|
|
|
function($scope, $http, $filter, $location, $log, $timeout, constants) {
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.readyToDisplay = false;
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.constants = constants;
|
2013-10-15 20:10:33 +00:00
|
|
|
$scope.windowTitle = "Loading GM Results...";
|
2013-12-05 18:03:24 +00:00
|
|
|
$scope.resultsToLoad = $location.search().resultsToLoad;
|
2014-03-17 14:22:02 +00:00
|
|
|
$scope.loadingMessage = "please wait...";
|
2013-10-11 18:45:33 +00:00
|
|
|
|
2014-08-08 14:21:00 +00:00
|
|
|
var currSortAsc = true;
|
|
|
|
|
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2014-03-13 14:56:29 +00:00
|
|
|
$http.get($scope.resultsToLoad).success(
|
2013-10-11 18:45:33 +00:00
|
|
|
function(data, status, header, config) {
|
2014-05-12 20:40:29 +00:00
|
|
|
var dataHeader = data[constants.KEY__ROOT__HEADER];
|
2014-03-17 14:22:02 +00:00
|
|
|
if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
|
2014-05-12 20:40:29 +00:00
|
|
|
constants.VALUE__HEADER__SCHEMA_VERSION) {
|
2014-03-17 14:22:02 +00:00
|
|
|
$scope.loadingMessage = "ERROR: Got JSON file with schema version "
|
|
|
|
+ dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
|
|
|
|
+ " but expected schema version "
|
2014-05-12 20:40:29 +00:00
|
|
|
+ constants.VALUE__HEADER__SCHEMA_VERSION;
|
2014-03-17 14:22:02 +00:00
|
|
|
} else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
|
2014-03-10 18:05:15 +00:00
|
|
|
// 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;
|
2013-12-05 16:05:16 +00:00
|
|
|
$scope.loadingMessage =
|
2014-03-17 14:22:02 +00:00
|
|
|
"server is still loading results; will retry at " +
|
2014-03-10 18:05:15 +00:00
|
|
|
$scope.localTimeString(timeToReload / 1000);
|
2013-12-05 16:05:16 +00:00
|
|
|
$timeout(
|
|
|
|
function(){location.reload();},
|
2014-03-10 18:05:15 +00:00
|
|
|
timeToReload - timeNow);
|
2013-12-05 16:05:16 +00:00
|
|
|
} else {
|
2014-03-17 14:22:02 +00:00
|
|
|
$scope.loadingMessage = "processing data, please wait...";
|
2013-12-05 16:05:16 +00:00
|
|
|
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.header = dataHeader;
|
2014-05-12 20:40:29 +00:00
|
|
|
$scope.extraColumnHeaders = data[constants.KEY__ROOT__EXTRACOLUMNHEADERS];
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.orderedColumnNames = data[constants.KEY__ROOT__EXTRACOLUMNORDER];
|
2014-05-12 20:40:29 +00:00
|
|
|
$scope.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS];
|
|
|
|
$scope.imageSets = data[constants.KEY__ROOT__IMAGESETS];
|
2014-08-08 14:21:00 +00:00
|
|
|
|
|
|
|
// set the default sort column and make it ascending.
|
2014-05-12 20:40:29 +00:00
|
|
|
$scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES;
|
|
|
|
$scope.sortColumnKey = constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF;
|
2014-08-08 14:21:00 +00:00
|
|
|
currSortAsc = true;
|
2013-12-05 16:05:16 +00:00
|
|
|
|
|
|
|
$scope.showSubmitAdvancedSettings = false;
|
|
|
|
$scope.submitAdvancedSettings = {};
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.submitAdvancedSettings[
|
|
|
|
constants.KEY__EXPECTATIONS__REVIEWED] = true;
|
|
|
|
$scope.submitAdvancedSettings[
|
|
|
|
constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false;
|
2013-12-05 16:05:16 +00:00
|
|
|
$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'
|
|
|
|
];
|
2014-02-26 19:05:20 +00:00
|
|
|
if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
|
2013-12-05 16:05:16 +00:00
|
|
|
$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;
|
|
|
|
}
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length;
|
2013-12-05 16:05:16 +00:00
|
|
|
|
|
|
|
// Add index and tab fields to all records.
|
2014-02-26 19:05:20 +00:00
|
|
|
for (var i = 0; i < $scope.imagePairs.length; i++) {
|
|
|
|
$scope.imagePairs[i].index = i;
|
|
|
|
$scope.imagePairs[i].tab = $scope.defaultTab;
|
2013-12-05 16:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Arrays within which the user can toggle individual elements.
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.selectedImagePairs = [];
|
2013-12-05 16:05:16 +00:00
|
|
|
|
2014-07-09 13:19:20 +00:00
|
|
|
// Set up filters.
|
|
|
|
//
|
|
|
|
// filterableColumnNames is a list of all column names we can filter on.
|
2014-07-07 15:49:14 +00:00
|
|
|
// allColumnValues[columnName] is a list of all known values
|
2014-07-09 13:19:20 +00:00
|
|
|
// for a given column.
|
2014-07-07 15:49:14 +00:00
|
|
|
// showingColumnValues[columnName] is a set indicating which values
|
2014-07-09 13:19:20 +00:00
|
|
|
// 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 = [];
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.allColumnValues = {};
|
|
|
|
$scope.showingColumnValues = {};
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.columnStringMatch = {};
|
2014-07-07 15:49:14 +00:00
|
|
|
|
2014-07-09 13:19:20 +00:00
|
|
|
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:
|
2014-07-07 15:49:14 +00:00
|
|
|
// 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;
|
|
|
|
|
2014-07-09 13:19:20 +00:00
|
|
|
// Set up mapping for URL parameters.
|
|
|
|
// parameter name -> copier object 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,
|
|
|
|
};
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2013-12-05 16:05:16 +00:00
|
|
|
|
2013-12-05 18:03:24 +00:00
|
|
|
// If any defaults were overridden in the URL, get them now.
|
|
|
|
$scope.queryParameters.load();
|
|
|
|
|
2014-03-20 15:27:34 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.readyToDisplay = true;
|
2013-12-05 16:05:16 +00:00
|
|
|
$scope.updateResults();
|
|
|
|
$scope.loadingMessage = "";
|
|
|
|
$scope.windowTitle = "Current GM Results";
|
2014-08-06 19:39:59 +00:00
|
|
|
|
|
|
|
$timeout( function() {
|
|
|
|
make_results_header_sticky();
|
|
|
|
});
|
2013-10-23 15:07:26 +00:00
|
|
|
}
|
2013-10-11 18:45:33 +00:00
|
|
|
}
|
|
|
|
).error(
|
|
|
|
function(data, status, header, config) {
|
2014-03-17 14:22:02 +00:00
|
|
|
$scope.loadingMessage = "FAILED to load.";
|
2013-10-15 20:10:33 +00:00
|
|
|
$scope.windowTitle = "Failed to Load GM Results";
|
2013-09-27 15:02:44 +00:00
|
|
|
}
|
|
|
|
);
|
2013-10-02 18:57:48 +00:00
|
|
|
|
|
|
|
|
2013-10-26 14:31:11 +00:00
|
|
|
//
|
|
|
|
// Select/Clear/Toggle all tests.
|
|
|
|
//
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Select all currently showing tests.
|
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$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);
|
2013-10-26 14:31:11 +00:00
|
|
|
}
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-26 14:31:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Deselect all currently showing tests.
|
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$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);
|
2013-10-26 14:31:11 +00:00
|
|
|
}
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-26 14:31:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle selection of all currently showing tests.
|
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$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);
|
2013-10-26 14:31:11 +00:00
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-26 14:31:11 +00:00
|
|
|
|
2014-06-11 21:01:35 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-06-11 21:01:35 +00:00
|
|
|
|
2013-10-26 14:31:11 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
//
|
|
|
|
// Tab operations.
|
|
|
|
//
|
2013-10-02 18:57:48 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
/**
|
|
|
|
* Change the selected tab.
|
|
|
|
*
|
|
|
|
* @param tab (string): name of the tab to select
|
|
|
|
*/
|
2013-10-23 15:07:26 +00:00
|
|
|
$scope.setViewingTab = function(tab) {
|
|
|
|
$scope.viewingTab = tab;
|
|
|
|
$scope.updateResults();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-23 15:07:26 +00:00
|
|
|
|
|
|
|
/**
|
2014-02-26 19:05:20 +00:00
|
|
|
* Move the imagePairs in $scope.selectedImagePairs to a different tab,
|
|
|
|
* and then clear $scope.selectedImagePairs.
|
2013-10-23 15:07:26 +00:00
|
|
|
*
|
|
|
|
* @param newTab (string): name of the tab to move the tests to
|
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.moveSelectedImagePairsToTab = function(newTab) {
|
|
|
|
$scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
|
|
|
|
$scope.selectedImagePairs = [];
|
2013-10-23 15:07:26 +00:00
|
|
|
$scope.updateResults();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-23 15:07:26 +00:00
|
|
|
|
|
|
|
/**
|
2014-02-26 19:05:20 +00:00
|
|
|
* Move a subset of $scope.imagePairs to a different tab.
|
2013-10-23 15:07:26 +00:00
|
|
|
*
|
2014-02-26 19:05:20 +00:00
|
|
|
* @param imagePairIndices (array of ints): indices into $scope.imagePairs
|
2013-10-23 15:07:26 +00:00
|
|
|
* indicating which test results to move
|
|
|
|
* @param newTab (string): name of the tab to move the tests to
|
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$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;
|
2013-10-23 15:07:26 +00:00
|
|
|
}
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.numResultsPerTab[newTab] += numImagePairs;
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-23 15:07:26 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
|
2013-12-05 18:03:24 +00:00
|
|
|
//
|
|
|
|
// $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];
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-07-09 13:19:20 +00:00
|
|
|
'columnStringMatch': {
|
2013-12-05 18:03:24 +00:00
|
|
|
'load': function(nameValuePairs, name) {
|
|
|
|
var value = nameValuePairs[name];
|
|
|
|
if (value) {
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.columnStringMatch[name] = value;
|
2013-12-05 18:03:24 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
'save': function(nameValuePairs, name) {
|
2014-07-09 13:19:20 +00:00
|
|
|
nameValuePairs[name] = $scope.columnStringMatch[name];
|
2013-12-05 18:03:24 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-07-07 15:49:14 +00:00
|
|
|
'showingColumnValuesSet': {
|
2013-12-05 18:03:24 +00:00
|
|
|
'load': function(nameValuePairs, name) {
|
|
|
|
var value = nameValuePairs[name];
|
|
|
|
if (value) {
|
|
|
|
var valueArray = value.split(',');
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.showingColumnValues[name] = {};
|
|
|
|
$scope.toggleValuesInSet(valueArray, $scope.showingColumnValues[name]);
|
2013-12-05 18:03:24 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
'save': function(nameValuePairs, name) {
|
2014-07-07 15:49:14 +00:00
|
|
|
nameValuePairs[name] = Object.keys($scope.showingColumnValues[name]).join(',');
|
2013-12-05 18:03:24 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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();
|
2014-07-02 20:51:47 +00:00
|
|
|
|
2014-07-03 17:50:30 +00:00
|
|
|
// 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];
|
2014-07-07 15:49:14 +00:00
|
|
|
} 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;
|
2014-07-02 20:51:47 +00:00
|
|
|
}
|
2014-07-03 17:50:30 +00:00
|
|
|
$scope.urlSchemaVersionLoaded = urlSchemaVersion;
|
2014-07-02 20:51:47 +00:00
|
|
|
|
2014-07-07 15:49:14 +00:00
|
|
|
if (urlSchemaVersion != constants.URL_VALUE__SCHEMA_VERSION__CURRENT) {
|
|
|
|
nameValuePairs = $scope.upconvertUrlNameValuePairs(nameValuePairs, urlSchemaVersion);
|
|
|
|
}
|
2013-12-05 18:03:24 +00:00
|
|
|
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 = {};
|
2014-07-02 20:51:47 +00:00
|
|
|
nameValuePairs[constants.URL_KEY__SCHEMA_VERSION] = constants.URL_VALUE__SCHEMA_VERSION__CURRENT;
|
2013-12-05 18:03:24 +00:00
|
|
|
angular.forEach($scope.queryParameters.map,
|
|
|
|
function(copier, paramName) {
|
|
|
|
copier.save(nameValuePairs, paramName);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
$location.search(nameValuePairs);
|
|
|
|
};
|
|
|
|
|
2014-07-07 15:49:14 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2013-12-05 18:03:24 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
//
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-12-05 18:03:24 +00:00
|
|
|
* Update the displayed results, based on filters/settings,
|
|
|
|
* and call $scope.queryParameters.save() so that the new filter results
|
|
|
|
* can be bookmarked.
|
2013-10-24 15:38:27 +00:00
|
|
|
*/
|
2013-10-02 18:57:48 +00:00
|
|
|
$scope.updateResults = function() {
|
2014-03-10 15:36:06 +00:00
|
|
|
$scope.renderStartTime = window.performance.now();
|
|
|
|
$log.debug("renderStartTime: " + $scope.renderStartTime);
|
2013-10-02 18:57:48 +00:00
|
|
|
$scope.displayLimit = $scope.displayLimitPending;
|
2014-06-11 21:01:35 +00:00
|
|
|
$scope.mergeIdenticalRows = $scope.mergeIdenticalRowsPending;
|
2014-07-09 13:19:20 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2013-10-02 18:57:48 +00:00
|
|
|
// TODO(epoger): Every time we apply a filter, AngularJS creates
|
|
|
|
// another copy of the array. Is there a way we can filter out
|
2014-02-26 19:05:20 +00:00
|
|
|
// the imagePairs as they are displayed, rather than storing multiple
|
2013-10-02 18:57:48 +00:00
|
|
|
// array copies? (For better performance.)
|
2013-10-23 15:07:26 +00:00
|
|
|
if ($scope.viewingTab == $scope.defaultTab) {
|
2014-08-08 14:21:00 +00:00
|
|
|
var doReverse = !currSortAsc;
|
2013-11-08 16:25:25 +00:00
|
|
|
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.filteredImagePairs =
|
2013-10-23 15:07:26 +00:00
|
|
|
$filter("orderBy")(
|
2014-02-26 19:05:20 +00:00
|
|
|
$filter("removeHiddenImagePairs")(
|
|
|
|
$scope.imagePairs,
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.filterableColumnNames,
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.showingColumnValues,
|
2013-10-23 15:07:26 +00:00
|
|
|
$scope.viewingTab
|
|
|
|
),
|
2014-08-08 14:21:00 +00:00
|
|
|
// [$scope.getSortColumnValue, $scope.getSecondOrderSortValue],
|
|
|
|
$scope.getSortColumnValue,
|
2014-06-11 21:01:35 +00:00
|
|
|
doReverse);
|
|
|
|
$scope.limitedImagePairs = $filter("mergeAndLimit")(
|
|
|
|
$scope.filteredImagePairs, $scope.displayLimit, $scope.mergeIdenticalRows);
|
2013-10-23 15:07:26 +00:00
|
|
|
} else {
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.filteredImagePairs =
|
2013-10-23 15:07:26 +00:00
|
|
|
$filter("orderBy")(
|
|
|
|
$filter("filter")(
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.imagePairs,
|
2013-10-23 15:07:26 +00:00
|
|
|
{tab: $scope.viewingTab},
|
|
|
|
true
|
|
|
|
),
|
2014-08-08 14:21:00 +00:00
|
|
|
// [$scope.getSortColumnValue, $scope.getSecondOrderSortValue]);
|
|
|
|
$scope.getSortColumnValue);
|
2014-06-11 21:01:35 +00:00
|
|
|
$scope.limitedImagePairs = $filter("mergeAndLimit")(
|
|
|
|
$scope.filteredImagePairs, -1, $scope.mergeIdenticalRows);
|
2013-10-23 15:07:26 +00:00
|
|
|
}
|
2014-03-05 19:46:17 +00:00
|
|
|
$scope.showThumbnails = $scope.showThumbnailsPending;
|
2013-10-02 18:57:48 +00:00
|
|
|
$scope.imageSize = $scope.imageSizePending;
|
2013-10-24 15:38:27 +00:00
|
|
|
$scope.setUpdatesPending(false);
|
2013-12-05 18:03:24 +00:00
|
|
|
$scope.queryParameters.save();
|
2013-10-02 18:57:48 +00:00
|
|
|
}
|
|
|
|
|
2014-03-10 15:36:06 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-03-10 15:36:06 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
/**
|
|
|
|
* Re-sort the displayed results.
|
|
|
|
*
|
2014-05-12 20:40:29 +00:00
|
|
|
* @param subdict (string): which KEY__IMAGEPAIRS__* subdictionary
|
2014-06-11 21:01:35 +00:00
|
|
|
* the sort column key is within, or 'none' if the sort column
|
|
|
|
* key is one of KEY__IMAGEPAIRS__*
|
2014-02-26 19:05:20 +00:00
|
|
|
* @param key (string): sort by value associated with this key in subdict
|
2013-10-24 15:38:27 +00:00
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.sortResultsBy = function(subdict, key) {
|
2014-08-08 14:21:00 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2013-10-02 18:57:48 +00:00
|
|
|
$scope.updateResults();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|
2013-10-23 15:07:26 +00:00
|
|
|
|
2014-02-26 19:05:20 +00:00
|
|
|
/**
|
|
|
|
* 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];
|
2014-06-11 21:01:35 +00:00
|
|
|
} else if ($scope.sortColumnKey in imagePair) {
|
|
|
|
return imagePair[$scope.sortColumnKey];
|
2014-02-26 19:05:20 +00:00
|
|
|
} else {
|
|
|
|
return undefined;
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-02-26 19:05:20 +00:00
|
|
|
|
2014-06-11 21:01:35 +00:00
|
|
|
/**
|
|
|
|
* 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];
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-06-11 21:01:35 +00:00
|
|
|
|
2013-10-29 15:49:40 +00:00
|
|
|
/**
|
2014-07-09 13:19:20 +00:00
|
|
|
* Set $scope.columnStringMatch[name] = value, and update results.
|
2013-10-29 15:49:40 +00:00
|
|
|
*
|
|
|
|
* @param name
|
|
|
|
* @param value
|
|
|
|
*/
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.setColumnStringMatch = function(name, value) {
|
|
|
|
$scope.columnStringMatch[name] = value;
|
2013-10-29 15:49:40 +00:00
|
|
|
$scope.updateResults();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-29 15:49:40 +00:00
|
|
|
|
|
|
|
/**
|
2014-07-09 13:19:20 +00:00
|
|
|
* 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.)
|
2013-10-29 15:49:40 +00:00
|
|
|
*
|
2014-07-07 15:49:14 +00:00
|
|
|
* @param columnName
|
|
|
|
* @param columnValue
|
2013-10-29 15:49:40 +00:00
|
|
|
*/
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.showOnlyColumnValue = function(columnName, columnValue) {
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.columnStringMatch[columnName] = columnValue;
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.showingColumnValues[columnName] = {};
|
|
|
|
$scope.toggleValueInSet(columnValue, $scope.showingColumnValues[columnName]);
|
2013-10-29 15:49:40 +00:00
|
|
|
$scope.updateResults();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-29 15:49:40 +00:00
|
|
|
|
2014-02-06 20:22:25 +00:00
|
|
|
/**
|
2014-07-09 13:19:20 +00:00
|
|
|
* 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.)
|
2013-10-29 15:49:40 +00:00
|
|
|
*
|
2014-07-07 15:49:14 +00:00
|
|
|
* @param columnName
|
2014-02-06 20:22:25 +00:00
|
|
|
*/
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.showAllColumnValues = function(columnName) {
|
2014-07-09 13:19:20 +00:00
|
|
|
$scope.columnStringMatch[columnName] = "";
|
2014-07-07 15:49:14 +00:00
|
|
|
$scope.showingColumnValues[columnName] = {};
|
|
|
|
$scope.toggleValuesInSet($scope.allColumnValues[columnName],
|
|
|
|
$scope.showingColumnValues[columnName]);
|
2014-02-06 20:22:25 +00:00
|
|
|
$scope.updateResults();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-02-06 20:22:25 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Operations for sending info back to the server.
|
|
|
|
//
|
|
|
|
|
2013-10-23 15:07:26 +00:00
|
|
|
/**
|
|
|
|
* Tell the server that the actual results of these particular tests
|
|
|
|
* are acceptable.
|
|
|
|
*
|
2014-02-26 19:05:20 +00:00
|
|
|
* 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)
|
2013-10-23 15:07:26 +00:00
|
|
|
*/
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.submitApprovals = function(imagePairsSubset) {
|
2013-10-23 15:07:26 +00:00
|
|
|
$scope.submitPending = true;
|
2013-10-26 14:31:11 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
2014-02-26 19:05:20 +00:00
|
|
|
var updatedExpectations = [];
|
|
|
|
for (var i = 0; i < imagePairsSubset.length; i++) {
|
|
|
|
var imagePair = imagePairsSubset[i];
|
|
|
|
var updatedExpectation = {};
|
2014-05-12 20:40:29 +00:00
|
|
|
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]
|
2014-02-26 19:05:20 +00:00
|
|
|
.indexOf('comparison-')) {
|
2013-10-26 14:31:11 +00:00
|
|
|
encounteredComparisonConfig = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Advanced settings...
|
2014-05-12 20:40:29 +00:00
|
|
|
if (null == updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]) {
|
|
|
|
updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] = {};
|
2014-02-26 19:05:20 +00:00
|
|
|
}
|
2014-05-12 20:40:29 +00:00
|
|
|
updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
|
2014-02-26 19:05:20 +00:00
|
|
|
[constants.KEY__EXPECTATIONS__REVIEWED] =
|
|
|
|
$scope.submitAdvancedSettings[
|
|
|
|
constants.KEY__EXPECTATIONS__REVIEWED];
|
|
|
|
if (true == $scope.submitAdvancedSettings[
|
|
|
|
constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
|
2013-10-26 14:31:11 +00:00
|
|
|
// if it's false, don't send it at all (just keep the default)
|
2014-05-12 20:40:29 +00:00
|
|
|
updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
|
2014-02-26 19:05:20 +00:00
|
|
|
[constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
|
2013-10-26 14:31:11 +00:00
|
|
|
}
|
2014-05-12 20:40:29 +00:00
|
|
|
updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
|
2014-02-26 19:05:20 +00:00
|
|
|
[constants.KEY__EXPECTATIONS__BUGS] = bugs;
|
2013-10-26 14:31:11 +00:00
|
|
|
|
2014-02-26 19:05:20 +00:00
|
|
|
updatedExpectations.push(updatedExpectation);
|
2013-10-23 15:07:26 +00:00
|
|
|
}
|
2013-10-26 14:31:11 +00:00
|
|
|
if (encounteredComparisonConfig) {
|
|
|
|
alert("Approval failed -- you cannot approve results with config " +
|
|
|
|
"type comparison-*");
|
|
|
|
$scope.submitPending = false;
|
|
|
|
return;
|
|
|
|
}
|
2014-02-26 19:05:20 +00:00
|
|
|
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];
|
2013-10-23 15:07:26 +00:00
|
|
|
$http({
|
|
|
|
method: "POST",
|
|
|
|
url: "/edits",
|
2014-02-26 19:05:20 +00:00
|
|
|
data: modificationData
|
2013-10-23 15:07:26 +00:00
|
|
|
}).success(function(data, status, headers, config) {
|
2014-02-26 19:05:20 +00:00
|
|
|
var imagePairIndicesToMove = [];
|
|
|
|
for (var i = 0; i < imagePairsSubset.length; i++) {
|
|
|
|
imagePairIndicesToMove.push(imagePairsSubset[i].index);
|
2013-10-23 15:07:26 +00:00
|
|
|
}
|
2014-02-26 19:05:20 +00:00
|
|
|
$scope.moveImagePairsToTab(imagePairIndicesToMove,
|
|
|
|
"HackToMakeSureThisImagePairDisappears");
|
2013-10-23 15:07:26 +00:00
|
|
|
$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" +
|
2013-12-17 18:06:13 +00:00
|
|
|
"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);
|
2013-10-23 15:07:26 +00:00
|
|
|
}).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;
|
|
|
|
});
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-24 15:38:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
// 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?
|
|
|
|
//
|
|
|
|
|
2014-02-06 20:22:25 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-02-06 20:22:25 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
/**
|
|
|
|
* 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]);
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-24 15:38:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-24 15:38:27 +00:00
|
|
|
|
2013-10-29 15:49:40 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-29 15:49:40 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// 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));
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-24 15:38:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-24 15:38:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
// Miscellaneous utility functions.
|
|
|
|
// TODO(epoger): move into a separate .js file?
|
|
|
|
//
|
|
|
|
|
2014-05-08 21:15:20 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-05-08 21:15:20 +00:00
|
|
|
|
2013-10-24 15:38:27 +00:00
|
|
|
/**
|
|
|
|
* 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();
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2013-10-24 15:38:27 +00:00
|
|
|
|
2014-02-05 19:49:17 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-02-05 19:49:17 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
2014-08-08 14:21:00 +00:00
|
|
|
};
|
2014-02-05 19:49:17 +00:00
|
|
|
|
2013-09-27 15:02:44 +00:00
|
|
|
}
|
|
|
|
);
|