blob: dbded05f2500684664a2653d37368f715d3b0df0 [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];
commit-bot@chromium.org68a38152014-05-12 20:40:29 +000038 var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
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[
commit-bot@chromium.org68a38152014-05-12 20:40:29 +000043 constants.KEY__EXTRACOLUMNS__RESULT_TYPE]]) &&
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000044 !(true == hiddenConfigs[extraColumnValues[
commit-bot@chromium.org68a38152014-05-12 20:40:29 +000045 constants.KEY__EXTRACOLUMNS__CONFIG]]) &&
46 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__BUILDER]
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000047 .indexOf(builderSubstring)) &&
commit-bot@chromium.org68a38152014-05-12 20:40:29 +000048 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__TEST]
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000049 .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
epoger6b281202014-06-11 14:01:35 -070059/**
60 * Limit the input imagePairs to some max number, and merge identical rows
61 * (adjacent rows which have the same (imageA, imageB) pair).
62 *
63 * @param unfilteredImagePairs imagePairs to filter
64 * @param maxPairs maximum number of pairs to output, or <0 for no limit
65 * @param mergeIdenticalRows if true, merge identical rows by setting
66 * ROWSPAN>1 on the first merged row, and ROWSPAN=0 for the rest
67 */
68Loader.filter(
69 'mergeAndLimit',
70 function(constants) {
71 return function(unfilteredImagePairs, maxPairs, mergeIdenticalRows) {
72 var numPairs = unfilteredImagePairs.length;
73 if ((maxPairs > 0) && (maxPairs < numPairs)) {
74 numPairs = maxPairs;
75 }
76 var filteredImagePairs = [];
77 if (!mergeIdenticalRows || (numPairs == 1)) {
78 // Take a shortcut if we're not merging identical rows.
79 // We still need to set ROWSPAN to 1 for each row, for the HTML viewer.
80 for (var i = numPairs-1; i >= 0; i--) {
81 var imagePair = unfilteredImagePairs[i];
82 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
83 filteredImagePairs[i] = imagePair;
84 }
85 } else if (numPairs > 1) {
86 // General case--there are at least 2 rows, so we may need to merge some.
87 // Work from the bottom up, so we can keep a running total of how many
88 // rows should be merged, and set ROWSPAN of the top row accordingly.
89 var imagePair = unfilteredImagePairs[numPairs-1];
90 var nextRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL];
91 var nextRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
92 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
93 filteredImagePairs[numPairs-1] = imagePair;
94 for (var i = numPairs-2; i >= 0; i--) {
95 imagePair = unfilteredImagePairs[i];
96 var thisRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL];
97 var thisRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
98 if ((thisRowImageAUrl == nextRowImageAUrl) &&
99 (thisRowImageBUrl == nextRowImageBUrl)) {
100 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] =
101 filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] + 1;
102 filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] = 0;
103 } else {
104 imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
105 nextRowImageAUrl = thisRowImageAUrl;
106 nextRowImageBUrl = thisRowImageBUrl;
107 }
108 filteredImagePairs[i] = imagePair;
109 }
110 } else {
111 // No results.
112 }
113 return filteredImagePairs;
114 };
115 }
116);
117
epoger@google.comad0e5522013-10-24 15:38:27 +0000118
epoger@google.comf9d134d2013-09-27 15:02:44 +0000119Loader.controller(
120 'Loader.Controller',
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000121 function($scope, $http, $filter, $location, $log, $timeout, constants) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000122 $scope.constants = constants;
epoger@google.com542b65f2013-10-15 20:10:33 +0000123 $scope.windowTitle = "Loading GM Results...";
epoger@google.com62a5ef02013-12-05 18:03:24 +0000124 $scope.resultsToLoad = $location.search().resultsToLoad;
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000125 $scope.loadingMessage = "please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +0000126
epoger@google.comad0e5522013-10-24 15:38:27 +0000127 /**
128 * On initial page load, load a full dictionary of results.
129 * Once the dictionary is loaded, unhide the page elements so they can
130 * render the data.
131 */
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000132 $http.get($scope.resultsToLoad).success(
epoger@google.comdcb4e652013-10-11 18:45:33 +0000133 function(data, status, header, config) {
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000134 var dataHeader = data[constants.KEY__ROOT__HEADER];
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000135 if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000136 constants.VALUE__HEADER__SCHEMA_VERSION) {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000137 $scope.loadingMessage = "ERROR: Got JSON file with schema version "
138 + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
139 + " but expected schema version "
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000140 + constants.VALUE__HEADER__SCHEMA_VERSION;
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000141 } else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
commit-bot@chromium.orged191072014-03-10 18:05:15 +0000142 // Apply the server's requested reload delay to local time,
143 // so we will wait the right number of seconds regardless of clock
144 // skew between client and server.
145 var reloadDelayInSeconds =
146 dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] -
147 dataHeader[constants.KEY__HEADER__TIME_UPDATED];
148 var timeNow = new Date().getTime();
149 var timeToReload = timeNow + reloadDelayInSeconds * 1000;
epoger@google.com2682c902013-12-05 16:05:16 +0000150 $scope.loadingMessage =
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000151 "server is still loading results; will retry at " +
commit-bot@chromium.orged191072014-03-10 18:05:15 +0000152 $scope.localTimeString(timeToReload / 1000);
epoger@google.com2682c902013-12-05 16:05:16 +0000153 $timeout(
154 function(){location.reload();},
commit-bot@chromium.orged191072014-03-10 18:05:15 +0000155 timeToReload - timeNow);
epoger@google.com2682c902013-12-05 16:05:16 +0000156 } else {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000157 $scope.loadingMessage = "processing data, please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +0000158
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000159 $scope.header = dataHeader;
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000160 $scope.extraColumnHeaders = data[constants.KEY__ROOT__EXTRACOLUMNHEADERS];
161 $scope.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS];
162 $scope.imageSets = data[constants.KEY__ROOT__IMAGESETS];
163 $scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES;
164 $scope.sortColumnKey = constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000165
epoger@google.com2682c902013-12-05 16:05:16 +0000166 $scope.showSubmitAdvancedSettings = false;
167 $scope.submitAdvancedSettings = {};
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000168 $scope.submitAdvancedSettings[
169 constants.KEY__EXPECTATIONS__REVIEWED] = true;
170 $scope.submitAdvancedSettings[
171 constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false;
epoger@google.com2682c902013-12-05 16:05:16 +0000172 $scope.submitAdvancedSettings['bug'] = '';
epoger@google.com055e3b52013-10-26 14:31:11 +0000173
epoger@google.com2682c902013-12-05 16:05:16 +0000174 // Create the list of tabs (lists into which the user can file each
175 // test). This may vary, depending on isEditable.
176 $scope.tabs = [
177 'Unfiled', 'Hidden'
178 ];
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000179 if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
epoger@google.com2682c902013-12-05 16:05:16 +0000180 $scope.tabs = $scope.tabs.concat(
181 ['Pending Approval']);
182 }
183 $scope.defaultTab = $scope.tabs[0];
184 $scope.viewingTab = $scope.defaultTab;
185
186 // Track the number of results on each tab.
187 $scope.numResultsPerTab = {};
188 for (var i = 0; i < $scope.tabs.length; i++) {
189 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
190 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000191 $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length;
epoger@google.com2682c902013-12-05 16:05:16 +0000192
193 // Add index and tab fields to all records.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000194 for (var i = 0; i < $scope.imagePairs.length; i++) {
195 $scope.imagePairs[i].index = i;
196 $scope.imagePairs[i].tab = $scope.defaultTab;
epoger@google.com2682c902013-12-05 16:05:16 +0000197 }
198
199 // Arrays within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000200 $scope.selectedImagePairs = [];
epoger@google.com2682c902013-12-05 16:05:16 +0000201
202 // Sets within which the user can toggle individual elements.
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000203 $scope.hiddenResultTypes = {};
204 $scope.hiddenResultTypes[
205 constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true;
206 $scope.hiddenResultTypes[
207 constants.KEY__RESULT_TYPE__NOCOMPARISON] = true;
208 $scope.hiddenResultTypes[
209 constants.KEY__RESULT_TYPE__SUCCEEDED] = true;
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000210 $scope.allResultTypes = $scope.columnSliceOf2DArray(
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000211 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000212 [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
213 0);
epoger@google.com2682c902013-12-05 16:05:16 +0000214 $scope.hiddenConfigs = {};
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000215 $scope.allConfigs = $scope.columnSliceOf2DArray(
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000216 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__CONFIG]
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000217 [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
218 0);
epoger@google.com2682c902013-12-05 16:05:16 +0000219
220 // Associative array of partial string matches per category.
221 $scope.categoryValueMatch = {};
222 $scope.categoryValueMatch.builder = "";
223 $scope.categoryValueMatch.test = "";
224
epoger@google.com62a5ef02013-12-05 18:03:24 +0000225 // If any defaults were overridden in the URL, get them now.
226 $scope.queryParameters.load();
227
commit-bot@chromium.orgb42105a2014-03-20 15:27:34 +0000228 // Any image URLs which are relative should be relative to the JSON
229 // file's source directory; absolute URLs should be left alone.
230 var baseUrlKey = constants.KEY__IMAGESETS__FIELD__BASE_URL;
231 angular.forEach(
232 $scope.imageSets,
233 function(imageSet) {
234 var baseUrl = imageSet[baseUrlKey];
235 if ((baseUrl.substring(0, 1) != '/') &&
236 (baseUrl.indexOf('://') == -1)) {
237 imageSet[baseUrlKey] = $scope.resultsToLoad + '/../' + baseUrl;
238 }
239 }
240 );
241
epoger@google.com2682c902013-12-05 16:05:16 +0000242 $scope.updateResults();
243 $scope.loadingMessage = "";
244 $scope.windowTitle = "Current GM Results";
epoger@google.comeb832592013-10-23 15:07:26 +0000245 }
epoger@google.comdcb4e652013-10-11 18:45:33 +0000246 }
247 ).error(
248 function(data, status, header, config) {
commit-bot@chromium.orgd1c85d22014-03-17 14:22:02 +0000249 $scope.loadingMessage = "FAILED to load.";
epoger@google.com542b65f2013-10-15 20:10:33 +0000250 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000251 }
252 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000253
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000254
epoger@google.comad0e5522013-10-24 15:38:27 +0000255 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000256 // Select/Clear/Toggle all tests.
257 //
258
259 /**
260 * Select all currently showing tests.
261 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000262 $scope.selectAllImagePairs = function() {
263 var numImagePairsShowing = $scope.limitedImagePairs.length;
264 for (var i = 0; i < numImagePairsShowing; i++) {
265 var index = $scope.limitedImagePairs[i].index;
266 if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) {
267 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000268 }
269 }
270 }
271
272 /**
273 * Deselect all currently showing tests.
274 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000275 $scope.clearAllImagePairs = function() {
276 var numImagePairsShowing = $scope.limitedImagePairs.length;
277 for (var i = 0; i < numImagePairsShowing; i++) {
278 var index = $scope.limitedImagePairs[i].index;
279 if ($scope.isValueInArray(index, $scope.selectedImagePairs)) {
280 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000281 }
282 }
283 }
284
285 /**
286 * Toggle selection of all currently showing tests.
287 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000288 $scope.toggleAllImagePairs = function() {
289 var numImagePairsShowing = $scope.limitedImagePairs.length;
290 for (var i = 0; i < numImagePairsShowing; i++) {
291 var index = $scope.limitedImagePairs[i].index;
292 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
epoger@google.com055e3b52013-10-26 14:31:11 +0000293 }
294 }
295
epoger6b281202014-06-11 14:01:35 -0700296 /**
297 * Toggle selection state of a subset of the currently showing tests.
298 *
299 * @param startIndex index within $scope.limitedImagePairs of the first
300 * test to toggle selection state of
301 * @param num number of tests (in a contiguous block) to toggle
302 */
303 $scope.toggleSomeImagePairs = function(startIndex, num) {
304 var numImagePairsShowing = $scope.limitedImagePairs.length;
305 for (var i = startIndex; i < startIndex + num; i++) {
306 var index = $scope.limitedImagePairs[i].index;
307 $scope.toggleValueInArray(index, $scope.selectedImagePairs);
308 }
309 }
310
epoger@google.com055e3b52013-10-26 14:31:11 +0000311
312 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000313 // Tab operations.
314 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000315
epoger@google.comad0e5522013-10-24 15:38:27 +0000316 /**
317 * Change the selected tab.
318 *
319 * @param tab (string): name of the tab to select
320 */
epoger@google.comeb832592013-10-23 15:07:26 +0000321 $scope.setViewingTab = function(tab) {
322 $scope.viewingTab = tab;
323 $scope.updateResults();
324 }
325
epoger@google.comeb832592013-10-23 15:07:26 +0000326 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000327 * Move the imagePairs in $scope.selectedImagePairs to a different tab,
328 * and then clear $scope.selectedImagePairs.
epoger@google.comeb832592013-10-23 15:07:26 +0000329 *
330 * @param newTab (string): name of the tab to move the tests to
331 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000332 $scope.moveSelectedImagePairsToTab = function(newTab) {
333 $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
334 $scope.selectedImagePairs = [];
epoger@google.comeb832592013-10-23 15:07:26 +0000335 $scope.updateResults();
336 }
337
338 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000339 * Move a subset of $scope.imagePairs to a different tab.
epoger@google.comeb832592013-10-23 15:07:26 +0000340 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000341 * @param imagePairIndices (array of ints): indices into $scope.imagePairs
epoger@google.comeb832592013-10-23 15:07:26 +0000342 * indicating which test results to move
343 * @param newTab (string): name of the tab to move the tests to
344 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000345 $scope.moveImagePairsToTab = function(imagePairIndices, newTab) {
346 var imagePairIndex;
347 var numImagePairs = imagePairIndices.length;
348 for (var i = 0; i < numImagePairs; i++) {
349 imagePairIndex = imagePairIndices[i];
350 $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--;
351 $scope.imagePairs[imagePairIndex].tab = newTab;
epoger@google.comeb832592013-10-23 15:07:26 +0000352 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000353 $scope.numResultsPerTab[newTab] += numImagePairs;
epoger@google.comeb832592013-10-23 15:07:26 +0000354 }
355
epoger@google.comad0e5522013-10-24 15:38:27 +0000356
357 //
epoger@google.com62a5ef02013-12-05 18:03:24 +0000358 // $scope.queryParameters:
359 // Transfer parameter values between $scope and the URL query string.
360 //
361 $scope.queryParameters = {};
362
363 // load and save functions for parameters of each type
364 // (load a parameter value into $scope from nameValuePairs,
365 // save a parameter value from $scope into nameValuePairs)
366 $scope.queryParameters.copiers = {
367 'simple': {
368 'load': function(nameValuePairs, name) {
369 var value = nameValuePairs[name];
370 if (value) {
371 $scope[name] = value;
372 }
373 },
374 'save': function(nameValuePairs, name) {
375 nameValuePairs[name] = $scope[name];
376 }
377 },
378
379 'categoryValueMatch': {
380 'load': function(nameValuePairs, name) {
381 var value = nameValuePairs[name];
382 if (value) {
383 $scope.categoryValueMatch[name] = value;
384 }
385 },
386 'save': function(nameValuePairs, name) {
387 nameValuePairs[name] = $scope.categoryValueMatch[name];
388 }
389 },
390
391 'set': {
392 'load': function(nameValuePairs, name) {
393 var value = nameValuePairs[name];
394 if (value) {
395 var valueArray = value.split(',');
396 $scope[name] = {};
397 $scope.toggleValuesInSet(valueArray, $scope[name]);
398 }
399 },
400 'save': function(nameValuePairs, name) {
401 nameValuePairs[name] = Object.keys($scope[name]).join(',');
402 }
403 },
404
405 };
406
407 // parameter name -> copier objects to load/save parameter value
408 $scope.queryParameters.map = {
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000409 'resultsToLoad': $scope.queryParameters.copiers.simple,
410 'displayLimitPending': $scope.queryParameters.copiers.simple,
411 'showThumbnailsPending': $scope.queryParameters.copiers.simple,
epoger6b281202014-06-11 14:01:35 -0700412 'mergeIdenticalRowsPending': $scope.queryParameters.copiers.simple,
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000413 'imageSizePending': $scope.queryParameters.copiers.simple,
414 'sortColumnSubdict': $scope.queryParameters.copiers.simple,
415 'sortColumnKey': $scope.queryParameters.copiers.simple,
epoger@google.com62a5ef02013-12-05 18:03:24 +0000416
417 'hiddenResultTypes': $scope.queryParameters.copiers.set,
418 'hiddenConfigs': $scope.queryParameters.copiers.set,
419 };
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000420 $scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__BUILDER] =
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000421 $scope.queryParameters.copiers.categoryValueMatch;
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000422 $scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__TEST] =
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000423 $scope.queryParameters.copiers.categoryValueMatch;
epoger@google.com62a5ef02013-12-05 18:03:24 +0000424
425 // Loads all parameters into $scope from the URL query string;
426 // any which are not found within the URL will keep their current value.
427 $scope.queryParameters.load = function() {
428 var nameValuePairs = $location.search();
epogere5481ec2014-07-02 13:51:47 -0700429
430 var urlSchemaVersion = nameValuePairs[constants.URL_KEY__SCHEMA_VERSION];
431 if (!urlSchemaVersion) {
432 $scope.urlSchemaVersionLoaded = 0;
433 } else if (urlSchemaVersion == constants.URL_VALUE__SCHEMA_VERSION__ALWAYS_CURRENT) {
434 $scope.urlSchemaVersionLoaded = constants.URL_VALUE__SCHEMA_VERSION__CURRENT;
435 } else {
436 $scope.urlSchemaVersionLoaded = urlSchemaVersion;
437 }
438
epoger@google.com62a5ef02013-12-05 18:03:24 +0000439 angular.forEach($scope.queryParameters.map,
440 function(copier, paramName) {
441 copier.load(nameValuePairs, paramName);
442 }
443 );
444 };
445
446 // Saves all parameters from $scope into the URL query string.
447 $scope.queryParameters.save = function() {
448 var nameValuePairs = {};
epogere5481ec2014-07-02 13:51:47 -0700449 nameValuePairs[constants.URL_KEY__SCHEMA_VERSION] = constants.URL_VALUE__SCHEMA_VERSION__CURRENT;
epoger@google.com62a5ef02013-12-05 18:03:24 +0000450 angular.forEach($scope.queryParameters.map,
451 function(copier, paramName) {
452 copier.save(nameValuePairs, paramName);
453 }
454 );
455 $location.search(nameValuePairs);
456 };
457
458
459 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000460 // updateResults() and friends.
461 //
462
463 /**
464 * Set $scope.areUpdatesPending (to enable/disable the Update Results
465 * button).
466 *
467 * TODO(epoger): We could reduce the amount of code by just setting the
468 * variable directly (from, e.g., a button's ng-click handler). But when
469 * I tried that, the HTML elements depending on the variable did not get
470 * updated.
471 * It turns out that this is due to variable scoping within an ng-repeat
472 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
473 *
474 * @param val boolean value to set $scope.areUpdatesPending to
475 */
476 $scope.setUpdatesPending = function(val) {
477 $scope.areUpdatesPending = val;
478 }
479
480 /**
epoger@google.com62a5ef02013-12-05 18:03:24 +0000481 * Update the displayed results, based on filters/settings,
482 * and call $scope.queryParameters.save() so that the new filter results
483 * can be bookmarked.
epoger@google.comad0e5522013-10-24 15:38:27 +0000484 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000485 $scope.updateResults = function() {
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000486 $scope.renderStartTime = window.performance.now();
487 $log.debug("renderStartTime: " + $scope.renderStartTime);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000488 $scope.displayLimit = $scope.displayLimitPending;
epoger6b281202014-06-11 14:01:35 -0700489 $scope.mergeIdenticalRows = $scope.mergeIdenticalRowsPending;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000490 // TODO(epoger): Every time we apply a filter, AngularJS creates
491 // another copy of the array. Is there a way we can filter out
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000492 // the imagePairs as they are displayed, rather than storing multiple
epoger@google.com5f2bb002013-10-02 18:57:48 +0000493 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000494
495 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000496
497 // TODO(epoger): Until we allow the user to reverse sort order,
498 // there are certain columns we want to sort in a different order.
499 var doReverse = (
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000500 ($scope.sortColumnKey ==
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000501 constants.KEY__DIFFERENCES__PERCENT_DIFF_PIXELS) ||
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000502 ($scope.sortColumnKey ==
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000503 constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF));
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000504
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000505 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000506 $filter("orderBy")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000507 $filter("removeHiddenImagePairs")(
508 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000509 $scope.hiddenResultTypes,
510 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000511 $scope.categoryValueMatch.builder,
512 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000513 $scope.viewingTab
514 ),
epoger6b281202014-06-11 14:01:35 -0700515 [$scope.getSortColumnValue, $scope.getSecondOrderSortValue],
516 doReverse);
517 $scope.limitedImagePairs = $filter("mergeAndLimit")(
518 $scope.filteredImagePairs, $scope.displayLimit, $scope.mergeIdenticalRows);
epoger@google.comeb832592013-10-23 15:07:26 +0000519 } else {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000520 $scope.filteredImagePairs =
epoger@google.comeb832592013-10-23 15:07:26 +0000521 $filter("orderBy")(
522 $filter("filter")(
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000523 $scope.imagePairs,
epoger@google.comeb832592013-10-23 15:07:26 +0000524 {tab: $scope.viewingTab},
525 true
526 ),
epoger6b281202014-06-11 14:01:35 -0700527 [$scope.getSortColumnValue, $scope.getSecondOrderSortValue]);
528 $scope.limitedImagePairs = $filter("mergeAndLimit")(
529 $scope.filteredImagePairs, -1, $scope.mergeIdenticalRows);
epoger@google.comeb832592013-10-23 15:07:26 +0000530 }
commit-bot@chromium.org61f16062014-03-05 19:46:17 +0000531 $scope.showThumbnails = $scope.showThumbnailsPending;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000532 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000533 $scope.setUpdatesPending(false);
epoger@google.com62a5ef02013-12-05 18:03:24 +0000534 $scope.queryParameters.save();
epoger@google.com5f2bb002013-10-02 18:57:48 +0000535 }
536
epoger@google.comad0e5522013-10-24 15:38:27 +0000537 /**
commit-bot@chromium.orgfadc7492014-03-10 15:36:06 +0000538 * This function is called when the results have been completely rendered
539 * after updateResults().
540 */
541 $scope.resultsUpdatedCallback = function() {
542 $scope.renderEndTime = window.performance.now();
543 $log.debug("renderEndTime: " + $scope.renderEndTime);
544 }
545
546 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000547 * Re-sort the displayed results.
548 *
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000549 * @param subdict (string): which KEY__IMAGEPAIRS__* subdictionary
epoger6b281202014-06-11 14:01:35 -0700550 * the sort column key is within, or 'none' if the sort column
551 * key is one of KEY__IMAGEPAIRS__*
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000552 * @param key (string): sort by value associated with this key in subdict
epoger@google.comad0e5522013-10-24 15:38:27 +0000553 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000554 $scope.sortResultsBy = function(subdict, key) {
555 $scope.sortColumnSubdict = subdict;
556 $scope.sortColumnKey = key;
epoger@google.com5f2bb002013-10-02 18:57:48 +0000557 $scope.updateResults();
558 }
epoger@google.comeb832592013-10-23 15:07:26 +0000559
epoger@google.comf4394d52013-10-29 15:49:40 +0000560 /**
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000561 * For a particular ImagePair, return the value of the column we are
562 * sorting on (according to $scope.sortColumnSubdict and
563 * $scope.sortColumnKey).
564 *
565 * @param imagePair: imagePair to get a column value out of.
566 */
567 $scope.getSortColumnValue = function(imagePair) {
568 if ($scope.sortColumnSubdict in imagePair) {
569 return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey];
epoger6b281202014-06-11 14:01:35 -0700570 } else if ($scope.sortColumnKey in imagePair) {
571 return imagePair[$scope.sortColumnKey];
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000572 } else {
573 return undefined;
574 }
575 }
576
577 /**
epoger6b281202014-06-11 14:01:35 -0700578 * For a particular ImagePair, return the value we use for the
579 * second-order sort (tiebreaker when multiple rows have
580 * the same getSortColumnValue()).
581 *
582 * We join the imageA and imageB urls for this value, so that we merge
583 * adjacent rows as much as possible.
584 *
585 * @param imagePair: imagePair to get a column value out of.
586 */
587 $scope.getSecondOrderSortValue = function(imagePair) {
588 return imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" +
589 imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
590 }
591
592 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000593 * Set $scope.categoryValueMatch[name] = value, and update results.
594 *
595 * @param name
596 * @param value
597 */
598 $scope.setCategoryValueMatch = function(name, value) {
599 $scope.categoryValueMatch[name] = value;
600 $scope.updateResults();
601 }
602
603 /**
604 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
605 * and update the visible results.
606 *
607 * @param resultType
608 */
609 $scope.showOnlyResultType = function(resultType) {
610 $scope.hiddenResultTypes = {};
611 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
612 // $scope.hiddenResultTypes (rather than an array), so this operation is
613 // simpler (just assign or add allResultTypes to hiddenResultTypes).
614 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
615 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
616 $scope.updateResults();
617 }
618
619 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000620 * Update $scope.hiddenResultTypes so that ALL resultTypes are showing,
621 * and update the visible results.
622 */
623 $scope.showAllResultTypes = function() {
624 $scope.hiddenResultTypes = {};
625 $scope.updateResults();
626 }
627
628 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000629 * Update $scope.hiddenConfigs so that ONLY this config is showing,
630 * and update the visible results.
631 *
632 * @param config
633 */
634 $scope.showOnlyConfig = function(config) {
635 $scope.hiddenConfigs = {};
636 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
637 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
638 $scope.updateResults();
639 }
640
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000641 /**
642 * Update $scope.hiddenConfigs so that ALL configs are showing,
643 * and update the visible results.
644 */
645 $scope.showAllConfigs = function() {
646 $scope.hiddenConfigs = {};
647 $scope.updateResults();
648 }
649
epoger@google.comad0e5522013-10-24 15:38:27 +0000650
651 //
652 // Operations for sending info back to the server.
653 //
654
epoger@google.comeb832592013-10-23 15:07:26 +0000655 /**
656 * Tell the server that the actual results of these particular tests
657 * are acceptable.
658 *
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000659 * TODO(epoger): This assumes that the original expectations are in
660 * imageSetA, and the actuals are in imageSetB.
661 *
662 * @param imagePairsSubset an array of test results, most likely a subset of
663 * $scope.imagePairs (perhaps with some modifications)
epoger@google.comeb832592013-10-23 15:07:26 +0000664 */
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000665 $scope.submitApprovals = function(imagePairsSubset) {
epoger@google.comeb832592013-10-23 15:07:26 +0000666 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000667
668 // Convert bug text field to null or 1-item array.
669 var bugs = null;
670 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
671 if (!isNaN(bugNumber)) {
672 bugs = [bugNumber];
673 }
674
675 // TODO(epoger): This is a suboptimal way to prevent users from
676 // rebaselining failures in alternative renderModes, but it does work.
677 // For a better solution, see
678 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
679 // result type, RenderModeMismatch')
680 var encounteredComparisonConfig = false;
681
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000682 var updatedExpectations = [];
683 for (var i = 0; i < imagePairsSubset.length; i++) {
684 var imagePair = imagePairsSubset[i];
685 var updatedExpectation = {};
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000686 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] =
687 imagePair[constants.KEY__IMAGEPAIRS__EXPECTATIONS];
688 updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS] =
689 imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
690 // IMAGE_B_URL contains the actual image (which is now the expectation)
691 updatedExpectation[constants.KEY__IMAGEPAIRS__IMAGE_B_URL] =
692 imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
693 if (0 == updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS]
694 [constants.KEY__EXTRACOLUMNS__CONFIG]
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000695 .indexOf('comparison-')) {
epoger@google.com055e3b52013-10-26 14:31:11 +0000696 encounteredComparisonConfig = true;
697 }
698
699 // Advanced settings...
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000700 if (null == updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]) {
701 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] = {};
epoger@google.com055e3b52013-10-26 14:31:11 +0000702 }
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000703 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000704 [constants.KEY__EXPECTATIONS__REVIEWED] =
705 $scope.submitAdvancedSettings[
706 constants.KEY__EXPECTATIONS__REVIEWED];
707 if (true == $scope.submitAdvancedSettings[
708 constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
709 // if it's false, don't send it at all (just keep the default)
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000710 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000711 [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
712 }
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000713 updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000714 [constants.KEY__EXPECTATIONS__BUGS] = bugs;
epoger@google.com055e3b52013-10-26 14:31:11 +0000715
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000716 updatedExpectations.push(updatedExpectation);
epoger@google.comeb832592013-10-23 15:07:26 +0000717 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000718 if (encounteredComparisonConfig) {
719 alert("Approval failed -- you cannot approve results with config " +
720 "type comparison-*");
721 $scope.submitPending = false;
722 return;
723 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000724 var modificationData = {};
725 modificationData[constants.KEY__EDITS__MODIFICATIONS] =
726 updatedExpectations;
727 modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] =
728 $scope.header[constants.KEY__HEADER__DATAHASH];
729 modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] =
730 $scope.header[constants.KEY__HEADER__TYPE];
epoger@google.comeb832592013-10-23 15:07:26 +0000731 $http({
732 method: "POST",
733 url: "/edits",
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000734 data: modificationData
epoger@google.comeb832592013-10-23 15:07:26 +0000735 }).success(function(data, status, headers, config) {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000736 var imagePairIndicesToMove = [];
737 for (var i = 0; i < imagePairsSubset.length; i++) {
738 imagePairIndicesToMove.push(imagePairsSubset[i].index);
epoger@google.comeb832592013-10-23 15:07:26 +0000739 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000740 $scope.moveImagePairsToTab(imagePairIndicesToMove,
741 "HackToMakeSureThisImagePairDisappears");
epoger@google.comeb832592013-10-23 15:07:26 +0000742 $scope.updateResults();
743 alert("New baselines submitted successfully!\n\n" +
744 "You still need to commit the updated expectations files on " +
745 "the server side to the Skia repo.\n\n" +
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +0000746 "When you click OK, your web UI will reload; after that " +
747 "completes, you will see the updated data (once the server has " +
748 "finished loading the update results into memory!) and you can " +
749 "submit more baselines if you want.");
750 // I don't know why, but if I just call reload() here it doesn't work.
751 // Making a timer call it fixes the problem.
752 $timeout(function(){location.reload();}, 1);
epoger@google.comeb832592013-10-23 15:07:26 +0000753 }).error(function(data, status, headers, config) {
754 alert("There was an error submitting your baselines.\n\n" +
755 "Please see server-side log for details.");
756 $scope.submitPending = false;
757 });
758 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000759
760
761 //
762 // Operations we use to mimic Set semantics, in such a way that
763 // checking for presence within the Set is as fast as possible.
764 // But getting a list of all values within the Set is not necessarily
765 // possible.
766 // TODO(epoger): move into a separate .js file?
767 //
768
769 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000770 * Returns the number of values present within set "set".
771 *
772 * @param set an Object which we use to mimic set semantics
773 */
774 $scope.setSize = function(set) {
775 return Object.keys(set).length;
776 }
777
778 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000779 * Returns true if value "value" is present within set "set".
780 *
781 * @param value a value of any type
782 * @param set an Object which we use to mimic set semantics
783 * (this should make isValueInSet faster than if we used an Array)
784 */
785 $scope.isValueInSet = function(value, set) {
786 return (true == set[value]);
787 }
788
789 /**
790 * If value "value" is already in set "set", remove it; otherwise, add it.
791 *
792 * @param value a value of any type
793 * @param set an Object which we use to mimic set semantics
794 */
795 $scope.toggleValueInSet = function(value, set) {
796 if (true == set[value]) {
797 delete set[value];
798 } else {
799 set[value] = true;
800 }
801 }
802
epoger@google.comf4394d52013-10-29 15:49:40 +0000803 /**
804 * For each value in valueArray, call toggleValueInSet(value, set).
805 *
806 * @param valueArray
807 * @param set
808 */
809 $scope.toggleValuesInSet = function(valueArray, set) {
810 var arrayLength = valueArray.length;
811 for (var i = 0; i < arrayLength; i++) {
812 $scope.toggleValueInSet(valueArray[i], set);
813 }
814 }
815
epoger@google.comad0e5522013-10-24 15:38:27 +0000816
817 //
818 // Array operations; similar to our Set operations, but operate on a
819 // Javascript Array so we *can* easily get a list of all values in the Set.
820 // TODO(epoger): move into a separate .js file?
821 //
822
823 /**
824 * Returns true if value "value" is present within array "array".
825 *
826 * @param value a value of any type
827 * @param array a Javascript Array
828 */
829 $scope.isValueInArray = function(value, array) {
830 return (-1 != array.indexOf(value));
831 }
832
833 /**
834 * If value "value" is already in array "array", remove it; otherwise,
835 * add it.
836 *
837 * @param value a value of any type
838 * @param array a Javascript Array
839 */
840 $scope.toggleValueInArray = function(value, array) {
841 var i = array.indexOf(value);
842 if (-1 == i) {
843 array.push(value);
844 } else {
845 array.splice(i, 1);
846 }
847 }
848
849
850 //
851 // Miscellaneous utility functions.
852 // TODO(epoger): move into a separate .js file?
853 //
854
855 /**
commit-bot@chromium.org97f0b082014-05-08 21:15:20 +0000856 * Returns a single "column slice" of a 2D array.
857 *
858 * For example, if array is:
859 * [[A0, A1],
860 * [B0, B1],
861 * [C0, C1]]
862 * and index is 0, this this will return:
863 * [A0, B0, C0]
864 *
865 * @param array a Javascript Array
866 * @param column (numeric): index within each row array
867 */
868 $scope.columnSliceOf2DArray = function(array, column) {
869 var slice = [];
870 var numRows = array.length;
871 for (var row = 0; row < numRows; row++) {
872 slice.push(array[row][column]);
873 }
874 return slice;
875 }
876
877 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000878 * Returns a human-readable (in local time zone) time string for a
879 * particular moment in time.
880 *
881 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
882 */
883 $scope.localTimeString = function(secondsPastEpoch) {
884 var d = new Date(secondsPastEpoch * 1000);
885 return d.toString();
886 }
887
commit-bot@chromium.orgceba0792014-02-05 19:49:17 +0000888 /**
889 * Returns a hex color string (such as "#aabbcc") for the given RGB values.
890 *
891 * @param r (numeric): red channel value, 0-255
892 * @param g (numeric): green channel value, 0-255
893 * @param b (numeric): blue channel value, 0-255
894 */
895 $scope.hexColorString = function(r, g, b) {
896 var rString = r.toString(16);
897 if (r < 16) {
898 rString = "0" + rString;
899 }
900 var gString = g.toString(16);
901 if (g < 16) {
902 gString = "0" + gString;
903 }
904 var bString = b.toString(16);
905 if (b < 16) {
906 bString = "0" + bString;
907 }
908 return '#' + rString + gString + bString;
909 }
910
911 /**
912 * Returns a hex color string (such as "#aabbcc") for the given brightness.
913 *
914 * @param brightnessString (string): 0-255, 0 is completely black
915 *
916 * TODO(epoger): It might be nice to tint the color when it's not completely
917 * black or completely white.
918 */
919 $scope.brightnessStringToHexColor = function(brightnessString) {
920 var v = parseInt(brightnessString);
921 return $scope.hexColorString(v, v, v);
922 }
923
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000924 /**
925 * Returns the last path component of image diff URL for a given ImagePair.
926 *
927 * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this
928 * will be relative to different base URLs.
929 *
930 * We must keep this function in sync with _get_difference_locator() in
931 * ../imagediffdb.py
932 *
933 * @param imagePair: ImagePair to generate image diff URL for
934 */
935 $scope.getImageDiffRelativeUrl = function(imagePair) {
936 var before =
commit-bot@chromium.org68a38152014-05-12 20:40:29 +0000937 imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" +
938 imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000939 return before.replace(/[^\w\-]/g, "_") + ".png";
940 }
941
epoger@google.comf9d134d2013-09-27 15:02:44 +0000942 }
943);