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

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

Yet Another Commit (trying to fix error in fieldId creation)

File size: 57.8 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            /*
95            Gather fields related to this study from modules.
96            This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
97            It does not actually return measurements (the getMeasurementData call does).
98            The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
99            types related to these assays.
100            So, the required variables for such a call are:
101              - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
102              - an assay, which can be obtained with study.getAssays()
103             */
104            study.getAssays().each { assay ->
105                def list = []
106                if(!offlineModules.contains(assay.module.id)){
107                    list = getFields(assay.module.toString(), assay)
108                    if(list!=null){
109                        if(list.size()!=0){
110                            fields += list
111                        }
112                    }
113                }
114            }
115            offlineModules = []
116
117            // Make sure any informational messages regarding offline modules are submitted to the client
118            setInfoMessageOfflineModules()
119
120
121            // TODO: Maybe we should add study's own fields
122        } else {
123            log.error("VisualizationController: getFields: The requested study could not be found. Id: "+studies)
124            return returnError(404, "The requested study could not be found.")
125        }
126
127        fields.unique() // Todo: find out root cause of why some fields occur more than once
128        fields.sort { a, b ->
129            def sourceEquality = a.source.toString().toLowerCase().compareTo(b.source.toString().toLowerCase())
130            if( sourceEquality == 0 ) {
131                def categoryEquality = a.category.toString().toLowerCase().compareTo(b.category.toString().toLowerCase())
132                if( categoryEquality == 0 ){
133                    a.name.toString().toLowerCase().compareTo(b.name.toString().toLowerCase())
134                } else return categoryEquality
135            } else return sourceEquality
136        }
137                return sendResults(['studyIds': studies, 'fields': fields])
138        }
139
140        /**
141         * 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.
142         * @return List containing the possible visualization types, with each element containing
143     *          - a unique id
144     *          - a unique name
145     *         For example: ["id": "barchart", "name": "Barchart"]
146     * @see parseGetDataParams
147         * @see determineFieldType
148     * @see determineVisualizationTypes
149         */
150        def getVisualizationTypes = {
151        def inputData = parseGetDataParams();
152
153        if(inputData.columnIds == null || inputData.columnIds == [] || inputData.columnIds[0] == null || inputData.columnIds[0] == ""){
154            setInfoMessage("Please select a data source for the x-axis.")
155            return sendInfoMessage()
156        }
157        if(inputData.rowIds == null || inputData.rowIds == [] ||  inputData.rowIds[0] == null ||   inputData.rowIds[0] == ""){
158            setInfoMessage("Please select a data source for the y-axis.")
159            return sendInfoMessage()
160        }
161
162        // TODO: handle the case of multiple fields on an axis
163        // Determine data types
164        log.trace "Determining rowType: "+inputData.rowIds[0]
165        def rowType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0])
166       
167                log.trace "Determining columnType: "+inputData.columnIds[0]
168        def columnType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0])
169
170        // Determine possible visualization types
171        def types = determineVisualizationTypes(rowType, columnType)
172
173        log.trace  "types: "+types+", determined this based on "+rowType+" and "+columnType
174        return sendResults(['types':types,'rowIds':inputData.rowIds[0],'columnIds':inputData.columnIds[0]])
175        }
176
177    /**
178     * Gather fields related to this study from modules.
179        This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
180        getMeasurements does not actually return measurements (the getMeasurementData call does).
181     * @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)
182     * @param assay     The assay that the source module and the requested fields belong to
183     * @return  A list of map objects, containing the following:
184     *           - a key 'id' with a value formatted by the createFieldId function
185     *           - a key 'source' with a value equal to the input parameter 'source'
186     *           - a key 'category' with a value equal to the 'name' field of the input paramater 'assay'
187     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
188     */
189    def getFields(source, assay) {
190        def fields = []
191        def callUrl = ""
192
193        // Making a different call for each assay
194        def urlVars = "assayToken="+assay.assayUUID
195        try {
196            callUrl = ""+assay.module.url + "/rest/getMeasurementMetaData/query?"+urlVars
197            def json = moduleCommunicationService.callModuleRestMethodJSON( assay.module.url /* consumer */, callUrl );
198            println("JSON:"+json)
199            def collection = []
200            json.each{ jason ->
201                collection.add(jason)
202            }
203            // Formatting the data
204            collection.each { field ->
205                // For getting this field from this assay
206                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+")":"")  ]
207            }
208        } catch(Exception e){
209            //returnError(404, "An error occured while trying to collect field data from a module. Most likely, this module is offline.")
210            offlineModules.add(assay.module.id)
211            infoMessageOfflineModules.add(assay.module.name)
212            log.error("VisualizationController: getFields: "+e)
213        }
214
215        return fields
216    }
217
218    /**
219     * Gather fields related to this study from GSCF.
220     * @param study The study that is the source of the requested fields
221     * @param category  The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
222     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
223     * @return A list of map objects, formatted by the formatGSCFFields function
224     */
225    def getFields(study, category, type){
226        // Collecting the data from it's source
227        def collection = []
228        def fields = []
229        def source = "GSCF"
230               
231                if( type == "domainfields" ) 
232                        collection = domainObjectCallback( category )?.giveDomainFields();
233                else
234                        collection = templateObjectCallback( category, study )?.template?.fields
235
236        collection?.unique()
237
238        // Formatting the data
239        fields += formatGSCFFields(type, collection, source, category)
240
241        // Here we will remove those fields, whose set of datapoints only contain null
242        def fieldsToBeRemoved = []
243        fields.each{ field ->
244            def fieldData = getFieldData( study, study.samples, field.id )
245            fieldData.removeAll([null])
246            if(fieldData==[]){
247                // Field only contained nulls, so don't show it as a visualization option
248                fieldsToBeRemoved << field
249            }
250        }
251        fields.removeAll(fieldsToBeRemoved)
252
253        return fields
254    }
255
256    /**
257     * 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
258     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
259     * @param collectionOfFields A collection of fields, which could also contain only one item
260     * @param source Likely to be "GSCF"
261     * @param category The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
262     * @return A list containing list objects, containing the following:
263     *           - a key 'id' with a value formatted by the createFieldId function
264     *           - a key 'source' with a value equal to the input parameter 'source'
265     *           - a key 'category' with a value equal to the input parameter 'category'
266     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
267     */
268    def formatGSCFFields(type, collectionOfFields, source, category){
269
270        if(collectionOfFields==null || collectionOfFields == []){
271            return []
272        }
273        def fields = []
274        if(collectionOfFields instanceof Collection){
275            // Apparently this field is actually a list of fields.
276            // We will call ourselves again with the list's elements as input.
277            // These list elements will themselves go through this check again, effectively flattening the original input
278            for(int i = 0; i < collectionOfFields.size(); i++){
279                fields += formatGSCFFields(type, collectionOfFields[i], source, category)
280            }
281            return fields
282        } else {
283            // This is a single field. Format it and return the result.
284            if(type=="domainfields"){
285                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+")":"") ]
286            }
287            if(type=="templatefields"){
288                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+")":"")]
289            }
290            return fields
291        }
292    }
293
294        /**
295         * Retrieves data for the visualization itself.
296     * 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.
297     * @return A map containing containing (at least, in the case of a barchart) the following:
298     *           - a key 'type' containing the type of chart that will be visualized
299     *           - a key 'xaxis' containing the title and unit that should be displayed for the x-axis
300     *           - a key 'yaxis' containing the title and unit that should be displayed for the y-axis*
301     *           - a key 'series' containing a list, that contains one or more maps, which contain the following:
302     *                - a key 'name', containing, for example, a feature name or field name
303     *                - a key 'y', containing a list of y-values
304     *                - 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
305         */
306        def getData = {
307                // Extract parameters
308                // TODO: handle erroneous input data
309                def inputData = parseGetDataParams();
310
311        if(inputData.columnIds == null || inputData.rowIds == null){
312            infoMessage = "Please select data sources for the y- and x-axes."
313            return sendInfoMessage()
314        }
315
316                // TODO: handle the case that we have multiple studies
317                def studyId = inputData.studyIds[ 0 ];
318                def study = Study.get( studyId as Integer );
319
320                // Find out what samples are involved
321                def samples = study.samples
322
323        // If the user is requesting data that concerns only subjects, then make sure those subjects appear only once
324        if(parseFieldId( inputData.columnIds[ 0 ] ).type=='subjects' && parseFieldId( inputData.rowIds[ 0 ] ).type=='subjects'){
325            samples.unique { it.parentSubject }
326        }
327       
328                // Retrieve the data for both axes for all samples
329                // TODO: handle the case of multiple fields on an axis
330                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ];
331                def data = getAllFieldData( study, samples, fields );
332
333                // Group data based on the y-axis if categorical axis is selected
334        def groupedData
335        if(inputData.visualizationType=='horizontal_barchart'){
336            groupedData = groupFieldData( inputData.visualizationType, data, "y", "x" ); // Indicate non-standard axis ordering
337        } else {
338            groupedData = groupFieldData( inputData.visualizationType, data ); // Don't indicate axis ordering, standard <"x", "y"> will be used
339        }
340        // Format data so it can be rendered as JSON
341        def returnData
342        if(inputData.visualizationType=='horizontal_barchart'){
343            def valueAxisType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0], groupedData["x"])
344            def groupAxisType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0], groupedData["y"])
345            returnData = formatData( inputData.visualizationType, groupedData, fields, groupAxisType, valueAxisType , "y", "x" ); // Indicate non-standard axis ordering
346        } else {
347            def valueAxisType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0], groupedData["y"])
348            def groupAxisType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0], groupedData["x"])
349            returnData = formatData( inputData.visualizationType, groupedData, fields, groupAxisType, valueAxisType ); // Don't indicate axis ordering, standard <"x", "y"> will be used
350        }
351
352        // Make sure no changes are written to the database
353        study.discard()
354        samples*.discard()
355       
356        return sendResults(returnData)
357        }
358
359        /**
360         * Parses the parameters given by the user into a proper list
361         * @return Map with 4 keys:
362         *              studyIds:       list with studyIds selected
363         *              rowIds:         list with fieldIds selected for the rows
364         *              columnIds:      list with fieldIds selected for the columns
365         *              visualizationType:      String with the type of visualization required
366         * @see getFields
367         * @see getVisualizationTypes
368         */
369        def parseGetDataParams() {
370                def studyIds, rowIds, columnIds, visualizationType;
371               
372                studyIds = params.list( 'study' );
373                rowIds = params.list( 'rows' );
374                columnIds = params.list( 'columns' );
375                visualizationType = params.get( 'types')
376
377                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ];
378        }
379
380        /**
381         * Retrieve the field data for the selected fields
382         * @param study         Study for which the data should be retrieved
383         * @param samples       Samples for which the data should be retrieved
384         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
385         *                                              [ "x": "field-id-1", "y": "field-id-3" ]
386         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the
387         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example:
388         *                                              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ]
389         */
390        def getAllFieldData( study, samples, fields ) {
391                def fieldData = [:]
392                fields.each{ field ->
393                        fieldData[ field.key ] = getFieldData( study, samples, field.value );
394                }
395               
396                return fieldData;
397        }
398       
399        /**
400        * Retrieve the field data for the selected field
401        * @param study          Study for which the data should be retrieved
402        * @param samples        Samples for which the data should be retrieved
403        * @param fieldId        ID of the field to return data for
404        * @return                       A list of values of the selected field for all samples. If a value
405        *                                       could not be retrieved for a sample, null is returned. Examples:
406        *                                               [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
407        */
408        def getFieldData( study, samples, fieldId ) {
409                // Parse the fieldId as given by the user
410                def parsedField = parseFieldId( fieldId );
411               
412                def data = []
413               
414                if( parsedField.source == "GSCF" ) {
415                        // Retrieve data from GSCF itself
416                        def closure = valueCallback( parsedField.type )
417
418                        if( closure ) {
419                                samples.each { sample ->
420                                        // Retrieve the value for the selected field for this sample
421                                        def value = closure( sample, parsedField.name );
422
423                    data << value;
424                                }
425                        } else {
426                                // TODO: Handle error properly
427                                // Closure could not be retrieved, probably because the type is incorrect
428                                data = samples.collect { return null }
429                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
430                        }
431                } else {
432                        // Data must be retrieved from a module
433                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
434                }
435               
436                return data
437        }
438       
439        /**
440         * Retrieve data for a given field from a data module
441         * @param study                 Study to retrieve data for
442         * @param samples               Samples to retrieve data for
443         * @param source_module Name of the module to retrieve data from
444         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
445         * @return                              A list of values of the selected field for all samples. If a value
446         *                                              could not be retrieved for a sample, null is returned. Examples:
447         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
448         */
449        def getModuleData( study, samples, assay_id, fieldName ) {
450                def data = []
451               
452                // TODO: Handle values that should be retrieved from multiple assays
453        def assay = Assay.get(assay_id);
454
455        if( assay ) {
456            // Request for a particular assay and a particular feature
457            def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName.encodeAsURL()
458            urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
459
460            def callUrl
461            try {
462                callUrl = assay.module.url + "/rest/getMeasurementData"
463                def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
464
465                if( json ) {
466                    // First element contains sampletokens
467                    // Second element contains the featurename
468                    // Third element contains the measurement value
469                    def sampleTokens = json[ 0 ]
470                    def measurements = json[ 2 ]
471
472                    // Loop through the samples
473                    samples.each { sample ->
474                        // Search for this sampletoken
475                        def sampleToken = sample.sampleUUID;
476                        def index = sampleTokens.findIndexOf { it == sampleToken }
477
478                        if( index > -1 ) {
479                            data << measurements[ index ];
480                        } else {
481                            data << null
482                        }
483                    }
484                } else {
485                    // TODO: handle error
486                    // Returns an empty list with as many elements as there are samples
487                    data = samples.collect { return null }
488                }
489
490            } catch(Exception e){
491                log.error("VisualizationController: getFields: "+e)
492                //return returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
493                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.")
494            }
495        } else {
496            // TODO: Handle error correctly
497            // Returns an empty list with as many elements as there are samples
498            data = samples.collect { return null }
499        }
500
501        //println "\t data request: "+data
502                return data
503        }
504
505        /**
506         * Group the field data on the values of the specified axis. For example, for a bar chart, the values
507         * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard
508         * error of the mean is returned in the 'error' property
509     * @param visualizationType Some types require a different formatting/grouping of the data, such as 'table'
510         * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input
511         * @param groupAxis     Name of the axis to group on. Defaults to "x"
512         * @param valueAxis     Name of the axis where the values are. Defaults to "y"
513         * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"
514         * @param unknownName   Name of the group for all null groups. Defaults to "unknown"
515         * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the
516         *                                      selected field for all groups. For example, if the input is
517         *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ]
518         *                                      the output will be:
519         *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ]
520         *
521         *                                      As you can see: null values in the valueAxis are ignored. Null values in the
522         *                                      group axis are combined into a 'unknown' category.
523         */
524        def groupFieldData( visualizationType, data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) {
525                // TODO: Give the user the possibility to change this value in the user interface
526                def showEmptyCategories = false;
527               
528                // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a
529                // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list
530                // elements. A few lines below, this case is handled again by checking whether a specific sample belongs
531                // to this group.
532                // After flattening, the list is uniqued. The closure makes sure that values with different classes are
533                // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator)
534                def groups = data[ groupAxis ]
535                                                .flatten()
536                                                .unique { it == null ? "null" : it.class.name + it.toString() }
537                                               
538                // Make sure the null category is last
539                groups = groups.findAll { it != null } + groups.findAll { it == null }
540               
541                // Generate the output object
542                def outputData = [:]
543                outputData[ valueAxis ] = [];
544                outputData[ errorName ] = [];
545                outputData[ groupAxis ] = [];
546               
547                // Loop through all groups, and gather the values for this group
548        // A visualization of type 'table' is a special case. There, the counts of two combinations of 'groupAxis'
549                // and 'valueAxis' items are computed
550        if( visualizationType=='table' ){
551            // For each 'valueAxis' item and 'groupAxis' item combination, count how often they appear together.
552            def counts = [:]
553                       
554            // The 'counts' list uses keys like this: ['item1':group, 'item2':value]
555            // The value will be an integer (the count)
556            data[ groupAxis ].eachWithIndex { group, index ->
557                def value =  data[ valueAxis ][index]
558                if(!counts.get(['item1':group, 'item2':value])){
559                    counts[['item1':group, 'item2':value]] = 1
560                } else {
561                    counts[['item1':group, 'item2':value]] = counts[['item1':group, 'item2':value]] + 1
562                }
563            }
564           
565                        def valueData =  data[ valueAxis ]
566                                        .flatten()
567                                        .unique { it == null ? "null" : it.class.name + it.toString() }
568                                                                                                       
569                        // Now we will first check whether any of the categories is empty. If some of the rows
570                        // or columns are empty, don't include them in the output
571                        if( !showEmptyCategories ) {
572                                groups.eachWithIndex { group, index ->
573                                        if( counts.findAll { it.key.item1 == group } )
574                                                outputData[groupAxis] << group
575                                }
576                               
577                                valueData.each { value ->
578                                        if( counts.findAll { it.key.item2 == value } )
579                                                outputData[valueAxis] << value
580                                }
581                        } else {
582                                outputData[groupAxis] = groups.collect { it != null ? it : unknownName }
583                                ouputData[valueAxis] = valueData
584                        }
585                                                                                                                               
586            // Because we are collecting counts, we do not set the 'errorName' item of the 'outputData' map.
587            // We do however set the 'data' map to contain the counts. We set it in such a manner that it has
588                        // a 'table' layout with respect to 'groupAxis' and 'valueAxis'.
589            def rows = []
590            outputData[groupAxis].each{ group ->
591                def row = []
592                outputData[valueAxis].each{ value ->
593                    row << counts[['item1':group, 'item2':value]]
594                }
595                while(row.contains(null)){
596                    row[row.indexOf(null)] = 0
597                } // 'null' should count as '0'. Items of count zero have never been added to the 'counts' list and as such will appear as a 'null' value.
598                if(row!=[]) rows << row
599            }
600            outputData['data']= rows
601                       
602                        // Convert groups to group names
603                        outputData[ groupAxis ] = outputData[ groupAxis ].collect { it != null ? it : unknownName }
604        } else {
605            groups.each { group ->
606                // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
607                // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
608                def indices = data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group };
609                def values  = data[ valueAxis ][ indices ]
610
611                                // The computation for mean and error will return null if no (numerical) values are found
612                                // In that case, the user won't see this category
613                def dataForGroup = null;
614                switch( params.get( 'aggregation') ) {
615                                case "average":
616                        dataForGroup = computeMeanAndError( values );
617                        break;
618                    case "count":
619                        dataForGroup = computeCount( values );
620                        break;
621                    case "median":
622                        dataForGroup = computeMedian( values );
623                        break;
624                    case "none":
625                        // Currently disabled, create another function
626                        dataForGroup = computeMeanAndError( values );
627                        break;
628                    case "sum":
629                        dataForGroup = computeSum( values );
630                        break;
631                    default:
632                        // Default is "average"
633                        dataForGroup = computeMeanAndError( values );
634                }
635
636
637                                if( showEmptyCategories || dataForGroup.value != null ) {
638                                        // Gather names for the groups. Most of the times, the group names are just the names, only with
639                                        // a null value, the unknownName must be used
640                                        outputData[ groupAxis ] << ( group != null ? group : unknownName )
641                        outputData[ valueAxis ] << dataForGroup.value ?: 0
642                        outputData[ errorName ] << dataForGroup.error ?: 0
643                                }
644            }
645        }
646               
647                return outputData
648        }
649       
650        /**
651         * Formats the grouped data in such a way that the clientside visualization method
652         * can handle the data correctly.
653         * @param groupedData   Data that has been grouped using the groupFields method
654         * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
655         *                                                      [ "x": "field-id-1", "y": "field-id-3" ]
656     * @param groupAxisType Integer, either CATEGORICAL or NUMERIACAL
657     * @param valueAxisType Integer, either CATEGORICAL or NUMERIACAL
658         * @param groupAxis             Name of the axis to with group data. Defaults to "x"
659         * @param valueAxis             Name of the axis where the values are stored. Defaults to "y"
660         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
661         * @return                              A map like the following:
662         *
663                        {
664                                "type": "barchart",
665                                "xaxis": { "title": "quarter 2011", "unit": "" },
666                                "yaxis": { "title": "temperature", "unit": "degrees C" },
667                                "series": [
668                                        {
669                                                "name": "series name",
670                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
671                        "x": [ "Q1", "Q2", "Q3", "Q4" ],
672                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
673                                        },
674                                ]
675                        }
676         *
677         */
678        def formatData( type, groupedData, fields, groupAxisType, valueAxisType, groupAxis = "x", valueAxis = "y", errorName = "error" ) {
679                // We want to sort the data based on the group-axis, but keep the values on the value-axis in sync.
680                // The only way seems to be to combine data from both axes.
681        def combined = []
682        if(type=="table"){
683            groupedData[ groupAxis ].eachWithIndex { group, i ->
684                combined << [ "group": group, "data": groupedData[ 'data' ][ i ] ]
685            }
686            combined.sort { it.group.toString() }
687            groupedData[groupAxis] = renderTimesAndDatesHumanReadable(combined*.group, groupAxisType)
688            groupedData[valueAxis] = renderTimesAndDatesHumanReadable(groupedData[valueAxis], valueAxisType)
689            groupedData["data"] = combined*.data
690        } else {
691            groupedData[ groupAxis ].eachWithIndex { group, i ->
692                combined << [ "group": (groupAxisType==CATEGORICALDATA ? group.toString() : group), "value": groupedData[ valueAxis ][ i ] ]
693            }
694            combined.sort { it.group }
695            groupedData[groupAxis] = renderTimesAndDatesHumanReadable(combined*.group, groupAxisType)
696            groupedData[valueAxis] = renderTimesAndDatesHumanReadable(combined*.value, valueAxisType)
697        }
698        // TODO: Handle name and unit of fields correctly
699        def valueAxisTypeString = (valueAxisType==CATEGORICALDATA || valueAxisType==DATE || valueAxisType==RELTIME ? "categorical" : "numerical")
700        def groupAxisTypeString = (groupAxisType==CATEGORICALDATA || groupAxisType==DATE || groupAxisType==RELTIME ? "categorical" : "numerical")
701
702        if(type=="table"){
703            def return_data = [:]
704            return_data[ "type" ] = type
705            return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : parseFieldId( fields[ valueAxis ] ).unit, "type":valueAxisTypeString ])
706            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": parseFieldId( fields[ groupAxis ] ).unit, "type":groupAxisTypeString ])
707            return_data.put("series", [[
708                    "x": groupedData[ groupAxis ].collect { it.toString() },
709                    "y": groupedData[ valueAxis ].collect { it.toString() },
710                    "data": groupedData["data"]
711            ]])
712            return return_data;
713        } else {
714            def xAxis = groupedData[ groupAxis ].collect { it.toString() };
715            def yName = parseFieldId( fields[ valueAxis ] ).name;
716
717            def return_data = [:]
718            return_data[ "type" ] = type
719            return_data.put("yaxis", ["title" : yName, "unit" : parseFieldId( fields[ valueAxis ] ).unit, "type":valueAxisTypeString ])
720            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": parseFieldId( fields[ groupAxis ] ).unit, "type":groupAxisTypeString  ])
721            return_data.put("series", [[
722                "name": yName,
723                "x": xAxis,
724                "y": groupedData[ valueAxis ],
725                "error": groupedData[ errorName ]
726            ]])
727
728            return return_data;
729        }
730        }
731
732    /**
733     * 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.
734     * @param data  The list of items that needs to be checked/converted
735     * @param axisType As determined by determineFieldType
736     * @return The input variable 'data', with it's date and time elements converted.
737     * @see determineFieldType
738     */
739    def renderTimesAndDatesHumanReadable(data, axisType){
740        if(axisType==RELTIME){
741            data = renderTimesHumanReadable(data)
742        }
743        if(axisType==DATE){
744           data = renderDatesHumanReadable(data)
745        }
746        return data
747    }
748
749    /**
750     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
751     * @param data
752     * @return
753     */
754    def renderTimesHumanReadable(data){
755        def tmpTimeContainer = []
756        data. each {
757            if(it instanceof Number) {
758                try{
759                    tmpTimeContainer << new RelTime( it ).toPrettyString()
760                } catch(IllegalArgumentException e){
761                    tmpTimeContainer << it
762                }
763            } else {
764                tmpTimeContainer << it // To handle items such as 'unknown'
765            }
766        }
767        return tmpTimeContainer
768    }
769
770    /**
771     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
772     * @param data
773     * @return
774     */
775    def renderDatesHumanReadable(data) {
776        def tmpDateContainer = []
777        data. each {
778            if(it instanceof Number) {
779                try{
780                    tmpDateContainer << new java.util.Date( (Long) it ).toString()
781                } catch(IllegalArgumentException e){
782                    tmpDateContainer << it
783                }
784            } else {
785                tmpDateContainer << it // To handle items such as 'unknown'
786            }
787        }
788        return tmpDateContainer
789    }
790        /**
791         * Returns a closure for the given entitytype that determines the value for a criterion
792         * on the given object. The closure receives two parameters: the sample and a field.
793         *
794         * For example:
795         *              How can one retrieve the value for subject.name, given a sample? This can be done by
796         *              returning the field values sample.parentSubject:
797         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
798         * @return      Closure that retrieves the value for a field and the given field
799         */
800        protected Closure valueCallback( String entity ) {
801                switch( entity ) {
802                        case "Study":
803                        case "studies":
804                                return { sample, field -> return getFieldValue( sample.parent, field ) }
805                        case "Subject":
806                        case "subjects":
807                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
808                        case "Sample":
809                        case "samples":
810                                return { sample, field -> return getFieldValue( sample, field ) }
811                        case "Event":
812                        case "events":
813                                return { sample, field ->
814                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
815                                                return null
816
817                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
818                                }
819                        case "SamplingEvent":
820                        case "samplingEvents":
821                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
822                        case "Assay":
823                        case "assays":
824                                return { sample, field ->
825                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
826                                        if( sampleAssays && sampleAssays.size() > 0 )
827                                                return sampleAssays.collect { getFieldValue( it, field ) }
828                                        else
829                                                return null
830                                }
831                }
832        }
833       
834        /**
835        * Returns the domain object that should be used with the given entity string
836        *
837        * For example:
838        *               What object should be consulted if the user asks for "studies"
839        *               Response: Study
840        * @return       Domain object that should be used with the given entity string
841        */
842   protected def domainObjectCallback( String entity ) {
843           switch( entity ) {
844                   case "Study":
845                   case "studies":
846                           return Study
847                   case "Subject":
848                   case "subjects":
849                           return Subject
850                   case "Sample":
851                   case "samples":
852                           return Sample
853                   case "Event":
854                   case "events":
855                        return Event
856                   case "SamplingEvent":
857                   case "samplingEvents":
858                           return SamplingEvent
859                   case "Assay":
860                   case "assays":
861                                return Assay
862           }
863   }
864
865    /**
866    * Returns the objects within the given study that should be used with the given entity string
867    *
868    * For example:
869    *           What object should be consulted if the user asks for "samples"
870    *           Response: study.samples
871    * @return   List of domain objects that should be used with the given entity string
872    */
873    protected def templateObjectCallback( String entity, Study study ) {
874      switch( entity ) {
875          case "Study":
876          case "studies":
877              return study
878          case "Subject":
879          case "subjects":
880              return study?.subjects
881          case "Sample":
882          case "samples":
883              return study?.samples
884          case "Event":
885          case "events":
886               return study?.events
887          case "SamplingEvent":
888          case "samplingEvents":
889              return study?.samplingEvents
890          case "Assay":
891          case "assays":
892                  return study?.assays
893      }
894    }
895       
896        /**
897         * Computes the mean value and Standard Error of the mean (SEM) for the given values
898         * @param values        List of values to compute the mean and SEM for. Strings and null
899         *                                      values are ignored
900         * @return                      Map with two keys: 'value' and 'error'
901         */
902        protected Map computeMeanAndError( values ) {
903                // TODO: Handle the case that one of the values is a list. In that case,
904                // all values should be taken into account.     
905                def mean = computeMean( values );
906                def error = computeSEM( values, mean );
907               
908                return [ 
909                        "value": mean,
910                        "error": error
911                ]
912        }
913       
914        /**
915         * Computes the mean of the given values. Values that can not be parsed to a number
916         * are ignored. If no values are given, null is returned.
917         * @param values        List of values to compute the mean for
918         * @return                      Arithmetic mean of the values
919         */
920        protected def computeMean( List values ) {
921                def sumOfValues = 0;
922                def sizeOfValues = 0;
923                values.each { value ->
924                        def num = getNumericValue( value );
925                        if( num != null ) {
926                                sumOfValues += num;
927                                sizeOfValues++
928                        }
929                }
930
931                if( sizeOfValues > 0 )
932                        return sumOfValues / sizeOfValues;
933                else
934                        return null;
935        }
936
937        /**
938        * Computes the standard error of mean of the given values. 
939        * Values that can not be parsed to a number are ignored. 
940        * If no values are given, null is returned.
941        * @param values         List of values to compute the standard deviation for
942        * @param mean           Mean of the list (if already computed). If not given, the mean
943        *                                       will be computed using the computeMean method
944        * @return                       Standard error of the mean of the values or 0 if no values can be used.
945        */
946    protected def computeSEM( List values, def mean = null ) {
947       if( mean == null )
948            mean = computeMean( values )
949
950       def sumOfDifferences = 0;
951       def sizeOfValues = 0;
952       values.each { value ->
953           def num = getNumericValue( value );
954           if( num != null ) {
955               sumOfDifferences += Math.pow( num - mean, 2 );
956               sizeOfValues++
957           }
958       }
959
960       if( sizeOfValues > 0 ) {
961           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
962           return std / Math.sqrt( sizeOfValues );
963       } else {
964           return null;
965       }
966    }
967
968    /**
969         * Computes the median of the given values. Values that can not be parsed to a number
970         * are ignored. If no values are given, null is returned.
971         * @param values        List of values to compute the median for
972         * @return                      Median of the values
973         */
974        protected def computeMedian( List values ) {
975                def listOfValues = [];
976                values.each { value ->
977                        def num = getNumericValue( value );
978                        if( num != null ) {
979                                listOfValues << num;
980                        }
981                }
982
983        listOfValues.sort();
984
985        def listSize = listOfValues.size();
986
987        def objReturn = null;
988
989                if( listSize > 0 ) {
990            def listHalf = (int) Math.abs(listSize/2);
991            if(listSize%2==0) {
992                // If the list is of an even size, take the mean of the middle two value's
993                objReturn = (listOfValues.get(listHalf)+listOfValues.get(listHalf-1))/2;
994            } else {
995                // If the list is of an odd size, take the middle value
996                objReturn = listOfValues.get(listHalf);
997            }
998        }
999
1000                return ["value": objReturn];
1001        }
1002
1003    /**
1004         * Computes the count of the given values. Values that can not be parsed to a number
1005         * are ignored. If no values are given, null is returned.
1006         * @param values        List of values to compute the count for
1007         * @return                      Count of the values
1008         */
1009        protected def computeCount( List values ) {
1010                def sumOfValues = 0;
1011                def sizeOfValues = 0;
1012                values.each { value ->
1013                        def num = getNumericValue( value );
1014                        if( num != null ) {
1015                                sumOfValues += num;
1016                                sizeOfValues++
1017                        }
1018                }
1019
1020                if( sizeOfValues > 0 )
1021                        return ["value": sizeOfValues];
1022                else
1023                        return ["value": null];
1024        }
1025
1026    /**
1027         * Computes the sum of the given values. Values that can not be parsed to a number
1028         * are ignored. If no values are given, null is returned.
1029         * @param values        List of values to compute the sum for
1030         * @return                      Arithmetic sum of the values
1031         */
1032        protected def computeSum( List values ) {
1033                def sumOfValues = 0;
1034                def sizeOfValues = 0;
1035                values.each { value ->
1036                        def num = getNumericValue( value );
1037                        if( num != null ) {
1038                                sumOfValues += num;
1039                                sizeOfValues++
1040                        }
1041                }
1042
1043                if( sizeOfValues > 0 )
1044                        return ["value": sumOfValues];
1045                else
1046                        return ["value": null];
1047        }
1048   
1049        /**
1050         * Return the numeric value of the given object, or null if no numeric value could be returned
1051         * @param       value   Object to return the value for
1052         * @return                      Number that represents the given value
1053         */
1054        protected Number getNumericValue( value ) {
1055                // TODO: handle special types of values
1056                if( value instanceof Number ) {
1057                        return value;
1058                } else if( value instanceof RelTime ) {
1059                        return value.value;
1060                }
1061               
1062                return null
1063        }
1064
1065        /** 
1066         * Returns a field for a given templateentity
1067         * @param object        TemplateEntity (or subclass) to retrieve data for
1068         * @param fieldName     Name of the field to return data for.
1069         * @return                      Value of the field or null if the value could not be retrieved
1070         */
1071        protected def getFieldValue( TemplateEntity object, String fieldName ) {
1072                if( !object || !fieldName )
1073                        return null;
1074               
1075                try {
1076                        return object.getFieldValue( fieldName );
1077                } catch( Exception e ) {
1078                        return null;
1079                }
1080        }
1081
1082        /**
1083         * Parses a fieldId that has been created earlier by createFieldId
1084         * @param fieldId       FieldId to parse
1085         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
1086         * @see createFieldId
1087         */
1088        protected Map parseFieldId( String fieldId ) {
1089                def attrs = [:]
1090
1091                def parts = fieldId.split(",",5)
1092               
1093                attrs = [
1094                        "id": new String(parts[ 0 ].decodeBase64()),
1095                        "name": new String(parts[ 1 ].decodeBase64()),
1096                        "source": new String(parts[ 2 ].decodeBase64()),
1097                        "type": new String(parts[ 3 ].decodeBase64()),
1098            "unit": parts.length>4? new String(parts[ 4 ].decodeBase64()) : null
1099                ]
1100
1101        return attrs
1102        }
1103       
1104        /**
1105         * Create a fieldId based on the given attributes
1106         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
1107         * @return                      Unique field ID for these parameters
1108         * @see parseFieldId
1109         */
1110        protected String createFieldId( Map attrs ) {
1111                // TODO: What if one of the attributes contains a comma?
1112                def name = attrs.name+"";
1113                def id = (attrs.id ?: name)+"";
1114                def source = attrs.source+"";
1115                def type = (attrs.type ?: "")+"";
1116        def unit = (attrs.unit ?: "")+"";
1117
1118                return id.bytes.encodeBase64().toString() + "," +
1119                name.bytes.encodeBase64().toString() + "," +
1120                source.bytes.encodeBase64().toString() + "," +
1121                type.bytes.encodeBase64().toString() + "," +
1122                unit.bytes.encodeBase64().toString();
1123        }
1124
1125    /**
1126     * Set the response code and an error message
1127     * @param code HTTP status code
1128     * @param msg Error message, string
1129     */
1130    protected void returnError(code, msg){
1131        response.sendError(code , msg)
1132    }
1133
1134    /**
1135     * Determines what type of data a field contains
1136     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
1137     * @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
1138     * @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
1139     * @return Either CATEGORICALDATA, NUMERICALDATA, DATE or RELTIME
1140     */
1141    protected int determineFieldType(studyId, fieldId, inputData = null){
1142                def parsedField = parseFieldId( fieldId );
1143        def study = Study.get(studyId)
1144                def data = []
1145
1146        try{
1147            if( parsedField.source == "GSCF" ) {
1148                if(parsedField.id.isNumber()){
1149                        return determineCategoryFromTemplateFieldId(parsedField.id)
1150                } else { // Domainfield or memberclass
1151                    def callback = domainObjectCallback( parsedField.type )
1152
1153                    // 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
1154                    if(callback?.giveDomainFields().name.contains(parsedField.name.toString())){
1155                        // Use the associated templateField to determine the field type
1156                        return determineCategoryFromTemplateField(
1157                                callback?.giveDomainFields()[
1158                                    callback?.giveDomainFields().name.indexOf(parsedField.name.toString())
1159                                ]
1160                        )
1161                    }
1162                    // Apparently it is not a templatefield as well as a memberclass
1163
1164                    def field = callback?.declaredFields.find { it.name == parsedField.name };
1165                    if( field ) {
1166                        return determineCategoryFromClass( field.getType() )
1167                    } else {
1168                        // TODO: how do we communicate this to the user? Do we allow the process to proceed?
1169                        log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
1170                    }
1171                }
1172            } else {
1173                if(inputData == null){ // If we did not get data, we need to request it from the module first
1174                    data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
1175                    return determineCategoryFromData(data)
1176                } else {
1177                    return determineCategoryFromData(inputData)
1178                }
1179            }
1180        } catch(Exception e){
1181            log.error("VisualizationController: determineFieldType: "+e)
1182            e.printStackTrace()
1183            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1184            return CATEGORICALDATA
1185        }
1186    }
1187
1188    /**
1189     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
1190     * @param classObject
1191     * @return Either CATEGORICALDATA of NUMERICALDATA
1192     */
1193    protected int determineCategoryFromClass(classObject){
1194        log.trace "Determine category from class: "+classObject+", of class: "+classObject?.class
1195        if(classObject==java.lang.String){
1196            return CATEGORICALDATA
1197        } else {
1198            return NUMERICALDATA
1199        }
1200    }
1201
1202    /**
1203     * 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.
1204     * @param inputObject Either a single item, or a collection of items
1205     * @return Either CATEGORICALDATA of NUMERICALDATA
1206     */
1207    protected int determineCategoryFromData(inputObject){
1208        def results = []
1209               
1210        if(inputObject instanceof Collection){
1211            // This data is more complex than a single value, so we will call ourselves again so we c
1212            inputObject.each {
1213                                if( it != null )
1214                        results << determineCategoryFromData(it)
1215            }
1216        } else {
1217                        // Unfortunately, the JSON null object doesn't resolve to false or equals null. For that reason, we
1218                        // exclude those objects explicitly here.
1219                        if( inputObject != null && inputObject?.class != org.codehaus.groovy.grails.web.json.JSONObject$Null ) {
1220                    if(inputObject.toString().isDouble()){
1221                        results << NUMERICALDATA
1222                    } else {
1223                        results << CATEGORICALDATA
1224                    }
1225                        }
1226        }
1227
1228        results.unique()
1229
1230        if(results.size() > 1) {
1231            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1232            results[0] = CATEGORICALDATA
1233        } else if( results.size() == 0 ) {
1234                        // If the list is empty, return the numerical type. If it is the only value, if will
1235                        // be discarded later on. If there are more entries (e.g part of a collection)
1236                        // the values will be regarded as numerical, if the other values are numerical 
1237                        results[ 0 ] = NUMERICALDATA
1238        }
1239
1240                return results[0]
1241    }
1242
1243    /**
1244     * Determines a field category, based on the TemplateFieldId of a Templatefield
1245     * @param id A database ID for a TemplateField
1246     * @return Either CATEGORICALDATA of NUMERICALDATA
1247     */
1248    protected int determineCategoryFromTemplateFieldId(id){
1249        TemplateField tf = TemplateField.get(id)
1250        return determineCategoryFromTemplateField(tf)
1251    }
1252
1253    /**
1254     * Determines a field category, based on the TemplateFieldType of a Templatefield
1255     * @param id A database ID for a TemplateField
1256     * @return Either CATEGORICALDATA of NUMERICALDATA
1257     */
1258    protected int determineCategoryFromTemplateField(tf){
1259        if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG){
1260            log.trace "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
1261            return NUMERICALDATA
1262        }
1263        if(tf.type==TemplateFieldType.DATE){
1264            log.trace "GSCF templatefield: DATE ("+DATE+") (based on "+tf.type+")"
1265            return DATE
1266        }
1267        if(tf.type==TemplateFieldType.RELTIME){
1268            log.trace "GSCF templatefield: RELTIME ("+RELTIME+") (based on "+tf.type+")"
1269            return RELTIME
1270        }
1271        log.trace "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
1272        return CATEGORICALDATA
1273    }
1274    /**
1275     * 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.
1276     * @param returnData The object containing the data
1277     * @return results A JSON object
1278     */
1279    protected void sendResults(returnData){
1280        def results = [:]
1281        if(infoMessage.size()!=0){
1282            results.put("infoMessage", infoMessage)
1283            infoMessage = []
1284        }
1285        results.put("returnData", returnData)
1286        render results as JSON
1287    }
1288
1289    /**
1290     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
1291     * @param returnData The object containing the data
1292     * @return results A JSON object
1293     */
1294    protected void sendInfoMessage(){
1295        def results = [:]
1296        results.put("infoMessage", infoMessage)
1297        infoMessage = []
1298        render results as JSON
1299    }
1300
1301    /**
1302     * Adds a new message to the infoMessage
1303     * @param message The information that needs to be added to the infoMessage
1304     */
1305    protected void setInfoMessage(message){
1306        infoMessage.add(message)
1307        log.trace "setInfoMessage: "+infoMessage
1308    }
1309
1310    /**
1311     * Adds a message to the infoMessage that gives the client information about offline modules
1312     */
1313    protected void setInfoMessageOfflineModules(){
1314        infoMessageOfflineModules.unique()
1315        if(infoMessageOfflineModules.size()>0){
1316            String message = "Unfortunately"
1317            infoMessageOfflineModules.eachWithIndex{ it, index ->
1318                if(index==(infoMessageOfflineModules.size()-2)){
1319                    message += ', the '+it+' and '
1320                } else {
1321                    if(index==(infoMessageOfflineModules.size()-1)){
1322                        message += ' the '+it
1323                    } else {
1324                        message += ', the '+it
1325                    }
1326                }
1327            }
1328            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
1329            if(infoMessageOfflineModules.size()>1){
1330                message += "these modules."
1331            } else {
1332                message += "this module."
1333            }
1334            setInfoMessage(message)
1335        }
1336        infoMessageOfflineModules = []
1337    }
1338
1339    /**
1340     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1341     * @param inputData Contains a list of maps, of the following format
1342     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1343     *            - a key 'name', containing, for example, a feature name or field name
1344     *            - a key 'y', containing a list of y-values
1345     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1346     */
1347    protected def formatCategoryData(inputData){
1348        // NOTE: This function is no longer up to date with the current inputData layout.
1349        def series = []
1350        inputData.eachWithIndex { it, i ->
1351            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1352        }
1353        def ret = [:]
1354        ret.put('type', inputData[0]['type'])
1355        ret.put('x', inputData[0]['x'])
1356        ret.put('yaxis',['title': 'title', 'unit': ''])
1357        ret.put('xaxis', inputData[0]['xaxis'])
1358        ret.put('series', series)
1359        return ret
1360    }
1361
1362    /**
1363     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1364     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1365     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1366     * @return
1367     */
1368    protected def determineVisualizationTypes(rowType, columnType){
1369         def types = []
1370        if(rowType==CATEGORICALDATA || DATE || RELTIME){
1371            if(columnType==CATEGORICALDATA || DATE || RELTIME){
1372                types = [ [ "id": "table", "name": "Table"] ];
1373            }
1374            if(columnType==NUMERICALDATA){
1375                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1376            }
1377        }
1378        if(rowType==NUMERICALDATA){
1379            if(columnType==CATEGORICALDATA || DATE || RELTIME){
1380                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
1381            }
1382            if(columnType==NUMERICALDATA){
1383                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1384            }
1385        }
1386        return types
1387    }
1388}
Note: See TracBrowser for help on using the repository browser.