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