OLD | NEW |
---|---|
(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 })(); | |
OLD | NEW |