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

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