source: trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy @ 2133

Last change on this file since 2133 was 2133, checked in by tjeerd@…, 11 years ago

VIS-70, VIS-74 and some boxplot stuff

File size: 67.6 KB
Line 
1/**
2 * Visualize Controller
3 *
4 * This controller enables the user to visualize his data
5 *
6 * @author  robert@thehyve.nl
7 * @since       20110825
8 * @package     dbnp.visualization
9 *
10 * Revision information:
11 * $Rev$
12 * $Author$
13 * $Date$
14 */
15package dbnp.visualization
16
17import dbnp.studycapturing.*;
18import grails.converters.JSON
19
20import org.dbnp.gdt.*
21
22class VisualizeController {
23        def authenticationService
24        def moduleCommunicationService
25    def infoMessage = []
26    def offlineModules = []
27    def infoMessageOfflineModules = []
28    final int CATEGORICALDATA = 0
29    final int NUMERICALDATA = 1
30    final int RELTIME = 2
31    final int DATE = 3
32
33        /**
34         * Shows the visualization screen
35         */
36        def index = {
37                [ studies: Study.giveReadableStudies( authenticationService.getLoggedInUser() )]
38        }
39
40        def getStudies = {
41                def studies = Study.giveReadableStudies( authenticationService.getLoggedInUser() );
42        return sendResults(studies)
43        }
44
45        /**
46         * Based on the study id contained in the parameters given by the user, a list of 'fields' is returned. This list can be used to select what data should be visualized
47         * @return List containing fields
48     * @see parseGetDataParams
49         * @see getFields
50         */
51    def getFields = {
52                def input_object
53                def studies
54
55                try{
56                        input_object = parseGetDataParams();
57                } catch(Exception e) {
58                        log.error("VisualizationController: getFields: "+e)
59            return returnError(400, "An error occured while retrieving the user input.")
60                }
61
62        // Check to see if we have enough information
63        if(input_object==null || input_object?.studyIds==null){
64            setInfoMessage("Please select a study.")
65            return sendInfoMessage()
66        } else {
67            studies = input_object.studyIds[0]
68        }
69
70                def fields = [];
71
72        /*
73         Gather fields related to this study from GSCF.
74         This requires:
75         - a study.
76         - a category variable, e.g. "events".
77         - a type variable, either "domainfields" or "templatefields".
78         */
79        // TODO: Handle multiple studies
80        def study = Study.get(studies)
81
82        if(study!=null){
83            fields += getFields(study, "subjects", "domainfields")
84            fields += getFields(study, "subjects", "templatefields")
85            /*fields += getFields(study, "events", "domainfields")
86            fields += getFields(study, "events", "templatefields")*/
87            fields += getFields(study, "samplingEvents", "domainfields")
88            fields += getFields(study, "samplingEvents", "templatefields")
89            fields += getFields(study, "assays", "domainfields")
90            fields += getFields(study, "assays", "templatefields")
91            fields += getFields(study, "samples", "domainfields")
92            fields += getFields(study, "samples", "templatefields")
93
94                        // Also make sure the user can select eventGroup to visualize
95                        fields += formatGSCFFields( "domainfields", [ name: "name" ], "GSCF", "eventGroups" );
96                       
97            /*
98            Gather fields related to this study from modules.
99            This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
100            It does not actually return measurements (the getMeasurementData call does).
101            The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
102            types related to these assays.
103            So, the required variables for such a call are:
104              - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
105              - an assay, which can be obtained with study.getAssays()
106             */
107            study.getAssays().each { assay ->
108                def list = []
109                if(!offlineModules.contains(assay.module.id)){
110                    list = getFields(assay.module.toString(), assay)
111                    if(list!=null){
112                        if(list.size()!=0){
113                            fields += list
114                        }
115                    }
116                }
117            }
118            offlineModules = []
119
120            // Make sure any informational messages regarding offline modules are submitted to the client
121            setInfoMessageOfflineModules()
122
123
124            // TODO: Maybe we should add study's own fields
125        } else {
126            log.error("VisualizationController: getFields: The requested study could not be found. Id: "+studies)
127            return returnError(404, "The requested study could not be found.")
128        }
129
130        fields.unique() // Todo: find out root cause of why some fields occur more than once
131        fields.sort { a, b ->
132            def sourceEquality = a.source.toString().toLowerCase().compareTo(b.source.toString().toLowerCase())
133            if( sourceEquality == 0 ) {
134                def categoryEquality = a.category.toString().toLowerCase().compareTo(b.category.toString().toLowerCase())
135                if( categoryEquality == 0 ){
136                    a.name.toString().toLowerCase().compareTo(b.name.toString().toLowerCase())
137                } else return categoryEquality
138            } else return sourceEquality
139        }
140                return sendResults(['studyIds': studies, 'fields': fields])
141        }
142
143        /**
144         * Based on the field ids contained in the parameters given by the user, a list of possible visualization types is returned. This list can be used to select how data should be visualized.
145         * @return List containing the possible visualization types, with each element containing
146     *          - a unique id
147     *          - a unique name
148     *         For example: ["id": "barchart", "name": "Barchart"]
149     * @see parseGetDataParams
150         * @see determineFieldType
151     * @see determineVisualizationTypes
152         */
153        def getVisualizationTypes = {
154        def inputData = parseGetDataParams();
155
156        if(inputData.columnIds == null || inputData.columnIds == [] || inputData.columnIds[0] == null || inputData.columnIds[0] == ""){
157            setInfoMessage("Please select a data source for the x-axis.")
158            return sendInfoMessage()
159        }
160        if(inputData.rowIds == null || inputData.rowIds == [] ||  inputData.rowIds[0] == null ||   inputData.rowIds[0] == ""){
161            setInfoMessage("Please select a data source for the y-axis.")
162            return sendInfoMessage()
163        }
164
165        // TODO: handle the case of multiple fields on an axis
166        // Determine data types
167        log.trace "Determining rowType: "+inputData.rowIds[0]
168        def rowType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0])
169       
170                log.trace "Determining columnType: "+inputData.columnIds[0]
171        def columnType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0])
172
173                log.trace "Determining groupType: "+inputData.groupIds[0]
174                def groupType = determineFieldType(inputData.studyIds[0], inputData.groupIds[0])
175               
176                        // Determine possible visualization- and aggregationtypes
177        def visualizationTypes = determineVisualizationTypes(rowType, columnType)
178                def aggregationTypes = determineAggregationTypes(rowType, columnType, groupType)
179               
180        log.trace  "visualization types: " + visualizationTypes + ", determined this based on "+rowType+" and "+columnType
181                log.trace  "aggregation   types: " + aggregationTypes + ", determined this based on "+rowType+" and "+columnType + " and " + groupType
182               
183                def fieldData = [ 'x': parseFieldId( inputData.columnIds[ 0 ] ), 'y': parseFieldId( inputData.rowIds[ 0 ] ) ];
184               
185        return sendResults([
186                        'types': visualizationTypes,
187                        'aggregations': aggregationTypes,
188                       
189                        // TODO: Remove these ids when the view has been updated. Use xaxis.id and yaxis.id instead
190                        'rowIds':inputData.rowIds[0],
191                        'columnIds':inputData.columnIds[0],
192                       
193                        'xaxis': [ 
194                                'id': fieldData.x.fieldId,
195                                'name': fieldData.x.name,
196                                'unit': fieldData.x.unit,
197                                'type': dataTypeString( columnType )
198                        ],
199                        'yaxis': [
200                                'id': fieldData.y.fieldId,
201                                'name': fieldData.y.name,
202                                'unit': fieldData.y.unit,
203                                'type': dataTypeString( rowType )
204                        ],
205
206                ])
207        }
208
209    /**
210     * Gather fields related to this study from modules.
211        This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
212        getMeasurements does not actually return measurements (the getMeasurementData call does).
213     * @param source    The id of the module that is the source of the requested fields, as can be obtained from AssayModule.list() (use the 'id' field)
214     * @param assay     The assay that the source module and the requested fields belong to
215     * @return  A list of map objects, containing the following:
216     *           - a key 'id' with a value formatted by the createFieldId function
217     *           - a key 'source' with a value equal to the input parameter 'source'
218     *           - a key 'category' with a value equal to the 'name' field of the input paramater 'assay'
219     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
220     */
221    def getFields(source, assay) {
222        def fields = []
223        def callUrl = ""
224
225        // Making a different call for each assay
226        def urlVars = "assayToken="+assay.assayUUID
227        try {
228            callUrl = ""+assay.module.url + "/rest/getMeasurementMetaData/query?"+urlVars
229            def json = moduleCommunicationService.callModuleRestMethodJSON( assay.module.url /* consumer */, callUrl );
230
231            def collection = []
232            json.each{ jason ->
233                collection.add(jason)
234            }
235            // Formatting the data
236            collection.each { field ->
237                // For getting this field from this assay
238                fields << [ "id": createFieldId( id: field.name, name: field.name, source: ""+assay.id, type: ""+assay.name, unit: (field.unit?:"")), "source": source, "category": ""+assay.name, "name": field.name + (field.unit?" ("+field.unit+")":"")  ]
239            }
240        } catch(Exception e){
241            //returnError(404, "An error occured while trying to collect field data from a module. Most likely, this module is offline.")
242            offlineModules.add(assay.module.id)
243            infoMessageOfflineModules.add(assay.module.name)
244            log.error("VisualizationController: getFields: "+e)
245        }
246
247        return fields
248    }
249
250    /**
251     * Gather fields related to this study from GSCF.
252     * @param study The study that is the source of the requested fields
253     * @param category  The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
254     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
255     * @return A list of map objects, formatted by the formatGSCFFields function
256     */
257    def getFields(study, category, type){
258        // Collecting the data from it's source
259        def collection = []
260        def fields = []
261        def source = "GSCF"
262               
263                if( type == "domainfields" ) 
264                        collection = domainObjectCallback( category )?.giveDomainFields();
265                else
266                        collection = templateObjectCallback( category, study )?.template?.fields
267
268        collection?.unique()
269
270        // Formatting the data
271        fields += formatGSCFFields(type, collection, source, category)
272
273        // Here we will remove those fields, whose set of datapoints only contain null
274        def fieldsToBeRemoved = []
275        fields.each{ field ->
276            def fieldData = getFieldData( study, study.samples, field.id )
277            fieldData.removeAll([null])
278            if(fieldData==[]){
279                // Field only contained nulls, so don't show it as a visualization option
280                fieldsToBeRemoved << field
281            }
282        }
283        fields.removeAll(fieldsToBeRemoved)
284
285        return fields
286    }
287
288    /**
289     * Format the data contained in the input parameter 'collection' for use as so-called fields, that will be used by the user interface to allow the user to select data from GSCF for visualization
290     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
291     * @param collectionOfFields A collection of fields, which could also contain only one item
292     * @param source Likely to be "GSCF"
293     * @param category The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
294     * @return A list containing list objects, containing the following:
295     *           - a key 'id' with a value formatted by the createFieldId function
296     *           - a key 'source' with a value equal to the input parameter 'source'
297     *           - a key 'category' with a value equal to the input parameter 'category'
298     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
299     */
300    def formatGSCFFields(type, collectionOfFields, source, category){
301
302        if(collectionOfFields==null || collectionOfFields == []){
303            return []
304        }
305        def fields = []
306        if(collectionOfFields instanceof Collection){
307            // Apparently this field is actually a list of fields.
308            // We will call ourselves again with the list's elements as input.
309            // These list elements will themselves go through this check again, effectively flattening the original input
310            for(int i = 0; i < collectionOfFields.size(); i++){
311                fields += formatGSCFFields(type, collectionOfFields[i], source, category)
312            }
313            return fields
314        } else {
315            // This is a single field. Format it and return the result.
316            if(type=="domainfields"){
317                fields << [ "id": createFieldId( id: collectionOfFields.name, name: collectionOfFields.name, source: source, type: category, unit: (collectionOfFields.unit?:"") ), "source": source, "category": category, "name": collectionOfFields.name + (collectionOfFields.unit?" ("+collectionOfFields.unit+")":"") ]
318            }
319            if(type=="templatefields"){
320                fields << [ "id": createFieldId( id: collectionOfFields.id.toString(), name: collectionOfFields.name, source: source, type: category, unit: (collectionOfFields.unit?:"") ), "source": source, "category": category, "name": collectionOfFields.name + (collectionOfFields.unit?" ("+collectionOfFields.unit+")":"")]
321            }
322            return fields
323        }
324    }
325
326        /**
327         * Retrieves data for the visualization itself.
328     * Returns, based on the field ids contained in the parameters given by the user, a map containing the actual data and instructions on how the data should be visualized.
329     * @return A map containing containing (at least, in the case of a barchart) the following:
330     *           - a key 'type' containing the type of chart that will be visualized
331     *           - a key 'xaxis' containing the title and unit that should be displayed for the x-axis
332     *           - a key 'yaxis' containing the title and unit that should be displayed for the y-axis*
333     *           - a key 'series' containing a list, that contains one or more maps, which contain the following:
334     *                - a key 'name', containing, for example, a feature name or field name
335     *                - a key 'y', containing a list of y-values
336     *                - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values, each having the same index as the 'y'-values they are associated with
337         */
338        def getData = {
339                // Extract parameters
340                // TODO: handle erroneous input data
341                def inputData = parseGetDataParams();
342               
343        if(inputData.columnIds == null || inputData.rowIds == null){
344            infoMessage = "Please select data sources for the y- and x-axes."
345            return sendInfoMessage()
346        }
347
348                // TODO: handle the case that we have multiple studies
349                def studyId = inputData.studyIds[ 0 ];
350                def study = Study.get( studyId as Integer );
351
352                // Find out what samples are involved
353                def samples = study.samples
354
355        // If the user is requesting data that concerns only subjects, then make sure those subjects appear only once
356        if(parseFieldId( inputData.columnIds[ 0 ] ).type=='subjects' && parseFieldId( inputData.rowIds[ 0 ] ).type=='subjects'){
357            samples.unique { it.parentSubject }
358        }
359       
360                // Retrieve the data for both axes for all samples
361                // TODO: handle the case of multiple fields on an axis
362                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ], "group": inputData.groupIds[ 0 ] ];
363                def fieldInfo = [:]
364                fields.each { 
365                        fieldInfo[ it.key ] = parseFieldId( it.value ) 
366                        if( fieldInfo[ it.key ] )
367                                fieldInfo[ it.key ].fieldType = determineFieldType( study.id, it.value );
368                               
369                }
370               
371                // If the groupAxis is numerical, we should ignore it, unless a table is asked for
372                if( fieldInfo.group && fieldInfo.group.fieldType == NUMERICALDATA && inputData.visualizationType != "table" ) {
373                        fields.group = null;
374                        fieldInfo.group = null;
375                }
376
377                println "Fields: "
378                fieldInfo.each { println it }
379
380                // Fetch all data from the system. data will be in the format:
381                //              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "NL", "NL" ]
382                //      If a field is not given, the data will be NULL
383                def data = getAllFieldData( study, samples, fields );
384
385                println "All Data: "
386                data.each { println it }
387
388        // Aggregate the data based on the requested aggregation
389                def aggregatedData = aggregateData( data, fieldInfo, inputData.aggregation );
390               
391                println "Aggregated Data: "
392                aggregatedData.each { println it }
393               
394                // No convert the aggregated data into a format we can use
395                def returnData = formatData( inputData.visualizationType, aggregatedData, fieldInfo );
396
397                println "Returndata: " 
398                returnData.each { println it }
399               
400        // Make sure no changes are written to the database
401        study.discard()
402        samples*.discard()
403       
404        return sendResults(returnData)
405        }
406
407        /**
408         * Parses the parameters given by the user into a proper list
409         * @return Map with 4 keys:
410         *              studyIds:       list with studyIds selected
411         *              rowIds:         list with fieldIds selected for the rows
412         *              columnIds:      list with fieldIds selected for the columns
413         *              visualizationType:      String with the type of visualization required
414         * @see getFields
415         * @see getVisualizationTypes
416         */
417        def parseGetDataParams() {
418                def studyIds = params.list( 'study' );
419                def rowIds = params.list( 'rows' );
420                def columnIds = params.list( 'columns' );
421                def groupIds = params.list( 'groups' ); 
422                def visualizationType = params.get( 'types');
423                def aggregation = params.get( 'aggregation' );
424
425                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "groupIds": groupIds, "visualizationType": visualizationType, "aggregation": aggregation ];
426        }
427
428        /**
429         * Retrieve the field data for the selected fields
430         * @param study         Study for which the data should be retrieved
431         * @param samples       Samples for which the data should be retrieved
432         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
433         *                                              [ "x": "field-id-1", "y": "field-id-3", "group": "field-id-6" ]
434         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the
435         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example:
436         *                                              [ "numValues": 4, "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "NL", "NL" ] ]
437         */
438        def getAllFieldData( study, samples, fields ) {
439                def fieldData = [:]
440                def numValues = 0;
441                fields.each{ field ->
442                        def fieldId = field.value ?: null;
443                        fieldData[ field.key ] = getFieldData( study, samples, fieldId );
444                       
445                        if( fieldData[ field.key ] )
446                                numValues = Math.max( numValues, fieldData[ field.key ].size() );
447                }
448               
449                fieldData.numValues = numValues;
450               
451                return fieldData;
452        }
453       
454        /**
455        * Retrieve the field data for the selected field
456        * @param study          Study for which the data should be retrieved
457        * @param samples        Samples for which the data should be retrieved
458        * @param fieldId        ID of the field to return data for
459        * @return                       A list of values of the selected field for all samples. If a value
460        *                                       could not be retrieved for a sample, null is returned. Examples:
461        *                                               [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
462        */
463        def getFieldData( study, samples, fieldId ) {
464                if( !fieldId )
465                        return null
466                       
467                // Parse the fieldId as given by the user
468                def parsedField = parseFieldId( fieldId );
469               
470                def data = []
471               
472                if( parsedField.source == "GSCF" ) {
473                        // Retrieve data from GSCF itself
474                        def closure = valueCallback( parsedField.type )
475
476                        if( closure ) {
477                                samples.each { sample ->
478                                        // Retrieve the value for the selected field for this sample
479                                        def value = closure( sample, parsedField.name );
480
481                    data << value;
482                                }
483                        } else {
484                                // TODO: Handle error properly
485                                // Closure could not be retrieved, probably because the type is incorrect
486                                data = samples.collect { return null }
487                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
488                        }
489                } else {
490                        // Data must be retrieved from a module
491                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
492                }
493               
494                return data
495        }
496       
497        /**
498         * Retrieve data for a given field from a data module
499         * @param study                 Study to retrieve data for
500         * @param samples               Samples to retrieve data for
501         * @param source_module Name of the module to retrieve data from
502         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
503         * @return                              A list of values of the selected field for all samples. If a value
504         *                                              could not be retrieved for a sample, null is returned. Examples:
505         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
506         */
507        def getModuleData( study, samples, assay_id, fieldName ) {
508                def data = []
509               
510                // TODO: Handle values that should be retrieved from multiple assays
511        def assay = Assay.get(assay_id);
512
513        if( assay ) {
514            // Request for a particular assay and a particular feature
515            def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName.encodeAsURL()
516            urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
517
518            def callUrl
519            try {
520                callUrl = assay.module.url + "/rest/getMeasurementData"
521                def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
522
523                if( json ) {
524                    // First element contains sampletokens
525                    // Second element contains the featurename
526                    // Third element contains the measurement value
527                    def sampleTokens = json[ 0 ]
528                    def measurements = json[ 2 ]
529
530                    // Loop through the samples
531                    samples.each { sample ->
532                        // Search for this sampletoken
533                        def sampleToken = sample.sampleUUID;
534                        def index = sampleTokens.findIndexOf { it == sampleToken }
535                                               
536                                                // Store the measurement value if found and if it is not JSONObject$Null
537                                                // See http://grails.1312388.n4.nabble.com/The-groovy-truth-of-JSONObject-Null-td3661040.html
538                                                // for this comparison
539                        if( index > -1  && !measurements[ index ].equals( null ) ) {
540                            data << measurements[ index ];
541                        } else {
542                            data << null
543                        }
544                    }
545                } else {
546                    // TODO: handle error
547                    // Returns an empty list with as many elements as there are samples
548                    data = samples.collect { return null }
549                }
550
551            } catch(Exception e){
552                log.error("VisualizationController: getFields: "+e)
553                //return returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
554                return returnError(404, "Unfortunately, "+assay.module.name+" could not be reached. As a result, we cannot at this time visualize data contained in this module.")
555            }
556        } else {
557            // TODO: Handle error correctly
558            // Returns an empty list with as many elements as there are samples
559            data = samples.collect { return null }
560        }
561
562        //println "\t data request: "+data
563                return data
564        }
565       
566        /**
567         * Aggregates the data based on the requested aggregation on the categorical fields
568         * @param data                  Map with data for each dimension as retrieved using getAllFieldData. For example:
569         *                                                      [ "x": [ 3, 6, 8, 10 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "NL", "NL" ] ]
570         * @param fieldInfo             Map with field information for each dimension. For example:
571         *                                                      [ "x": [ id: "abc", "type": NUMERICALDATA ], "y": [ "id": "def", "type": CATEGORICALDATA ] ]
572         * @param aggregation   Kind of aggregation requested
573         * @return                              Data that is aggregated on the categorical fields
574         *                                                      [ "x": [ 3, 6, null, 9 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "US", "NL" ] ]
575         *
576         */
577        def aggregateData( data, fieldInfo, aggregation ) {
578                // If no aggregation is requested, we just return the original object (but filtering out null values)
579                if( aggregation == "none" ) {
580                        return sortNonAggregatedData( filterNullValues( data ), [ 'x', 'group' ] );
581                }
582               
583                // Determine the categorical fields
584                def dimensions = [ "categorical": [], "numerical": [] ];
585                fieldInfo.each { 
586                        // If fieldInfo value is NULL, the field is not requested
587                        if( it && it.value ) {
588                                if( [ CATEGORICALDATA, RELTIME, DATE ].contains( it.value.fieldType ) ) {
589                                        dimensions.categorical << it.key
590                                } else {
591                                        dimensions.numerical << it.key
592                                }
593                        }
594                }
595               
596                // Compose a map with aggregated data
597                def aggregatedData = [:];
598                fieldInfo.each { aggregatedData[ it.key ] = [] }
599               
600                // Loop through all categorical fields and aggregate the values for each combination
601                if( dimensions.categorical.size() > 0 ) {
602                        return aggregate( data, dimensions.categorical, dimensions.numerical, aggregation, fieldInfo );
603                } else {
604                        // No categorical dimensions. Just compute the aggregation for all values together
605                        def returnData = [ "count": [ data.numValues ] ];
606                 
607                        // Now compute the correct aggregation for each numerical dimension.
608                        dimensions.numerical.each { numericalDimension ->
609                                def currentData = data[ numericalDimension ];
610                                returnData[ numericalDimension ] = [ computeAggregation( aggregation, currentData ).value ];
611                        }
612                       
613                        return returnData;
614                }
615        }
616       
617        /**
618         * Sort the data that has not been aggregated by the given columns
619         * @param data
620         * @param sortBy
621         * @return
622         */
623        protected def sortNonAggregatedData( data, sortBy ) {
624                if( !sortBy )
625                        return data;
626                       
627                // First combine the lists within the data
628                def combined = [];
629                for( int i = 0; i < data.numValues; i++ ) {
630                        def element = [:]
631                        data.each {
632                                if( it.value instanceof Collection && i < it.value.size() ) {
633                                        element[ it.key ] = it.value[ i ];
634                                }
635                        }
636                       
637                        combined << element;
638                }
639               
640                // Now sort the combined element with a comparator
641                def comparator = { a, b ->
642                        for( column in sortBy ) {
643                                if( a[ column ] != null ) {
644                                        if( a[ column ] != b[ column ] ) {
645                                                if( a[ column ] instanceof Number )
646                                                        return a[ column ] <=> b[ column ];
647                                                else
648                                                        return a[ column ].toString() <=> b[ column ].toString();
649                                        }
650                                }
651                        }
652               
653                        return 0;
654                }
655               
656                combined.sort( comparator as Comparator );
657               
658                // Put the elements back again. First empty the original lists
659                data.each {
660                        if( it.value instanceof Collection ) {
661                                it.value = [];
662                        }
663                }
664               
665                combined.each { element ->
666                        element.each { key, value ->
667                                data[ key ] << value;
668                        } 
669                }
670
671                return data;
672        }
673       
674        /**
675         * Filter null values from the different lists of data. The associated values in other lists will also be removed
676         * @param data 
677         * @return      Filtered data.
678         *
679         * [ 'x': [0, 2, 4], 'y': [1,null,2] ] will result in [ 'x': [0, 4], 'y': [1, 2] ]
680         * [ 'x': [0, 2, 4], 'y': [1,null,2], 'z': [4, null, null] ] will result in [ 'x': [0], 'y': [1], 'z': [4] ]
681         */
682        protected def filterNullValues( data ) {
683                def filteredData = [:];
684               
685                // Create a copy of the data object
686                data.each {
687                        filteredData[ it.key ] = it.value;
688                }
689               
690                // Loop through all values and return all null-values (and the same indices on other elements
691                int num = filteredData.numValues;
692                filteredData.keySet().each { fieldName ->
693                        // If values are found, do filtering. If not, skip this one
694                        if( filteredData[ fieldName ] != null && filteredData[ fieldName ] instanceof Collection ) {
695                                // Find all non-null values in this list
696                                def indices = filteredData[ fieldName ].findIndexValues { it != null };
697                               
698                                // Remove the non-null values from each data object
699                                filteredData.each { key, value ->
700                                        if( value && value instanceof Collection )
701                                                filteredData[ key ] = value[ indices ];
702                                }
703                               
704                                // Store the number of values
705                                num = indices.size();
706                        }
707                }
708               
709                filteredData.numValues = num;
710                 
711                return filteredData
712        }
713       
714        /**
715         * Aggregates the given data on the categorical dimensions.
716         * @param data                                  Initial data
717         * @param categoricalDimensions List of categorical dimensions to group  by
718         * @param numericalDimensions   List of all numerical dimensions to compute the aggregation for
719         * @param aggregation                   Type of aggregation requested
720         * @param fieldInfo                             Information about the fields requested by the user      (e.g. [ "x": [ "id": 1, "fieldType": CATEGORICALDATA ] ] )
721         * @param criteria                              The criteria the current aggregation must keep (e.g. "x": "male")
722         * @param returnData                    Initial return object with the same keys as the data object, plus 'count'
723         * @return
724         */
725        protected def aggregate( Map data, Collection categoricalDimensions, Collection numericalDimensions, String aggregation, fieldInfo, criteria = [:], returnData = null ) {
726                if( !categoricalDimensions )
727                        return data;
728                       
729                // If no returndata is given, initialize the map
730                if( returnData == null ) {
731                        returnData = [ "count": [] ]
732                        data.each { returnData[ it.key ] = [] }
733                }
734               
735                def dimension = categoricalDimensions.head();
736               
737                // Determine the unique values on the categorical axis and sort by toString method
738                def unique = data[ dimension ].flatten()
739                                        .unique { it == null ? "null" : it.class.name + it.toString() }
740                                        .sort {
741                                                // Sort categoricaldata on its string value, but others (numerical, reltime, date)
742                                                // on its real value
743                                                switch( fieldInfo[ dimension ].fieldType ) {
744                                                        case CATEGORICALDATA:
745                                                                return it.toString()
746                                                        default:
747                                                                return it
748                                                } 
749                                        };
750                                       
751                // Make sure the null category is last
752                unique = unique.findAll { it != null } + unique.findAll { it == null }
753               
754                unique.each { el ->
755                        // Use this element to search on
756                        criteria[ dimension ] = el;
757                       
758                        // If the list of categoricalDimensions is empty after this dimension, do the real work
759                        if( categoricalDimensions.size() == 1 ) {
760                                // Search for all elements in the numericaldimensions that belong to the current group
761                                // The current group is defined by the criteria object
762                               
763                                // We start with all indices belonging to this group
764                                def indices = 0..data.numValues;
765                                criteria.each { criterion ->
766                                        // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
767                                        // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
768                                        def currentIndices = data[ criterion.key ].findIndexValues { it instanceof Collection ? it.contains( criterion.value ) : it == criterion.value };
769                                        indices = indices.intersect( currentIndices );
770                                       
771                                        // Store the value for the criterion in the returnData object
772                                        returnData[ criterion.key ] << criterion.value;
773                                }
774                               
775                                // If no numericalDimension is asked for, no aggregation is possible. For that reason, we
776                                // also return counts
777                                returnData[ "count" ] << indices.size();
778                                 
779                                // Now compute the correct aggregation for each numerical dimension.
780                                numericalDimensions.each { numericalDimension ->
781                                        def currentData = data[ numericalDimension ][ indices ]; 
782                                        returnData[ numericalDimension ] << computeAggregation( aggregation, currentData ).value;
783                                }
784                               
785                        } else {
786                                returnData = aggregate( data, categoricalDimensions.tail(), numericalDimensions, aggregation, fieldInfo, criteria, returnData );
787                        }
788                }
789               
790                return returnData;
791        }
792       
793        /**
794         * Compute the aggregation for a list of values
795         * @param aggregation
796         * @param currentData
797         * @return
798         */
799        def computeAggregation( String aggregation, List currentData ) {
800                switch( aggregation ) {
801                        case "count":
802                                return computeCount( currentData );
803                                break;
804                        case "median":
805                                return computePercentile( currentData, 50 );
806                                break;
807                        case "sum":
808                                return computeSum( currentData );
809                                break;
810                        case "average":
811                        default:
812                                // Default is "average"
813                                return computeMeanAndError( currentData );
814                                break;
815                }
816        }
817
818        /**
819         * Formats the grouped data in such a way that the clientside visualization method
820         * can handle the data correctly.
821         * @param groupedData   Data that has been grouped using the groupFields method
822         * @param fieldData             Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
823         *                                                      [ "x": { "id": ... }, "y": { "id": "field-id-3" }, "group": { "id": "field-id-6" } ]
824         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
825         * @return                              A map like the following:
826         *
827                        {
828                                "type": "barchart",
829                                "xaxis": { "title": "quarter 2011", "unit": "" },
830                                "yaxis": { "title": "temperature", "unit": "degrees C" },
831                                "series": [
832                                        {
833                                                "name": "series name",
834                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
835                        "x": [ "Q1", "Q2", "Q3", "Q4" ],
836                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
837                                        },
838                                ]
839                        }
840         *
841         */
842        def formatData( type, groupedData, fieldInfo, xAxis = "x", yAxis = "y", serieAxis = "group", errorName = "error" ) {
843                // Format categorical axes by setting the names correct
844                fieldInfo.each { field, info ->
845                        if( field && info ) {
846                                groupedData[ field ] = renderFieldsHumanReadable( groupedData[ field ], info.fieldType)
847                        }
848                }
849               
850                // TODO: Handle name and unit of fields correctly
851                def xAxisTypeString = dataTypeString( fieldInfo[ xAxis ]?.fieldType )
852                def yAxisTypeString = dataTypeString( fieldInfo[ yAxis ]?.fieldType )
853                def serieAxisTypeString = dataTypeString( fieldInfo[ serieAxis ]?.fieldType )
854               
855                // Create a return object
856                def return_data = [:]
857                return_data[ "type" ] = type
858                return_data.put("xaxis", ["title" : fieldInfo[ xAxis ]?.name, "unit": fieldInfo[ xAxis ]?.unit, "type": xAxisTypeString ])
859                return_data.put("yaxis", ["title" : fieldInfo[ yAxis ]?.name, "unit" : fieldInfo[ yAxis ]?.unit, "type": yAxisTypeString ])
860                return_data.put("groupaxis", ["title" : fieldInfo[ serieAxis ]?.name, "unit" : fieldInfo[ serieAxis ]?.unit, "type": serieAxisTypeString ])
861               
862                if(type=="table"){
863                        // Determine the lists on both axes. The strange addition is done because the unique() method
864                        // alters the object itself, instead of only returning a unique list
865                        def xAxisData = ([] + groupedData[ xAxis ]).unique()
866                        def yAxisData = ([] + groupedData[ yAxis ]).unique()
867
868                        if( !fieldInfo[ serieAxis ] ) {
869                                // If no value has been chosen on the serieAxis, we should show the counts for only one serie
870                                def tableData = formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, "count" );
871                               
872                                return_data.put("series", [[
873                                        "name": "count",
874                                        "x": xAxisData,
875                                        "y": yAxisData,
876                                        "data": tableData
877                                ]])
878                        } else if( fieldInfo[ serieAxis ].fieldType == NUMERICALDATA ) {
879                                // If no value has been chosen on the serieAxis, we should show the counts for only one serie
880                                def tableData = formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, serieAxis );
881
882                                // If a numerical field has been chosen on the serieAxis, we should show the requested aggregation
883                                // for only one serie
884                                return_data.put("series", [[
885                                        "name": fieldInfo[ xAxis ].name,
886                                        "x": xAxisData,
887                                        "y": yAxisData,
888                                        "data": tableData
889                                ]])
890                        } else {
891                                // If a categorical field has been chosen on the serieAxis, we should create a table for each serie
892                                // with counts as data. That table should include all data for that serie
893                                return_data[ "series" ] = [];
894                               
895                                // The strange addition is done because the unique() method
896                                // alters the object itself, instead of only returning a unique list
897                                def uniqueSeries = ([] + groupedData[ serieAxis ]).unique();
898                               
899                                uniqueSeries.each { serie -> 
900                                        def indices = groupedData[ serieAxis ].findIndexValues { it == serie }
901                                       
902                                        // If no value has been chosen on the serieAxis, we should show the counts for only one serie
903                                        def tableData = formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, "count", indices );
904       
905                                        return_data[ "series" ] << [
906                                                "name": serie,
907                                                "x": xAxisData,
908                                                "y": yAxisData,
909                                                "data": tableData,
910                                        ]
911                                }
912                        }
913                       
914                } else if(type=="boxplot") {
915            return_data[ "series" ] = [];
916            HashMap dataMap = new HashMap();
917            groupedData[ xAxis ].eachWithIndex { category, i ->
918                if(!dataMap.containsKey(category)) {
919                    dataMap.put(category, []);
920                }
921                dataMap.put(category, dataMap.get(category)+groupedData[ yAxis ][i]);
922            }
923
924            for ( String key : dataMap.keySet() ) {
925                def objInfos = computePercentile(dataMap.get(key),50);
926                double dblMEDIAN = objInfos.get("value");
927                double Q1 = computePercentile(dataMap.get(key),25).get("value");
928                double Q3 = computePercentile(dataMap.get(key),75).get("value");
929
930                // Calcultate 1.5* inter-quartile-distance
931                double dblIQD = (Q3-Q1)*1.5;
932
933                /* // DEBUG
934                println("---");
935                println("  dataMap["+key+"]:: "+dataMap.get(key));
936                println("  dblMEDIAN:: "+dblMEDIAN);
937                println("  dblIQD:: "+dblIQD);
938                println("  Q1:: "+Q1);
939                println("  Q3:: "+Q3);
940                println("---");
941                */
942
943                return_data[ "series" ] << [
944                        "name": key,
945                        "y" : [key, objInfos.get("max"), (dblMEDIAN+dblIQD), Q3, dblMEDIAN, Q1, (dblMEDIAN-dblIQD), objInfos.get("min")]
946                ];
947            }
948
949            //println(return_data);
950
951
952        } else {
953                        // For a horizontal barchart, the two axes should be swapped
954                        if( type == "horizontal_barchart" ) {
955                                def tmp = xAxis
956                                xAxis = yAxis
957                                yAxis = tmp
958                        }
959               
960                        if( !fieldInfo[ serieAxis ] ) {
961                                // If no series field has defined, we return all data in one serie
962                                return_data.put("series", [[
963                                        "name": "count",
964                                        "x": groupedData[ xAxis ],
965                                        "y": groupedData[ yAxis ],
966                                ]])
967                        } else if( fieldInfo[ serieAxis ].fieldType == NUMERICALDATA ) {
968                                // No numerical series field is allowed in a chart.
969                                throw new Exception( "No numerical series field is allowed here." );
970                        } else {
971                                // If a categorical field has been chosen on the serieAxis, we should create a group for each serie
972                                // with the correct values, belonging to that serie.
973                                return_data[ "series" ] = [];
974                               
975                                // The unique method alters the original object, so we
976                                // create a new object
977                                def uniqueSeries = ([] + groupedData[ serieAxis ]).unique();
978                               
979                                uniqueSeries.each { serie ->
980                                        def indices = groupedData[ serieAxis ].findIndexValues { it == serie }
981                                        return_data[ "series" ] << [
982                                                "name": serie,
983                                                "x": groupedData[ xAxis ][ indices ],
984                                                "y": groupedData[ yAxis ][ indices ]
985                                        ]
986                                }
987                        }
988                }
989               
990                return return_data;
991        }
992
993        /**
994         * Formats the requested data for a table       
995         * @param groupedData
996         * @param xAxisData
997         * @param yAxisData
998         * @param xAxis
999         * @param yAxis
1000         * @param dataAxis
1001         * @return
1002         */
1003        def formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, dataAxis, serieIndices = null ) {
1004                def tableData = []
1005               
1006                xAxisData.each { x ->
1007                        def colData = []
1008                       
1009                        def indices = groupedData[ xAxis ].findIndexValues { it == x }
1010                       
1011                        // If serieIndices are given, intersect the indices
1012                        if( serieIndices != null )
1013                                indices = indices.intersect( serieIndices );
1014                       
1015                        yAxisData.each { y ->
1016                                def index = indices.intersect( groupedData[ yAxis ].findIndexValues { it == y } );
1017                               
1018                                if( index.size() ) {
1019                                        colData << groupedData[ dataAxis ][ (int) index[ 0 ] ]
1020                                }
1021                        }
1022                        tableData << colData;
1023                }
1024               
1025                return tableData;
1026        }
1027
1028    /**
1029     * If the input variable 'data' contains dates or times according to input variable 'fieldInfo', these dates and times are converted to a human-readable version.
1030     * @param data  The list of items that needs to be checked/converted
1031     * @param axisType As determined by determineFieldType
1032     * @return The input variable 'data', with it's date and time elements converted.
1033     * @see determineFieldType
1034     */
1035    def renderFieldsHumanReadable(data, axisType){
1036        switch( axisType ) {
1037                        case RELTIME:
1038                                return renderTimesHumanReadable(data)
1039                        case DATE:
1040                                return renderDatesHumanReadable(data)
1041                        case CATEGORICALDATA:
1042                                return data.collect { it.toString() }
1043                        case NUMERICALDATA:
1044                        default:
1045                                return data;
1046                }
1047    }
1048
1049    /**
1050     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
1051     * @param data
1052     * @return
1053     */
1054    def renderTimesHumanReadable(data){
1055        def tmpTimeContainer = []
1056        data. each {
1057            if(it instanceof Number) {
1058                try{
1059                    tmpTimeContainer << new RelTime( it ).toPrettyString()
1060                } catch(IllegalArgumentException e){
1061                    tmpTimeContainer << it
1062                }
1063            } else {
1064                tmpTimeContainer << it // To handle items such as 'unknown'
1065            }
1066        }
1067        return tmpTimeContainer
1068    }
1069
1070    /**
1071     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
1072     * @param data
1073     * @return
1074     */
1075    def renderDatesHumanReadable(data) {
1076        def tmpDateContainer = []
1077        data. each {
1078            if(it instanceof Number) {
1079                try{
1080                    tmpDateContainer << new java.util.Date( (Long) it ).toString()
1081                } catch(IllegalArgumentException e){
1082                    tmpDateContainer << it
1083                }
1084            } else {
1085                tmpDateContainer << it // To handle items such as 'unknown'
1086            }
1087        }
1088        return tmpDateContainer
1089    }
1090        /**
1091         * Returns a closure for the given entitytype that determines the value for a criterion
1092         * on the given object. The closure receives two parameters: the sample and a field.
1093         *
1094         * For example:
1095         *              How can one retrieve the value for subject.name, given a sample? This can be done by
1096         *              returning the field values sample.parentSubject:
1097         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
1098         * @return      Closure that retrieves the value for a field and the given field
1099         */
1100        protected Closure valueCallback( String entity ) {
1101                switch( entity ) {
1102                        case "Study":
1103                        case "studies":
1104                                return { sample, field -> return getFieldValue( sample.parent, field ) }
1105                        case "Subject":
1106                        case "subjects":
1107                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
1108                        case "Sample":
1109                        case "samples":
1110                                return { sample, field -> return getFieldValue( sample, field ) }
1111                        case "Event":
1112                        case "events":
1113                                return { sample, field ->
1114                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
1115                                                return null
1116
1117                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
1118                                }
1119                        case "EventGroup":
1120                        case "eventGroups":
1121                                return { sample, field ->
1122                                        if( !sample || !sample.parentEventGroup )
1123                                                return null
1124
1125                                        // For eventgroups only the name is supported
1126                                        if( field == "name" )
1127                                                return sample.parentEventGroup.name
1128                                        else
1129                                                return null 
1130                                }
1131       
1132                        case "SamplingEvent":
1133                        case "samplingEvents":
1134                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
1135                        case "Assay":
1136                        case "assays":
1137                                return { sample, field ->
1138                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
1139                                        if( sampleAssays && sampleAssays.size() > 0 )
1140                                                return sampleAssays.collect { getFieldValue( it, field ) }
1141                                        else
1142                                                return null
1143                                }
1144                }
1145        }
1146       
1147        /**
1148        * Returns the domain object that should be used with the given entity string
1149        *
1150        * For example:
1151        *               What object should be consulted if the user asks for "studies"
1152        *               Response: Study
1153        * @return       Domain object that should be used with the given entity string
1154        */
1155   protected def domainObjectCallback( String entity ) {
1156           switch( entity ) {
1157                   case "Study":
1158                   case "studies":
1159                           return Study
1160                   case "Subject":
1161                   case "subjects":
1162                           return Subject
1163                   case "Sample":
1164                   case "samples":
1165                           return Sample
1166                   case "Event":
1167                   case "events":
1168                        return Event
1169                   case "SamplingEvent":
1170                   case "samplingEvents":
1171                           return SamplingEvent
1172                   case "Assay":
1173                   case "assays":
1174                                return Assay
1175                   case "EventGroup":
1176                   case "eventGroups":
1177                                   return EventGroup
1178               
1179           }
1180   }
1181
1182    /**
1183    * Returns the objects within the given study that should be used with the given entity string
1184    *
1185    * For example:
1186    *           What object should be consulted if the user asks for "samples"
1187    *           Response: study.samples
1188    * @return   List of domain objects that should be used with the given entity string
1189    */
1190    protected def templateObjectCallback( String entity, Study study ) {
1191      switch( entity ) {
1192          case "Study":
1193          case "studies":
1194              return study
1195          case "Subject":
1196          case "subjects":
1197              return study?.subjects
1198          case "Sample":
1199          case "samples":
1200              return study?.samples
1201          case "Event":
1202          case "events":
1203               return study?.events
1204          case "SamplingEvent":
1205          case "samplingEvents":
1206              return study?.samplingEvents
1207          case "Assay":
1208          case "assays":
1209                  return study?.assays
1210      }
1211    }
1212       
1213        /**
1214         * Computes the mean value and Standard Error of the mean (SEM) for the given values
1215         * @param values        List of values to compute the mean and SEM for. Strings and null
1216         *                                      values are ignored
1217         * @return                      Map with two keys: 'value' and 'error'
1218         */
1219        protected Map computeMeanAndError( values ) {
1220                // TODO: Handle the case that one of the values is a list. In that case,
1221                // all values should be taken into account.     
1222                def mean = computeMean( values );
1223                def error = computeSEM( values, mean );
1224               
1225                return [ 
1226                        "value": mean,
1227                        "error": error
1228                ]
1229        }
1230       
1231        /**
1232         * Computes the mean of the given values. Values that can not be parsed to a number
1233         * are ignored. If no values are given, null is returned.
1234         * @param values        List of values to compute the mean for
1235         * @return                      Arithmetic mean of the values
1236         */
1237        protected def computeMean( List values ) {
1238                def sumOfValues = 0;
1239                def sizeOfValues = 0;
1240                values.each { value ->
1241                        def num = getNumericValue( value );
1242                        if( num != null ) {
1243                                sumOfValues += num;
1244                                sizeOfValues++
1245                        }
1246                }
1247
1248                if( sizeOfValues > 0 )
1249                        return sumOfValues / sizeOfValues;
1250                else
1251                        return null;
1252        }
1253
1254        /**
1255        * Computes the standard error of mean of the given values. 
1256        * Values that can not be parsed to a number are ignored. 
1257        * If no values are given, null is returned.
1258        * @param values         List of values to compute the standard deviation for
1259        * @param mean           Mean of the list (if already computed). If not given, the mean
1260        *                                       will be computed using the computeMean method
1261        * @return                       Standard error of the mean of the values or 0 if no values can be used.
1262        */
1263    protected def computeSEM( List values, def mean = null ) {
1264       if( mean == null )
1265            mean = computeMean( values )
1266
1267       def sumOfDifferences = 0;
1268       def sizeOfValues = 0;
1269       values.each { value ->
1270           def num = getNumericValue( value );
1271           if( num != null ) {
1272               sumOfDifferences += Math.pow( num - mean, 2 );
1273               sizeOfValues++
1274           }
1275       }
1276
1277       if( sizeOfValues > 0 ) {
1278           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
1279           return std / Math.sqrt( sizeOfValues );
1280       } else {
1281           return null;
1282       }
1283    }
1284
1285    /**
1286         * Computes value of a percentile of the given values. Values that can not be parsed to a number
1287         * are ignored. If no values are given, null is returned.
1288         * @param values         List of values to compute the percentile for
1289     * @param Percentile Integer that indicates which percentile to calculae
1290     *                   Example: Percentile=50 calculates the median,
1291     *                            Percentile=25 calculates Q1
1292     *                            Percentile=75 calculates Q3
1293         * @return                      The value at the Percentile of the values
1294         */
1295        protected def computePercentile( List values, int Percentile ) {
1296                def listOfValues = [];
1297                values.each { value ->
1298                        def num = getNumericValue( value );
1299                        if( num != null ) {
1300                                listOfValues << num;
1301                        }
1302                }
1303
1304        listOfValues.sort();
1305
1306        def listSize = listOfValues.size()-1;
1307
1308        def objReturn = null;
1309        def objMin = null;
1310        def objMax = null;
1311
1312        def dblFactor = Percentile/100;
1313
1314                if( listSize >= 0 ) {
1315            def intPointer = (int) Math.abs(listSize*dblFactor);
1316            if(intPointer==listSize*dblFactor) {
1317                // If we exactly end up at an item, take this item
1318                objReturn = listOfValues.get(intPointer);
1319            } else {
1320                // If we don't exactly end up at an item, take the mean of the 2 adjecent values
1321                objReturn = (listOfValues.get(intPointer)+listOfValues.get(intPointer+1))/2;
1322            }
1323
1324            objMin = listOfValues.get(0);
1325            objMax = listOfValues.get(listSize);
1326        }
1327
1328                return ["value": objReturn, "min": objMin, "max": objMax];
1329        }
1330
1331    /**
1332         * Computes the count of the given values. Values that can not be parsed to a number
1333         * are ignored. If no values are given, null is returned.
1334         * @param values        List of values to compute the count for
1335         * @return                      Count of the values
1336         */
1337        protected def computeCount( List values ) {
1338                def sumOfValues = 0;
1339                def sizeOfValues = 0;
1340                values.each { value ->
1341                        def num = getNumericValue( value );
1342                        if( num != null ) {
1343                                sumOfValues += num;
1344                                sizeOfValues++
1345                        }
1346                }
1347
1348                if( sizeOfValues > 0 )
1349                        return ["value": sizeOfValues];
1350                else
1351                        return ["value": null];
1352        }
1353
1354    /**
1355         * Computes the sum of the given values. Values that can not be parsed to a number
1356         * are ignored. If no values are given, null is returned.
1357         * @param values        List of values to compute the sum for
1358         * @return                      Arithmetic sum of the values
1359         */
1360        protected def computeSum( List values ) {
1361                def sumOfValues = 0;
1362                def sizeOfValues = 0;
1363                values.each { value ->
1364                        def num = getNumericValue( value );
1365                        if( num != null ) {
1366                                sumOfValues += num;
1367                                sizeOfValues++
1368                        }
1369                }
1370
1371                if( sizeOfValues > 0 )
1372                        return ["value": sumOfValues];
1373                else
1374                        return ["value": null];
1375        }
1376   
1377        /**
1378         * Return the numeric value of the given object, or null if no numeric value could be returned
1379         * @param       value   Object to return the value for
1380         * @return                      Number that represents the given value
1381         */
1382        protected Number getNumericValue( value ) {
1383                // TODO: handle special types of values
1384                if( value instanceof Number ) {
1385                        return value;
1386                } else if( value instanceof RelTime ) {
1387                        return value.value;
1388                }
1389               
1390                return null
1391        }
1392
1393        /** 
1394         * Returns a field for a given templateentity
1395         * @param object        TemplateEntity (or subclass) to retrieve data for
1396         * @param fieldName     Name of the field to return data for.
1397         * @return                      Value of the field or null if the value could not be retrieved
1398         */
1399        protected def getFieldValue( TemplateEntity object, String fieldName ) {
1400                if( !object || !fieldName )
1401                        return null;
1402               
1403                try {
1404                        return object.getFieldValue( fieldName );
1405                } catch( Exception e ) {
1406                        return null;
1407                }
1408        }
1409
1410        /**
1411         * Parses a fieldId that has been created earlier by createFieldId
1412         * @param fieldId       FieldId to parse
1413         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
1414         * @see createFieldId
1415         */
1416        protected Map parseFieldId( String fieldId ) {
1417                def attrs = [:]
1418
1419                if( !fieldId )
1420                        return null;
1421               
1422                def parts = fieldId.split(",",5)
1423               
1424                attrs = [
1425                        "id": new String(parts[ 0 ].decodeBase64()),
1426                        "name": new String(parts[ 1 ].decodeBase64()),
1427                        "source": new String(parts[ 2 ].decodeBase64()),
1428                        "type": new String(parts[ 3 ].decodeBase64()),
1429            "unit": parts.length>4? new String(parts[ 4 ].decodeBase64()) : null,
1430                        "fieldId": fieldId
1431                ]
1432
1433        return attrs
1434        }
1435       
1436        /**
1437         * Returns a string representation of the given fieldType, which can be sent to the userinterface
1438         * @param fieldType     CATEGORICALDATA, DATE, RELTIME, NUMERICALDATA
1439         * @return      String representation
1440         */
1441        protected String dataTypeString( fieldType ) {
1442                return (fieldType==CATEGORICALDATA || fieldType==DATE || fieldType==RELTIME ? "categorical" : "numerical")
1443        }
1444       
1445        /**
1446         * Create a fieldId based on the given attributes
1447         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
1448         * @return                      Unique field ID for these parameters
1449         * @see parseFieldId
1450         */
1451        protected String createFieldId( Map attrs ) {
1452                // TODO: What if one of the attributes contains a comma?
1453                def name = attrs.name.toString();
1454                def id = (attrs.id ?: name).toString();
1455                def source = attrs.source.toString();
1456                def type = (attrs.type ?: "").toString();
1457        def unit = (attrs.unit ?: "").toString();
1458
1459                return id.bytes.encodeBase64().toString() + "," +
1460                name.bytes.encodeBase64().toString() + "," +
1461                source.bytes.encodeBase64().toString() + "," +
1462                type.bytes.encodeBase64().toString() + "," +
1463                unit.bytes.encodeBase64().toString();
1464        }
1465
1466    /**
1467     * Set the response code and an error message
1468     * @param code HTTP status code
1469     * @param msg Error message, string
1470     */
1471    protected void returnError(code, msg){
1472        response.sendError(code , msg)
1473    }
1474
1475    /**
1476     * Determines what type of data a field contains
1477     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
1478     * @param fieldId The field id as returned from the client, will be used to retrieve the data required to determine the type of data a field contains
1479     * @param inputData Optional parameter that contains the data we are computing the type of. When including in the function call we do not need to request data from a module, should the data belong to a module
1480     * @return Either CATEGORICALDATA, NUMERICALDATA, DATE or RELTIME
1481     */
1482    protected int determineFieldType(studyId, fieldId, inputData = null){
1483                def parsedField = parseFieldId( fieldId );
1484        def study = Study.get(studyId)
1485                def data = []
1486               
1487                // If the fieldId is incorrect, or the field is not asked for, return
1488                // CATEGORICALDATA
1489                if( !parsedField )
1490                        return CATEGORICALDATA;
1491
1492        try{
1493            if( parsedField.source == "GSCF" ) {
1494                if(parsedField.id.isNumber()){
1495                        return determineCategoryFromTemplateFieldId(parsedField.id)
1496                } else { // Domainfield or memberclass
1497                    def callback = domainObjectCallback( parsedField.type )
1498                                       
1499                    // Can the field be found in the domainFields as well? If so, treat it as a template field, so that dates and times can be properly rendered in a human-readable fashion
1500                    if(callback.metaClass.methods.contains( "giveDomainFields" ) && callback?.giveDomainFields()?.name?.contains(parsedField.name.toString())){
1501                        // Use the associated templateField to determine the field type
1502                        return determineCategoryFromTemplateField(
1503                                callback?.giveDomainFields()[
1504                                    callback?.giveDomainFields().name.indexOf(parsedField.name.toString())
1505                                ]
1506                        )
1507                    }
1508                    // Apparently it is not a templatefield as well as a memberclass
1509
1510                    def field = callback?.declaredFields.find { it.name == parsedField.name };
1511                    if( field ) {
1512                        return determineCategoryFromClass( field.getType() )
1513                    } else {
1514                        // TODO: how do we communicate this to the user? Do we allow the process to proceed?
1515                        log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
1516                    }
1517                }
1518            } else {
1519                if(inputData == null){ // If we did not get data, we need to request it from the module first
1520                    data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
1521                    return determineCategoryFromData(data)
1522                } else {
1523                    return determineCategoryFromData(inputData)
1524                }
1525            }
1526        } catch(Exception e){
1527            log.error("VisualizationController: determineFieldType: "+e)
1528            e.printStackTrace()
1529            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1530            return CATEGORICALDATA
1531        }
1532    }
1533
1534    /**
1535     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
1536     * @param classObject
1537     * @return Either CATEGORICALDATA of NUMERICALDATA
1538     */
1539    protected int determineCategoryFromClass(classObject){
1540        log.trace "Determine category from class: " + classObject
1541        switch( classObject ) {
1542                        case java.lang.String:
1543                        case org.dbnp.gdt.Term:
1544                        case org.dbnp.gdt.TemplateFieldListItem:
1545                                return CATEGORICALDATA;
1546                        default:
1547                                return NUMERICALDATA;
1548                }
1549    }
1550
1551    /**
1552     * Determines a field category based on the actual data contained in the field. The parameter 'inputObject' can be a single item with a toString() function, or a collection of such items.
1553     * @param inputObject Either a single item, or a collection of items
1554     * @return Either CATEGORICALDATA of NUMERICALDATA
1555     */
1556    protected int determineCategoryFromData(inputObject){
1557        def results = []
1558               
1559        if(inputObject instanceof Collection){
1560            // This data is more complex than a single value, so we will call ourselves again so we c
1561            inputObject.each {
1562                                if( it != null )
1563                        results << determineCategoryFromData(it)
1564            }
1565        } else {
1566                        // Unfortunately, the JSON null object doesn't resolve to false or equals null. For that reason, we
1567                        // exclude those objects explicitly here.
1568                        if( inputObject != null && inputObject?.class != org.codehaus.groovy.grails.web.json.JSONObject$Null ) {
1569                    if(inputObject.toString().isDouble()){
1570                        results << NUMERICALDATA
1571                    } else {
1572                        results << CATEGORICALDATA
1573                    }
1574                        }
1575        }
1576
1577        results.unique()
1578
1579        if(results.size() > 1) {
1580            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1581            results[0] = CATEGORICALDATA
1582        } else if( results.size() == 0 ) {
1583                        // If the list is empty, return the numerical type. If it is the only value, if will
1584                        // be discarded later on. If there are more entries (e.g part of a collection)
1585                        // the values will be regarded as numerical, if the other values are numerical 
1586                        results[ 0 ] = NUMERICALDATA
1587        }
1588
1589                return results[0]
1590    }
1591
1592    /**
1593     * Determines a field category, based on the TemplateFieldId of a Templatefield
1594     * @param id A database ID for a TemplateField
1595     * @return Either CATEGORICALDATA of NUMERICALDATA
1596     */
1597    protected int determineCategoryFromTemplateFieldId(id){
1598        TemplateField tf = TemplateField.get(id)
1599        return determineCategoryFromTemplateField(tf)
1600    }
1601
1602    /**
1603     * Determines a field category, based on the TemplateFieldType of a Templatefield
1604     * @param id A database ID for a TemplateField
1605     * @return Either CATEGORICALDATA of NUMERICALDATA
1606     */
1607    protected int determineCategoryFromTemplateField(tf){
1608        if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG){
1609            log.trace "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
1610            return NUMERICALDATA
1611        }
1612        if(tf.type==TemplateFieldType.DATE){
1613            log.trace "GSCF templatefield: DATE ("+DATE+") (based on "+tf.type+")"
1614            return DATE
1615        }
1616        if(tf.type==TemplateFieldType.RELTIME){
1617            log.trace "GSCF templatefield: RELTIME ("+RELTIME+") (based on "+tf.type+")"
1618            return RELTIME
1619        }
1620        log.trace "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
1621        return CATEGORICALDATA
1622    }
1623    /**
1624     * Properly formats the object that will be returned to the client. Also adds an informational message, if that message has been set by a function. Resets the informational message to the empty String.
1625     * @param returnData The object containing the data
1626     * @return results A JSON object
1627     */
1628    protected void sendResults(returnData){
1629        def results = [:]
1630        if(infoMessage.size()!=0){
1631            results.put("infoMessage", infoMessage)
1632            infoMessage = []
1633        }
1634        results.put("returnData", returnData)
1635        render results as JSON
1636    }
1637
1638    /**
1639     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
1640     * @param returnData The object containing the data
1641     * @return results A JSON object
1642     */
1643    protected void sendInfoMessage(){
1644        def results = [:]
1645        results.put("infoMessage", infoMessage)
1646        infoMessage = []
1647        render results as JSON
1648    }
1649
1650    /**
1651     * Adds a new message to the infoMessage
1652     * @param message The information that needs to be added to the infoMessage
1653     */
1654    protected void setInfoMessage(message){
1655        infoMessage.add(message)
1656        log.trace "setInfoMessage: "+infoMessage
1657    }
1658
1659    /**
1660     * Adds a message to the infoMessage that gives the client information about offline modules
1661     */
1662    protected void setInfoMessageOfflineModules(){
1663        infoMessageOfflineModules.unique()
1664        if(infoMessageOfflineModules.size()>0){
1665            String message = "Unfortunately"
1666            infoMessageOfflineModules.eachWithIndex{ it, index ->
1667                if(index==(infoMessageOfflineModules.size()-2)){
1668                    message += ', the '+it+' and '
1669                } else {
1670                    if(index==(infoMessageOfflineModules.size()-1)){
1671                        message += ' the '+it
1672                    } else {
1673                        message += ', the '+it
1674                    }
1675                }
1676            }
1677            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
1678            if(infoMessageOfflineModules.size()>1){
1679                message += "these modules."
1680            } else {
1681                message += "this module."
1682            }
1683            setInfoMessage(message)
1684        }
1685        infoMessageOfflineModules = []
1686    }
1687
1688    /**
1689     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1690     * @param inputData Contains a list of maps, of the following format
1691     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1692     *            - a key 'name', containing, for example, a feature name or field name
1693     *            - a key 'y', containing a list of y-values
1694     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1695     */
1696    protected def formatCategoryData(inputData){
1697        // NOTE: This function is no longer up to date with the current inputData layout.
1698        def series = []
1699        inputData.eachWithIndex { it, i ->
1700            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1701        }
1702        def ret = [:]
1703        ret.put('type', inputData[0]['type'])
1704        ret.put('x', inputData[0]['x'])
1705        ret.put('yaxis',['title': 'title', 'unit': ''])
1706        ret.put('xaxis', inputData[0]['xaxis'])
1707        ret.put('series', series)
1708        return ret
1709    }
1710
1711    /**
1712     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1713     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1714     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1715     * @return
1716     */
1717    protected def determineVisualizationTypes(rowType, columnType){
1718                def types = []
1719               
1720        if(rowType == CATEGORICALDATA || rowType == DATE || rowType == RELTIME){
1721            if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){
1722                                types = [ [ "id": "table", "name": "Table"] ];
1723            } else {    // NUMERICALDATA
1724                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1725            }
1726        } else {        // NUMERICALDATA
1727            if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){
1728                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"], [ "id": "boxplot", "name": "Boxplot"] ];
1729            } else {
1730                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1731            }
1732        }
1733        return types
1734    }
1735       
1736        /**
1737        * Returns the types of aggregation possible for the given two objects of either CATEGORICALDATA or NUMERICALDATA
1738        * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1739        * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1740        * @param groupType The type of the data that has been selected for the grouping, either CATEGORICALDATA or NUMERICALDATA
1741        * @return
1742        */
1743        protected def determineAggregationTypes(rowType, columnType, groupType = null ){
1744                // A list of all aggregation types. By default, every item is possible
1745                def types = [
1746                        [ "id": "average", "name": "Average", "disabled": false ],
1747                        [ "id": "count", "name": "Count", "disabled": false ],
1748                        [ "id": "median", "name": "Median", "disabled": false ],
1749                        [ "id": "none", "name": "No aggregation", "disabled": false ],
1750                        [ "id": "sum", "name": "Sum", "disabled": false ],
1751                ]
1752
1753                // Normally, all aggregation types are possible, with three exceptions:
1754                //              Categorical data on both axes. In that case, we don't have anything to aggregate, so we can only count
1755                //              Grouping on a numerical field is not possible. In that case, it is ignored
1756                //                      Grouping on a numerical field with categorical data on both axes (table) enabled aggregation,
1757                //                      In that case we can aggregate on the numerical field.
1758               
1759                if(rowType == CATEGORICALDATA || rowType == DATE || rowType == RELTIME){
1760                        if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){
1761                               
1762                                if( groupType == NUMERICALDATA ) {
1763                                        // Disable 'none', since that can not be visualized
1764                                        types.each {
1765                                                if( it.id == "none" )
1766                                                        it.disabled = true
1767                                        }
1768                                } else {
1769                                        // Disable everything but 'count'
1770                                        types.each { 
1771                                                if( it.id != "count" ) 
1772                                                        it.disabled = true
1773                                        }
1774                                }
1775                        }
1776                }
1777               
1778                return types
1779   }
1780}
Note: See TracBrowser for help on using the repository browser.