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

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

more verbose controller in order to debug online error

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