3 * Reads GM result reports written out by results.py, and imports
4 * them into $scope.extraColumnHeaders and $scope.imagePairs .
6 var Loader = angular.module(
11 // This configuration is needed to allow downloads of the diff patch.
12 // See https://github.com/angular/angular.js/issues/3889
13 Loader.config(['$compileProvider', function($compileProvider) {
14 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|file|blob):/);
18 'resultsUpdatedCallbackDirective',
21 return function(scope, element, attrs) {
24 scope.resultsUpdatedCallback();
32 // TODO(epoger): Combine ALL of our filtering operations (including
33 // truncation) into this one filter, so that runs most efficiently?
34 // (We would have to make sure truncation still took place after
37 'removeHiddenImagePairs',
39 return function(unfilteredImagePairs, filterableColumnNames, showingColumnValues,
41 var filteredImagePairs = [];
42 for (var i = 0; i < unfilteredImagePairs.length; i++) {
43 var imagePair = unfilteredImagePairs[i];
44 var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
45 var allColumnValuesAreVisible = true;
46 // Loop over all columns, and if any of them contain values not found in
47 // showingColumnValues[columnName], don't include this imagePair.
49 // We use this same filtering mechanism regardless of whether each column
50 // has USE_FREEFORM_FILTER set or not; if that flag is set, then we will
51 // have already used the freeform text entry block to populate
52 // showingColumnValues[columnName].
53 for (var j = 0; j < filterableColumnNames.length; j++) {
54 var columnName = filterableColumnNames[j];
55 var columnValue = extraColumnValues[columnName];
56 if (!showingColumnValues[columnName][columnValue]) {
57 allColumnValuesAreVisible = false;
61 if (allColumnValuesAreVisible && (viewingTab == imagePair.tab)) {
62 filteredImagePairs.push(imagePair);
65 return filteredImagePairs;
71 * Limit the input imagePairs to some max number, and merge identical rows
72 * (adjacent rows which have the same (imageA, imageB) pair).
74 * @param unfilteredImagePairs imagePairs to filter
75 * @param maxPairs maximum number of pairs to output, or <0 for no limit
76 * @param mergeIdenticalRows if true, merge identical rows by setting
77 * ROWSPAN>1 on the first merged row, and ROWSPAN=0 for the rest
82 return function(unfilteredImagePairs, maxPairs, mergeIdenticalRows) {
83 var numPairs = unfilteredImagePairs.length;
84 if ((maxPairs > 0) && (maxPairs < numPairs)) {
87 var filteredImagePairs = [];
88 if (!mergeIdenticalRows || (numPairs == 1)) {
89 // Take a shortcut if we're not merging identical rows.
90 // We still need to set ROWSPAN to 1 for each row, for the HTML viewer.
91 for (var i = numPairs-1; i >= 0; i--) {
92 var imagePair = unfilteredImagePairs[i];
93 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
94 filteredImagePairs[i] = imagePair;
96 } else if (numPairs > 1) {
97 // General case--there are at least 2 rows, so we may need to merge some.
98 // Work from the bottom up, so we can keep a running total of how many
99 // rows should be merged, and set ROWSPAN of the top row accordingly.
100 var imagePair = unfilteredImagePairs[numPairs-1];
101 var nextRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL];
102 var nextRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
103 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
104 filteredImagePairs[numPairs-1] = imagePair;
105 for (var i = numPairs-2; i >= 0; i--) {
106 imagePair = unfilteredImagePairs[i];
107 var thisRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL];
108 var thisRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
109 if ((thisRowImageAUrl == nextRowImageAUrl) &&
110 (thisRowImageBUrl == nextRowImageBUrl)) {
111 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] =
112 filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] + 1;
113 filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] = 0;
115 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
116 nextRowImageAUrl = thisRowImageAUrl;
117 nextRowImageBUrl = thisRowImageBUrl;
119 filteredImagePairs[i] = imagePair;
124 return filteredImagePairs;
132 function($scope, $http, $filter, $location, $log, $timeout, constants) {
133 $scope.readyToDisplay = false;
134 $scope.constants = constants;
135 $scope.windowTitle = "Loading GM Results...";
136 $scope.setADir = $location.search().setADir;
137 $scope.setASection = $location.search().setASection;
138 $scope.setBDir = $location.search().setBDir;
139 $scope.setBSection = $location.search().setBSection;
140 $scope.loadingMessage = "please wait...";
142 var currSortAsc = true;
146 * On initial page load, load a full dictionary of results.
147 * Once the dictionary is loaded, unhide the page elements so they can
150 $scope.liveQueryUrl =
151 "/live-results/setADir=" + encodeURIComponent($scope.setADir) +
152 "&setASection=" + encodeURIComponent($scope.setASection) +
153 "&setBDir=" + encodeURIComponent($scope.setBDir) +
154 "&setBSection=" + encodeURIComponent($scope.setBSection);
155 $http.get($scope.liveQueryUrl).success(
156 function(data, status, header, config) {
157 var dataHeader = data[constants.KEY__ROOT__HEADER];
158 if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
159 constants.VALUE__HEADER__SCHEMA_VERSION) {
160 $scope.loadingMessage = "ERROR: Got JSON file with schema version "
161 + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
162 + " but expected schema version "
163 + constants.VALUE__HEADER__SCHEMA_VERSION;
164 } else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
165 // Apply the server's requested reload delay to local time,
166 // so we will wait the right number of seconds regardless of clock
167 // skew between client and server.
168 var reloadDelayInSeconds =
169 dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] -
170 dataHeader[constants.KEY__HEADER__TIME_UPDATED];
171 var timeNow = new Date().getTime();
172 var timeToReload = timeNow + reloadDelayInSeconds * 1000;
173 $scope.loadingMessage =
174 "server is still loading results; will retry at " +
175 $scope.localTimeString(timeToReload / 1000);
177 function(){location.reload();},
178 timeToReload - timeNow);
180 $scope.loadingMessage = "processing data, please wait...";
182 $scope.header = dataHeader;
183 $scope.extraColumnHeaders = data[constants.KEY__ROOT__EXTRACOLUMNHEADERS];
184 $scope.orderedColumnNames = data[constants.KEY__ROOT__EXTRACOLUMNORDER];
185 $scope.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS];
186 $scope.imageSets = data[constants.KEY__ROOT__IMAGESETS];
188 // set the default sort column and make it ascending.
189 $scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES;
190 $scope.sortColumnKey = constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF;
193 $scope.showSubmitAdvancedSettings = false;
194 $scope.submitAdvancedSettings = {};
195 $scope.submitAdvancedSettings[
196 constants.KEY__EXPECTATIONS__REVIEWED] = true;
197 $scope.submitAdvancedSettings[
198 constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false;
199 $scope.submitAdvancedSettings['bug'] = '';
201 // Create the list of tabs (lists into which the user can file each
202 // test). This may vary, depending on isEditable.
206 if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
207 $scope.tabs = $scope.tabs.concat(
208 ['Pending Approval']);
210 $scope.defaultTab = $scope.tabs[0];
211 $scope.viewingTab = $scope.defaultTab;
213 // Track the number of results on each tab.
214 $scope.numResultsPerTab = {};
215 for (var i = 0; i < $scope.tabs.length; i++) {
216 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
218 $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length;
220 // Add index and tab fields to all records.
221 for (var i = 0; i < $scope.imagePairs.length; i++) {
222 $scope.imagePairs[i].index = i;
223 $scope.imagePairs[i].tab = $scope.defaultTab;
226 // Arrays within which the user can toggle individual elements.
227 $scope.selectedImagePairs = [];
231 // filterableColumnNames is a list of all column names we can filter on.
232 // allColumnValues[columnName] is a list of all known values
233 // for a given column.
234 // showingColumnValues[columnName] is a set indicating which values
235 // in a given column would cause us to show a row, rather than hiding it.
237 // columnStringMatch[columnName] is a string used as a pattern to generate
238 // showingColumnValues[columnName] for columns we filter using free-form text.
239 // It is ignored for any columns with USE_FREEFORM_FILTER == false.
240 $scope.filterableColumnNames = [];
241 $scope.allColumnValues = {};
242 $scope.showingColumnValues = {};
243 $scope.columnStringMatch = {};
246 Object.keys($scope.extraColumnHeaders),
247 function(columnName) {
248 var columnHeader = $scope.extraColumnHeaders[columnName];
249 if (columnHeader[constants.KEY__EXTRACOLUMNHEADERS__IS_FILTERABLE]) {
250 $scope.filterableColumnNames.push(columnName);
251 $scope.allColumnValues[columnName] = $scope.columnSliceOf2DArray(
252 columnHeader[constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS], 0);
253 $scope.showingColumnValues[columnName] = {};
254 $scope.toggleValuesInSet($scope.allColumnValues[columnName],
255 $scope.showingColumnValues[columnName]);
256 $scope.columnStringMatch[columnName] = "";
261 // TODO(epoger): Special handling for RESULT_TYPE column:
262 // by default, show only KEY__RESULT_TYPE__FAILED results
263 $scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE] = {};
264 $scope.showingColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE][
265 constants.KEY__RESULT_TYPE__FAILED] = true;
267 // Set up mapping for URL parameters.
268 // parameter name -> copier object to load/save parameter value
269 $scope.queryParameters.map = {
270 'setADir': $scope.queryParameters.copiers.simple,
271 'setASection': $scope.queryParameters.copiers.simple,
272 'setBDir': $scope.queryParameters.copiers.simple,
273 'setBSection': $scope.queryParameters.copiers.simple,
274 'displayLimitPending': $scope.queryParameters.copiers.simple,
275 'showThumbnailsPending': $scope.queryParameters.copiers.simple,
276 'mergeIdenticalRowsPending': $scope.queryParameters.copiers.simple,
277 'imageSizePending': $scope.queryParameters.copiers.simple,
278 'sortColumnSubdict': $scope.queryParameters.copiers.simple,
279 'sortColumnKey': $scope.queryParameters.copiers.simple,
281 // Some parameters are handled differently based on whether they USE_FREEFORM_FILTER.
283 $scope.filterableColumnNames,
284 function(columnName) {
285 if ($scope.extraColumnHeaders[columnName]
286 [constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]) {
287 $scope.queryParameters.map[columnName] =
288 $scope.queryParameters.copiers.columnStringMatch;
290 $scope.queryParameters.map[columnName] =
291 $scope.queryParameters.copiers.showingColumnValuesSet;
296 // If any defaults were overridden in the URL, get them now.
297 $scope.queryParameters.load();
299 // Any image URLs which are relative should be relative to the JSON
300 // file's source directory; absolute URLs should be left alone.
301 var baseUrlKey = constants.KEY__IMAGESETS__FIELD__BASE_URL;
305 var baseUrl = imageSet[baseUrlKey];
306 if ((baseUrl.substring(0, 1) != '/') &&
307 (baseUrl.indexOf('://') == -1)) {
308 imageSet[baseUrlKey] = '/' + baseUrl;
313 $scope.readyToDisplay = true;
314 $scope.updateResults();
315 $scope.loadingMessage = "";
316 $scope.windowTitle = "Current GM Results";
318 $timeout( function() {
319 make_results_header_sticky();
324 function(data, status, header, config) {
325 $scope.loadingMessage = "FAILED to load.";
326 $scope.windowTitle = "Failed to Load GM Results";
332 // Select/Clear/Toggle all tests.
336 * Select all currently showing tests.
338 $scope.selectAllImagePairs = function() {
339 var numImagePairsShowing = $scope.limitedImagePairs.length;
340 for (var i = 0; i < numImagePairsShowing; i++) {
341 var index = $scope.limitedImagePairs[i].index;
342 if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) {
343 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
349 * Deselect all currently showing tests.
351 $scope.clearAllImagePairs = function() {
352 var numImagePairsShowing = $scope.limitedImagePairs.length;
353 for (var i = 0; i < numImagePairsShowing; i++) {
354 var index = $scope.limitedImagePairs[i].index;
355 if ($scope.isValueInArray(index, $scope.selectedImagePairs)) {
356 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
362 * Toggle selection of all currently showing tests.
364 $scope.toggleAllImagePairs = function() {
365 var numImagePairsShowing = $scope.limitedImagePairs.length;
366 for (var i = 0; i < numImagePairsShowing; i++) {
367 var index = $scope.limitedImagePairs[i].index;
368 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
373 * Toggle selection state of a subset of the currently showing tests.
375 * @param startIndex index within $scope.limitedImagePairs of the first
376 * test to toggle selection state of
377 * @param num number of tests (in a contiguous block) to toggle
379 $scope.toggleSomeImagePairs = function(startIndex, num) {
380 var numImagePairsShowing = $scope.limitedImagePairs.length;
381 for (var i = startIndex; i < startIndex + num; i++) {
382 var index = $scope.limitedImagePairs[i].index;
383 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
393 * Change the selected tab.
395 * @param tab (string): name of the tab to select
397 $scope.setViewingTab = function(tab) {
398 $scope.viewingTab = tab;
399 $scope.updateResults();
403 * Move the imagePairs in $scope.selectedImagePairs to a different tab,
404 * and then clear $scope.selectedImagePairs.
406 * @param newTab (string): name of the tab to move the tests to
408 $scope.moveSelectedImagePairsToTab = function(newTab) {
409 $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
410 $scope.selectedImagePairs = [];
411 $scope.updateResults();
415 * Move a subset of $scope.imagePairs to a different tab.
417 * @param imagePairIndices (array of ints): indices into $scope.imagePairs
418 * indicating which test results to move
419 * @param newTab (string): name of the tab to move the tests to
421 $scope.moveImagePairsToTab = function(imagePairIndices, newTab) {
423 var numImagePairs = imagePairIndices.length;
424 for (var i = 0; i < numImagePairs; i++) {
425 imagePairIndex = imagePairIndices[i];
426 $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--;
427 $scope.imagePairs[imagePairIndex].tab = newTab;
429 $scope.numResultsPerTab[newTab] += numImagePairs;
434 // $scope.queryParameters:
435 // Transfer parameter values between $scope and the URL query string.
437 $scope.queryParameters = {};
439 // load and save functions for parameters of each type
440 // (load a parameter value into $scope from nameValuePairs,
441 // save a parameter value from $scope into nameValuePairs)
442 $scope.queryParameters.copiers = {
444 'load': function(nameValuePairs, name) {
445 var value = nameValuePairs[name];
447 $scope[name] = value;
450 'save': function(nameValuePairs, name) {
451 nameValuePairs[name] = $scope[name];
455 'columnStringMatch': {
456 'load': function(nameValuePairs, name) {
457 var value = nameValuePairs[name];
459 $scope.columnStringMatch[name] = value;
462 'save': function(nameValuePairs, name) {
463 nameValuePairs[name] = $scope.columnStringMatch[name];
467 'showingColumnValuesSet': {
468 'load': function(nameValuePairs, name) {
469 var value = nameValuePairs[name];
471 var valueArray = value.split(',');
472 $scope.showingColumnValues[name] = {};
473 $scope.toggleValuesInSet(valueArray, $scope.showingColumnValues[name]);
476 'save': function(nameValuePairs, name) {
477 nameValuePairs[name] = Object.keys($scope.showingColumnValues[name]).join(',');
483 // Loads all parameters into $scope from the URL query string;
484 // any which are not found within the URL will keep their current value.
485 $scope.queryParameters.load = function() {
486 var nameValuePairs = $location.search();
488 // If urlSchemaVersion is not specified, we assume the current version.
489 var urlSchemaVersion = constants.URL_VALUE__SCHEMA_VERSION__CURRENT;
490 if (constants.URL_KEY__SCHEMA_VERSION in nameValuePairs) {
491 urlSchemaVersion = nameValuePairs[constants.URL_KEY__SCHEMA_VERSION];
492 } else if ('hiddenResultTypes' in nameValuePairs) {
493 // The combination of:
494 // - absence of an explicit urlSchemaVersion, and
495 // - presence of the old 'hiddenResultTypes' field
496 // tells us that the URL is from the original urlSchemaVersion.
497 // See https://codereview.chromium.org/367173002/
498 urlSchemaVersion = 0;
500 $scope.urlSchemaVersionLoaded = urlSchemaVersion;
502 if (urlSchemaVersion != constants.URL_VALUE__SCHEMA_VERSION__CURRENT) {
503 nameValuePairs = $scope.upconvertUrlNameValuePairs(nameValuePairs, urlSchemaVersion);
505 angular.forEach($scope.queryParameters.map,
506 function(copier, paramName) {
507 copier.load(nameValuePairs, paramName);
512 // Saves all parameters from $scope into the URL query string.
513 $scope.queryParameters.save = function() {
514 var nameValuePairs = {};
515 nameValuePairs[constants.URL_KEY__SCHEMA_VERSION] = constants.URL_VALUE__SCHEMA_VERSION__CURRENT;
516 angular.forEach($scope.queryParameters.map,
517 function(copier, paramName) {
518 copier.save(nameValuePairs, paramName);
521 $location.search(nameValuePairs);
525 * Converts URL name/value pairs that were stored by a previous urlSchemaVersion
526 * to the currently needed format.
528 * @param oldNValuePairs name/value pairs found in the loaded URL
529 * @param oldUrlSchemaVersion which version of the schema was used to generate that URL
531 * @returns nameValuePairs as needed by the current URL parser
533 $scope.upconvertUrlNameValuePairs = function(oldNameValuePairs, oldUrlSchemaVersion) {
534 var newNameValuePairs = {};
535 angular.forEach(oldNameValuePairs,
536 function(value, name) {
537 if (oldUrlSchemaVersion < 1) {
538 if ('hiddenConfigs' == name) {
541 $scope.toggleValuesInSet(value.split(','), valueSet);
542 $scope.toggleValuesInSet(
543 $scope.allColumnValues[constants.KEY__EXTRACOLUMNS__CONFIG],
545 value = Object.keys(valueSet).join(',');
546 } else if ('hiddenResultTypes' == name) {
549 $scope.toggleValuesInSet(value.split(','), valueSet);
550 $scope.toggleValuesInSet(
551 $scope.allColumnValues[constants.KEY__EXTRACOLUMNS__RESULT_TYPE],
553 value = Object.keys(valueSet).join(',');
557 newNameValuePairs[name] = value;
560 return newNameValuePairs;
565 // updateResults() and friends.
569 * Set $scope.areUpdatesPending (to enable/disable the Update Results
572 * TODO(epoger): We could reduce the amount of code by just setting the
573 * variable directly (from, e.g., a button's ng-click handler). But when
574 * I tried that, the HTML elements depending on the variable did not get
576 * It turns out that this is due to variable scoping within an ng-repeat
577 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
579 * @param val boolean value to set $scope.areUpdatesPending to
581 $scope.setUpdatesPending = function(val) {
582 $scope.areUpdatesPending = val;
586 * Update the displayed results, based on filters/settings,
587 * and call $scope.queryParameters.save() so that the new filter results
590 $scope.updateResults = function() {
591 $scope.renderStartTime = window.performance.now();
592 $log.debug("renderStartTime: " + $scope.renderStartTime);
593 $scope.displayLimit = $scope.displayLimitPending;
594 $scope.mergeIdenticalRows = $scope.mergeIdenticalRowsPending;
596 // For each USE_FREEFORM_FILTER column, populate showingColumnValues.
597 // This is more efficient than applying the freeform filter within the
598 // tight loop in removeHiddenImagePairs.
600 $scope.filterableColumnNames,
601 function(columnName) {
602 var columnHeader = $scope.extraColumnHeaders[columnName];
603 if (columnHeader[constants.KEY__EXTRACOLUMNHEADERS__USE_FREEFORM_FILTER]) {
604 var columnStringMatch = $scope.columnStringMatch[columnName];
605 var showingColumnValues = {};
607 $scope.allColumnValues[columnName],
608 function(columnValue) {
609 if (-1 != columnValue.indexOf(columnStringMatch)) {
610 showingColumnValues[columnValue] = true;
614 $scope.showingColumnValues[columnName] = showingColumnValues;
619 // TODO(epoger): Every time we apply a filter, AngularJS creates
620 // another copy of the array. Is there a way we can filter out
621 // the imagePairs as they are displayed, rather than storing multiple
622 // array copies? (For better performance.)
624 if ($scope.viewingTab == $scope.defaultTab) {
625 var doReverse = !currSortAsc;
627 $scope.filteredImagePairs =
629 $filter("removeHiddenImagePairs")(
631 $scope.filterableColumnNames,
632 $scope.showingColumnValues,
635 [$scope.getSortColumnValue, $scope.getSecondOrderSortValue],
637 $scope.limitedImagePairs = $filter("mergeAndLimit")(
638 $scope.filteredImagePairs, $scope.displayLimit, $scope.mergeIdenticalRows);
640 $scope.filteredImagePairs =
644 {tab: $scope.viewingTab},
647 [$scope.getSortColumnValue, $scope.getSecondOrderSortValue]);
648 $scope.limitedImagePairs = $filter("mergeAndLimit")(
649 $scope.filteredImagePairs, -1, $scope.mergeIdenticalRows);
651 $scope.showThumbnails = $scope.showThumbnailsPending;
652 $scope.imageSize = $scope.imageSizePending;
653 $scope.setUpdatesPending(false);
654 $scope.queryParameters.save();
658 * This function is called when the results have been completely rendered
659 * after updateResults().
661 $scope.resultsUpdatedCallback = function() {
662 $scope.renderEndTime = window.performance.now();
663 $log.debug("renderEndTime: " + $scope.renderEndTime);
667 * Re-sort the displayed results.
669 * @param subdict (string): which KEY__IMAGEPAIRS__* subdictionary
670 * the sort column key is within, or 'none' if the sort column
671 * key is one of KEY__IMAGEPAIRS__*
672 * @param key (string): sort by value associated with this key in subdict
674 $scope.sortResultsBy = function(subdict, key) {
675 // if we are already sorting by this column then toggle between asc/desc
676 if ((subdict === $scope.sortColumnSubdict) && ($scope.sortColumnKey === key)) {
677 currSortAsc = !currSortAsc;
679 $scope.sortColumnSubdict = subdict;
680 $scope.sortColumnKey = key;
683 $scope.updateResults();
687 * Returns ASC or DESC (from constants) if currently the data
688 * is sorted by the provided column.
690 * @param colName: name of the column for which we need to get the class.
693 $scope.sortedByColumnsCls = function (colName) {
694 if ($scope.sortColumnKey !== colName) {
698 var result = (currSortAsc) ? constants.ASC : constants.DESC;
699 console.log("sort class:", result);
704 * For a particular ImagePair, return the value of the column we are
705 * sorting on (according to $scope.sortColumnSubdict and
706 * $scope.sortColumnKey).
708 * @param imagePair: imagePair to get a column value out of.
710 $scope.getSortColumnValue = function(imagePair) {
711 if ($scope.sortColumnSubdict in imagePair) {
712 return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey];
713 } else if ($scope.sortColumnKey in imagePair) {
714 return imagePair[$scope.sortColumnKey];
721 * For a particular ImagePair, return the value we use for the
722 * second-order sort (tiebreaker when multiple rows have
723 * the same getSortColumnValue()).
725 * We join the imageA and imageB urls for this value, so that we merge
726 * adjacent rows as much as possible.
728 * @param imagePair: imagePair to get a column value out of.
730 $scope.getSecondOrderSortValue = function(imagePair) {
731 return imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" +
732 imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
736 * Set $scope.columnStringMatch[name] = value, and update results.
741 $scope.setColumnStringMatch = function(name, value) {
742 $scope.columnStringMatch[name] = value;
743 $scope.updateResults();
747 * Update $scope.showingColumnValues[columnName] and $scope.columnStringMatch[columnName]
748 * so that ONLY entries with this columnValue are showing, and update the visible results.
749 * (We update both of those, so we cover both freeform and checkbox filtered columns.)
754 $scope.showOnlyColumnValue = function(columnName, columnValue) {
755 $scope.columnStringMatch[columnName] = columnValue;
756 $scope.showingColumnValues[columnName] = {};
757 $scope.toggleValueInSet(columnValue, $scope.showingColumnValues[columnName]);
758 $scope.updateResults();
762 * Update $scope.showingColumnValues[columnName] and $scope.columnStringMatch[columnName]
763 * so that ALL entries are showing, and update the visible results.
764 * (We update both of those, so we cover both freeform and checkbox filtered columns.)
768 $scope.showAllColumnValues = function(columnName) {
769 $scope.columnStringMatch[columnName] = "";
770 $scope.showingColumnValues[columnName] = {};
771 $scope.toggleValuesInSet($scope.allColumnValues[columnName],
772 $scope.showingColumnValues[columnName]);
773 $scope.updateResults();
778 // Operations for sending info back to the server.
782 * Tell the server that the actual results of these particular tests
785 * This assumes that the original expectations are in imageSetA, and the
786 * new expectations are in imageSetB. That's fine, because the server
787 * mandates that anyway (it will swap the sets if the user requests them
788 * in the opposite order).
790 * @param imagePairsSubset an array of test results, most likely a subset of
791 * $scope.imagePairs (perhaps with some modifications)
793 $scope.submitApprovals = function(imagePairsSubset) {
794 $scope.submitPending = true;
795 $scope.diffResults = "";
797 // Convert bug text field to null or 1-item array.
799 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
800 if (!isNaN(bugNumber)) {
804 var updatedExpectations = [];
805 for (var i = 0; i < imagePairsSubset.length; i++) {
806 var imagePair = imagePairsSubset[i];
807 var updatedExpectation = {};
808 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] =
809 imagePair[constants.KEY__IMAGEPAIRS__EXPECTATIONS];
810 updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS] =
811 imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
812 updatedExpectation[constants.KEY__IMAGEPAIRS__SOURCE_JSON_FILE] =
813 imagePair[constants.KEY__IMAGEPAIRS__SOURCE_JSON_FILE];
814 // IMAGE_B_URL contains the actual image (which is now the expectation)
815 updatedExpectation[constants.KEY__IMAGEPAIRS__IMAGE_B_URL] =
816 imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
818 // Advanced settings...
819 if (null == updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]) {
820 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] = {};
822 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
823 [constants.KEY__EXPECTATIONS__REVIEWED] =
824 $scope.submitAdvancedSettings[
825 constants.KEY__EXPECTATIONS__REVIEWED];
826 if (true == $scope.submitAdvancedSettings[
827 constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
828 // if it's false, don't send it at all (just keep the default)
829 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
830 [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
832 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
833 [constants.KEY__EXPECTATIONS__BUGS] = bugs;
835 updatedExpectations.push(updatedExpectation);
837 var modificationData = {};
838 modificationData[constants.KEY__LIVE_EDITS__MODIFICATIONS] =
840 modificationData[constants.KEY__LIVE_EDITS__SET_A_DESCRIPTIONS] =
841 $scope.header[constants.KEY__HEADER__SET_A_DESCRIPTIONS];
842 modificationData[constants.KEY__LIVE_EDITS__SET_B_DESCRIPTIONS] =
843 $scope.header[constants.KEY__HEADER__SET_B_DESCRIPTIONS];
847 data: modificationData
848 }).success(function(data, status, headers, config) {
849 $scope.diffResults = data;
850 var blob = new Blob([$scope.diffResults], {type: 'text/plain'});
851 $scope.diffResultsBlobUrl = window.URL.createObjectURL(blob);
852 $scope.submitPending = false;
853 }).error(function(data, status, headers, config) {
854 alert("There was an error submitting your baselines.\n\n" +
855 "Please see server-side log for details.");
856 $scope.submitPending = false;
862 // Operations we use to mimic Set semantics, in such a way that
863 // checking for presence within the Set is as fast as possible.
864 // But getting a list of all values within the Set is not necessarily
866 // TODO(epoger): move into a separate .js file?
870 * Returns the number of values present within set "set".
872 * @param set an Object which we use to mimic set semantics
874 $scope.setSize = function(set) {
875 return Object.keys(set).length;
879 * Returns true if value "value" is present within set "set".
881 * @param value a value of any type
882 * @param set an Object which we use to mimic set semantics
883 * (this should make isValueInSet faster than if we used an Array)
885 $scope.isValueInSet = function(value, set) {
886 return (true == set[value]);
890 * If value "value" is already in set "set", remove it; otherwise, add it.
892 * @param value a value of any type
893 * @param set an Object which we use to mimic set semantics
895 $scope.toggleValueInSet = function(value, set) {
896 if (true == set[value]) {
904 * For each value in valueArray, call toggleValueInSet(value, set).
909 $scope.toggleValuesInSet = function(valueArray, set) {
910 var arrayLength = valueArray.length;
911 for (var i = 0; i < arrayLength; i++) {
912 $scope.toggleValueInSet(valueArray[i], set);
918 // Array operations; similar to our Set operations, but operate on a
919 // Javascript Array so we *can* easily get a list of all values in the Set.
920 // TODO(epoger): move into a separate .js file?
924 * Returns true if value "value" is present within array "array".
926 * @param value a value of any type
927 * @param array a Javascript Array
929 $scope.isValueInArray = function(value, array) {
930 return (-1 != array.indexOf(value));
934 * If value "value" is already in array "array", remove it; otherwise,
937 * @param value a value of any type
938 * @param array a Javascript Array
940 $scope.toggleValueInArray = function(value, array) {
941 var i = array.indexOf(value);
951 // Miscellaneous utility functions.
952 // TODO(epoger): move into a separate .js file?
956 * Returns a single "column slice" of a 2D array.
958 * For example, if array is:
962 * and index is 0, this this will return:
965 * @param array a Javascript Array
966 * @param column (numeric): index within each row array
968 $scope.columnSliceOf2DArray = function(array, column) {
970 var numRows = array.length;
971 for (var row = 0; row < numRows; row++) {
972 slice.push(array[row][column]);
978 * Returns a human-readable (in local time zone) time string for a
979 * particular moment in time.
981 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
983 $scope.localTimeString = function(secondsPastEpoch) {
984 var d = new Date(secondsPastEpoch * 1000);
989 * Returns a hex color string (such as "#aabbcc") for the given RGB values.
991 * @param r (numeric): red channel value, 0-255
992 * @param g (numeric): green channel value, 0-255
993 * @param b (numeric): blue channel value, 0-255
995 $scope.hexColorString = function(r, g, b) {
996 var rString = r.toString(16);
998 rString = "0" + rString;
1000 var gString = g.toString(16);
1002 gString = "0" + gString;
1004 var bString = b.toString(16);
1006 bString = "0" + bString;
1008 return '#' + rString + gString + bString;
1012 * Returns a hex color string (such as "#aabbcc") for the given brightness.
1014 * @param brightnessString (string): 0-255, 0 is completely black
1016 * TODO(epoger): It might be nice to tint the color when it's not completely
1017 * black or completely white.
1019 $scope.brightnessStringToHexColor = function(brightnessString) {
1020 var v = parseInt(brightnessString);
1021 return $scope.hexColorString(v, v, v);