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

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

Fixed a bug in showing null values in charts

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