root/trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy @ 2061

Revision 2061, 56.7 KB (checked in by tjeerd@…, 3 years ago)

VIS-39, added a message to inform users that in this prototype samples are required. Also some small code improvements are present in this changeset

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