blob: 9ae84d613c9444411c81a260e69bcbeac3cad9ac [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
4 * them into $scope.categories and $scope.testData .
epoger@google.comf9d134d2013-09-27 15:02:44 +00005 */
6var Loader = angular.module(
7 'Loader',
commit-bot@chromium.org076f0782014-02-04 22:45:40 +00008 ['diff_viewer']
epoger@google.comf9d134d2013-09-27 15:02:44 +00009);
epoger@google.com5f2bb002013-10-02 18:57:48 +000010
epoger@google.comad0e5522013-10-24 15:38:27 +000011
epoger@google.com5f2bb002013-10-02 18:57:48 +000012// TODO(epoger): Combine ALL of our filtering operations (including
13// truncation) into this one filter, so that runs most efficiently?
14// (We would have to make sure truncation still took place after
15// sorting, though.)
16Loader.filter(
17 'removeHiddenItems',
18 function() {
epoger@google.comeb832592013-10-23 15:07:26 +000019 return function(unfilteredItems, hiddenResultTypes, hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +000020 builderSubstring, testSubstring, viewingTab) {
epoger@google.com5f2bb002013-10-02 18:57:48 +000021 var filteredItems = [];
22 for (var i = 0; i < unfilteredItems.length; i++) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000023 var item = unfilteredItems[i];
epoger@google.com055e3b52013-10-26 14:31:11 +000024 // For performance, we examine the "set" objects directly rather
25 // than calling $scope.isValueInSet().
26 // Besides, I don't think we have access to $scope in here...
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000027 if (!(true == hiddenResultTypes[item.resultType]) &&
epoger@google.comeb832592013-10-23 15:07:26 +000028 !(true == hiddenConfigs[item.config]) &&
epoger@google.comf4394d52013-10-29 15:49:40 +000029 !(-1 == item.builder.indexOf(builderSubstring)) &&
30 !(-1 == item.test.indexOf(testSubstring)) &&
epoger@google.comeb832592013-10-23 15:07:26 +000031 (viewingTab == item.tab)) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000032 filteredItems.push(item);
33 }
epoger@google.com5f2bb002013-10-02 18:57:48 +000034 }
35 return filteredItems;
36 };
37 }
38);
39
epoger@google.comad0e5522013-10-24 15:38:27 +000040
epoger@google.comf9d134d2013-09-27 15:02:44 +000041Loader.controller(
42 'Loader.Controller',
epoger@google.com2682c902013-12-05 16:05:16 +000043 function($scope, $http, $filter, $location, $timeout) {
epoger@google.com542b65f2013-10-15 20:10:33 +000044 $scope.windowTitle = "Loading GM Results...";
epoger@google.com62a5ef02013-12-05 18:03:24 +000045 $scope.resultsToLoad = $location.search().resultsToLoad;
46 $scope.loadingMessage = "Loading results of type '" + $scope.resultsToLoad +
epoger@google.comdcb4e652013-10-11 18:45:33 +000047 "', please wait...";
48
epoger@google.comad0e5522013-10-24 15:38:27 +000049 /**
50 * On initial page load, load a full dictionary of results.
51 * Once the dictionary is loaded, unhide the page elements so they can
52 * render the data.
53 */
epoger@google.com62a5ef02013-12-05 18:03:24 +000054 $http.get("/results/" + $scope.resultsToLoad).success(
epoger@google.comdcb4e652013-10-11 18:45:33 +000055 function(data, status, header, config) {
epoger@google.com2682c902013-12-05 16:05:16 +000056 if (data.header.resultsStillLoading) {
57 $scope.loadingMessage =
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +000058 "Server is still loading results; will retry at " +
epoger@google.com2682c902013-12-05 16:05:16 +000059 $scope.localTimeString(data.header.timeNextUpdateAvailable);
60 $timeout(
61 function(){location.reload();},
62 (data.header.timeNextUpdateAvailable * 1000) - new Date().getTime());
63 } else {
64 $scope.loadingMessage = "Processing data, please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000065
epoger@google.com2682c902013-12-05 16:05:16 +000066 $scope.header = data.header;
67 $scope.categories = data.categories;
68 $scope.testData = data.testData;
69 $scope.sortColumn = 'weightedDiffMeasure';
70 $scope.showTodos = false;
epoger@google.com5f2bb002013-10-02 18:57:48 +000071
epoger@google.com2682c902013-12-05 16:05:16 +000072 $scope.showSubmitAdvancedSettings = false;
73 $scope.submitAdvancedSettings = {};
74 $scope.submitAdvancedSettings['reviewed-by-human'] = true;
75 $scope.submitAdvancedSettings['ignore-failure'] = false;
76 $scope.submitAdvancedSettings['bug'] = '';
epoger@google.com055e3b52013-10-26 14:31:11 +000077
epoger@google.com2682c902013-12-05 16:05:16 +000078 // Create the list of tabs (lists into which the user can file each
79 // test). This may vary, depending on isEditable.
80 $scope.tabs = [
81 'Unfiled', 'Hidden'
82 ];
83 if (data.header.isEditable) {
84 $scope.tabs = $scope.tabs.concat(
85 ['Pending Approval']);
86 }
87 $scope.defaultTab = $scope.tabs[0];
88 $scope.viewingTab = $scope.defaultTab;
89
90 // Track the number of results on each tab.
91 $scope.numResultsPerTab = {};
92 for (var i = 0; i < $scope.tabs.length; i++) {
93 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
94 }
95 $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
96
97 // Add index and tab fields to all records.
98 for (var i = 0; i < $scope.testData.length; i++) {
99 $scope.testData[i].index = i;
100 $scope.testData[i].tab = $scope.defaultTab;
101 }
102
103 // Arrays within which the user can toggle individual elements.
104 $scope.selectedItems = [];
105
106 // Sets within which the user can toggle individual elements.
107 $scope.hiddenResultTypes = {
108 'failure-ignored': true,
109 'no-comparison': true,
110 'succeeded': true,
111 };
112 $scope.allResultTypes = Object.keys(data.categories['resultType']);
113 $scope.hiddenConfigs = {};
114 $scope.allConfigs = Object.keys(data.categories['config']);
115
116 // Associative array of partial string matches per category.
117 $scope.categoryValueMatch = {};
118 $scope.categoryValueMatch.builder = "";
119 $scope.categoryValueMatch.test = "";
120
epoger@google.com62a5ef02013-12-05 18:03:24 +0000121 // If any defaults were overridden in the URL, get them now.
122 $scope.queryParameters.load();
123
epoger@google.com2682c902013-12-05 16:05:16 +0000124 $scope.updateResults();
125 $scope.loadingMessage = "";
126 $scope.windowTitle = "Current GM Results";
epoger@google.comeb832592013-10-23 15:07:26 +0000127 }
epoger@google.comdcb4e652013-10-11 18:45:33 +0000128 }
129 ).error(
130 function(data, status, header, config) {
131 $scope.loadingMessage = "Failed to load results of type '"
epoger@google.com62a5ef02013-12-05 18:03:24 +0000132 + $scope.resultsToLoad + "'";
epoger@google.com542b65f2013-10-15 20:10:33 +0000133 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000134 }
135 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000136
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000137
epoger@google.comad0e5522013-10-24 15:38:27 +0000138 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000139 // Select/Clear/Toggle all tests.
140 //
141
142 /**
143 * Select all currently showing tests.
144 */
145 $scope.selectAllItems = function() {
146 var numItemsShowing = $scope.limitedTestData.length;
147 for (var i = 0; i < numItemsShowing; i++) {
148 var index = $scope.limitedTestData[i].index;
149 if (!$scope.isValueInArray(index, $scope.selectedItems)) {
150 $scope.toggleValueInArray(index, $scope.selectedItems);
151 }
152 }
153 }
154
155 /**
156 * Deselect all currently showing tests.
157 */
158 $scope.clearAllItems = function() {
159 var numItemsShowing = $scope.limitedTestData.length;
160 for (var i = 0; i < numItemsShowing; i++) {
161 var index = $scope.limitedTestData[i].index;
162 if ($scope.isValueInArray(index, $scope.selectedItems)) {
163 $scope.toggleValueInArray(index, $scope.selectedItems);
164 }
165 }
166 }
167
168 /**
169 * Toggle selection of all currently showing tests.
170 */
171 $scope.toggleAllItems = function() {
172 var numItemsShowing = $scope.limitedTestData.length;
173 for (var i = 0; i < numItemsShowing; i++) {
174 var index = $scope.limitedTestData[i].index;
175 $scope.toggleValueInArray(index, $scope.selectedItems);
176 }
177 }
178
179
180 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000181 // Tab operations.
182 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000183
epoger@google.comad0e5522013-10-24 15:38:27 +0000184 /**
185 * Change the selected tab.
186 *
187 * @param tab (string): name of the tab to select
188 */
epoger@google.comeb832592013-10-23 15:07:26 +0000189 $scope.setViewingTab = function(tab) {
190 $scope.viewingTab = tab;
191 $scope.updateResults();
192 }
193
epoger@google.comeb832592013-10-23 15:07:26 +0000194 /**
195 * Move the items in $scope.selectedItems to a different tab,
196 * and then clear $scope.selectedItems.
197 *
198 * @param newTab (string): name of the tab to move the tests to
199 */
200 $scope.moveSelectedItemsToTab = function(newTab) {
201 $scope.moveItemsToTab($scope.selectedItems, newTab);
202 $scope.selectedItems = [];
203 $scope.updateResults();
204 }
205
206 /**
207 * Move a subset of $scope.testData to a different tab.
208 *
209 * @param itemIndices (array of ints): indices into $scope.testData
210 * indicating which test results to move
211 * @param newTab (string): name of the tab to move the tests to
212 */
213 $scope.moveItemsToTab = function(itemIndices, newTab) {
214 var itemIndex;
215 var numItems = itemIndices.length;
216 for (var i = 0; i < numItems; i++) {
217 itemIndex = itemIndices[i];
218 $scope.numResultsPerTab[$scope.testData[itemIndex].tab]--;
219 $scope.testData[itemIndex].tab = newTab;
220 }
221 $scope.numResultsPerTab[newTab] += numItems;
222 }
223
epoger@google.comad0e5522013-10-24 15:38:27 +0000224
225 //
epoger@google.com62a5ef02013-12-05 18:03:24 +0000226 // $scope.queryParameters:
227 // Transfer parameter values between $scope and the URL query string.
228 //
229 $scope.queryParameters = {};
230
231 // load and save functions for parameters of each type
232 // (load a parameter value into $scope from nameValuePairs,
233 // save a parameter value from $scope into nameValuePairs)
234 $scope.queryParameters.copiers = {
235 'simple': {
236 'load': function(nameValuePairs, name) {
237 var value = nameValuePairs[name];
238 if (value) {
239 $scope[name] = value;
240 }
241 },
242 'save': function(nameValuePairs, name) {
243 nameValuePairs[name] = $scope[name];
244 }
245 },
246
247 'categoryValueMatch': {
248 'load': function(nameValuePairs, name) {
249 var value = nameValuePairs[name];
250 if (value) {
251 $scope.categoryValueMatch[name] = value;
252 }
253 },
254 'save': function(nameValuePairs, name) {
255 nameValuePairs[name] = $scope.categoryValueMatch[name];
256 }
257 },
258
259 'set': {
260 'load': function(nameValuePairs, name) {
261 var value = nameValuePairs[name];
262 if (value) {
263 var valueArray = value.split(',');
264 $scope[name] = {};
265 $scope.toggleValuesInSet(valueArray, $scope[name]);
266 }
267 },
268 'save': function(nameValuePairs, name) {
269 nameValuePairs[name] = Object.keys($scope[name]).join(',');
270 }
271 },
272
273 };
274
275 // parameter name -> copier objects to load/save parameter value
276 $scope.queryParameters.map = {
277 'resultsToLoad': $scope.queryParameters.copiers.simple,
278 'displayLimitPending': $scope.queryParameters.copiers.simple,
279 'imageSizePending': $scope.queryParameters.copiers.simple,
280 'sortColumn': $scope.queryParameters.copiers.simple,
281
282 'builder': $scope.queryParameters.copiers.categoryValueMatch,
283 'test': $scope.queryParameters.copiers.categoryValueMatch,
284
285 'hiddenResultTypes': $scope.queryParameters.copiers.set,
286 'hiddenConfigs': $scope.queryParameters.copiers.set,
287 };
288
289 // Loads all parameters into $scope from the URL query string;
290 // any which are not found within the URL will keep their current value.
291 $scope.queryParameters.load = function() {
292 var nameValuePairs = $location.search();
293 angular.forEach($scope.queryParameters.map,
294 function(copier, paramName) {
295 copier.load(nameValuePairs, paramName);
296 }
297 );
298 };
299
300 // Saves all parameters from $scope into the URL query string.
301 $scope.queryParameters.save = function() {
302 var nameValuePairs = {};
303 angular.forEach($scope.queryParameters.map,
304 function(copier, paramName) {
305 copier.save(nameValuePairs, paramName);
306 }
307 );
308 $location.search(nameValuePairs);
309 };
310
311
312 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000313 // updateResults() and friends.
314 //
315
316 /**
317 * Set $scope.areUpdatesPending (to enable/disable the Update Results
318 * button).
319 *
320 * TODO(epoger): We could reduce the amount of code by just setting the
321 * variable directly (from, e.g., a button's ng-click handler). But when
322 * I tried that, the HTML elements depending on the variable did not get
323 * updated.
324 * It turns out that this is due to variable scoping within an ng-repeat
325 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
326 *
327 * @param val boolean value to set $scope.areUpdatesPending to
328 */
329 $scope.setUpdatesPending = function(val) {
330 $scope.areUpdatesPending = val;
331 }
332
333 /**
epoger@google.com62a5ef02013-12-05 18:03:24 +0000334 * Update the displayed results, based on filters/settings,
335 * and call $scope.queryParameters.save() so that the new filter results
336 * can be bookmarked.
epoger@google.comad0e5522013-10-24 15:38:27 +0000337 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000338 $scope.updateResults = function() {
339 $scope.displayLimit = $scope.displayLimitPending;
340 // TODO(epoger): Every time we apply a filter, AngularJS creates
341 // another copy of the array. Is there a way we can filter out
342 // the items as they are displayed, rather than storing multiple
343 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000344
345 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000346
347 // TODO(epoger): Until we allow the user to reverse sort order,
348 // there are certain columns we want to sort in a different order.
349 var doReverse = (
350 ($scope.sortColumn == 'percentDifferingPixels') ||
351 ($scope.sortColumn == 'weightedDiffMeasure'));
352
epoger@google.comeb832592013-10-23 15:07:26 +0000353 $scope.filteredTestData =
354 $filter("orderBy")(
355 $filter("removeHiddenItems")(
356 $scope.testData,
357 $scope.hiddenResultTypes,
358 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000359 $scope.categoryValueMatch.builder,
360 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000361 $scope.viewingTab
362 ),
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000363 $scope.sortColumn, doReverse);
epoger@google.comeb832592013-10-23 15:07:26 +0000364 $scope.limitedTestData = $filter("limitTo")(
365 $scope.filteredTestData, $scope.displayLimit);
366 } else {
367 $scope.filteredTestData =
368 $filter("orderBy")(
369 $filter("filter")(
370 $scope.testData,
371 {tab: $scope.viewingTab},
372 true
373 ),
374 $scope.sortColumn);
epoger@google.com055e3b52013-10-26 14:31:11 +0000375 $scope.limitedTestData = $scope.filteredTestData;
epoger@google.comeb832592013-10-23 15:07:26 +0000376 }
epoger@google.com5f2bb002013-10-02 18:57:48 +0000377 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000378 $scope.setUpdatesPending(false);
epoger@google.com62a5ef02013-12-05 18:03:24 +0000379 $scope.queryParameters.save();
epoger@google.com5f2bb002013-10-02 18:57:48 +0000380 }
381
epoger@google.comad0e5522013-10-24 15:38:27 +0000382 /**
383 * Re-sort the displayed results.
384 *
385 * @param sortColumn (string): name of the column to sort on
386 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000387 $scope.sortResultsBy = function(sortColumn) {
388 $scope.sortColumn = sortColumn;
389 $scope.updateResults();
390 }
epoger@google.comeb832592013-10-23 15:07:26 +0000391
epoger@google.comf4394d52013-10-29 15:49:40 +0000392 /**
393 * Set $scope.categoryValueMatch[name] = value, and update results.
394 *
395 * @param name
396 * @param value
397 */
398 $scope.setCategoryValueMatch = function(name, value) {
399 $scope.categoryValueMatch[name] = value;
400 $scope.updateResults();
401 }
402
403 /**
404 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
405 * and update the visible results.
406 *
407 * @param resultType
408 */
409 $scope.showOnlyResultType = function(resultType) {
410 $scope.hiddenResultTypes = {};
411 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
412 // $scope.hiddenResultTypes (rather than an array), so this operation is
413 // simpler (just assign or add allResultTypes to hiddenResultTypes).
414 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
415 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
416 $scope.updateResults();
417 }
418
419 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000420 * Update $scope.hiddenResultTypes so that ALL resultTypes are showing,
421 * and update the visible results.
422 */
423 $scope.showAllResultTypes = function() {
424 $scope.hiddenResultTypes = {};
425 $scope.updateResults();
426 }
427
428 /**
epoger@google.comf4394d52013-10-29 15:49:40 +0000429 * Update $scope.hiddenConfigs so that ONLY this config is showing,
430 * and update the visible results.
431 *
432 * @param config
433 */
434 $scope.showOnlyConfig = function(config) {
435 $scope.hiddenConfigs = {};
436 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
437 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
438 $scope.updateResults();
439 }
440
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000441 /**
442 * Update $scope.hiddenConfigs so that ALL configs are showing,
443 * and update the visible results.
444 */
445 $scope.showAllConfigs = function() {
446 $scope.hiddenConfigs = {};
447 $scope.updateResults();
448 }
449
epoger@google.comad0e5522013-10-24 15:38:27 +0000450
451 //
452 // Operations for sending info back to the server.
453 //
454
epoger@google.comeb832592013-10-23 15:07:26 +0000455 /**
456 * Tell the server that the actual results of these particular tests
457 * are acceptable.
458 *
459 * @param testDataSubset an array of test results, most likely a subset of
460 * $scope.testData (perhaps with some modifications)
461 */
462 $scope.submitApprovals = function(testDataSubset) {
463 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000464
465 // Convert bug text field to null or 1-item array.
466 var bugs = null;
467 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
468 if (!isNaN(bugNumber)) {
469 bugs = [bugNumber];
470 }
471
472 // TODO(epoger): This is a suboptimal way to prevent users from
473 // rebaselining failures in alternative renderModes, but it does work.
474 // For a better solution, see
475 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
476 // result type, RenderModeMismatch')
477 var encounteredComparisonConfig = false;
478
epoger@google.comeb832592013-10-23 15:07:26 +0000479 var newResults = [];
480 for (var i = 0; i < testDataSubset.length; i++) {
481 var actualResult = testDataSubset[i];
482 var expectedResult = {
483 builder: actualResult['builder'],
484 test: actualResult['test'],
485 config: actualResult['config'],
486 expectedHashType: actualResult['actualHashType'],
487 expectedHashDigest: actualResult['actualHashDigest'],
488 };
epoger@google.com055e3b52013-10-26 14:31:11 +0000489 if (0 == expectedResult.config.indexOf('comparison-')) {
490 encounteredComparisonConfig = true;
491 }
492
493 // Advanced settings...
494 expectedResult['reviewed-by-human'] =
495 $scope.submitAdvancedSettings['reviewed-by-human'];
496 if (true == $scope.submitAdvancedSettings['ignore-failure']) {
497 // if it's false, don't send it at all (just keep the default)
epoger@google.com1e698af2013-11-05 21:00:24 +0000498 expectedResult['ignore-failure'] = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000499 }
500 expectedResult['bugs'] = bugs;
501
epoger@google.comeb832592013-10-23 15:07:26 +0000502 newResults.push(expectedResult);
503 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000504 if (encounteredComparisonConfig) {
505 alert("Approval failed -- you cannot approve results with config " +
506 "type comparison-*");
507 $scope.submitPending = false;
508 return;
509 }
epoger@google.comeb832592013-10-23 15:07:26 +0000510 $http({
511 method: "POST",
512 url: "/edits",
513 data: {
514 oldResultsType: $scope.header.type,
515 oldResultsHash: $scope.header.dataHash,
516 modifications: newResults
517 }
518 }).success(function(data, status, headers, config) {
519 var itemIndicesToMove = [];
520 for (var i = 0; i < testDataSubset.length; i++) {
521 itemIndicesToMove.push(testDataSubset[i].index);
522 }
523 $scope.moveItemsToTab(itemIndicesToMove,
524 "HackToMakeSureThisItemDisappears");
525 $scope.updateResults();
526 alert("New baselines submitted successfully!\n\n" +
527 "You still need to commit the updated expectations files on " +
528 "the server side to the Skia repo.\n\n" +
commit-bot@chromium.org50ad8e42013-12-17 18:06:13 +0000529 "When you click OK, your web UI will reload; after that " +
530 "completes, you will see the updated data (once the server has " +
531 "finished loading the update results into memory!) and you can " +
532 "submit more baselines if you want.");
533 // I don't know why, but if I just call reload() here it doesn't work.
534 // Making a timer call it fixes the problem.
535 $timeout(function(){location.reload();}, 1);
epoger@google.comeb832592013-10-23 15:07:26 +0000536 }).error(function(data, status, headers, config) {
537 alert("There was an error submitting your baselines.\n\n" +
538 "Please see server-side log for details.");
539 $scope.submitPending = false;
540 });
541 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000542
543
544 //
545 // Operations we use to mimic Set semantics, in such a way that
546 // checking for presence within the Set is as fast as possible.
547 // But getting a list of all values within the Set is not necessarily
548 // possible.
549 // TODO(epoger): move into a separate .js file?
550 //
551
552 /**
commit-bot@chromium.org6f0ba472014-02-06 20:22:25 +0000553 * Returns the number of values present within set "set".
554 *
555 * @param set an Object which we use to mimic set semantics
556 */
557 $scope.setSize = function(set) {
558 return Object.keys(set).length;
559 }
560
561 /**
epoger@google.comad0e5522013-10-24 15:38:27 +0000562 * Returns true if value "value" is present within set "set".
563 *
564 * @param value a value of any type
565 * @param set an Object which we use to mimic set semantics
566 * (this should make isValueInSet faster than if we used an Array)
567 */
568 $scope.isValueInSet = function(value, set) {
569 return (true == set[value]);
570 }
571
572 /**
573 * If value "value" is already in set "set", remove it; otherwise, add it.
574 *
575 * @param value a value of any type
576 * @param set an Object which we use to mimic set semantics
577 */
578 $scope.toggleValueInSet = function(value, set) {
579 if (true == set[value]) {
580 delete set[value];
581 } else {
582 set[value] = true;
583 }
584 }
585
epoger@google.comf4394d52013-10-29 15:49:40 +0000586 /**
587 * For each value in valueArray, call toggleValueInSet(value, set).
588 *
589 * @param valueArray
590 * @param set
591 */
592 $scope.toggleValuesInSet = function(valueArray, set) {
593 var arrayLength = valueArray.length;
594 for (var i = 0; i < arrayLength; i++) {
595 $scope.toggleValueInSet(valueArray[i], set);
596 }
597 }
598
epoger@google.comad0e5522013-10-24 15:38:27 +0000599
600 //
601 // Array operations; similar to our Set operations, but operate on a
602 // Javascript Array so we *can* easily get a list of all values in the Set.
603 // TODO(epoger): move into a separate .js file?
604 //
605
606 /**
607 * Returns true if value "value" is present within array "array".
608 *
609 * @param value a value of any type
610 * @param array a Javascript Array
611 */
612 $scope.isValueInArray = function(value, array) {
613 return (-1 != array.indexOf(value));
614 }
615
616 /**
617 * If value "value" is already in array "array", remove it; otherwise,
618 * add it.
619 *
620 * @param value a value of any type
621 * @param array a Javascript Array
622 */
623 $scope.toggleValueInArray = function(value, array) {
624 var i = array.indexOf(value);
625 if (-1 == i) {
626 array.push(value);
627 } else {
628 array.splice(i, 1);
629 }
630 }
631
632
633 //
634 // Miscellaneous utility functions.
635 // TODO(epoger): move into a separate .js file?
636 //
637
638 /**
639 * Returns a human-readable (in local time zone) time string for a
640 * particular moment in time.
641 *
642 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
643 */
644 $scope.localTimeString = function(secondsPastEpoch) {
645 var d = new Date(secondsPastEpoch * 1000);
646 return d.toString();
647 }
648
commit-bot@chromium.orgceba0792014-02-05 19:49:17 +0000649 /**
650 * Returns a hex color string (such as "#aabbcc") for the given RGB values.
651 *
652 * @param r (numeric): red channel value, 0-255
653 * @param g (numeric): green channel value, 0-255
654 * @param b (numeric): blue channel value, 0-255
655 */
656 $scope.hexColorString = function(r, g, b) {
657 var rString = r.toString(16);
658 if (r < 16) {
659 rString = "0" + rString;
660 }
661 var gString = g.toString(16);
662 if (g < 16) {
663 gString = "0" + gString;
664 }
665 var bString = b.toString(16);
666 if (b < 16) {
667 bString = "0" + bString;
668 }
669 return '#' + rString + gString + bString;
670 }
671
672 /**
673 * Returns a hex color string (such as "#aabbcc") for the given brightness.
674 *
675 * @param brightnessString (string): 0-255, 0 is completely black
676 *
677 * TODO(epoger): It might be nice to tint the color when it's not completely
678 * black or completely white.
679 */
680 $scope.brightnessStringToHexColor = function(brightnessString) {
681 var v = parseInt(brightnessString);
682 return $scope.hexColorString(v, v, v);
683 }
684
epoger@google.comf9d134d2013-09-27 15:02:44 +0000685 }
686);