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

Last change on this file since 2139 was 2139, checked in by robert@…, 12 years ago

Boxplot sorting is now improved

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