blob: f5a4f7bf4da680be4eb25db2da4afc40714ecb11 [file] [log] [blame]
epoger@google.comf9d134d2013-09-27 15:02:44 +00001/*
2 * Loader:
epoger@google.comafaad3d2013-09-30 15:06:25 +00003 * Reads GM result reports written out by results.py, and imports
commit-bot@chromium.org16f41802014-02-26 19:05:20 +00004 * them into $scope.extraColumnHeaders and $scope.imagePairs .
epoger@google.comf9d134d2013-09-27 15:02:44 +00005 */
6var Loader = angular.module(
7 'Loader',
commit-bot@chromium.org6e3ee332014-03-10 19:12:53 +00008 ['ConstantsModule']
epoger@google.comf9d134d2013-09-27 15:02:44 +00009);
epoger@google.com5f2bb002013-10-02 18:57:48 +000010
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +000011Loader.directive(
12 'resultsUpdatedCallbackDirective',
13 ['$timeout',
14 function($timeout) {
15 return function(scope, element, attrs) {
16 if (scope.$last) {
17 $timeout(function() {
18 scope.resultsUpdatedCallback();
19 });
20 }
21 };
22 }
23 ]
24);
25
epoger@google.com5f2bb002013-10-02 18:57:48 +000026// TODO(epoger): Combine ALL of our filtering operations (including
27// truncation) into this one filter, so that runs most efficiently?
28// (We would have to make sure truncation still took place after
29// sorting, though.)
30Loader.filter(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000031 'removeHiddenImagePairs',
32 function(constants) {
33 return function(unfilteredImagePairs, hiddenResultTypes, hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +000034 builderSubstring, testSubstring, viewingTab) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000035 var filteredImagePairs = [];
36 for (var i = 0; i < unfilteredImagePairs.length; i++) {
37 var imagePair = unfilteredImagePairs[i];
38 var extraColumnValues = imagePair[constants.KEY__EXTRA_COLUMN_VALUES];
epoger@google.com055e3b52013-10-26 14:31:11 +000039 // For performance, we examine the "set" objects directly rather
40 // than calling $scope.isValueInSet().
41 // Besides, I don't think we have access to $scope in here...
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000042 if (!(true == hiddenResultTypes[extraColumnValues[
43 constants.KEY__EXTRACOLUMN__RESULT_TYPE]]) &&
44 !(true == hiddenConfigs[extraColumnValues[
45 constants.KEY__EXTRACOLUMN__CONFIG]]) &&
46 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMN__BUILDER]
47 .indexOf(builderSubstring)) &&
48 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMN__TEST]
49 .indexOf(testSubstring)) &&
50 (viewingTab == imagePair.tab)) {
51 filteredImagePairs.push(imagePair);
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000052 }
epoger@google.com5f2bb002013-10-02 18:57:48 +000053 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000054 return filteredImagePairs;
epoger@google.com5f2bb002013-10-02 18:57:48 +000055 };
56 }
57);
58
epoger@google.comad0e5522013-10-24 15:38:27 +000059
epoger@google.comf9d134d2013-09-27 15:02:44 +000060Loader.controller(
61 'Loader.Controller',
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +000062 function($scope, $http, $filter, $location, $log, $timeout, constants) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000063 $scope.constants = constants;
epoger@google.com542b65f2013-10-15 20:10:33 +000064 $scope.windowTitle = "Loading GM Results...";
epoger@google.com62a5ef02013-12-05 18:03:24 +000065 $scope.resultsToLoad = $location.search().resultsToLoad;
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000066 $scope.loadingMessage = "please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000067
epoger@google.comad0e5522013-10-24 15:38:27 +000068 /**
69 * On initial page load, load a full dictionary of results.
70 * Once the dictionary is loaded, unhide the page elements so they can
71 * render the data.
72 */
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000073 $http.get($scope.resultsToLoad).success(
epoger@google.comdcb4e652013-10-11 18:45:33 +000074 function(data, status, header, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000075 var dataHeader = data[constants.KEY__HEADER];
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000076 if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
77 constants.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER) {
78 $scope.loadingMessage = "ERROR: Got JSON file with schema version "
79 + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
80 + " but expected schema version "
81 + constants.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER;
82 } else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
commit-bot@chromium.orged191072014-03-10 18:05:15 +000083 // Apply the server's requested reload delay to local time,
84 // so we will wait the right number of seconds regardless of clock
85 // skew between client and server.
86 var reloadDelayInSeconds =
87 dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] -
88 dataHeader[constants.KEY__HEADER__TIME_UPDATED];
89 var timeNow = new Date().getTime();
90 var timeToReload = timeNow + reloadDelayInSeconds * 1000;
epoger@google.com2682c902013-12-05 16:05:16 +000091 $scope.loadingMessage =
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000092 "server is still loading results; will retry at " +
commit-bot@chromium.orged191072014-03-10 18:05:15 +000093 $scope.localTimeString(timeToReload / 1000);
epoger@google.com2682c902013-12-05 16:05:16 +000094 $timeout(
95 function(){location.reload();},
commit-bot@chromium.orged191072014-03-10 18:05:15 +000096 timeToReload - timeNow);
epoger@google.com2682c902013-12-05 16:05:16 +000097 } else {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +000098 $scope.loadingMessage = "processing data, please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000099
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000100 $scope.header = dataHeader;
101 $scope.extraColumnHeaders = data[constants.KEY__EXTRACOLUMNHEADERS];
102 $scope.imagePairs = data[constants.KEY__IMAGEPAIRS];
103 $scope.imageSets = data[constants.KEY__IMAGESETS];
104 $scope.sortColumnSubdict = constants.KEY__DIFFERENCE_DATA;
commit-bot@chromium.orge93a4c02014-04-11 21:45:41 +0000105 $scope.sortColumnKey = constants.KEY__DIFFERENCE_DATA__PERCEPTUAL_DIFF;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000106
epoger@google.com2682c902013-12-05 16:05:16 +0000107 $scope.showSubmitAdvancedSettings = false;
108 $scope.submitAdvancedSettings = {};
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000109 $scope.submitAdvancedSettings[
110 constants.KEY__EXPECTATIONS__REVIEWED] = true;
111 $scope.submitAdvancedSettings[
112 constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false;
epoger@google.com2682c902013-12-05 16:05:16 +0000113 $scope.submitAdvancedSettings['bug'] = '';
epoger@google.com055e3b52013-10-26 14:31:11 +0000114
epoger@google.com2682c902013-12-05 16:05:16 +0000115 // Create the list of tabs (lists into which the user can file each
116 // test). This may vary, depending on isEditable.
117 $scope.tabs = [
118 'Unfiled', 'Hidden'
119 ];
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000120 if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
epoger@google.com2682c902013-12-05 16:05:16 +0000121 $scope.tabs = $scope.tabs.concat(
122 ['Pending Approval']);
123 }
124 $scope.defaultTab = $scope.tabs[0];
125 $scope.viewingTab = $scope.defaultTab;
126
127 // Track the number of results on each tab.
128 $scope.numResultsPerTab = {};
129 for (var i = 0; i < $scope.tabs.length; i++) {
130 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
131 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000132 $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length;
epoger@google.com2682c902013-12-05 16:05:16 +0000133
134 // Add index and tab fields to all records.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000135 for (var i = 0; i < $scope.imagePairs.length; i++) {
136 $scope.imagePairs[i].index = i;
137 $scope.imagePairs[i].tab = $scope.defaultTab;
epoger@google.com2682c902013-12-05 16:05:16 +0000138 }
139
140 // Arrays within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000141 $scope.selectedImagePairs = [];
epoger@google.com2682c902013-12-05 16:05:16 +0000142
143 // Sets within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000144 $scope.hiddenResultTypes = {};
145 $scope.hiddenResultTypes[
146 constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true;
147 $scope.hiddenResultTypes[
148 constants.KEY__RESULT_TYPE__NOCOMPARISON] = true;
149 $scope.hiddenResultTypes[
150 constants.KEY__RESULT_TYPE__SUCCEEDED] = true;
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000151 $scope.allResultTypes = $scope.columnSliceOf2DArray(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000152 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__RESULT_TYPE]
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000153 [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
154 0);
epoger@google.com2682c902013-12-05 16:05:16 +0000155 $scope.hiddenConfigs = {};
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000156 $scope.allConfigs = $scope.columnSliceOf2DArray(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000157 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__CONFIG]
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000158 [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
159 0);
epoger@google.com2682c902013-12-05 16:05:16 +0000160
161 // Associative array of partial string matches per category.
162 $scope.categoryValueMatch = {};
163 $scope.categoryValueMatch.builder = "";
164 $scope.categoryValueMatch.test = "";
165
epoger@google.com62a5ef02013-12-05 18:03:24 +0000166 // If any defaults were overridden in the URL, get them now.
167 $scope.queryParameters.load();
168
commit-bot@chromium.orgb42105a2014-03-20 15:27:34 +0000169 // Any image URLs which are relative should be relative to the JSON
170 // file's source directory; absolute URLs should be left alone.
171 var baseUrlKey = constants.KEY__IMAGESETS__FIELD__BASE_URL;
172 angular.forEach(
173 $scope.imageSets,
174 function(imageSet) {
175 var baseUrl = imageSet[baseUrlKey];
176 if ((baseUrl.substring(0, 1) != '/') &&
177 (baseUrl.indexOf('://') == -1)) {
178 imageSet[baseUrlKey] = $scope.resultsToLoad + '/../' + baseUrl;
179 }
180 }
181 );
182
epoger@google.com2682c902013-12-05 16:05:16 +0000183 $scope.updateResults();
184 $scope.loadingMessage = "";
185 $scope.windowTitle = "Current GM Results";
epoger@google.comeb832592013-10-23 15:07:26 +0000186 }
epoger@google.comdcb4e652013-10-11 18:45:33 +0000187 }
188 ).error(
189 function(data, status, header, config) {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000190 $scope.loadingMessage = "FAILED to load.";
epoger@google.com542b65f2013-10-15 20:10:33 +0000191 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000192 }
193 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000194
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000195
epoger@google.comad0e5522013-10-24 15:38:27 +0000196 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000197 // Select/Clear/Toggle all tests.
198 //
199
200 /**
201 * Select all currently showing tests.
202 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000203 $scope.selectAllImagePairs = function() {
204 var numImagePairsShowing = $scope.limitedImagePairs.length;
205 for (var i = 0; i < numImagePairsShowing; i++) {
206 var index = $scope.limitedImagePairs[i].index;
207 if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) {
208 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000209 }
210 }
211 }
212
213 /**
214 * Deselect all currently showing tests.
215 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000216 $scope.clearAllImagePairs = function() {
217 var numImagePairsShowing = $scope.limitedImagePairs.length;
218 for (var i = 0; i < numImagePairsShowing; i++) {
219 var index = $scope.limitedImagePairs[i].index;
220 if ($scope.isValueInArray(index, $scope.selectedImagePairs)) {
221 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000222 }
223 }
224 }
225
226 /**
227 * Toggle selection of all currently showing tests.
228 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000229 $scope.toggleAllImagePairs = function() {
230 var numImagePairsShowing = $scope.limitedImagePairs.length;
231 for (var i = 0; i < numImagePairsShowing; i++) {
232 var index = $scope.limitedImagePairs[i].index;
233 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000234 }
235 }
236
237
238 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000239 // Tab operations.
240 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000241
epoger@google.comad0e5522013-10-24 15:38:27 +0000242 /**
243 * Change the selected tab.
244 *
245 * @param tab (string): name of the tab to select
246 */
epoger@google.comeb832592013-10-23 15:07:26 +0000247 $scope.setViewingTab = function(tab) {
248 $scope.viewingTab = tab;
249 $scope.updateResults();
250 }
251
epoger@google.comeb832592013-10-23 15:07:26 +0000252 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000253 * Move the imagePairs in $scope.selectedImagePairs to a different tab,
254 * and then clear $scope.selectedImagePairs.
epoger@google.comeb832592013-10-23 15:07:26 +0000255 *
256 * @param newTab (string): name of the tab to move the tests to
257 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000258 $scope.moveSelectedImagePairsToTab = function(newTab) {
259 $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
260 $scope.selectedImagePairs = [];
epoger@google.comeb832592013-10-23 15:07:26 +0000261 $scope.updateResults();
262 }
263
264 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000265 * Move a subset of $scope.imagePairs to a different tab.
epoger@google.comeb832592013-10-23 15:07:26 +0000266 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000267 * @param imagePairIndices (array of ints): indices into $scope.imagePairs
epoger@google.comeb832592013-10-23 15:07:26 +0000268 * indicating which test results to move
269 * @param newTab (string): name of the tab to move the tests to
270 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000271 $scope.moveImagePairsToTab = function(imagePairIndices, newTab) {
272 var imagePairIndex;
273 var numImagePairs = imagePairIndices.length;
274 for (var i = 0; i < numImagePairs; i++) {
275 imagePairIndex = imagePairIndices[i];
276 $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--;
277 $scope.imagePairs[imagePairIndex].tab = newTab;
epoger@google.comeb832592013-10-23 15:07:26 +0000278 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000279 $scope.numResultsPerTab[newTab] += numImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000280 }
281
epoger@google.comad0e5522013-10-24 15:38:27 +0000282
283 //
epoger@google.com62a5ef02013-12-05 18:03:24 +0000284 // $scope.queryParameters:
285 // Transfer parameter values between $scope and the URL query string.
286 //
287 $scope.queryParameters = {};
288
289 // load and save functions for parameters of each type
290 // (load a parameter value into $scope from nameValuePairs,
291 // save a parameter value from $scope into nameValuePairs)
292 $scope.queryParameters.copiers = {
293 'simple': {
294 'load': function(nameValuePairs, name) {
295 var value = nameValuePairs[name];
296 if (value) {
297 $scope[name] = value;
298 }
299 },
300 'save': function(nameValuePairs, name) {
301 nameValuePairs[name] = $scope[name];
302 }
303 },
304
305 'categoryValueMatch': {
306 'load': function(nameValuePairs, name) {
307 var value = nameValuePairs[name];
308 if (value) {
309 $scope.categoryValueMatch[name] = value;
310 }
311 },
312 'save': function(nameValuePairs, name) {
313 nameValuePairs[name] = $scope.categoryValueMatch[name];
314 }
315 },
316
317 'set': {
318 'load': function(nameValuePairs, name) {
319 var value = nameValuePairs[name];
320 if (value) {
321 var valueArray = value.split(',');
322 $scope[name] = {};
323 $scope.toggleValuesInSet(valueArray, $scope[name]);
324 }
325 },
326 'save': function(nameValuePairs, name) {
327 nameValuePairs[name] = Object.keys($scope[name]).join(',');
328 }
329 },
330
331 };
332
333 // parameter name -> copier objects to load/save parameter value
334 $scope.queryParameters.map = {
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000335 'resultsToLoad': $scope.queryParameters.copiers.simple,
336 'displayLimitPending': $scope.queryParameters.copiers.simple,
337 'showThumbnailsPending': $scope.queryParameters.copiers.simple,
338 'imageSizePending': $scope.queryParameters.copiers.simple,
339 'sortColumnSubdict': $scope.queryParameters.copiers.simple,
340 'sortColumnKey': $scope.queryParameters.copiers.simple,
epoger@google.com62a5ef02013-12-05 18:03:24 +0000341
342 'hiddenResultTypes': $scope.queryParameters.copiers.set,
343 'hiddenConfigs': $scope.queryParameters.copiers.set,
344 };
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000345 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__BUILDER] =
346 $scope.queryParameters.copiers.categoryValueMatch;
347 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__TEST] =
348 $scope.queryParameters.copiers.categoryValueMatch;
epoger@google.com62a5ef02013-12-05 18:03:24 +0000349
350 // Loads all parameters into $scope from the URL query string;
351 // any which are not found within the URL will keep their current value.
352 $scope.queryParameters.load = function() {
353 var nameValuePairs = $location.search();
354 angular.forEach($scope.queryParameters.map,
355 function(copier, paramName) {
356 copier.load(nameValuePairs, paramName);
357 }
358 );
359 };
360
361 // Saves all parameters from $scope into the URL query string.
362 $scope.queryParameters.save = function() {
363 var nameValuePairs = {};
364 angular.forEach($scope.queryParameters.map,
365 function(copier, paramName) {
366 copier.save(nameValuePairs, paramName);
367 }
368 );
369 $location.search(nameValuePairs);
370 };
371
372
373 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000374 // updateResults() and friends.
375 //
376
377 /**
378 * Set $scope.areUpdatesPending (to enable/disable the Update Results
379 * button).
380 *
381 * TODO(epoger): We could reduce the amount of code by just setting the
382 * variable directly (from, e.g., a button's ng-click handler). But when
383 * I tried that, the HTML elements depending on the variable did not get
384 * updated.
385 * It turns out that this is due to variable scoping within an ng-repeat
386 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
387 *
388 * @param val boolean value to set $scope.areUpdatesPending to
389 */
390 $scope.setUpdatesPending = function(val) {
391 $scope.areUpdatesPending = val;
392 }
393
394 /**
epoger@google.com62a5ef02013-12-05 18:03:24 +0000395 * Update the displayed results, based on filters/settings,
396 * and call $scope.queryParameters.save() so that the new filter results
397 * can be bookmarked.
epoger@google.comad0e5522013-10-24 15:38:27 +0000398 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000399 $scope.updateResults = function() {
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000400 $scope.renderStartTime = window.performance.now();
401 $log.debug("renderStartTime: " + $scope.renderStartTime);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000402 $scope.displayLimit = $scope.displayLimitPending;
403 // TODO(epoger): Every time we apply a filter, AngularJS creates
404 // another copy of the array. Is there a way we can filter out
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000405 // the imagePairs as they are displayed, rather than storing multiple
epoger@google.com5f2bb002013-10-02 18:57:48 +0000406 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000407
408 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000409
410 // TODO(epoger): Until we allow the user to reverse sort order,
411 // there are certain columns we want to sort in a different order.
412 var doReverse = (
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000413 ($scope.sortColumnKey ==
414 constants.KEY__DIFFERENCE_DATA__PERCENT_DIFF_PIXELS) ||
415 ($scope.sortColumnKey ==
commit-bot@chromium.orge93a4c02014-04-11 21:45:41 +0000416 constants.KEY__DIFFERENCE_DATA__PERCEPTUAL_DIFF));
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000417
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000418 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000419 $filter("orderBy")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000420 $filter("removeHiddenImagePairs")(
421 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000422 $scope.hiddenResultTypes,
423 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000424 $scope.categoryValueMatch.builder,
425 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000426 $scope.viewingTab
427 ),
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000428 $scope.getSortColumnValue, doReverse);
429 $scope.limitedImagePairs = $filter("limitTo")(
430 $scope.filteredImagePairs, $scope.displayLimit);
epoger@google.comeb832592013-10-23 15:07:26 +0000431 } else {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000432 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000433 $filter("orderBy")(
434 $filter("filter")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000435 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000436 {tab: $scope.viewingTab},
437 true
438 ),
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000439 $scope.getSortColumnValue);
440 $scope.limitedImagePairs = $scope.filteredImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000441 }
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000442 $scope.showThumbnails = $scope.showThumbnailsPending;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000443 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000444 $scope.setUpdatesPending(false);
epoger@google.com62a5ef02013-12-05 18:03:24 +0000445 $scope.queryParameters.save();
epoger@google.com5f2bb002013-10-02 18:57:48 +0000446 }
447
epoger@google.comad0e5522013-10-24 15:38:27 +0000448 /**
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000449 * This function is called when the results have been completely rendered
450 * after updateResults().
451 */
452 $scope.resultsUpdatedCallback = function() {
453 $scope.renderEndTime = window.performance.now();
454 $log.debug("renderEndTime: " + $scope.renderEndTime);
455 }
456
457 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000458 * Re-sort the displayed results.
459 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000460 * @param subdict (string): which subdictionary
461 * (constants.KEY__DIFFERENCE_DATA, constants.KEY__EXPECTATIONS_DATA,
462 * constants.KEY__EXTRA_COLUMN_VALUES) the sort column key is within
463 * @param key (string): sort by value associated with this key in subdict
epoger@google.comad0e5522013-10-24 15:38:27 +0000464 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000465 $scope.sortResultsBy = function(subdict, key) {
466 $scope.sortColumnSubdict = subdict;
467 $scope.sortColumnKey = key;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000468 $scope.updateResults();
469 }
epoger@google.comeb832592013-10-23 15:07:26 +0000470
epoger@google.comf4394d52013-10-29 15:49:40 +0000471 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000472 * For a particular ImagePair, return the value of the column we are
473 * sorting on (according to $scope.sortColumnSubdict and
474 * $scope.sortColumnKey).
475 *
476 * @param imagePair: imagePair to get a column value out of.
477 */
478 $scope.getSortColumnValue = function(imagePair) {
479 if ($scope.sortColumnSubdict in imagePair) {
480 return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey];
481 } else {
482 return undefined;
483 }
484 }
485
486 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000487 * Set $scope.categoryValueMatch[name] = value, and update results.
488 *
489 * @param name
490 * @param value
491 */
492 $scope.setCategoryValueMatch = function(name, value) {
493 $scope.categoryValueMatch[name] = value;
494 $scope.updateResults();
495 }
496
497 /**
498 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
499 * and update the visible results.
500 *
501 * @param resultType
502 */
503 $scope.showOnlyResultType = function(resultType) {
504 $scope.hiddenResultTypes = {};
505 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
506 // $scope.hiddenResultTypes (rather than an array), so this operation is
507 // simpler (just assign or add allResultTypes to hiddenResultTypes).
508 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
509 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
510 $scope.updateResults();
511 }
512
513 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000514 * Update $scope.hiddenResultTypes so that ALL resultTypes are showing,
515 * and update the visible results.
516 */
517 $scope.showAllResultTypes = function() {
518 $scope.hiddenResultTypes = {};
519 $scope.updateResults();
520 }
521
522 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000523 * Update $scope.hiddenConfigs so that ONLY this config is showing,
524 * and update the visible results.
525 *
526 * @param config
527 */
528 $scope.showOnlyConfig = function(config) {
529 $scope.hiddenConfigs = {};
530 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
531 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
532 $scope.updateResults();
533 }
534
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000535 /**
536 * Update $scope.hiddenConfigs so that ALL configs are showing,
537 * and update the visible results.
538 */
539 $scope.showAllConfigs = function() {
540 $scope.hiddenConfigs = {};
541 $scope.updateResults();
542 }
543
epoger@google.comad0e5522013-10-24 15:38:27 +0000544
545 //
546 // Operations for sending info back to the server.
547 //
548
epoger@google.comeb832592013-10-23 15:07:26 +0000549 /**
550 * Tell the server that the actual results of these particular tests
551 * are acceptable.
552 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000553 * TODO(epoger): This assumes that the original expectations are in
554 * imageSetA, and the actuals are in imageSetB.
555 *
556 * @param imagePairsSubset an array of test results, most likely a subset of
557 * $scope.imagePairs (perhaps with some modifications)
epoger@google.comeb832592013-10-23 15:07:26 +0000558 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000559 $scope.submitApprovals = function(imagePairsSubset) {
epoger@google.comeb832592013-10-23 15:07:26 +0000560 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000561
562 // Convert bug text field to null or 1-item array.
563 var bugs = null;
564 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
565 if (!isNaN(bugNumber)) {
566 bugs = [bugNumber];
567 }
568
569 // TODO(epoger): This is a suboptimal way to prevent users from
570 // rebaselining failures in alternative renderModes, but it does work.
571 // For a better solution, see
572 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
573 // result type, RenderModeMismatch')
574 var encounteredComparisonConfig = false;
575
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000576 var updatedExpectations = [];
577 for (var i = 0; i < imagePairsSubset.length; i++) {
578 var imagePair = imagePairsSubset[i];
579 var updatedExpectation = {};
580 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] =
581 imagePair[constants.KEY__EXPECTATIONS_DATA];
582 updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES] =
583 imagePair[constants.KEY__EXTRA_COLUMN_VALUES];
584 updatedExpectation[constants.KEY__NEW_IMAGE_URL] =
585 imagePair[constants.KEY__IMAGE_B_URL];
586 if (0 == updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES]
587 [constants.KEY__EXTRACOLUMN__CONFIG]
588 .indexOf('comparison-')) {
epoger@google.com055e3b52013-10-26 14:31:11 +0000589 encounteredComparisonConfig = true;
590 }
591
592 // Advanced settings...
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000593 if (null == updatedExpectation[constants.KEY__EXPECTATIONS_DATA]) {
594 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] = {};
epoger@google.com055e3b52013-10-26 14:31:11 +0000595 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000596 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
597 [constants.KEY__EXPECTATIONS__REVIEWED] =
598 $scope.submitAdvancedSettings[
599 constants.KEY__EXPECTATIONS__REVIEWED];
600 if (true == $scope.submitAdvancedSettings[
601 constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
602 // if it's false, don't send it at all (just keep the default)
603 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
604 [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
605 }
606 updatedExpectation[constants.KEY__EXPECTATIONS_DATA]
607 [constants.KEY__EXPECTATIONS__BUGS] = bugs;
epoger@google.com055e3b52013-10-26 14:31:11 +0000608
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000609 updatedExpectations.push(updatedExpectation);
epoger@google.comeb832592013-10-23 15:07:26 +0000610 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000611 if (encounteredComparisonConfig) {
612 alert("Approval failed -- you cannot approve results with config " +
613 "type comparison-*");
614 $scope.submitPending = false;
615 return;
616 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000617 var modificationData = {};
618 modificationData[constants.KEY__EDITS__MODIFICATIONS] =
619 updatedExpectations;
620 modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] =
621 $scope.header[constants.KEY__HEADER__DATAHASH];
622 modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] =
623 $scope.header[constants.KEY__HEADER__TYPE];
epoger@google.comeb832592013-10-23 15:07:26 +0000624 $http({
625 method: "POST",
626 url: "/edits",
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000627 data: modificationData
epoger@google.comeb832592013-10-23 15:07:26 +0000628 }).success(function(data, status, headers, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000629 var imagePairIndicesToMove = [];
630 for (var i = 0; i < imagePairsSubset.length; i++) {
631 imagePairIndicesToMove.push(imagePairsSubset[i].index);
epoger@google.comeb832592013-10-23 15:07:26 +0000632 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000633 $scope.moveImagePairsToTab(imagePairIndicesToMove,
634 "HackToMakeSureThisImagePairDisappears");
epoger@google.comeb832592013-10-23 15:07:26 +0000635 $scope.updateResults();
636 alert("New baselines submitted successfully!\n\n" +
637 "You still need to commit the updated expectations files on " +
638 "the server side to the Skia repo.\n\n" +
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +0000639 "When you click OK, your web UI will reload; after that " +
640 "completes, you will see the updated data (once the server has " +
641 "finished loading the update results into memory!) and you can " +
642 "submit more baselines if you want.");
643 // I don't know why, but if I just call reload() here it doesn't work.
644 // Making a timer call it fixes the problem.
645 $timeout(function(){location.reload();}, 1);
epoger@google.comeb832592013-10-23 15:07:26 +0000646 }).error(function(data, status, headers, config) {
647 alert("There was an error submitting your baselines.\n\n" +
648 "Please see server-side log for details.");
649 $scope.submitPending = false;
650 });
651 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000652
653
654 //
655 // Operations we use to mimic Set semantics, in such a way that
656 // checking for presence within the Set is as fast as possible.
657 // But getting a list of all values within the Set is not necessarily
658 // possible.
659 // TODO(epoger): move into a separate .js file?
660 //
661
662 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000663 * Returns the number of values present within set "set".
664 *
665 * @param set an Object which we use to mimic set semantics
666 */
667 $scope.setSize = function(set) {
668 return Object.keys(set).length;
669 }
670
671 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000672 * Returns true if value "value" is present within set "set".
673 *
674 * @param value a value of any type
675 * @param set an Object which we use to mimic set semantics
676 * (this should make isValueInSet faster than if we used an Array)
677 */
678 $scope.isValueInSet = function(value, set) {
679 return (true == set[value]);
680 }
681
682 /**
683 * If value "value" is already in set "set", remove it; otherwise, add it.
684 *
685 * @param value a value of any type
686 * @param set an Object which we use to mimic set semantics
687 */
688 $scope.toggleValueInSet = function(value, set) {
689 if (true == set[value]) {
690 delete set[value];
691 } else {
692 set[value] = true;
693 }
694 }
695
epoger@google.comf4394d52013-10-29 15:49:40 +0000696 /**
697 * For each value in valueArray, call toggleValueInSet(value, set).
698 *
699 * @param valueArray
700 * @param set
701 */
702 $scope.toggleValuesInSet = function(valueArray, set) {
703 var arrayLength = valueArray.length;
704 for (var i = 0; i < arrayLength; i++) {
705 $scope.toggleValueInSet(valueArray[i], set);
706 }
707 }
708
epoger@google.comad0e5522013-10-24 15:38:27 +0000709
710 //
711 // Array operations; similar to our Set operations, but operate on a
712 // Javascript Array so we *can* easily get a list of all values in the Set.
713 // TODO(epoger): move into a separate .js file?
714 //
715
716 /**
717 * Returns true if value "value" is present within array "array".
718 *
719 * @param value a value of any type
720 * @param array a Javascript Array
721 */
722 $scope.isValueInArray = function(value, array) {
723 return (-1 != array.indexOf(value));
724 }
725
726 /**
727 * If value "value" is already in array "array", remove it; otherwise,
728 * add it.
729 *
730 * @param value a value of any type
731 * @param array a Javascript Array
732 */
733 $scope.toggleValueInArray = function(value, array) {
734 var i = array.indexOf(value);
735 if (-1 == i) {
736 array.push(value);
737 } else {
738 array.splice(i, 1);
739 }
740 }
741
742
743 //
744 // Miscellaneous utility functions.
745 // TODO(epoger): move into a separate .js file?
746 //
747
748 /**
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000749 * Returns a single "column slice" of a 2D array.
750 *
751 * For example, if array is:
752 * [[A0, A1],
753 * [B0, B1],
754 * [C0, C1]]
755 * and index is 0, this this will return:
756 * [A0, B0, C0]
757 *
758 * @param array a Javascript Array
759 * @param column (numeric): index within each row array
760 */
761 $scope.columnSliceOf2DArray = function(array, column) {
762 var slice = [];
763 var numRows = array.length;
764 for (var row = 0; row < numRows; row++) {
765 slice.push(array[row][column]);
766 }
767 return slice;
768 }
769
770 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000771 * Returns a human-readable (in local time zone) time string for a
772 * particular moment in time.
773 *
774 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
775 */
776 $scope.localTimeString = function(secondsPastEpoch) {
777 var d = new Date(secondsPastEpoch * 1000);
778 return d.toString();
779 }
780
commit-bot@chromium.orgceba0792014-02-05 19:49:17 +0000781 /**
782 * Returns a hex color string (such as "#aabbcc") for the given RGB values.
783 *
784 * @param r (numeric): red channel value, 0-255
785 * @param g (numeric): green channel value, 0-255
786 * @param b (numeric): blue channel value, 0-255
787 */
788 $scope.hexColorString = function(r, g, b) {
789 var rString = r.toString(16);
790 if (r < 16) {
791 rString = "0" + rString;
792 }
793 var gString = g.toString(16);
794 if (g < 16) {
795 gString = "0" + gString;
796 }
797 var bString = b.toString(16);
798 if (b < 16) {
799 bString = "0" + bString;
800 }
801 return '#' + rString + gString + bString;
802 }
803
804 /**
805 * Returns a hex color string (such as "#aabbcc") for the given brightness.
806 *
807 * @param brightnessString (string): 0-255, 0 is completely black
808 *
809 * TODO(epoger): It might be nice to tint the color when it's not completely
810 * black or completely white.
811 */
812 $scope.brightnessStringToHexColor = function(brightnessString) {
813 var v = parseInt(brightnessString);
814 return $scope.hexColorString(v, v, v);
815 }
816
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000817 /**
818 * Returns the last path component of image diff URL for a given ImagePair.
819 *
820 * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this
821 * will be relative to different base URLs.
822 *
823 * We must keep this function in sync with _get_difference_locator() in
824 * ../imagediffdb.py
825 *
826 * @param imagePair: ImagePair to generate image diff URL for
827 */
828 $scope.getImageDiffRelativeUrl = function(imagePair) {
829 var before =
830 imagePair[constants.KEY__IMAGE_A_URL] + "-vs-" +
831 imagePair[constants.KEY__IMAGE_B_URL];
832 return before.replace(/[^\w\-]/g, "_") + ".png";
833 }
834
epoger@google.comf9d134d2013-09-27 15:02:44 +0000835 }
836);