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

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

jqPlot boxplot (VIS-33) added. some minor issues remain (see comment of VIS-33)

File size: 67.2 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                double dblMEAN = computeMean(dataMap.get(key));
926                double dblSEM = computeSEM(dataMap.get(key),dblMEAN);
927
928                double Q1 = computePercentile(dataMap.get(key),25).get("value");
929                double Q3 = computePercentile(dataMap.get(key),75).get("value");
930
931                /* DEBUG
932                println("---");
933                println("  dataMap["+key+"]:: "+dataMap.get(key));
934                println("  dblMEAN:: "+dblMEAN);
935                println("  dblSEM:: "+dblSEM);
936                println("  Q1:: "+Q1);
937                println("  Q3:: "+Q3);
938                println("---");
939                */
940
941                return_data[ "series" ] << [
942                        "name": key,
943                        "y" : [key, (dblMEAN-dblSEM), Q1, dblMEAN, Q3, (dblMEAN+dblSEM)]
944                ];
945            }
946
947            println(return_data);
948
949
950        } else {
951                        // For a horizontal barchart, the two axes should be swapped
952                        if( type == "horizontal_barchart" ) {
953                                def tmp = xAxis
954                                xAxis = yAxis
955                                yAxis = tmp
956                        }
957               
958                        if( !fieldInfo[ serieAxis ] ) {
959                                // If no series field has defined, we return all data in one serie
960                                return_data.put("series", [[
961                                        "name": "count",
962                                        "x": groupedData[ xAxis ],
963                                        "y": groupedData[ yAxis ],
964                                ]])
965                        } else if( fieldInfo[ serieAxis ].fieldType == NUMERICALDATA ) {
966                                // No numerical series field is allowed in a chart.
967                                throw new Exception( "No numerical series field is allowed here." );
968                        } else {
969                                // If a categorical field has been chosen on the serieAxis, we should create a group for each serie
970                                // with the correct values, belonging to that serie.
971                                return_data[ "series" ] = [];
972                               
973                                // The unique method alters the original object, so we
974                                // create a new object
975                                def uniqueSeries = ([] + groupedData[ serieAxis ]).unique();
976                               
977                                uniqueSeries.each { serie ->
978                                        def indices = groupedData[ serieAxis ].findIndexValues { it == serie }
979                                        return_data[ "series" ] << [
980                                                "name": serie,
981                                                "x": groupedData[ xAxis ][ indices ],
982                                                "y": groupedData[ yAxis ][ indices ]
983                                        ]
984                                }
985                        }
986                }
987               
988                return return_data;
989        }
990
991        /**
992         * Formats the requested data for a table       
993         * @param groupedData
994         * @param xAxisData
995         * @param yAxisData
996         * @param xAxis
997         * @param yAxis
998         * @param dataAxis
999         * @return
1000         */
1001        def formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, dataAxis, serieIndices = null ) {
1002                def tableData = []
1003               
1004                xAxisData.each { x ->
1005                        def colData = []
1006                       
1007                        def indices = groupedData[ xAxis ].findIndexValues { it == x }
1008                       
1009                        // If serieIndices are given, intersect the indices
1010                        if( serieIndices != null )
1011                                indices = indices.intersect( serieIndices );
1012                       
1013                        yAxisData.each { y ->
1014                                def index = indices.intersect( groupedData[ yAxis ].findIndexValues { it == y } );
1015                               
1016                                if( index.size() ) {
1017                                        colData << groupedData[ dataAxis ][ (int) index[ 0 ] ]
1018                                }
1019                        }
1020                        tableData << colData;
1021                }
1022               
1023                return tableData;
1024        }
1025
1026    /**
1027     * 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.
1028     * @param data  The list of items that needs to be checked/converted
1029     * @param axisType As determined by determineFieldType
1030     * @return The input variable 'data', with it's date and time elements converted.
1031     * @see determineFieldType
1032     */
1033    def renderFieldsHumanReadable(data, axisType){
1034        switch( axisType ) {
1035                        case RELTIME:
1036                                return renderTimesHumanReadable(data)
1037                        case DATE:
1038                                return renderDatesHumanReadable(data)
1039                        case CATEGORICALDATA:
1040                                return data.collect { it.toString() }
1041                        case NUMERICALDATA:
1042                        default:
1043                                return data;
1044                }
1045    }
1046
1047    /**
1048     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
1049     * @param data
1050     * @return
1051     */
1052    def renderTimesHumanReadable(data){
1053        def tmpTimeContainer = []
1054        data. each {
1055            if(it instanceof Number) {
1056                try{
1057                    tmpTimeContainer << new RelTime( it ).toPrettyString()
1058                } catch(IllegalArgumentException e){
1059                    tmpTimeContainer << it
1060                }
1061            } else {
1062                tmpTimeContainer << it // To handle items such as 'unknown'
1063            }
1064        }
1065        return tmpTimeContainer
1066    }
1067
1068    /**
1069     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
1070     * @param data
1071     * @return
1072     */
1073    def renderDatesHumanReadable(data) {
1074        def tmpDateContainer = []
1075        data. each {
1076            if(it instanceof Number) {
1077                try{
1078                    tmpDateContainer << new java.util.Date( (Long) it ).toString()
1079                } catch(IllegalArgumentException e){
1080                    tmpDateContainer << it
1081                }
1082            } else {
1083                tmpDateContainer << it // To handle items such as 'unknown'
1084            }
1085        }
1086        return tmpDateContainer
1087    }
1088        /**
1089         * Returns a closure for the given entitytype that determines the value for a criterion
1090         * on the given object. The closure receives two parameters: the sample and a field.
1091         *
1092         * For example:
1093         *              How can one retrieve the value for subject.name, given a sample? This can be done by
1094         *              returning the field values sample.parentSubject:
1095         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
1096         * @return      Closure that retrieves the value for a field and the given field
1097         */
1098        protected Closure valueCallback( String entity ) {
1099                switch( entity ) {
1100                        case "Study":
1101                        case "studies":
1102                                return { sample, field -> return getFieldValue( sample.parent, field ) }
1103                        case "Subject":
1104                        case "subjects":
1105                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
1106                        case "Sample":
1107                        case "samples":
1108                                return { sample, field -> return getFieldValue( sample, field ) }
1109                        case "Event":
1110                        case "events":
1111                                return { sample, field ->
1112                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
1113                                                return null
1114
1115                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
1116                                }
1117                        case "EventGroup":
1118                        case "eventGroups":
1119                                return { sample, field ->
1120                                        if( !sample || !sample.parentEventGroup )
1121                                                return null
1122
1123                                        // For eventgroups only the name is supported
1124                                        if( field == "name" )
1125                                                return sample.parentEventGroup.name
1126                                        else
1127                                                return null 
1128                                }
1129       
1130                        case "SamplingEvent":
1131                        case "samplingEvents":
1132                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
1133                        case "Assay":
1134                        case "assays":
1135                                return { sample, field ->
1136                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
1137                                        if( sampleAssays && sampleAssays.size() > 0 )
1138                                                return sampleAssays.collect { getFieldValue( it, field ) }
1139                                        else
1140                                                return null
1141                                }
1142                }
1143        }
1144       
1145        /**
1146        * Returns the domain object that should be used with the given entity string
1147        *
1148        * For example:
1149        *               What object should be consulted if the user asks for "studies"
1150        *               Response: Study
1151        * @return       Domain object that should be used with the given entity string
1152        */
1153   protected def domainObjectCallback( String entity ) {
1154           switch( entity ) {
1155                   case "Study":
1156                   case "studies":
1157                           return Study
1158                   case "Subject":
1159                   case "subjects":
1160                           return Subject
1161                   case "Sample":
1162                   case "samples":
1163                           return Sample
1164                   case "Event":
1165                   case "events":
1166                        return Event
1167                   case "SamplingEvent":
1168                   case "samplingEvents":
1169                           return SamplingEvent
1170                   case "Assay":
1171                   case "assays":
1172                                return Assay
1173                   case "EventGroup":
1174                   case "eventGroups":
1175                                   return EventGroup
1176               
1177           }
1178   }
1179
1180    /**
1181    * Returns the objects within the given study that should be used with the given entity string
1182    *
1183    * For example:
1184    *           What object should be consulted if the user asks for "samples"
1185    *           Response: study.samples
1186    * @return   List of domain objects that should be used with the given entity string
1187    */
1188    protected def templateObjectCallback( String entity, Study study ) {
1189      switch( entity ) {
1190          case "Study":
1191          case "studies":
1192              return study
1193          case "Subject":
1194          case "subjects":
1195              return study?.subjects
1196          case "Sample":
1197          case "samples":
1198              return study?.samples
1199          case "Event":
1200          case "events":
1201               return study?.events
1202          case "SamplingEvent":
1203          case "samplingEvents":
1204              return study?.samplingEvents
1205          case "Assay":
1206          case "assays":
1207                  return study?.assays
1208      }
1209    }
1210       
1211        /**
1212         * Computes the mean value and Standard Error of the mean (SEM) for the given values
1213         * @param values        List of values to compute the mean and SEM for. Strings and null
1214         *                                      values are ignored
1215         * @return                      Map with two keys: 'value' and 'error'
1216         */
1217        protected Map computeMeanAndError( values ) {
1218                // TODO: Handle the case that one of the values is a list. In that case,
1219                // all values should be taken into account.     
1220                def mean = computeMean( values );
1221                def error = computeSEM( values, mean );
1222               
1223                return [ 
1224                        "value": mean,
1225                        "error": error
1226                ]
1227        }
1228       
1229        /**
1230         * Computes the mean of the given values. Values that can not be parsed to a number
1231         * are ignored. If no values are given, null is returned.
1232         * @param values        List of values to compute the mean for
1233         * @return                      Arithmetic mean of the values
1234         */
1235        protected def computeMean( List values ) {
1236                def sumOfValues = 0;
1237                def sizeOfValues = 0;
1238                values.each { value ->
1239                        def num = getNumericValue( value );
1240                        if( num != null ) {
1241                                sumOfValues += num;
1242                                sizeOfValues++
1243                        }
1244                }
1245
1246                if( sizeOfValues > 0 )
1247                        return sumOfValues / sizeOfValues;
1248                else
1249                        return null;
1250        }
1251
1252        /**
1253        * Computes the standard error of mean of the given values. 
1254        * Values that can not be parsed to a number are ignored. 
1255        * If no values are given, null is returned.
1256        * @param values         List of values to compute the standard deviation for
1257        * @param mean           Mean of the list (if already computed). If not given, the mean
1258        *                                       will be computed using the computeMean method
1259        * @return                       Standard error of the mean of the values or 0 if no values can be used.
1260        */
1261    protected def computeSEM( List values, def mean = null ) {
1262       if( mean == null )
1263            mean = computeMean( values )
1264
1265       def sumOfDifferences = 0;
1266       def sizeOfValues = 0;
1267       values.each { value ->
1268           def num = getNumericValue( value );
1269           if( num != null ) {
1270               sumOfDifferences += Math.pow( num - mean, 2 );
1271               sizeOfValues++
1272           }
1273       }
1274
1275       if( sizeOfValues > 0 ) {
1276           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
1277           return std / Math.sqrt( sizeOfValues );
1278       } else {
1279           return null;
1280       }
1281    }
1282
1283    /**
1284         * Computes value of a percentile of the given values. Values that can not be parsed to a number
1285         * are ignored. If no values are given, null is returned.
1286         * @param values         List of values to compute the percentile for
1287     * @param Percentile Integer that indicates which percentile to calculae
1288     *                   Example: Percentile=50 calculates the median,
1289     *                            Percentile=25 calculates Q1
1290     *                            Percentile=75 calculates Q3
1291         * @return                      The value at the Percentile of the values
1292         */
1293        protected def computePercentile( List values, int Percentile ) {
1294                def listOfValues = [];
1295                values.each { value ->
1296                        def num = getNumericValue( value );
1297                        if( num != null ) {
1298                                listOfValues << num;
1299                        }
1300                }
1301
1302        listOfValues.sort();
1303
1304        def listSize = listOfValues.size();
1305
1306        def objReturn = null;
1307
1308        def dblFactor = Percentile/100;
1309
1310                if( listSize > 0 ) {
1311            def listHalf = (int) Math.abs(listSize*dblFactor);
1312            if(listHalf==listSize*dblFactor) {
1313                // If we don't exactly end up at an item, take the mean of the 2 adjecent values
1314                objReturn = (listOfValues.get(listHalf)+listOfValues.get(listHalf-1))/2;
1315            } else {
1316                // If we exactly end up at an item, take this item
1317                objReturn = listOfValues.get(listHalf);
1318            }
1319        }
1320
1321                return ["value": objReturn];
1322        }
1323
1324    /**
1325         * Computes the count of the given values. Values that can not be parsed to a number
1326         * are ignored. If no values are given, null is returned.
1327         * @param values        List of values to compute the count for
1328         * @return                      Count of the values
1329         */
1330        protected def computeCount( List values ) {
1331                def sumOfValues = 0;
1332                def sizeOfValues = 0;
1333                values.each { value ->
1334                        def num = getNumericValue( value );
1335                        if( num != null ) {
1336                                sumOfValues += num;
1337                                sizeOfValues++
1338                        }
1339                }
1340
1341                if( sizeOfValues > 0 )
1342                        return ["value": sizeOfValues];
1343                else
1344                        return ["value": null];
1345        }
1346
1347    /**
1348         * Computes the sum of the given values. Values that can not be parsed to a number
1349         * are ignored. If no values are given, null is returned.
1350         * @param values        List of values to compute the sum for
1351         * @return                      Arithmetic sum of the values
1352         */
1353        protected def computeSum( List values ) {
1354                def sumOfValues = 0;
1355                def sizeOfValues = 0;
1356                values.each { value ->
1357                        def num = getNumericValue( value );
1358                        if( num != null ) {
1359                                sumOfValues += num;
1360                                sizeOfValues++
1361                        }
1362                }
1363
1364                if( sizeOfValues > 0 )
1365                        return ["value": sumOfValues];
1366                else
1367                        return ["value": null];
1368        }
1369   
1370        /**
1371         * Return the numeric value of the given object, or null if no numeric value could be returned
1372         * @param       value   Object to return the value for
1373         * @return                      Number that represents the given value
1374         */
1375        protected Number getNumericValue( value ) {
1376                // TODO: handle special types of values
1377                if( value instanceof Number ) {
1378                        return value;
1379                } else if( value instanceof RelTime ) {
1380                        return value.value;
1381                }
1382               
1383                return null
1384        }
1385
1386        /** 
1387         * Returns a field for a given templateentity
1388         * @param object        TemplateEntity (or subclass) to retrieve data for
1389         * @param fieldName     Name of the field to return data for.
1390         * @return                      Value of the field or null if the value could not be retrieved
1391         */
1392        protected def getFieldValue( TemplateEntity object, String fieldName ) {
1393                if( !object || !fieldName )
1394                        return null;
1395               
1396                try {
1397                        return object.getFieldValue( fieldName );
1398                } catch( Exception e ) {
1399                        return null;
1400                }
1401        }
1402
1403        /**
1404         * Parses a fieldId that has been created earlier by createFieldId
1405         * @param fieldId       FieldId to parse
1406         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
1407         * @see createFieldId
1408         */
1409        protected Map parseFieldId( String fieldId ) {
1410                def attrs = [:]
1411
1412                if( !fieldId )
1413                        return null;
1414               
1415                def parts = fieldId.split(",",5)
1416               
1417                attrs = [
1418                        "id": new String(parts[ 0 ].decodeBase64()),
1419                        "name": new String(parts[ 1 ].decodeBase64()),
1420                        "source": new String(parts[ 2 ].decodeBase64()),
1421                        "type": new String(parts[ 3 ].decodeBase64()),
1422            "unit": parts.length>4? new String(parts[ 4 ].decodeBase64()) : null,
1423                        "fieldId": fieldId
1424                ]
1425
1426        return attrs
1427        }
1428       
1429        /**
1430         * Returns a string representation of the given fieldType, which can be sent to the userinterface
1431         * @param fieldType     CATEGORICALDATA, DATE, RELTIME, NUMERICALDATA
1432         * @return      String representation
1433         */
1434        protected String dataTypeString( fieldType ) {
1435                return (fieldType==CATEGORICALDATA || fieldType==DATE || fieldType==RELTIME ? "categorical" : "numerical")
1436        }
1437       
1438        /**
1439         * Create a fieldId based on the given attributes
1440         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
1441         * @return                      Unique field ID for these parameters
1442         * @see parseFieldId
1443         */
1444        protected String createFieldId( Map attrs ) {
1445                // TODO: What if one of the attributes contains a comma?
1446                def name = attrs.name.toString();
1447                def id = (attrs.id ?: name).toString();
1448                def source = attrs.source.toString();
1449                def type = (attrs.type ?: "").toString();
1450        def unit = (attrs.unit ?: "").toString();
1451
1452                return id.bytes.encodeBase64().toString() + "," +
1453                name.bytes.encodeBase64().toString() + "," +
1454                source.bytes.encodeBase64().toString() + "," +
1455                type.bytes.encodeBase64().toString() + "," +
1456                unit.bytes.encodeBase64().toString();
1457        }
1458
1459    /**
1460     * Set the response code and an error message
1461     * @param code HTTP status code
1462     * @param msg Error message, string
1463     */
1464    protected void returnError(code, msg){
1465        response.sendError(code , msg)
1466    }
1467
1468    /**
1469     * Determines what type of data a field contains
1470     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
1471     * @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
1472     * @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
1473     * @return Either CATEGORICALDATA, NUMERICALDATA, DATE or RELTIME
1474     */
1475    protected int determineFieldType(studyId, fieldId, inputData = null){
1476                def parsedField = parseFieldId( fieldId );
1477        def study = Study.get(studyId)
1478                def data = []
1479               
1480                // If the fieldId is incorrect, or the field is not asked for, return
1481                // CATEGORICALDATA
1482                if( !parsedField )
1483                        return CATEGORICALDATA;
1484
1485        try{
1486            if( parsedField.source == "GSCF" ) {
1487                if(parsedField.id.isNumber()){
1488                        return determineCategoryFromTemplateFieldId(parsedField.id)
1489                } else { // Domainfield or memberclass
1490                    def callback = domainObjectCallback( parsedField.type )
1491                                       
1492                    // 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
1493                    if(callback.metaClass.methods.contains( "giveDomainFields" ) && callback?.giveDomainFields()?.name?.contains(parsedField.name.toString())){
1494                        // Use the associated templateField to determine the field type
1495                        return determineCategoryFromTemplateField(
1496                                callback?.giveDomainFields()[
1497                                    callback?.giveDomainFields().name.indexOf(parsedField.name.toString())
1498                                ]
1499                        )
1500                    }
1501                    // Apparently it is not a templatefield as well as a memberclass
1502
1503                    def field = callback?.declaredFields.find { it.name == parsedField.name };
1504                    if( field ) {
1505                        return determineCategoryFromClass( field.getType() )
1506                    } else {
1507                        // TODO: how do we communicate this to the user? Do we allow the process to proceed?
1508                        log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
1509                    }
1510                }
1511            } else {
1512                if(inputData == null){ // If we did not get data, we need to request it from the module first
1513                    data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
1514                    return determineCategoryFromData(data)
1515                } else {
1516                    return determineCategoryFromData(inputData)
1517                }
1518            }
1519        } catch(Exception e){
1520            log.error("VisualizationController: determineFieldType: "+e)
1521            e.printStackTrace()
1522            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1523            return CATEGORICALDATA
1524        }
1525    }
1526
1527    /**
1528     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
1529     * @param classObject
1530     * @return Either CATEGORICALDATA of NUMERICALDATA
1531     */
1532    protected int determineCategoryFromClass(classObject){
1533        log.trace "Determine category from class: " + classObject
1534        switch( classObject ) {
1535                        case java.lang.String:
1536                        case org.dbnp.gdt.Term:
1537                        case org.dbnp.gdt.TemplateFieldListItem:
1538                                return CATEGORICALDATA;
1539                        default:
1540                                return NUMERICALDATA;
1541                }
1542    }
1543
1544    /**
1545     * 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.
1546     * @param inputObject Either a single item, or a collection of items
1547     * @return Either CATEGORICALDATA of NUMERICALDATA
1548     */
1549    protected int determineCategoryFromData(inputObject){
1550        def results = []
1551               
1552        if(inputObject instanceof Collection){
1553            // This data is more complex than a single value, so we will call ourselves again so we c
1554            inputObject.each {
1555                                if( it != null )
1556                        results << determineCategoryFromData(it)
1557            }
1558        } else {
1559                        // Unfortunately, the JSON null object doesn't resolve to false or equals null. For that reason, we
1560                        // exclude those objects explicitly here.
1561                        if( inputObject != null && inputObject?.class != org.codehaus.groovy.grails.web.json.JSONObject$Null ) {
1562                    if(inputObject.toString().isDouble()){
1563                        results << NUMERICALDATA
1564                    } else {
1565                        results << CATEGORICALDATA
1566                    }
1567                        }
1568        }
1569
1570        results.unique()
1571
1572        if(results.size() > 1) {
1573            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1574            results[0] = CATEGORICALDATA
1575        } else if( results.size() == 0 ) {
1576                        // If the list is empty, return the numerical type. If it is the only value, if will
1577                        // be discarded later on. If there are more entries (e.g part of a collection)
1578                        // the values will be regarded as numerical, if the other values are numerical 
1579                        results[ 0 ] = NUMERICALDATA
1580        }
1581
1582                return results[0]
1583    }
1584
1585    /**
1586     * Determines a field category, based on the TemplateFieldId of a Templatefield
1587     * @param id A database ID for a TemplateField
1588     * @return Either CATEGORICALDATA of NUMERICALDATA
1589     */
1590    protected int determineCategoryFromTemplateFieldId(id){
1591        TemplateField tf = TemplateField.get(id)
1592        return determineCategoryFromTemplateField(tf)
1593    }
1594
1595    /**
1596     * Determines a field category, based on the TemplateFieldType of a Templatefield
1597     * @param id A database ID for a TemplateField
1598     * @return Either CATEGORICALDATA of NUMERICALDATA
1599     */
1600    protected int determineCategoryFromTemplateField(tf){
1601        if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG){
1602            log.trace "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
1603            return NUMERICALDATA
1604        }
1605        if(tf.type==TemplateFieldType.DATE){
1606            log.trace "GSCF templatefield: DATE ("+DATE+") (based on "+tf.type+")"
1607            return DATE
1608        }
1609        if(tf.type==TemplateFieldType.RELTIME){
1610            log.trace "GSCF templatefield: RELTIME ("+RELTIME+") (based on "+tf.type+")"
1611            return RELTIME
1612        }
1613        log.trace "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
1614        return CATEGORICALDATA
1615    }
1616    /**
1617     * 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.
1618     * @param returnData The object containing the data
1619     * @return results A JSON object
1620     */
1621    protected void sendResults(returnData){
1622        def results = [:]
1623        if(infoMessage.size()!=0){
1624            results.put("infoMessage", infoMessage)
1625            infoMessage = []
1626        }
1627        results.put("returnData", returnData)
1628        render results as JSON
1629    }
1630
1631    /**
1632     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
1633     * @param returnData The object containing the data
1634     * @return results A JSON object
1635     */
1636    protected void sendInfoMessage(){
1637        def results = [:]
1638        results.put("infoMessage", infoMessage)
1639        infoMessage = []
1640        render results as JSON
1641    }
1642
1643    /**
1644     * Adds a new message to the infoMessage
1645     * @param message The information that needs to be added to the infoMessage
1646     */
1647    protected void setInfoMessage(message){
1648        infoMessage.add(message)
1649        log.trace "setInfoMessage: "+infoMessage
1650    }
1651
1652    /**
1653     * Adds a message to the infoMessage that gives the client information about offline modules
1654     */
1655    protected void setInfoMessageOfflineModules(){
1656        infoMessageOfflineModules.unique()
1657        if(infoMessageOfflineModules.size()>0){
1658            String message = "Unfortunately"
1659            infoMessageOfflineModules.eachWithIndex{ it, index ->
1660                if(index==(infoMessageOfflineModules.size()-2)){
1661                    message += ', the '+it+' and '
1662                } else {
1663                    if(index==(infoMessageOfflineModules.size()-1)){
1664                        message += ' the '+it
1665                    } else {
1666                        message += ', the '+it
1667                    }
1668                }
1669            }
1670            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
1671            if(infoMessageOfflineModules.size()>1){
1672                message += "these modules."
1673            } else {
1674                message += "this module."
1675            }
1676            setInfoMessage(message)
1677        }
1678        infoMessageOfflineModules = []
1679    }
1680
1681    /**
1682     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1683     * @param inputData Contains a list of maps, of the following format
1684     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1685     *            - a key 'name', containing, for example, a feature name or field name
1686     *            - a key 'y', containing a list of y-values
1687     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1688     */
1689    protected def formatCategoryData(inputData){
1690        // NOTE: This function is no longer up to date with the current inputData layout.
1691        def series = []
1692        inputData.eachWithIndex { it, i ->
1693            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1694        }
1695        def ret = [:]
1696        ret.put('type', inputData[0]['type'])
1697        ret.put('x', inputData[0]['x'])
1698        ret.put('yaxis',['title': 'title', 'unit': ''])
1699        ret.put('xaxis', inputData[0]['xaxis'])
1700        ret.put('series', series)
1701        return ret
1702    }
1703
1704    /**
1705     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1706     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1707     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1708     * @return
1709     */
1710    protected def determineVisualizationTypes(rowType, columnType){
1711                def types = []
1712               
1713        if(rowType == CATEGORICALDATA || rowType == DATE || rowType == RELTIME){
1714            if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){
1715                                types = [ [ "id": "table", "name": "Table"] ];
1716            } else {    // NUMERICALDATA
1717                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1718            }
1719        } else {        // NUMERICALDATA
1720            if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){
1721                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"], [ "id": "boxplot", "name": "Boxplot"] ];
1722            } else {
1723                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1724            }
1725        }
1726        return types
1727    }
1728       
1729        /**
1730        * Returns the types of aggregation possible for the given two objects of either CATEGORICALDATA or NUMERICALDATA
1731        * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1732        * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1733        * @param groupType The type of the data that has been selected for the grouping, either CATEGORICALDATA or NUMERICALDATA
1734        * @return
1735        */
1736        protected def determineAggregationTypes(rowType, columnType, groupType = null ){
1737                // A list of all aggregation types. By default, every item is possible
1738                def types = [
1739                        [ "id": "average", "name": "Average", "disabled": false ],
1740                        [ "id": "count", "name": "Count", "disabled": false ],
1741                        [ "id": "median", "name": "Median", "disabled": false ],
1742                        [ "id": "none", "name": "No aggregation", "disabled": false ],
1743                        [ "id": "sum", "name": "Sum", "disabled": false ],
1744                ]
1745
1746                // Normally, all aggregation types are possible, with three exceptions:
1747                //              Categorical data on both axes. In that case, we don't have anything to aggregate, so we can only count
1748                //              Grouping on a numerical field is not possible. In that case, it is ignored
1749                //                      Grouping on a numerical field with categorical data on both axes (table) enabled aggregation,
1750                //                      In that case we can aggregate on the numerical field.
1751               
1752                if(rowType == CATEGORICALDATA || rowType == DATE || rowType == RELTIME){
1753                        if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){
1754                               
1755                                if( groupType == NUMERICALDATA ) {
1756                                        // Disable 'none', since that can not be visualized
1757                                        types.each {
1758                                                if( it.id == "none" )
1759                                                        it.disabled = true
1760                                        }
1761                                } else {
1762                                        // Disable everything but 'count'
1763                                        types.each { 
1764                                                if( it.id != "count" ) 
1765                                                        it.disabled = true
1766                                        }
1767                                }
1768                        }
1769                }
1770               
1771                return types
1772   }
1773}
Note: See TracBrowser for help on using the repository browser.