loader.js revision 16f418080ff6751e15e0193263149412de9c848a
1/* 2 * Loader: 3 * Reads GM result reports written out by results.py, and imports 4 * them into $scope.extraColumnHeaders and $scope.imagePairs . 5 */ 6var Loader = angular.module( 7 'Loader', 8 ['ConstantsModule', 'diff_viewer'] 9); 10 11// TODO(epoger): Combine ALL of our filtering operations (including 12// truncation) into this one filter, so that runs most efficiently? 13// (We would have to make sure truncation still took place after 14// sorting, though.) 15Loader.filter( 16 'removeHiddenImagePairs', 17 function(constants) { 18 return function(unfilteredImagePairs, hiddenResultTypes, hiddenConfigs, 19 builderSubstring, testSubstring, viewingTab) { 20 var filteredImagePairs = []; 21 for (var i = 0; i < unfilteredImagePairs.length; i++) { 22 var imagePair = unfilteredImagePairs[i]; 23 var extraColumnValues = imagePair[constants.KEY__EXTRA_COLUMN_VALUES]; 24 // 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... 27 if (!(true == hiddenResultTypes[extraColumnValues[ 28 constants.KEY__EXTRACOLUMN__RESULT_TYPE]]) && 29 !(true == hiddenConfigs[extraColumnValues[ 30 constants.KEY__EXTRACOLUMN__CONFIG]]) && 31 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMN__BUILDER] 32 .indexOf(builderSubstring)) && 33 !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMN__TEST] 34 .indexOf(testSubstring)) && 35 (viewingTab == imagePair.tab)) { 36 filteredImagePairs.push(imagePair); 37 } 38 } 39 return filteredImagePairs; 40 }; 41 } 42); 43 44 45Loader.controller( 46 'Loader.Controller', 47 function($scope, $http, $filter, $location, $timeout, constants) { 48 $scope.constants = constants; 49 $scope.windowTitle = "Loading GM Results..."; 50 $scope.resultsToLoad = $location.search().resultsToLoad; 51 $scope.loadingMessage = "Loading results of type '" + $scope.resultsToLoad + 52 "', please wait..."; 53 54 /** 55 * On initial page load, load a full dictionary of results. 56 * Once the dictionary is loaded, unhide the page elements so they can 57 * render the data. 58 */ 59 $http.get("/results/" + $scope.resultsToLoad).success( 60 function(data, status, header, config) { 61 var dataHeader = data[constants.KEY__HEADER]; 62 if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) { 63 $scope.loadingMessage = 64 "Server is still loading results; will retry at " + 65 $scope.localTimeString(dataHeader[ 66 constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE]); 67 $timeout( 68 function(){location.reload();}, 69 (dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] 70 * 1000) - new Date().getTime()); 71 } else { 72 $scope.loadingMessage = "Processing data, please wait..."; 73 74 $scope.header = dataHeader; 75 $scope.extraColumnHeaders = data[constants.KEY__EXTRACOLUMNHEADERS]; 76 $scope.imagePairs = data[constants.KEY__IMAGEPAIRS]; 77 $scope.imageSets = data[constants.KEY__IMAGESETS]; 78 $scope.sortColumnSubdict = constants.KEY__DIFFERENCE_DATA; 79 $scope.sortColumnKey = constants.KEY__DIFFERENCE_DATA__WEIGHTED_DIFF; 80 $scope.showTodos = false; 81 82 $scope.showSubmitAdvancedSettings = false; 83 $scope.submitAdvancedSettings = {}; 84 $scope.submitAdvancedSettings[ 85 constants.KEY__EXPECTATIONS__REVIEWED] = true; 86 $scope.submitAdvancedSettings[ 87 constants.KEY__EXPECTATIONS__IGNOREFAILURE] = false; 88 $scope.submitAdvancedSettings['bug'] = ''; 89 90 // Create the list of tabs (lists into which the user can file each 91 // test). This may vary, depending on isEditable. 92 $scope.tabs = [ 93 'Unfiled', 'Hidden' 94 ]; 95 if (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) { 96 $scope.tabs = $scope.tabs.concat( 97 ['Pending Approval']); 98 } 99 $scope.defaultTab = $scope.tabs[0]; 100 $scope.viewingTab = $scope.defaultTab; 101 102 // Track the number of results on each tab. 103 $scope.numResultsPerTab = {}; 104 for (var i = 0; i < $scope.tabs.length; i++) { 105 $scope.numResultsPerTab[$scope.tabs[i]] = 0; 106 } 107 $scope.numResultsPerTab[$scope.defaultTab] = $scope.imagePairs.length; 108 109 // Add index and tab fields to all records. 110 for (var i = 0; i < $scope.imagePairs.length; i++) { 111 $scope.imagePairs[i].index = i; 112 $scope.imagePairs[i].tab = $scope.defaultTab; 113 } 114 115 // Arrays within which the user can toggle individual elements. 116 $scope.selectedImagePairs = []; 117 118 // Sets within which the user can toggle individual elements. 119 $scope.hiddenResultTypes = {}; 120 $scope.hiddenResultTypes[ 121 constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true; 122 $scope.hiddenResultTypes[ 123 constants.KEY__RESULT_TYPE__NOCOMPARISON] = true; 124 $scope.hiddenResultTypes[ 125 constants.KEY__RESULT_TYPE__SUCCEEDED] = true; 126 $scope.allResultTypes = Object.keys( 127 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__RESULT_TYPE] 128 [constants.KEY__VALUES_AND_COUNTS]); 129 $scope.hiddenConfigs = {}; 130 $scope.allConfigs = Object.keys( 131 $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMN__CONFIG] 132 [constants.KEY__VALUES_AND_COUNTS]); 133 134 // Associative array of partial string matches per category. 135 $scope.categoryValueMatch = {}; 136 $scope.categoryValueMatch.builder = ""; 137 $scope.categoryValueMatch.test = ""; 138 139 // If any defaults were overridden in the URL, get them now. 140 $scope.queryParameters.load(); 141 142 $scope.updateResults(); 143 $scope.loadingMessage = ""; 144 $scope.windowTitle = "Current GM Results"; 145 } 146 } 147 ).error( 148 function(data, status, header, config) { 149 $scope.loadingMessage = "Failed to load results of type '" 150 + $scope.resultsToLoad + "'"; 151 $scope.windowTitle = "Failed to Load GM Results"; 152 } 153 ); 154 155 156 // 157 // Select/Clear/Toggle all tests. 158 // 159 160 /** 161 * Select all currently showing tests. 162 */ 163 $scope.selectAllImagePairs = function() { 164 var numImagePairsShowing = $scope.limitedImagePairs.length; 165 for (var i = 0; i < numImagePairsShowing; i++) { 166 var index = $scope.limitedImagePairs[i].index; 167 if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) { 168 $scope.toggleValueInArray(index, $scope.selectedImagePairs); 169 } 170 } 171 } 172 173 /** 174 * Deselect all currently showing tests. 175 */ 176 $scope.clearAllImagePairs = function() { 177 var numImagePairsShowing = $scope.limitedImagePairs.length; 178 for (var i = 0; i < numImagePairsShowing; i++) { 179 var index = $scope.limitedImagePairs[i].index; 180 if ($scope.isValueInArray(index, $scope.selectedImagePairs)) { 181 $scope.toggleValueInArray(index, $scope.selectedImagePairs); 182 } 183 } 184 } 185 186 /** 187 * Toggle selection of all currently showing tests. 188 */ 189 $scope.toggleAllImagePairs = function() { 190 var numImagePairsShowing = $scope.limitedImagePairs.length; 191 for (var i = 0; i < numImagePairsShowing; i++) { 192 var index = $scope.limitedImagePairs[i].index; 193 $scope.toggleValueInArray(index, $scope.selectedImagePairs); 194 } 195 } 196 197 198 // 199 // Tab operations. 200 // 201 202 /** 203 * Change the selected tab. 204 * 205 * @param tab (string): name of the tab to select 206 */ 207 $scope.setViewingTab = function(tab) { 208 $scope.viewingTab = tab; 209 $scope.updateResults(); 210 } 211 212 /** 213 * Move the imagePairs in $scope.selectedImagePairs to a different tab, 214 * and then clear $scope.selectedImagePairs. 215 * 216 * @param newTab (string): name of the tab to move the tests to 217 */ 218 $scope.moveSelectedImagePairsToTab = function(newTab) { 219 $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab); 220 $scope.selectedImagePairs = []; 221 $scope.updateResults(); 222 } 223 224 /** 225 * Move a subset of $scope.imagePairs to a different tab. 226 * 227 * @param imagePairIndices (array of ints): indices into $scope.imagePairs 228 * indicating which test results to move 229 * @param newTab (string): name of the tab to move the tests to 230 */ 231 $scope.moveImagePairsToTab = function(imagePairIndices, newTab) { 232 var imagePairIndex; 233 var numImagePairs = imagePairIndices.length; 234 for (var i = 0; i < numImagePairs; i++) { 235 imagePairIndex = imagePairIndices[i]; 236 $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--; 237 $scope.imagePairs[imagePairIndex].tab = newTab; 238 } 239 $scope.numResultsPerTab[newTab] += numImagePairs; 240 } 241 242 243 // 244 // $scope.queryParameters: 245 // Transfer parameter values between $scope and the URL query string. 246 // 247 $scope.queryParameters = {}; 248 249 // load and save functions for parameters of each type 250 // (load a parameter value into $scope from nameValuePairs, 251 // save a parameter value from $scope into nameValuePairs) 252 $scope.queryParameters.copiers = { 253 'simple': { 254 'load': function(nameValuePairs, name) { 255 var value = nameValuePairs[name]; 256 if (value) { 257 $scope[name] = value; 258 } 259 }, 260 'save': function(nameValuePairs, name) { 261 nameValuePairs[name] = $scope[name]; 262 } 263 }, 264 265 'categoryValueMatch': { 266 'load': function(nameValuePairs, name) { 267 var value = nameValuePairs[name]; 268 if (value) { 269 $scope.categoryValueMatch[name] = value; 270 } 271 }, 272 'save': function(nameValuePairs, name) { 273 nameValuePairs[name] = $scope.categoryValueMatch[name]; 274 } 275 }, 276 277 'set': { 278 'load': function(nameValuePairs, name) { 279 var value = nameValuePairs[name]; 280 if (value) { 281 var valueArray = value.split(','); 282 $scope[name] = {}; 283 $scope.toggleValuesInSet(valueArray, $scope[name]); 284 } 285 }, 286 'save': function(nameValuePairs, name) { 287 nameValuePairs[name] = Object.keys($scope[name]).join(','); 288 } 289 }, 290 291 }; 292 293 // parameter name -> copier objects to load/save parameter value 294 $scope.queryParameters.map = { 295 'resultsToLoad': $scope.queryParameters.copiers.simple, 296 'displayLimitPending': $scope.queryParameters.copiers.simple, 297 'imageSizePending': $scope.queryParameters.copiers.simple, 298 'sortColumnSubdict': $scope.queryParameters.copiers.simple, 299 'sortColumnKey': $scope.queryParameters.copiers.simple, 300 301 'hiddenResultTypes': $scope.queryParameters.copiers.set, 302 'hiddenConfigs': $scope.queryParameters.copiers.set, 303 }; 304 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__BUILDER] = 305 $scope.queryParameters.copiers.categoryValueMatch; 306 $scope.queryParameters.map[constants.KEY__EXTRACOLUMN__TEST] = 307 $scope.queryParameters.copiers.categoryValueMatch; 308 309 // Loads all parameters into $scope from the URL query string; 310 // any which are not found within the URL will keep their current value. 311 $scope.queryParameters.load = function() { 312 var nameValuePairs = $location.search(); 313 angular.forEach($scope.queryParameters.map, 314 function(copier, paramName) { 315 copier.load(nameValuePairs, paramName); 316 } 317 ); 318 }; 319 320 // Saves all parameters from $scope into the URL query string. 321 $scope.queryParameters.save = function() { 322 var nameValuePairs = {}; 323 angular.forEach($scope.queryParameters.map, 324 function(copier, paramName) { 325 copier.save(nameValuePairs, paramName); 326 } 327 ); 328 $location.search(nameValuePairs); 329 }; 330 331 332 // 333 // updateResults() and friends. 334 // 335 336 /** 337 * Set $scope.areUpdatesPending (to enable/disable the Update Results 338 * button). 339 * 340 * TODO(epoger): We could reduce the amount of code by just setting the 341 * variable directly (from, e.g., a button's ng-click handler). But when 342 * I tried that, the HTML elements depending on the variable did not get 343 * updated. 344 * It turns out that this is due to variable scoping within an ng-repeat 345 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat 346 * 347 * @param val boolean value to set $scope.areUpdatesPending to 348 */ 349 $scope.setUpdatesPending = function(val) { 350 $scope.areUpdatesPending = val; 351 } 352 353 /** 354 * Update the displayed results, based on filters/settings, 355 * and call $scope.queryParameters.save() so that the new filter results 356 * can be bookmarked. 357 */ 358 $scope.updateResults = function() { 359 $scope.displayLimit = $scope.displayLimitPending; 360 // TODO(epoger): Every time we apply a filter, AngularJS creates 361 // another copy of the array. Is there a way we can filter out 362 // the imagePairs as they are displayed, rather than storing multiple 363 // array copies? (For better performance.) 364 365 if ($scope.viewingTab == $scope.defaultTab) { 366 367 // TODO(epoger): Until we allow the user to reverse sort order, 368 // there are certain columns we want to sort in a different order. 369 var doReverse = ( 370 ($scope.sortColumnKey == 371 constants.KEY__DIFFERENCE_DATA__PERCENT_DIFF_PIXELS) || 372 ($scope.sortColumnKey == 373 constants.KEY__DIFFERENCE_DATA__WEIGHTED_DIFF)); 374 375 $scope.filteredImagePairs = 376 $filter("orderBy")( 377 $filter("removeHiddenImagePairs")( 378 $scope.imagePairs, 379 $scope.hiddenResultTypes, 380 $scope.hiddenConfigs, 381 $scope.categoryValueMatch.builder, 382 $scope.categoryValueMatch.test, 383 $scope.viewingTab 384 ), 385 $scope.getSortColumnValue, doReverse); 386 $scope.limitedImagePairs = $filter("limitTo")( 387 $scope.filteredImagePairs, $scope.displayLimit); 388 } else { 389 $scope.filteredImagePairs = 390 $filter("orderBy")( 391 $filter("filter")( 392 $scope.imagePairs, 393 {tab: $scope.viewingTab}, 394 true 395 ), 396 $scope.getSortColumnValue); 397 $scope.limitedImagePairs = $scope.filteredImagePairs; 398 } 399 $scope.imageSize = $scope.imageSizePending; 400 $scope.setUpdatesPending(false); 401 $scope.queryParameters.save(); 402 } 403 404 /** 405 * Re-sort the displayed results. 406 * 407 * @param subdict (string): which subdictionary 408 * (constants.KEY__DIFFERENCE_DATA, constants.KEY__EXPECTATIONS_DATA, 409 * constants.KEY__EXTRA_COLUMN_VALUES) the sort column key is within 410 * @param key (string): sort by value associated with this key in subdict 411 */ 412 $scope.sortResultsBy = function(subdict, key) { 413 $scope.sortColumnSubdict = subdict; 414 $scope.sortColumnKey = key; 415 $scope.updateResults(); 416 } 417 418 /** 419 * For a particular ImagePair, return the value of the column we are 420 * sorting on (according to $scope.sortColumnSubdict and 421 * $scope.sortColumnKey). 422 * 423 * @param imagePair: imagePair to get a column value out of. 424 */ 425 $scope.getSortColumnValue = function(imagePair) { 426 if ($scope.sortColumnSubdict in imagePair) { 427 return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey]; 428 } else { 429 return undefined; 430 } 431 } 432 433 /** 434 * Set $scope.categoryValueMatch[name] = value, and update results. 435 * 436 * @param name 437 * @param value 438 */ 439 $scope.setCategoryValueMatch = function(name, value) { 440 $scope.categoryValueMatch[name] = value; 441 $scope.updateResults(); 442 } 443 444 /** 445 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing, 446 * and update the visible results. 447 * 448 * @param resultType 449 */ 450 $scope.showOnlyResultType = function(resultType) { 451 $scope.hiddenResultTypes = {}; 452 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like 453 // $scope.hiddenResultTypes (rather than an array), so this operation is 454 // simpler (just assign or add allResultTypes to hiddenResultTypes). 455 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes); 456 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes); 457 $scope.updateResults(); 458 } 459 460 /** 461 * Update $scope.hiddenResultTypes so that ALL resultTypes are showing, 462 * and update the visible results. 463 */ 464 $scope.showAllResultTypes = function() { 465 $scope.hiddenResultTypes = {}; 466 $scope.updateResults(); 467 } 468 469 /** 470 * Update $scope.hiddenConfigs so that ONLY this config is showing, 471 * and update the visible results. 472 * 473 * @param config 474 */ 475 $scope.showOnlyConfig = function(config) { 476 $scope.hiddenConfigs = {}; 477 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs); 478 $scope.toggleValueInSet(config, $scope.hiddenConfigs); 479 $scope.updateResults(); 480 } 481 482 /** 483 * Update $scope.hiddenConfigs so that ALL configs are showing, 484 * and update the visible results. 485 */ 486 $scope.showAllConfigs = function() { 487 $scope.hiddenConfigs = {}; 488 $scope.updateResults(); 489 } 490 491 492 // 493 // Operations for sending info back to the server. 494 // 495 496 /** 497 * Tell the server that the actual results of these particular tests 498 * are acceptable. 499 * 500 * TODO(epoger): This assumes that the original expectations are in 501 * imageSetA, and the actuals are in imageSetB. 502 * 503 * @param imagePairsSubset an array of test results, most likely a subset of 504 * $scope.imagePairs (perhaps with some modifications) 505 */ 506 $scope.submitApprovals = function(imagePairsSubset) { 507 $scope.submitPending = true; 508 509 // Convert bug text field to null or 1-item array. 510 var bugs = null; 511 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']); 512 if (!isNaN(bugNumber)) { 513 bugs = [bugNumber]; 514 } 515 516 // TODO(epoger): This is a suboptimal way to prevent users from 517 // rebaselining failures in alternative renderModes, but it does work. 518 // For a better solution, see 519 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new 520 // result type, RenderModeMismatch') 521 var encounteredComparisonConfig = false; 522 523 var updatedExpectations = []; 524 for (var i = 0; i < imagePairsSubset.length; i++) { 525 var imagePair = imagePairsSubset[i]; 526 var updatedExpectation = {}; 527 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] = 528 imagePair[constants.KEY__EXPECTATIONS_DATA]; 529 updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES] = 530 imagePair[constants.KEY__EXTRA_COLUMN_VALUES]; 531 updatedExpectation[constants.KEY__NEW_IMAGE_URL] = 532 imagePair[constants.KEY__IMAGE_B_URL]; 533 if (0 == updatedExpectation[constants.KEY__EXTRA_COLUMN_VALUES] 534 [constants.KEY__EXTRACOLUMN__CONFIG] 535 .indexOf('comparison-')) { 536 encounteredComparisonConfig = true; 537 } 538 539 // Advanced settings... 540 if (null == updatedExpectation[constants.KEY__EXPECTATIONS_DATA]) { 541 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] = {}; 542 } 543 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] 544 [constants.KEY__EXPECTATIONS__REVIEWED] = 545 $scope.submitAdvancedSettings[ 546 constants.KEY__EXPECTATIONS__REVIEWED]; 547 if (true == $scope.submitAdvancedSettings[ 548 constants.KEY__EXPECTATIONS__IGNOREFAILURE]) { 549 // if it's false, don't send it at all (just keep the default) 550 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] 551 [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true; 552 } 553 updatedExpectation[constants.KEY__EXPECTATIONS_DATA] 554 [constants.KEY__EXPECTATIONS__BUGS] = bugs; 555 556 updatedExpectations.push(updatedExpectation); 557 } 558 if (encounteredComparisonConfig) { 559 alert("Approval failed -- you cannot approve results with config " + 560 "type comparison-*"); 561 $scope.submitPending = false; 562 return; 563 } 564 var modificationData = {}; 565 modificationData[constants.KEY__EDITS__MODIFICATIONS] = 566 updatedExpectations; 567 modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] = 568 $scope.header[constants.KEY__HEADER__DATAHASH]; 569 modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] = 570 $scope.header[constants.KEY__HEADER__TYPE]; 571 $http({ 572 method: "POST", 573 url: "/edits", 574 data: modificationData 575 }).success(function(data, status, headers, config) { 576 var imagePairIndicesToMove = []; 577 for (var i = 0; i < imagePairsSubset.length; i++) { 578 imagePairIndicesToMove.push(imagePairsSubset[i].index); 579 } 580 $scope.moveImagePairsToTab(imagePairIndicesToMove, 581 "HackToMakeSureThisImagePairDisappears"); 582 $scope.updateResults(); 583 alert("New baselines submitted successfully!\n\n" + 584 "You still need to commit the updated expectations files on " + 585 "the server side to the Skia repo.\n\n" + 586 "When you click OK, your web UI will reload; after that " + 587 "completes, you will see the updated data (once the server has " + 588 "finished loading the update results into memory!) and you can " + 589 "submit more baselines if you want."); 590 // I don't know why, but if I just call reload() here it doesn't work. 591 // Making a timer call it fixes the problem. 592 $timeout(function(){location.reload();}, 1); 593 }).error(function(data, status, headers, config) { 594 alert("There was an error submitting your baselines.\n\n" + 595 "Please see server-side log for details."); 596 $scope.submitPending = false; 597 }); 598 } 599 600 601 // 602 // Operations we use to mimic Set semantics, in such a way that 603 // checking for presence within the Set is as fast as possible. 604 // But getting a list of all values within the Set is not necessarily 605 // possible. 606 // TODO(epoger): move into a separate .js file? 607 // 608 609 /** 610 * Returns the number of values present within set "set". 611 * 612 * @param set an Object which we use to mimic set semantics 613 */ 614 $scope.setSize = function(set) { 615 return Object.keys(set).length; 616 } 617 618 /** 619 * Returns true if value "value" is present within set "set". 620 * 621 * @param value a value of any type 622 * @param set an Object which we use to mimic set semantics 623 * (this should make isValueInSet faster than if we used an Array) 624 */ 625 $scope.isValueInSet = function(value, set) { 626 return (true == set[value]); 627 } 628 629 /** 630 * If value "value" is already in set "set", remove it; otherwise, add it. 631 * 632 * @param value a value of any type 633 * @param set an Object which we use to mimic set semantics 634 */ 635 $scope.toggleValueInSet = function(value, set) { 636 if (true == set[value]) { 637 delete set[value]; 638 } else { 639 set[value] = true; 640 } 641 } 642 643 /** 644 * For each value in valueArray, call toggleValueInSet(value, set). 645 * 646 * @param valueArray 647 * @param set 648 */ 649 $scope.toggleValuesInSet = function(valueArray, set) { 650 var arrayLength = valueArray.length; 651 for (var i = 0; i < arrayLength; i++) { 652 $scope.toggleValueInSet(valueArray[i], set); 653 } 654 } 655 656 657 // 658 // Array operations; similar to our Set operations, but operate on a 659 // Javascript Array so we *can* easily get a list of all values in the Set. 660 // TODO(epoger): move into a separate .js file? 661 // 662 663 /** 664 * Returns true if value "value" is present within array "array". 665 * 666 * @param value a value of any type 667 * @param array a Javascript Array 668 */ 669 $scope.isValueInArray = function(value, array) { 670 return (-1 != array.indexOf(value)); 671 } 672 673 /** 674 * If value "value" is already in array "array", remove it; otherwise, 675 * add it. 676 * 677 * @param value a value of any type 678 * @param array a Javascript Array 679 */ 680 $scope.toggleValueInArray = function(value, array) { 681 var i = array.indexOf(value); 682 if (-1 == i) { 683 array.push(value); 684 } else { 685 array.splice(i, 1); 686 } 687 } 688 689 690 // 691 // Miscellaneous utility functions. 692 // TODO(epoger): move into a separate .js file? 693 // 694 695 /** 696 * Returns a human-readable (in local time zone) time string for a 697 * particular moment in time. 698 * 699 * @param secondsPastEpoch (numeric): seconds past epoch in UTC 700 */ 701 $scope.localTimeString = function(secondsPastEpoch) { 702 var d = new Date(secondsPastEpoch * 1000); 703 return d.toString(); 704 } 705 706 /** 707 * Returns a hex color string (such as "#aabbcc") for the given RGB values. 708 * 709 * @param r (numeric): red channel value, 0-255 710 * @param g (numeric): green channel value, 0-255 711 * @param b (numeric): blue channel value, 0-255 712 */ 713 $scope.hexColorString = function(r, g, b) { 714 var rString = r.toString(16); 715 if (r < 16) { 716 rString = "0" + rString; 717 } 718 var gString = g.toString(16); 719 if (g < 16) { 720 gString = "0" + gString; 721 } 722 var bString = b.toString(16); 723 if (b < 16) { 724 bString = "0" + bString; 725 } 726 return '#' + rString + gString + bString; 727 } 728 729 /** 730 * Returns a hex color string (such as "#aabbcc") for the given brightness. 731 * 732 * @param brightnessString (string): 0-255, 0 is completely black 733 * 734 * TODO(epoger): It might be nice to tint the color when it's not completely 735 * black or completely white. 736 */ 737 $scope.brightnessStringToHexColor = function(brightnessString) { 738 var v = parseInt(brightnessString); 739 return $scope.hexColorString(v, v, v); 740 } 741 742 /** 743 * Returns the last path component of image diff URL for a given ImagePair. 744 * 745 * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this 746 * will be relative to different base URLs. 747 * 748 * We must keep this function in sync with _get_difference_locator() in 749 * ../imagediffdb.py 750 * 751 * @param imagePair: ImagePair to generate image diff URL for 752 */ 753 $scope.getImageDiffRelativeUrl = function(imagePair) { 754 var before = 755 imagePair[constants.KEY__IMAGE_A_URL] + "-vs-" + 756 imagePair[constants.KEY__IMAGE_B_URL]; 757 return before.replace(/[^\w\-]/g, "_") + ".png"; 758 } 759 760 } 761); 762