Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(444)

Side by Side Diff: gm/rebaseline_server/static/new/js/app.js

Issue 538613002: Refactored frontend for the rebaseline server. (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Addressing issues raised during CR Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 'use strict';
2
3 /**
4 * TODO (stephana@): This is still work in progress.
5 * It does not offer the same functionality as the current version, but
6 * will serve as the starting point for a new backend.
7 * It works with the current backend, but does not support rebaselining.
8 */
9
10 /*
11 * Wrap everything into an IIFE to not polute the global namespace.
12 */
13 (function () {
14
15 // Declare app level module which contains everything of the current app.
16 // ui.bootstrap refers to directives defined in the AngularJS Bootstrap
17 // UI package (http://angular-ui.github.io/bootstrap/).
18 var app = angular.module('rbtApp', ['ngRoute', 'ui.bootstrap']);
19
20 // Configure the different within app views.
21 app.config(['$routeProvider', function($routeProvider) {
22 $routeProvider.when('/', {templateUrl: 'partials/index-view.html',
23 controller: 'IndexCtrl'});
24 $routeProvider.when('/view', {templateUrl: 'partials/rebaseline-view.html',
25 controller: 'RebaselineCrtrl'});
26 $routeProvider.otherwise({redirectTo: '/'});
27 }]);
28
29
30 // TODO (stephana): Some of these constants are 'gm' specific. In the
31 // next iteration we need to remove those as we move the more generic
32 // 'dm' testing tool.
33 //
34 // Shared constants used here and in the markup. These are exported when
35 // when used by a controller.
36 var c = {
37 // Define different view states as we load the data.
38 ST_LOADING: 1,
39 ST_STILL_LOADING: 2,
40 ST_READY: 3,
41
42 // These column types are used by the Column class.
43 COL_T_FILTER: 'filter',
44 COL_T_IMAGE: 'image',
45 COL_T_REGULAR: 'regular',
46
47 // Request parameters used to select between subsets of results.
48 RESULTS_ALL: 'all',
49 RESULTS_FAILURES: 'failures',
50
51 // Filter types are used by the Column class.
52 FILTER_FREE_FORM: 'free_form',
53 FILTER_CHECK_BOX: 'checkbox',
54
55 // Columns either provided by the backend response or added in code.
56 // TODO (stephana): This should go away once we switch to 'dm'.
57 COL_BUGS: 'bugs',
58 COL_IGNORE_FAILURE: 'ignore-failure',
59 COL_REVIEWED_BY_HUMANS: 'reviewed-by-human',
60
61 // Defines the order in which image columns appear.
62 // TODO (stephana@): needs to be driven by backend data.
63 IMG_COL_ORDER: [
64 {
65 key: 'imageA',
66 urlField: ['imageAUrl']
67 },
68 {
69 key: 'imageB',
70 urlField: ['imageBUrl']
71 },
72 {
73 key: 'whiteDiffs',
74 urlField: ['differenceData', 'whiteDiffUrl'],
75 percentField: ['differenceData', 'percentDifferingPixels'],
76 valueField: ['differenceData', 'numDifferingPixels']
77 },
78 {
79 key: 'diffs',
80 urlField: ['differenceData', 'diffUrl'],
81 percentField: ['differenceData', 'perceptualDifference'],
82 valueField: ['differenceData', 'maxDiffPerChannel']
83 }
84 ],
85
86 // Choice of availabe image size selection.
87 IMAGE_SIZES: [
88 100,
89 200,
90 400
91 ],
92
93 // Choice of available number of records selection.
94 MAX_RECORDS: [
95 '100',
96 '200',
97 '300'
98 ]
99 }; // end constants
100
101 /*
102 * Index Controller
103 */
104 // TODO (stephana): Remove $timeout since it only simulates loading delay.
105 app.controller('IndexCtrl', ['$scope', '$timeout', 'dataService',
106 function($scope, $timeout, dataService) {
107 // init the scope
108 $scope.c = c;
109 $scope.state = c.ST_LOADING;
110 $scope.qStr = dataService.getQueryString;
111
112 // TODO (stephana): Remove and replace with index data generated by the
113 // backend to reflect the current "known" image sets to compare.
114 $scope.allSKPs = [
115 {
116 params: {
117 setBSection: 'actual-results',
118 setASection: 'expected-results',
119 setBDir: 'gs://chromium-skia-skp-summaries/' +
120 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug',
121 setADir: 'repo:expectations/skp/' +
122 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
123 },
124 title: 'expected vs actuals on ' +
125 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
126 },
127 {
128 params: {
129 setBSection: 'actual-results',
130 setASection: 'expected-results',
131 setBDir: 'gs://chromium-skia-skp-summaries/' +
132 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
133 setADir: 'repo:expectations/skp/'+
134 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
135 },
136 title: 'expected vs actuals on Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
137 },
138 {
139 params: {
140 setBSection: 'actual-results',
141 setASection: 'actual-results',
142 setBDir: 'gs://chromium-skia-skp-summaries/' +
143 'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
144 setADir: 'gs://chromium-skia-skp-summaries/' +
145 'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug'
146 },
147 title: 'Actuals on Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug ' +
148 'vs Test-Ubuntu12-ShuttleA-GTX660-x86-Release'
149 }
150 ];
151
152 // TODO (stephana): Remove this once we load index data from the server.
153 $timeout(function () {
154 $scope.state = c.ST_READY;
155 });
156 }]);
157
158 /*
159 * RebaselineCtrl
160 * Controls the main comparison view.
161 *
162 * @param {service} dataService Service that encapsulates functions to
163 * retrieve data from the backend.
164 *
165 */
166 app.controller('RebaselineCrtrl', ['$scope', '$timeout', 'dataService',
167 function($scope, $timeout, dataService) {
168 // determine which to request
169 // TODO (stephana): This should be extracted from the query parameters.
170 var target = c.TARGET_GM;
171
172 // process the rquest arguments
173 // TODO (stephana): This should be determined from the query parameters.
174 var loadFn = dataService.loadAll;
175
176 // controller state variables
177 var allData = null;
178 var filterFuncs = null;
179 var currentData = null;
180 var selectedData = null;
181
182 // Index of the column that should provide the sort key
183 var sortByIdx = 0;
184
185 // Sort in asending (true) or descending (false) order
186 var sortOrderAsc = true;
187
188 // Array of functions for each column used for comparison during sort.
189 var compareFunctions = null;
190
191 // Variables to track load and render times
192 var startTime;
193 var loadStartTime;
194
195
196 /** Load the data from the backend **/
197 loadStartTime = Date.now();
198 function loadData() {
199 loadFn().then(
200 function (serverData) {
201 $scope.header = serverData.header;
202 $scope.loadTime = (Date.now() - loadStartTime)/1000;
203
204 // keep polling if the data are not ready yet
205 if ($scope.header.resultsStillLoading) {
206 $scope.state = c.ST_STILL_LOADING;
207 $timeout(loadData, 5000);
208 return;
209 }
210
211 // get the filter colunms and an array to hold filter data by user
212 var fcol = getFilterColumns(serverData);
213 $scope.filterCols = fcol[0];
214 $scope.filterVals = fcol[1];
215
216 // Add extra columns and retrieve the image columns
217 var otherCols = [ Column.regular(c.COL_BUGS) ];
218 var imageCols = getImageColumns(serverData);
219
220 // Concat to get all columns
221 // NOTE: The order is important since filters are rendered first,
222 // followed by regular columns and images
223 $scope.allCols = $scope.filterCols.concat(otherCols, imageCols);
224
225 // Pre-process the data and get the filter functions.
226 var dataFilters = getDataAndFilters(serverData, $scope.filterCols,
227 otherCols, imageCols);
228 allData = dataFilters[0];
229 filterFuncs = dataFilters[1];
230
231 // Get regular columns (== not image columns)
232 var regularCols = $scope.filterCols.concat(otherCols);
233
234 // Get the compare functions for regular and image columns. These
235 // are then used to sort by the respective columns.
236 compareFunctions = DataRow.getCompareFunctions(regularCols,
237 imageCols);
238
239 // Filter and sort the results to get them ready for rendering
240 updateResults();
241
242 // Data are ready for display
243 $scope.state = c.ST_READY;
244 },
245 function (httpErrResponse) {
246 console.log(httpErrResponse);
247 });
248 };
249
250 /*
251 * updateResults
252 * Central render function. Everytime settings/filters/etc. changed
253 * this function is called to filter, sort and splice the data.
254 *
255 * NOTE (stephana): There is room for improvement here: before filtering
256 * and sorting we could check if this is necessary. But this has not been
257 * a bottleneck so far.
258 */
259 function updateResults () {
260 // run digest before we update the results. This allows
261 // updateResults to be called from functions trigger by ngChange
262 $scope.updating = true;
263 startTime = Date.now();
264
265 // delay by one render cycle so it can be called via ng-change
266 $timeout(function() {
267 // filter data
268 selectedData = filterData(allData, filterFuncs, $scope.filterVals);
269
270 // sort the selected data.
271 sortData(selectedData, compareFunctions, sortByIdx, sortOrderAsc);
272
273 // only conside the elements that we really need
274 var nRecords = $scope.settings.nRecords;
275 currentData = selectedData.slice(0, parseInt(nRecords));
276
277 DataRow.setRowspanValues(currentData, $scope.mergeIdenticalRows);
278
279 // update the scope with relevant data for rendering.
280 $scope.data = currentData;
281 $scope.totalRecords = allData.length;
282 $scope.showingRecords = currentData.length;
283 $scope.selectedRecords = selectedData.length;
284 $scope.updating = false;
285
286 // measure the filter time and total render time (via timeout).
287 $scope.filterTime = Date.now() - startTime;
288 $timeout(function() {
289 $scope.renderTime = Date.now() - startTime;
290 });
291 });
292 };
293
294 /**
295 * Generate the style value to set the width of images.
296 *
297 * @param {Column} col Column that we are trying to render.
298 * @param {int} paddingPx Number of padding pixels.
299 * @param {string} defaultVal Default value if not an image column.
300 *
301 * @return {string} Value to be used in ng-style element to set the width
302 * of a image column.
303 **/
304 $scope.getImageWidthStyle = function (col, paddingPx, defaultVal) {
305 var result = (col.ctype === c.COL_T_IMAGE) ?
306 ($scope.imageSize + paddingPx + 'px') : defaultVal;
307 return result;
308 };
309
310 /**
311 * Sets the column by which to sort the data. If called for the
312 * currently sorted column it will cause the sort to toggle between
313 * ascending and descending.
314 *
315 * @param {int} colIdx Index of the column to use for sorting.
316 **/
317 $scope.sortBy = function (colIdx) {
318 if (sortByIdx === colIdx) {
319 sortOrderAsc = !sortOrderAsc;
320 } else {
321 sortByIdx = colIdx;
322 sortOrderAsc = true;
323 }
324 updateResults();
325 };
326
327 /**
328 * Helper function to generate a CSS class indicating whether this column
329 * is the sort key. If it is a class name with the sort direction (Asc/Desc) is
330 * return otherwise the default value is returned. In markup we use this
331 * to display (or not display) an arrow next to the column name.
332 *
333 * @param {string} prefix Prefix of the classname to be generated.
334 * @param {int} idx Index of the target column.
335 * @param {string} defaultVal Value to return if current column is not used
336 * for sorting.
337 *
338 * @return {string} CSS class name that a combination of the prefix and
339 * direction indicator ('Asc' or 'Desc') if the column is
340 * used for sorting. Otherwise the defaultVal is returned.
341 **/
342 $scope.getSortedClass = function (prefix, idx, defaultVal) {
343 if (idx === sortByIdx) {
344 return prefix + ((sortOrderAsc) ? 'Asc' : 'Desc');
345 }
346
347 return defaultVal;
348 };
349
350 /**
351 * Checkbox to merge identical records has change. Force an update.
352 **/
353 $scope.mergeRowsChanged = function () {
354 updateResults();
355 }
356
357 /**
358 * Max number of records to display has changed. Force an update.
359 **/
360 $scope.maxRecordsChanged = function () {
361 updateResults();
362 };
363
364 /**
365 * Filter settings changed. Force an update.
366 **/
367 $scope.filtersChanged = function () {
368 updateResults();
369 };
370
371 /**
372 * Sets all possible values of the specified values to the given value.
373 * That means all checkboxes are eiter selected or unselected.
374 * Then force an update.
375 *
376 * @param {int} idx Index of the target filter column.
377 * @param {boolean} val Value to set the filter values to.
378 *
379 **/
380 $scope.setFilterAll = function (idx, val) {
381 for(var i=0, len=$scope.filterVals[idx].length; i<len; i++) {
382 $scope.filterVals[idx][i] = val;
383 }
384 updateResults();
385 };
386
387 /**
388 * Toggle the values of a filter. This toggles all values in a
389 * filter.
390 *
391 * @param {int} idx Index of the target filter column.
392 **/
393 $scope.setFilterToggle = function (idx) {
394 for(var i=0, len=$scope.filterVals[idx].length; i<len; i++) {
395 $scope.filterVals[idx][i] = !$scope.filterVals[idx][i];
396 }
397 updateResults();
398 };
399
400 // ****************************************
401 // Initialize the scope.
402 // ****************************************
403
404 // Inject the constants into the scope and set the initial state.
405 $scope.c = c;
406 $scope.state = c.ST_LOADING;
407
408 // Initial settings
409 $scope.settings = {
410 showThumbnails: true,
411 imageSize: c.IMAGE_SIZES[0],
412 nRecords: c.MAX_RECORDS[0],
413 mergeIdenticalRows: true
414 };
415
416 // Initial values for filters set in loadData()
417 $scope.filterVals = [];
418
419 // Information about records - set in loadData()
420 $scope.totalRecords = 0;
421 $scope.showingRecords = 0;
422 $scope.updating = false;
423
424 // Trigger the data loading.
425 loadData();
426
427 }]);
428
429 // data structs to interface with markup and backend
430 /**
431 * Models a column. It aggregates attributes of all
432 * columns types. Some might be empty. See convenience
433 * factory methods below for different column types.
434 *
435 * @param {string} key Uniquely identifies this columns
436 * @param {string} ctype Type of columns. Use COL_* constants.
437 * @param {string} ctitle Human readable title of the column.
438 * @param {string} ftype Filter type. Use FILTER_* constants.
439 * @param {FilterOpt[]} foptions Filter options. For 'checkbox' filters this
440 is used to render all the checkboxes.
441 For freeform filters this is a list of all
442 available values.
443 * @param {string} baseUrl Baseurl for image columns. All URLs are relative
444 to this.
445 *
446 * @return {Column} Instance of the Column class.
447 **/
448 function Column(key, ctype, ctitle, ftype, foptions, baseUrl) {
449 this.key = key;
450 this.ctype = ctype;
451 this.ctitle = ctitle;
452 this.ftype = ftype;
453 this.foptions = foptions;
454 this.baseUrl = baseUrl;
455 this.foptionsArr = [];
456
457 // get the array of filter options for lookup in indexOfOptVal
458 if (this.foptions) {
459 for(var i=0, len=foptions.length; i<len; i++) {
460 this.foptionsArr.push(this.foptions[i].value);
461 }
462 }
463 }
464
465 /**
466 * Find the index of an value in a column with a fixed set
467 * of options.
468 *
469 * @param {string} optVal Value of the column.
470 *
471 * @return {int} Index of optVal in this column.
472 **/
473 Column.prototype.indexOfOptVal = function (optVal) {
474 return this.foptionsArr.indexOf(optVal);
475 };
476
477 /**
478 * Set filter options for this column.
479 *
480 * @param {FilterOpt[]} foptions Possible values for this column.
481 **/
482 Column.prototype.setFilterOptions = function (foptions) {
483 this.foptions = foptions;
484 };
485
486 /**
487 * Factory function to create a filter column. Same args as Column()
488 **/
489 Column.filter = function(key, ctitle, ftype, foptions) {
490 return new Column(key, c.COL_T_FILTER, ctitle || key, ftype, foptions);
491 }
492
493 /**
494 * Factory function to create an image column. Same args as Column()
495 **/
496 Column.image = function (key, ctitle, baseUrl) {
497 return new Column(key, c.COL_T_IMAGE, ctitle || key, null, null, baseUrl);
498 };
499
500 /**
501 * Factory function to create a regular column. Same args as Column()
502 **/
503 Column.regular = function (key, ctitle) {
504 return new Column(key, c.COL_T_REGULAR, ctitle || key);
505 };
506
507 /**
508 * Helper class to wrap a single option in a filter.
509 *
510 * @param {string} value Option value.
511 * @param {int} count Number of instances of this option in the dataset.
512 *
513 * @return {} Instance of FiltertOpt
514 **/
515 function FilterOpt(value, count) {
516 this.value = value;
517 this.count = count;
518 }
519
520 /**
521 * Container for a single row in the dataset.
522 *
523 * @param {int} rowspan Number of rows (including this and following rows)
524 that have identical values.
525 * @param {string[]} dataCols Values of the respective columns (combination
526 of filter and regular columns)
527 * @param {ImgVal[]} imageCols Image meta data for the image columns.
528 *
529 * @return {DataRow} Instance of DataRow.
530 **/
531 function DataRow(rowspan, dataCols, imageCols) {
532 this.rowspan = rowspan;
533 this.dataCols = dataCols;
534 this.imageCols = imageCols;
535 }
536
537 /**
538 * Gets the comparator functions for the columns in this dataset.
539 * The comparators are then used to sort the dataset by the respective
540 * column.
541 *
542 * @param {Column[]} dataCols Data columns (= non-image columns)
543 * @param {Column[]} imgCols Image columns.
544 *
545 * @return {Function[]} Array of functions that can be used to sort by the
546 * respective column.
547 **/
548 DataRow.getCompareFunctions = function (dataCols, imgCols) {
549 var result = [];
550 for(var i=0, len=dataCols.length; i<len; i++) {
551 result.push(( function (col, idx) {
552 return function (a, b) {
553 return (a.dataCols[idx] < b.dataCols[idx]) ? -1 :
554 ((a.dataCols[idx] === b.dataCols[idx]) ? 0 : 1);
555 };
556 }(dataCols[i], i) ));
557 }
558
559 for(var i=0, len=imgCols.length; i<len; i++) {
560 result.push((function (col, idx) {
561 return function (a,b) {
562 var aVal = a.imageCols[idx].percent;
563 var bVal = b.imageCols[idx].percent;
564
565 return (aVal < bVal) ? -1 : ((aVal === bVal) ? 0 : 1);
566 };
567 }(imgCols[i], i) ));
568 }
569
570 return result;
571 };
572
573 /**
574 * Set the rowspan values of a given array of DataRow instances.
575 *
576 * @param {DataRow[]} data Dataset in desired order (after sorting).
577 * @param {mergeRows} mergeRows Indicate whether to sort
578 **/
579 DataRow.setRowspanValues = function (data, mergeRows) {
580 var curIdx, rowspan, cur;
581 if (mergeRows) {
582 for(var i=0, len=data.length; i<len;) {
583 curIdx = i;
584 cur = data[i];
585 rowspan = 1;
586 for(i++; ((i<len) && (data[i].dataCols === cur.dataCols)); i++) {
587 rowspan++;
588 data[i].rowspan=0;
589 }
590 data[curIdx].rowspan = rowspan;
591 }
592 } else {
593 for(var i=0, len=data.length; i<len; i++) {
594 data[i].rowspan = 1;
595 }
596 }
597 };
598
599 /**
600 * Wrapper class for image related data.
601 *
602 * @param {string} url Relative Url of the image or null if not available.
603 * @param {float} percent Percent of pixels that are differing.
604 * @param {int} value Absolute number of pixes differing.
605 *
606 * @return {ImgVal} Instance of ImgVal.
607 **/
608 function ImgVal(url, percent, value) {
609 this.url = url;
610 this.percent = percent;
611 this.value = value;
612 }
613
614 /**
615 * Extracts the filter columns from the JSON response of the server.
616 *
617 * @param {object} data Server response.
618 *
619 * @return {Column[]} List of filter columns as described in 'header' field.
620 **/
621 function getFilterColumns(data) {
622 var result = [];
623 var vals = [];
624 var colOrder = data.extraColumnOrder;
625 var colHeaders = data.extraColumnHeaders;
626 var fopts, optVals, val;
627
628 for(var i=0, len=colOrder.length; i<len; i++) {
629 if (colHeaders[colOrder[i]].isFilterable) {
630 if (colHeaders[colOrder[i]].useFreeformFilter) {
631 result.push(Column.filter(colOrder[i],
632 colHeaders[colOrder[i]].headerText,
633 c.FILTER_FREE_FORM));
634 vals.push('');
635 }
636 else {
637 fopts = [];
638 optVals = [];
639
640 // extract the different options for this column
641 for(var j=0, jlen=colHeaders[colOrder[i]].valuesAndCounts.length;
642 j<jlen; j++) {
643 val = colHeaders[colOrder[i]].valuesAndCounts[j];
644 fopts.push(new FilterOpt(val[0], val[1]));
645 optVals.push(false);
646 }
647
648 // ad the column and values
649 result.push(Column.filter(colOrder[i],
650 colHeaders[colOrder[i]].headerText,
651 c.FILTER_CHECK_BOX,
652 fopts));
653 vals.push(optVals);
654 }
655 }
656 }
657
658 return [result, vals];
659 }
660
661 /**
662 * Extracts the image columns from the JSON response of the server.
663 *
664 * @param {object} data Server response.
665 *
666 * @return {Column[]} List of images columns as described in 'header' field.
667 **/
668 function getImageColumns(data) {
669 var CO = c.IMG_COL_ORDER;
670 var imgSet;
671 var result = [];
672 for(var i=0, len=CO.length; i<len; i++) {
673 imgSet = data.imageSets[CO[i].key];
674 result.push(Column.image(CO[i].key,
675 imgSet.description,
676 ensureTrailingSlash(imgSet.baseUrl)));
677 }
678 return result;
679 }
680
681 /**
682 * Make sure Url has a trailing '/'.
683 *
684 * @param {string} url Base url.
685 * @return {string} Same url with a trailing '/' or same as input if it
686 already contained '/'.
687 **/
688 function ensureTrailingSlash(url) {
689 var result = url.trim();
690
691 // TODO: remove !!!
692 result = fixUrl(url);
693 if (result[result.length-1] !== '/') {
694 result += '/';
695 }
696 return result;
697 }
698
699 // TODO: remove. The backend should provide absoute URLs
700 function fixUrl(url) {
701 url = url.trim();
702 if ('http' === url.substr(0, 4)) {
703 return url;
704 }
705
706 var idx = url.indexOf('static');
707 if (idx != -1) {
708 return '/' + url.substr(idx);
709 }
710
711 return url;
712 };
713
714 /**
715 * Processes that data and returns filter functions.
716 *
717 * @param {object} Server response.
718 * @param {Column[]} filterCols Filter columns.
719 * @param {Column[]} otherCols Columns that are neither filters nor images.
720 * @param {Column[]} imageCols Image columns.
721 *
722 * @return {[]} Returns a pair [dataRows, filterFunctions] where:
723 * - dataRows is an array of DataRow instances.
724 * - filterFunctions is an array of functions that can be used to
725 * filter the column at the corresponding index.
726 *
727 **/
728 function getDataAndFilters(data, filterCols, otherCols, imageCols) {
729 var el;
730 var result = [];
731 var lookupIndices = [];
732 var indexerFuncs = [];
733 var temp;
734
735 // initialize the lookupIndices
736 var filterFuncs = initIndices(filterCols, lookupIndices, indexerFuncs);
737
738 // iterate over the data and get the rows
739 for(var i=0, len=data.imagePairs.length; i<len; i++) {
740 el = data.imagePairs[i];
741 temp = new DataRow(1, getColValues(el, filterCols, otherCols),
742 getImageValues(el, imageCols));
743 result.push(temp);
744
745 // index the row
746 for(var j=0, jlen=filterCols.length; j < jlen; j++) {
747 indexerFuncs[j](lookupIndices[j], filterCols[j], temp.dataCols[j], i);
748 }
749 }
750
751 setFreeFormFilterOptions(filterCols, lookupIndices);
752 return [result, filterFuncs];
753 }
754
755 /**
756 * Initiazile the lookup indices and indexer functions for the filter
757 * columns.
758 *
759 * @param {Column} filterCols Filter columns
760 * @param {[]} lookupIndices Will be filled with datastructures for
761 fast lookup (output parameter)
762 * @param {[]} lookupIndices Will be filled with functions to index data
763 of the column with the corresponding column.
764 *
765 * @return {[]} Returns an array of filter functions that can be used to
766 filter the respective column.
767 **/
768 function initIndices(filterCols, lookupIndices, indexerFuncs) {
769 var filterFuncs = [];
770 var temp;
771
772 for(var i=0, len=filterCols.length; i<len; i++) {
773 if (filterCols[i].ftype === c.FILTER_FREE_FORM) {
774 lookupIndices.push({});
775 indexerFuncs.push(indexFreeFormValue);
776 filterFuncs.push(
777 getFreeFormFilterFunc(lookupIndices[lookupIndices.length-1]));
778 }
779 else if (filterCols[i].ftype === c.FILTER_CHECK_BOX) {
780 temp = [];
781 for(var j=0, jlen=filterCols[i].foptions.length; j<jlen; j++) {
782 temp.push([]);
783 }
784 lookupIndices.push(temp);
785 indexerFuncs.push(indexDiscreteValue);
786 filterFuncs.push(
787 getDiscreteFilterFunc(lookupIndices[lookupIndices.length-1]));
788 }
789 }
790
791 return filterFuncs;
792 }
793
794 /**
795 * Helper function that extracts the values of free form columns from
796 * the lookupIndex and injects them into the Column object as FilterOpt
797 * objects.
798 **/
799 function setFreeFormFilterOptions(filterCols, lookupIndices) {
800 var temp, k;
801 for(var i=0, len=filterCols.length; i<len; i++) {
802 if (filterCols[i].ftype === c.FILTER_FREE_FORM) {
803 temp = []
804 for(k in lookupIndices[i]) {
805 if (lookupIndices[i].hasOwnProperty(k)) {
806 temp.push(new FilterOpt(k, lookupIndices[i][k].length));
807 }
808 }
809 filterCols[i].setFilterOptions(temp);
810 }
811 }
812 }
813
814 /**
815 * Index a discrete column (column with fixed number of values).
816 *
817 **/
818 function indexDiscreteValue(lookupIndex, col, dataVal, dataRowIndex) {
819 var i = col.indexOfOptVal(dataVal);
820 lookupIndex[i].push(dataRowIndex);
821 }
822
823 /**
824 * Index a column with free form text (= not fixed upfront)
825 *
826 **/
827 function indexFreeFormValue(lookupIndex, col, dataVal, dataRowIndex) {
828 if (!lookupIndex[dataVal]) {
829 lookupIndex[dataVal] = [];
830 }
831 lookupIndex[dataVal].push(dataRowIndex);
832 }
833
834
835 /**
836 * Get the function to filter a column with the given lookup index
837 * for discrete (fixed upfront) values.
838 *
839 **/
840 function getDiscreteFilterFunc(lookupIndex) {
841 return function(filterVal) {
842 var result = [];
843 for(var i=0, len=lookupIndex.length; i < len; i++) {
844 if (filterVal[i]) {
845 // append the indices to the current array
846 result.push.apply(result, lookupIndex[i]);
847 }
848 }
849 return { nofilter: false, records: result };
850 };
851 }
852
853 /**
854 * Get the function to filter a column with the given lookup index
855 * for free form values.
856 *
857 **/
858 function getFreeFormFilterFunc(lookupIndex) {
859 return function(filterVal) {
860 filterVal = filterVal.trim();
861 if (filterVal === '') {
862 return { nofilter: true };
863 }
864 return {
865 nofilter: false,
866 records: lookupIndex[filterVal] || []
867 };
868 };
869 }
870
871 /**
872 * Filters the data based on the given filterColumns and
873 * corresponding filter values.
874 *
875 * @return {[]} Subset of the input dataset based on the
876 * filter values.
877 **/
878 function filterData(data, filterFuncs, filterVals) {
879 var recordSets = [];
880 var filterResult;
881
882 // run through all the filters
883 for(var i=0, len=filterFuncs.length; i<len; i++) {
884 filterResult = filterFuncs[i](filterVals[i]);
885 if (!filterResult.nofilter) {
886 recordSets.push(filterResult.records);
887 }
888 }
889
890 // If there are no restrictions then return the whole dataset.
891 if (recordSets.length === 0) {
892 return data;
893 }
894
895 // intersect the records returned by filters.
896 var targets = intersectArrs(recordSets);
897 var result = [];
898 for(var i=0, len=targets.length; i<len; i++) {
899 result.push(data[targets[i]]);
900 }
901
902 return result;
903 }
904
905 /**
906 * Creates an object where the keys are the elements of the input array
907 * and the values are true. To be used for set operations with integer.
908 **/
909 function arrToObj(arr) {
910 var o = {};
911 var i,len;
912 for(i=0, len=arr.length; i<len; i++) {
913 o[arr[i]] = true;
914 }
915 return o;
916 }
917
918 /**
919 * Converts the keys of an object to an array after converting
920 * each key to integer. To be used for set operations with integers.
921 **/
922 function objToArr(obj) {
923 var result = [];
924 for(var k in obj) {
925 if (obj.hasOwnProperty(k)) {
926 result.push(parseInt(k));
927 }
928 }
929 return result;
930 }
931
932 /**
933 * Find the intersection of a set of arrays.
934 **/
935 function intersectArrs(sets) {
936 var temp, obj;
937
938 if (sets.length === 1) {
939 return sets[0];
940 }
941
942 // sort by size and load the smallest into the object
943 sets.sort(function(a,b) { return a.length - b.length; });
944 obj = arrToObj(sets[0]);
945
946 // shrink the hash as we fail to find elements in the other sets
947 for(var i=1, len=sets.length; i<len; i++) {
948 temp = arrToObj(sets[i]);
949 for(var k in obj) {
950 if (obj.hasOwnProperty(k) && !temp[k]) {
951 delete obj[k];
952 }
953 }
954 }
955
956 return objToArr(obj);
957 }
958
959 /**
960 * Extract the column values from an ImagePair (contained in the server
961 * response) into filter and data columns.
962 *
963 * @return {[]} Array of data contained in one data row.
964 **/
965 function getColValues(imagePair, filterCols, otherCols) {
966 var result = [];
967 for(var i=0, len=filterCols.length; i<len; i++) {
968 result.push(imagePair.extraColumns[filterCols[i].key]);
969 }
970
971 for(var i=0, len=otherCols.length; i<len; i++) {
972 result.push(get_robust(imagePair, ['expectations', otherCols[i].key]));
973 }
974
975 return result;
976 }
977
978 /**
979 * Extract the image meta data from an Image pair returned by the server.
980 **/
981 function getImageValues(imagePair, imageCols) {
982 var result=[];
983 var url, value, percent, diff;
984 var CO = c.IMG_COL_ORDER;
985
986 for(var i=0, len=imageCols.length; i<len; i++) {
987 percent = get_robust(imagePair, CO[i].percentField);
988 value = get_robust(imagePair, CO[i].valueField);
989 url = get_robust(imagePair, CO[i].urlField);
990 if (url) {
991 url = imageCols[i].baseUrl + url;
992 }
993 result.push(new ImgVal(url, percent, value));
994 }
995
996 return result;
997 }
998
999 /**
1000 * Given an object find sub objects for the given index without
1001 * throwing an error if any of the sub objects do not exist.
1002 **/
1003 function get_robust(obj, idx) {
1004 if (!idx) {
1005 return;
1006 }
1007
1008 for(var i=0, len=idx.length; i<len; i++) {
1009 if ((typeof obj === 'undefined') || (!idx[i])) {
1010 return; // returns 'undefined'
1011 }
1012
1013 obj = obj[idx[i]];
1014 }
1015
1016 return obj;
1017 }
1018
1019 /**
1020 * Set all elements in the array to the given value.
1021 **/
1022 function setArrVals(arr, newVal) {
1023 for(var i=0, len=arr.length; i<len; i++) {
1024 arr[i] = newVal;
1025 }
1026 }
1027
1028 /**
1029 * Toggle the elements of a boolean array.
1030 *
1031 **/
1032 function toggleArrVals(arr) {
1033 for(var i=0, len=arr.length; i<len; i++) {
1034 arr[i] = !arr[i];
1035 }
1036 }
1037
1038 /**
1039 * Sort the array of DataRow instances with the given compare functions
1040 * and the column at the given index either in ascending or descending order.
1041 **/
1042 function sortData (allData, compareFunctions, sortByIdx, sortOrderAsc) {
1043 var cmpFn = compareFunctions[sortByIdx];
1044 var useCmp = cmpFn;
1045 if (!sortOrderAsc) {
1046 useCmp = function ( _ ) {
1047 return -cmpFn.apply(this, arguments);
1048 };
1049 }
1050 allData.sort(useCmp);
1051 }
1052
1053
1054 // ***************************** Services *********************************
1055
1056 /**
1057 * Encapsulates all interactions with the backend by handling
1058 * Urls and HTTP requests. Also exposes some utility functions
1059 * related to processing Urls.
1060 */
1061 app.factory('dataService', [ '$http', function ($http) {
1062 /** Backend related constants **/
1063 var c = {
1064 /** Url to retrieve failures */
1065 FAILURES: '/results/failures',
1066
1067 /** Url to retrieve all GM results */
1068 ALL: '/results/all'
1069 };
1070
1071 /**
1072 * Convenience function to retrieve all results.
1073 *
1074 * @return {Promise} Will resolve to either the data (success) or to
1075 * the HTTP response (error).
1076 **/
1077 function loadAll() {
1078 return httpGetData(c.ALL);
1079 }
1080
1081 /**
1082 * Make a HTTP get request with the given query parameters.
1083 *
1084 * @param {}
1085 * @param {}
1086 *
1087 * @return {}
1088 **/
1089 function httpGetData(url, queryParams) {
1090 var reqConfig = {
1091 method: 'GET',
1092 url: url,
1093 params: queryParams
1094 };
1095
1096 return $http(reqConfig).then(
1097 function(successResp) {
1098 return successResp.data;
1099 });
1100 }
1101
1102 /**
1103 * Takes an arbitrary number of objects and generates a Url encoded
1104 * query string.
1105 *
1106 **/
1107 function getQueryString( _params_ ) {
1108 var result = [];
1109 for(var i=0, len=arguments.length; i < len; i++) {
1110 if (arguments[i]) {
1111 for(var k in arguments[i]) {
1112 if (arguments[i].hasOwnProperty(k)) {
1113 result.push(encodeURIComponent(k) + '=' +
1114 encodeURIComponent(arguments[i][k]));
1115 }
1116 }
1117 }
1118 }
1119 return result.join("&");
1120 }
1121
1122 // Interface of the service:
1123 return {
1124 getQueryString: getQueryString,
1125 loadAll: loadAll
1126 };
1127
1128 }]);
1129
1130 })();
OLDNEW
« no previous file with comments | « gm/rebaseline_server/static/new/css/app.css ('k') | gm/rebaseline_server/static/new/new-index.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698