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