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

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

Powered by Google App Engine
This is Rietveld 408576698