blob: fa74ed8eff04583e743fc47ba401e8e80b7f6509 [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',
8 []
9);
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 =
58 "Server is still loading initial results; will retry at " +
59 $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 /**
420 * Update $scope.hiddenConfigs so that ONLY this config is showing,
421 * and update the visible results.
422 *
423 * @param config
424 */
425 $scope.showOnlyConfig = function(config) {
426 $scope.hiddenConfigs = {};
427 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
428 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
429 $scope.updateResults();
430 }
431
epoger@google.comad0e5522013-10-24 15:38:27 +0000432
433 //
434 // Operations for sending info back to the server.
435 //
436
epoger@google.comeb832592013-10-23 15:07:26 +0000437 /**
438 * Tell the server that the actual results of these particular tests
439 * are acceptable.
440 *
441 * @param testDataSubset an array of test results, most likely a subset of
442 * $scope.testData (perhaps with some modifications)
443 */
444 $scope.submitApprovals = function(testDataSubset) {
445 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000446
447 // Convert bug text field to null or 1-item array.
448 var bugs = null;
449 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
450 if (!isNaN(bugNumber)) {
451 bugs = [bugNumber];
452 }
453
454 // TODO(epoger): This is a suboptimal way to prevent users from
455 // rebaselining failures in alternative renderModes, but it does work.
456 // For a better solution, see
457 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
458 // result type, RenderModeMismatch')
459 var encounteredComparisonConfig = false;
460
epoger@google.comeb832592013-10-23 15:07:26 +0000461 var newResults = [];
462 for (var i = 0; i < testDataSubset.length; i++) {
463 var actualResult = testDataSubset[i];
464 var expectedResult = {
465 builder: actualResult['builder'],
466 test: actualResult['test'],
467 config: actualResult['config'],
468 expectedHashType: actualResult['actualHashType'],
469 expectedHashDigest: actualResult['actualHashDigest'],
470 };
epoger@google.com055e3b52013-10-26 14:31:11 +0000471 if (0 == expectedResult.config.indexOf('comparison-')) {
472 encounteredComparisonConfig = true;
473 }
474
475 // Advanced settings...
476 expectedResult['reviewed-by-human'] =
477 $scope.submitAdvancedSettings['reviewed-by-human'];
478 if (true == $scope.submitAdvancedSettings['ignore-failure']) {
479 // if it's false, don't send it at all (just keep the default)
epoger@google.com1e698af2013-11-05 21:00:24 +0000480 expectedResult['ignore-failure'] = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000481 }
482 expectedResult['bugs'] = bugs;
483
epoger@google.comeb832592013-10-23 15:07:26 +0000484 newResults.push(expectedResult);
485 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000486 if (encounteredComparisonConfig) {
487 alert("Approval failed -- you cannot approve results with config " +
488 "type comparison-*");
489 $scope.submitPending = false;
490 return;
491 }
epoger@google.comeb832592013-10-23 15:07:26 +0000492 $http({
493 method: "POST",
494 url: "/edits",
495 data: {
496 oldResultsType: $scope.header.type,
497 oldResultsHash: $scope.header.dataHash,
498 modifications: newResults
499 }
500 }).success(function(data, status, headers, config) {
501 var itemIndicesToMove = [];
502 for (var i = 0; i < testDataSubset.length; i++) {
503 itemIndicesToMove.push(testDataSubset[i].index);
504 }
505 $scope.moveItemsToTab(itemIndicesToMove,
506 "HackToMakeSureThisItemDisappears");
507 $scope.updateResults();
508 alert("New baselines submitted successfully!\n\n" +
509 "You still need to commit the updated expectations files on " +
510 "the server side to the Skia repo.\n\n" +
511 "Also: in order to see the complete updated data, or to submit " +
512 "more baselines, you will need to reload your client.");
513 $scope.submitPending = false;
514 }).error(function(data, status, headers, config) {
515 alert("There was an error submitting your baselines.\n\n" +
516 "Please see server-side log for details.");
517 $scope.submitPending = false;
518 });
519 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000520
521
522 //
523 // Operations we use to mimic Set semantics, in such a way that
524 // checking for presence within the Set is as fast as possible.
525 // But getting a list of all values within the Set is not necessarily
526 // possible.
527 // TODO(epoger): move into a separate .js file?
528 //
529
530 /**
531 * Returns true if value "value" is present within set "set".
532 *
533 * @param value a value of any type
534 * @param set an Object which we use to mimic set semantics
535 * (this should make isValueInSet faster than if we used an Array)
536 */
537 $scope.isValueInSet = function(value, set) {
538 return (true == set[value]);
539 }
540
541 /**
542 * If value "value" is already in set "set", remove it; otherwise, add it.
543 *
544 * @param value a value of any type
545 * @param set an Object which we use to mimic set semantics
546 */
547 $scope.toggleValueInSet = function(value, set) {
548 if (true == set[value]) {
549 delete set[value];
550 } else {
551 set[value] = true;
552 }
553 }
554
epoger@google.comf4394d52013-10-29 15:49:40 +0000555 /**
556 * For each value in valueArray, call toggleValueInSet(value, set).
557 *
558 * @param valueArray
559 * @param set
560 */
561 $scope.toggleValuesInSet = function(valueArray, set) {
562 var arrayLength = valueArray.length;
563 for (var i = 0; i < arrayLength; i++) {
564 $scope.toggleValueInSet(valueArray[i], set);
565 }
566 }
567
epoger@google.comad0e5522013-10-24 15:38:27 +0000568
569 //
570 // Array operations; similar to our Set operations, but operate on a
571 // Javascript Array so we *can* easily get a list of all values in the Set.
572 // TODO(epoger): move into a separate .js file?
573 //
574
575 /**
576 * Returns true if value "value" is present within array "array".
577 *
578 * @param value a value of any type
579 * @param array a Javascript Array
580 */
581 $scope.isValueInArray = function(value, array) {
582 return (-1 != array.indexOf(value));
583 }
584
585 /**
586 * If value "value" is already in array "array", remove it; otherwise,
587 * add it.
588 *
589 * @param value a value of any type
590 * @param array a Javascript Array
591 */
592 $scope.toggleValueInArray = function(value, array) {
593 var i = array.indexOf(value);
594 if (-1 == i) {
595 array.push(value);
596 } else {
597 array.splice(i, 1);
598 }
599 }
600
601
602 //
603 // Miscellaneous utility functions.
604 // TODO(epoger): move into a separate .js file?
605 //
606
607 /**
608 * Returns a human-readable (in local time zone) time string for a
609 * particular moment in time.
610 *
611 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
612 */
613 $scope.localTimeString = function(secondsPastEpoch) {
614 var d = new Date(secondsPastEpoch * 1000);
615 return d.toString();
616 }
617
epoger@google.comf9d134d2013-09-27 15:02:44 +0000618 }
619);