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

Last change on this file since 2037 was 2037, checked in by taco@…, 7 years ago

visualization/VisualizeController.groovy, added support for visualizationType 'table'

File size: 44.6 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
31        /**
32         * Shows the visualization screen
33         */
34        def index = {
35                [ studies: Study.giveReadableStudies( authenticationService.getLoggedInUser() )]
36        }
37
38        def getStudies = {
39                def studies = Study.giveReadableStudies( authenticationService.getLoggedInUser() );
40        return sendResults(studies)
41        }
42
43        /**
44         * 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
45         * @return List containing fields
46     * @see parseGetDataParams
47         * @see getFields
48         */
49    def getFields = {
50                def input_object
51                def studies
52
53                try{
54                        input_object = parseGetDataParams();
55                } catch(Exception e) {
56                        log.error("VisualizationController: getFields: "+e)
57            return returnError(400, "An error occured while retrieving the user input.")
58                }
59
60        // Check to see if we have enough information
61        if(input_object==null || input_object?.studyIds==null){
62            setInfoMessage("Please select a study.")
63            return sendInfoMessage()
64        } else {
65            studies = input_object.studyIds[0]
66        }
67
68                def fields = [];
69
70        /*
71         Gather fields related to this study from GSCF.
72         This requires:
73         - a study.
74         - a category variable, e.g. "events".
75         - a type variable, either "domainfields" or "templatefields".
76         */
77        // TODO: Handle multiple studies
78        def study = Study.get(studies)
79
80        if(study!=null){
81            fields += getFields(study, "subjects", "domainfields")
82            fields += getFields(study, "subjects", "templatefields")
83            fields += getFields(study, "events", "domainfields")
84            fields += getFields(study, "events", "templatefields")
85            fields += getFields(study, "samplingEvents", "domainfields")
86            fields += getFields(study, "samplingEvents", "templatefields")
87            fields += getFields(study, "assays", "domainfields")
88            fields += getFields(study, "assays", "templatefields")
89            fields += getFields(study, "samples", "domainfields")
90            fields += getFields(study, "samples", "templatefields")
91
92            /*
93            Gather fields related to this study from modules.
94            This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
95            It does not actually return measurements (the getMeasurementData call does).
96            The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
97            types related to these assays.
98            So, the required variables for such a call are:
99              - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
100              - an assay, which can be obtained with study.getAssays()
101             */
102            study.getAssays().each { assay ->
103                def list = []
104                if(!offlineModules.contains(assay.module.id)){
105                    list = getFields(assay.module.id, assay)
106                    if(list!=null){
107                        if(list.size()!=0){
108                            fields += list
109                        }
110                    }
111                }
112            }
113            offlineModules = []
114
115            // Make sure any informational messages regarding offline modules are submitted to the client
116            setInfoMessageOfflineModules()
117
118
119            // TODO: Maybe we should add study's own fields
120        } else {
121            log.error("VisualizationController: getFields: The requested study could not be found. Id: "+studies)
122            return returnError(404, "The requested study could not be found.")
123        }
124
125                return sendResults(['studyIds': studies, 'fields': fields])
126        }
127
128        /**
129         * 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.
130         * @return List containing the possible visualization types, with each element containing
131     *          - a unique id
132     *          - a unique name
133     *         For example: ["id": "barchart", "name": "Barchart"]
134     * @see parseGetDataParams
135         * @see determineFieldType
136     * @see determineVisualizationTypes
137         */
138        def getVisualizationTypes = {
139        def inputData = parseGetDataParams();
140
141        if(inputData.columnIds == null || inputData.columnIds == [] || inputData.columnIds[0] == null || inputData.columnIds[0] == ""){
142            setInfoMessage("Please select a data source for the x-axis.")
143            return sendInfoMessage()
144        }
145        if(inputData.rowIds == null || inputData.rowIds == [] ||  inputData.rowIds[0] == null ||   inputData.rowIds[0] == ""){
146            setInfoMessage("Please select a data source for the y-axis.")
147            return sendInfoMessage()
148        }
149
150        // TODO: handle the case of multiple fields on an axis
151        // Determine data types
152        println "Determining rowType: "+inputData.rowIds[0]
153        def rowType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0])
154        println "Determining columnType: "+inputData.columnIds[0]
155        def columnType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0])
156
157        // Determine possible visualization types
158        def types = determineVisualizationTypes(rowType, columnType)
159
160        println "types: "+types+", determined this based on "+rowType+" and "+columnType
161        return sendResults(['types':types,'rowIds':inputData.rowIds[0],'columnIds':inputData.columnIds[0]])
162        }
163
164    /**
165     * Gather fields related to this study from modules.
166        This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
167        getMeasurements does not actually return measurements (the getMeasurementData call does).
168     * @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)
169     * @param assay     The assay that the source module and the requested fields belong to
170     * @return  A list of map objects, containing the following:
171     *           - a key 'id' with a value formatted by the createFieldId function
172     *           - a key 'source' with a value equal to the input parameter 'source'
173     *           - a key 'category' with a value equal to the 'name' field of the input paramater 'assay'
174     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
175     */
176    def getFields(source, assay){
177        def fields = []
178        def callUrl = ""
179
180        // Making a different call for each assay
181        def urlVars = "assayToken="+assay.assayUUID
182        try {
183            callUrl = ""+assay.module.url + "/rest/getMeasurements/query?"+urlVars
184            def json = moduleCommunicationService.callModuleRestMethodJSON( assay.module.url /* consumer */, callUrl );
185            def collection = []
186            json.each{ jason ->
187                collection.add(jason)
188            }
189            // Formatting the data
190            collection.each { field ->
191                // For getting this field from this assay
192                fields << [ "id": createFieldId( id: field, name: field, source: assay.id, type: ""+assay.name), "source": source, "category": ""+assay.name, "name": field ]
193            }
194        } catch(Exception e){
195            //returnError(404, "An error occured while trying to collect field data from a module. Most likely, this module is offline.")
196            offlineModules.add(assay.module.id)
197            infoMessageOfflineModules.add(assay.module.name)
198            log.error("VisualizationController: getFields: "+e)
199        }
200
201        return fields
202    }
203
204    /**
205     * Gather fields related to this study from GSCF.
206     * @param study The study that is the source of the requested fields
207     * @param category  The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
208     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
209     * @return A list of map objects, formatted by the formatGSCFFields function
210     */
211    def getFields(study, category, type){
212        // Collecting the data from it's source
213        def collection = []
214        def fields = []
215        def source = "GSCF"
216               
217                if( type == "domainfields" ) 
218                        collection = domainObjectCallback( category )?.giveDomainFields();
219                else 
220                        collection = templateObjectCallback( category, study )?.template?.fields
221
222        collection?.unique()
223
224        // Formatting the data
225        fields += formatGSCFFields(type, collection, source, category)
226
227        return fields
228    }
229
230    /**
231     * 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
232     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
233     * @param collectionOfFields A collection of fields, which could also contain only one item
234     * @param source Likely to be "GSCF"
235     * @param category The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
236     * @return A list containing list objects, containing the following:
237     *           - a key 'id' with a value formatted by the createFieldId function
238     *           - a key 'source' with a value equal to the input parameter 'source'
239     *           - a key 'category' with a value equal to the input parameter 'category'
240     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
241     */
242    def formatGSCFFields(type, collectionOfFields, source, category){
243
244        if(collectionOfFields==null || collectionOfFields == []){
245            return []
246        }
247        def fields = []
248        if(collectionOfFields instanceof Collection){
249            // Apparently this field is actually a list of fields.
250            // We will call ourselves again with the list's elements as input.
251            // These list elements will themselves go through this check again, effectively flattening the original input
252            for(int i = 0; i < collectionOfFields.size(); i++){
253                fields += formatGSCFFields(type, collectionOfFields[i], source, category)
254            }
255            return fields
256        } else {
257            // This is a single field. Format it and return the result.
258            if(type=="domainfields"){
259                fields << [ "id": createFieldId( id: collectionOfFields.name, name: collectionOfFields.name, source: source, type: category ), "source": source, "category": category, "name": collectionOfFields.name ]
260            }
261            if(type=="templatefields"){
262                fields << [ "id": createFieldId( id: collectionOfFields.id, name: collectionOfFields.name, source: source, type: category ), "source": source, "category": category, "name": collectionOfFields.name ]
263            }
264            return fields
265        }
266    }
267
268        /**
269         * Retrieves data for the visualization itself.
270     * 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.
271     * @return A map containing containing (at least, in the case of a barchart) the following:
272     *           - a key 'type' containing the type of chart that will be visualized
273     *           - a key 'xaxis' containing the title and unit that should be displayed for the x-axis
274     *           - a key 'yaxis' containing the title and unit that should be displayed for the y-axis*
275     *           - a key 'series' containing a list, that contains one or more maps, which contain the following:
276     *                - a key 'name', containing, for example, a feature name or field name
277     *                - a key 'y', containing a list of y-values
278     *                - 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
279         */
280        def getData = {
281                // Extract parameters
282                // TODO: handle erroneous input data
283                def inputData = parseGetDataParams();
284
285        if(inputData.columnIds == null || inputData.rowIds == null){
286            infoMessage = "Please select data sources for the y- and x-axes."
287            return sendInfoMessage()
288        }
289
290                // TODO: handle the case that we have multiple studies
291                def studyId = inputData.studyIds[ 0 ];
292                def study = Study.get( studyId as Integer );
293
294                // Find out what samples are involved
295                def samples = study.samples
296
297                // Retrieve the data for both axes for all samples
298                // TODO: handle the case of multiple fields on an axis
299                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ];
300                def data = getAllFieldData( study, samples, fields );
301
302                // Group data based on the y-axis if categorical axis is selected
303        def groupedData
304        if(inputData.visualizationType=='horizontal_barchart'){
305            groupedData = groupFieldData( inputData.visualizationType, data, "y", "x" ); // Indicate non-standard axis ordering
306        } else {
307            groupedData = groupFieldData( inputData.visualizationType, data ); // Don't indicate axis ordering, standard <"x", "y"> will be used
308        }
309   
310        // Format data so it can be rendered as JSON
311        def returnData
312        if(inputData.visualizationType=='horizontal_barchart'){
313            returnData = formatData( inputData.visualizationType, groupedData, fields, "y", "x" ); // Indicate non-standard axis ordering
314        } else {
315            returnData = formatData( inputData.visualizationType, groupedData, fields); // Don't indicate axis ordering, standard <"x", "y"> will be used
316        }
317        return sendResults(returnData)
318        }
319
320        /**
321         * Parses the parameters given by the user into a proper list
322         * @return Map with 4 keys:
323         *              studyIds:       list with studyIds selected
324         *              rowIds:         list with fieldIds selected for the rows
325         *              columnIds:      list with fieldIds selected for the columns
326         *              visualizationType:      String with the type of visualization required
327         * @see getFields
328         * @see getVisualizationTypes
329         */
330        def parseGetDataParams() {
331                def studyIds, rowIds, columnIds, visualizationType;
332               
333                studyIds = params.list( 'study' );
334                rowIds = params.list( 'rows' );
335                columnIds = params.list( 'columns' );
336                visualizationType = params.get( 'types')
337
338                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ];
339        }
340
341        /**
342         * Retrieve the field data for the selected fields
343         * @param study         Study for which the data should be retrieved
344         * @param samples       Samples for which the data should be retrieved
345         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
346         *                                              [ "x": "field-id-1", "y": "field-id-3" ]
347         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the
348         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example:
349         *                                              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ]
350         */
351        def getAllFieldData( study, samples, fields ) {
352                def fieldData = [:]
353                fields.each{ field ->
354                        fieldData[ field.key ] = getFieldData( study, samples, field.value );
355                }
356               
357                return fieldData;
358        }
359       
360        /**
361        * Retrieve the field data for the selected field
362        * @param study          Study for which the data should be retrieved
363        * @param samples        Samples for which the data should be retrieved
364        * @param fieldId        ID of the field to return data for
365        * @return                       A list of values of the selected field for all samples. If a value
366        *                                       could not be retrieved for a sample, null is returned. Examples:
367        *                                               [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
368        */
369        def getFieldData( study, samples, fieldId ) {
370                // Parse the fieldId as given by the user
371                def parsedField = parseFieldId( fieldId );
372               
373                def data = []
374               
375                if( parsedField.source == "GSCF" ) {
376                        // Retrieve data from GSCF itself
377                        def closure = valueCallback( parsedField.type )
378                       
379                        if( closure ) {
380                                samples.each { sample ->
381                                        // Retrieve the value for the selected field for this sample
382                                        def value = closure( sample, parsedField.name );
383                                       
384                                        if( value ) {
385                                                data << value;
386                                        } else {
387                                                // Return null if the value is not found
388                                                data << null
389                                        }
390                                }
391                        } else {
392                                // TODO: Handle error properly
393                                // Closure could not be retrieved, probably because the type is incorrect
394                                data = samples.collect { return null }
395                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
396                        }
397                } else {
398                        // Data must be retrieved from a module
399                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
400                }
401               
402                return data
403        }
404       
405        /**
406         * Retrieve data for a given field from a data module
407         * @param study                 Study to retrieve data for
408         * @param samples               Samples to retrieve data for
409         * @param source_module Name of the module to retrieve data from
410         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
411         * @return                              A list of values of the selected field for all samples. If a value
412         *                                              could not be retrieved for a sample, null is returned. Examples:
413         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
414         */
415        def getModuleData( study, samples, assay_id, fieldName ) {
416                def data = []
417                //println "assay_id: "+assay_id+", fieldName: "+fieldName
418                // TODO: Handle values that should be retrieved from multiple assays
419        def assay = Assay.get(assay_id);
420
421        if( assay ) {
422            // Request for a particular assay and a particular feature
423            def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName
424            urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
425
426            def callUrl
427            try {
428                callUrl = assay.module.url + "/rest/getMeasurementData"
429                def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
430
431                if( json ) {
432                    // First element contains sampletokens
433                    // Second element contains the featurename
434                    // Third element contains the measurement value
435                    def sampleTokens = json[ 0 ]
436                    def measurements = json[ 2 ]
437
438                    // Loop through the samples
439                    samples.each { sample ->
440                        // Search for this sampletoken
441                        def sampleToken = sample.sampleUUID;
442                        def index = sampleTokens.findIndexOf { it == sampleToken }
443
444                        if( index > -1 ) {
445                            data << measurements[ index ];
446                        } else {
447                            data << null
448                        }
449                    }
450                } else {
451                    // TODO: handle error
452                    // Returns an empty list with as many elements as there are samples
453                    data = samples.collect { return null }
454                }
455
456            } catch(Exception e){
457                log.error("VisualizationController: getFields: "+e)
458                //return returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
459                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.")
460            }
461        } else {
462            // TODO: Handle error correctly
463            // Returns an empty list with as many elements as there are samples
464            data = samples.collect { return null }
465        }
466
467        //println "\t data request: "+data
468                return data
469        }
470
471        /**
472         * Group the field data on the values of the specified axis. For example, for a bar chart, the values
473         * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard
474         * error of the mean is returned in the 'error' property
475     * @param visualizationType Some types require a different formatting/grouping of the data, such as 'table'
476         * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input
477         * @param groupAxis     Name of the axis to group on. Defaults to "x"
478         * @param valueAxis     Name of the axis where the values are. Defaults to "y"
479         * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"
480         * @param unknownName   Name of the group for all null groups. Defaults to "unknown"
481         * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the
482         *                                      selected field for all groups. For example, if the input is
483         *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ]
484         *                                      the output will be:
485         *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ]
486         *
487         *                                      As you can see: null values in the valueAxis are ignored. Null values in the
488         *                                      group axis are combined into a 'unknown' category.
489         */
490        def groupFieldData( visualizationType, data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) {
491                // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a
492                // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list
493                // elements. A few lines below, this case is handled again by checking whether a specific sample belongs
494                // to this group.
495                // After flattening, the list is uniqued. The closure makes sure that values with different classes are
496                // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator)
497                def groups = data[ groupAxis ]
498                                                .flatten()
499                                                .unique { it == null ? "null" : it.class.name + it.toString() }
500                // Make sure the null category is last
501                groups = groups.findAll { it != null } + groups.findAll { it == null }
502                // Gather names for the groups. Most of the times, the group names are just the names, only with
503                // a null value, the unknownName must be used
504                def groupNames = groups.collect { it != null ? it : unknownName }
505                // Generate the output object
506                def outputData = [:]
507                outputData[ valueAxis ] = [];
508                outputData[ errorName ] = [];
509                outputData[ groupAxis ] = groupNames;
510                // Loop through all groups, and gather the values for this group
511        // A visualization of type 'table' is a special case. There, the counts of two combinations of 'groupAxis' and 'valueAxis' items are computed
512        if(visualizationType=='table'){
513            // For each 'valueAxis' item and 'groupAxis' item combination, count how often they appear together.
514            def counts = [:]
515            // The 'counts' list uses keys like this: ['item1':group, 'item2':value]
516            // The value will be an integer (the count)
517            data[ groupAxis ].eachWithIndex { group, index ->
518                def value =  data[ valueAxis ][index]
519                if(!counts.get(['item1':group, 'item2':value])){
520                    counts[['item1':group, 'item2':value]] = 1
521                } else {
522                    counts[['item1':group, 'item2':value]] = counts[['item1':group, 'item2':value]] + 1
523                }
524            }
525            outputData[valueAxis] =  data[ valueAxis ]
526                                        .flatten()
527                                        .unique { it == null ? "null" : it.class.name + it.toString() }
528            // Because we are collecting counts, we do not set the 'errorName' item of the 'outputData' map.
529            // We do however set the 'data' map to contain the counts. We set it in such a manner that it has a 'table' layout with respect to 'groupAxis' and 'valueAxis'.
530            def rows = []
531            outputData[groupAxis].each{ group ->
532                def row = []
533                outputData[valueAxis].each{ value ->
534                    row << counts[['item1':group, 'item2':value]]
535                }
536                while(row.contains(null)){
537                    row[row.indexOf(null)] = 0
538                } // '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.
539                if(row!=[]) rows << row
540            }
541            outputData['data']= rows
542        } else {
543            groups.each { group ->
544                // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
545                // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
546                def indices= data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group };
547                def values = data[ valueAxis ][ indices ]
548
549                def dataForGroup = computeMeanAndError( values );
550
551                outputData[ valueAxis ] << dataForGroup.value
552                outputData[ errorName ] << dataForGroup.error
553            }
554        }
555                return outputData
556        }
557       
558        /**
559         * Formats the grouped data in such a way that the clientside visualization method
560         * can handle the data correctly.
561         * @param groupedData   Data that has been grouped using the groupFields method
562         * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
563         *                                                      [ "x": "field-id-1", "y": "field-id-3" ]
564         * @param groupAxis             Name of the axis to with group data. Defaults to "x"
565         * @param valueAxis             Name of the axis where the values are stored. Defaults to "y"
566         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
567         * @return                              A map like the following:
568         *
569                        {
570                                "type": "barchart",
571                                "xaxis": { "title": "quarter 2011", "unit": "" },
572                                "yaxis": { "title": "temperature", "unit": "degrees C" },
573                                "series": [
574                                        {
575                                                "name": "series name",
576                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
577                        "x": [ "Q1", "Q2", "Q3", "Q4" ],
578                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
579                                        },
580                                ]
581                        }
582         *
583         */
584        def formatData( type, groupedData, fields, groupAxis = "x", valueAxis = "y", errorName = "error" ) {
585        // TODO: Handle name and unit of fields correctly
586        if(type=="table"){
587            def yName = parseFieldId( fields[ valueAxis ] ).name;
588
589            def return_data = [:]
590            return_data[ "type" ] = type
591            return_data.put("yaxis", ["title" : "", "unit" : "" ])
592            return_data.put("xaxis", ["title" : "", "unit": "" ])
593            return_data.put("series", [[
594                    "x": groupedData[ groupAxis ].collect { it.toString() },
595                    "y": groupedData[ valueAxis ].collect { it.toString() },
596                    "data": groupedData["data"]
597            ]])
598            return return_data;
599        } else {
600            def xAxis = groupedData[ groupAxis ].collect { it.toString() };
601            def yName = parseFieldId( fields[ valueAxis ] ).name;
602
603            def return_data = [:]
604            return_data[ "type" ] = type
605            return_data.put("yaxis", ["title" : yName, "unit" : "" ])
606            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ])
607            return_data.put("series", [[
608                "name": yName,
609                "x": xAxis,
610                "y": groupedData[ valueAxis ],
611                "error": groupedData[ errorName ]
612            ]])
613
614            return return_data;
615        }
616        }
617
618        /**
619         * Returns a closure for the given entitytype that determines the value for a criterion
620         * on the given object. The closure receives two parameters: the sample and a field.
621         *
622         * For example:
623         *              How can one retrieve the value for subject.name, given a sample? This can be done by
624         *              returning the field values sample.parentSubject:
625         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
626         * @return      Closure that retrieves the value for a field and the given field
627         */
628        protected Closure valueCallback( String entity ) {
629                switch( entity ) {
630                        case "Study":
631                        case "studies":
632                                return { sample, field -> return getFieldValue( sample.parent, field ) }
633                        case "Subject":
634                        case "subjects":
635                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
636                        case "Sample":
637                        case "samples":
638                                return { sample, field -> return getFieldValue( sample, field ) }
639                        case "Event":
640                        case "events":
641                                return { sample, field ->
642                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
643                                                return null
644
645                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
646                                }
647                        case "SamplingEvent":
648                        case "samplingEvents":
649                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
650                        case "Assay":
651                        case "assays":
652                                return { sample, field ->
653                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
654                                        if( sampleAssays && sampleAssays.size() > 0 )
655                                                return sampleAssays.collect { getFieldValue( it, field ) }
656                                        else
657                                                return null
658                                }
659                }
660        }
661       
662        /**
663        * Returns the domain object that should be used with the given entity string
664        *
665        * For example:
666        *               What object should be consulted if the user asks for "studies"
667        *               Response: Study
668        * @return       Domain object that should be used with the given entity string
669        */
670   protected def domainObjectCallback( String entity ) {
671           switch( entity ) {
672                   case "Study":
673                   case "studies":
674                           return Study
675                   case "Subject":
676                   case "subjects":
677                           return Subject
678                   case "Sample":
679                   case "samples":
680                           return Sample
681                   case "Event":
682                   case "events":
683                        return Event
684                   case "SamplingEvent":
685                   case "samplingEvents":
686                           return SamplingEvent
687                   case "Assay":
688                   case "assays":
689                                return Assay
690           }
691   }
692
693    /**
694    * Returns the objects within the given study that should be used with the given entity string
695    *
696    * For example:
697    *           What object should be consulted if the user asks for "samples"
698    *           Response: study.samples
699    * @return   List of domain objects that should be used with the given entity string
700    */
701    protected def templateObjectCallback( String entity, Study study ) {
702      switch( entity ) {
703          case "Study":
704          case "studies":
705              return study
706          case "Subject":
707          case "subjects":
708              return study?.subjects
709          case "Sample":
710          case "samples":
711              return study?.samples
712          case "Event":
713          case "events":
714               return study?.events
715          case "SamplingEvent":
716          case "samplingEvents":
717              return study?.samplingEvents
718          case "Assay":
719          case "assays":
720                  return study?.assays
721      }
722    }
723       
724        /**
725         * Computes the mean value and Standard Error of the mean (SEM) for the given values
726         * @param values        List of values to compute the mean and SEM for. Strings and null
727         *                                      values are ignored
728         * @return                      Map with two keys: 'value' and 'error'
729         */
730        protected Map computeMeanAndError( values ) {
731                // TODO: Handle the case that one of the values is a list. In that case,
732                // all values should be taken into account.     
733                def mean = computeMean( values );
734                def error = computeSEM( values, mean );
735               
736                return [ 
737                        "value": mean,
738                        "error": error
739                ]
740        }
741       
742        /**
743         * Computes the mean of the given values. Values that can not be parsed to a number
744         * are ignored. If no values are given, the mean of 0 is returned.
745         * @param values        List of values to compute the mean for
746         * @return                      Arithmetic mean of the values
747         */
748        protected def computeMean( List values ) {
749                def sumOfValues = 0;
750                def sizeOfValues = 0;
751                values.each { value ->
752                        def num = getNumericValue( value );
753                        if( num != null ) {
754                                sumOfValues += num;
755                                sizeOfValues++
756                        } 
757                }
758
759                if( sizeOfValues > 0 )
760                        return sumOfValues / sizeOfValues;
761                else
762                        return 0;
763        }
764
765        /**
766        * Computes the standard error of mean of the given values. 
767        * Values that can not be parsed to a number are ignored. 
768        * If no values are given, the standard deviation of 0 is returned.
769        * @param values         List of values to compute the standard deviation for
770        * @param mean           Mean of the list (if already computed). If not given, the mean
771        *                                       will be computed using the computeMean method
772        * @return                       Standard error of the mean of the values or 0 if no values can be used.
773        */
774    protected def computeSEM( List values, def mean = null ) {
775       if( mean == null )
776            mean = computeMean( values )
777
778       def sumOfDifferences = 0;
779       def sizeOfValues = 0;
780       values.each { value ->
781           def num = getNumericValue( value );
782           if( num != null ) {
783               sumOfDifferences += Math.pow( num - mean, 2 );
784               sizeOfValues++
785           }
786       }
787
788       if( sizeOfValues > 0 ) {
789           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
790           return std / Math.sqrt( sizeOfValues );
791       } else {
792           return 0;
793       }
794    }
795   
796        /**
797         * Return the numeric value of the given object, or null if no numeric value could be returned
798         * @param       value   Object to return the value for
799         * @return                      Number that represents the given value
800         */
801        protected Number getNumericValue( value ) {
802                // TODO: handle special types of values
803                if( value instanceof Number ) {
804                        return value;
805                } else if( value instanceof RelTime ) {
806                        return value.value;
807                }
808               
809                return null
810        }
811
812        /** 
813         * Returns a field for a given templateentity
814         * @param object        TemplateEntity (or subclass) to retrieve data for
815         * @param fieldName     Name of the field to return data for.
816         * @return                      Value of the field or null if the value could not be retrieved
817         */
818        protected def getFieldValue( TemplateEntity object, String fieldName ) {
819                if( !object || !fieldName )
820                        return null;
821               
822                try {
823                        return object.getFieldValue( fieldName );
824                } catch( Exception e ) {
825                        return null;
826                }
827        }
828
829        /**
830         * Parses a fieldId that has been created earlier by createFieldId
831         * @param fieldId       FieldId to parse
832         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
833         * @see createFieldId
834         */
835        protected Map parseFieldId( String fieldId ) {
836                def attrs = [:]
837               
838                def parts = fieldId.split(",")
839               
840                attrs = [
841                        "id": parts[ 0 ],
842                        "name": parts[ 1 ],
843                        "source": parts[ 2 ],
844                        "type": parts[ 3 ]
845                ]
846        }
847       
848        /**
849         * Create a fieldId based on the given attributes
850         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
851         * @return                      Unique field ID for these parameters
852         * @see parseFieldId
853         */
854        protected String createFieldId( Map attrs ) {
855                // TODO: What if one of the attributes contains a comma?
856                def name = attrs.name;
857                def id = attrs.id ?: name;
858                def source = attrs.source;
859                def type = attrs.type ?: ""
860               
861                return id + "," + name + "," + source + "," + type;
862        }
863
864    /**
865     * Set the response code and an error message
866     * @param code HTTP status code
867     * @param msg Error message, string
868     */
869    protected void returnError(code, msg){
870        response.sendError(code , msg)
871    }
872
873    /**
874     * Determines what type of data a field contains
875     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
876     * @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
877     * @return Either CATEGORICALDATA of NUMERICALDATA
878     */
879    protected int determineFieldType(studyId, fieldId){
880                def parsedField = parseFieldId( fieldId );
881        def study = Study.get(studyId)
882                def data = []
883
884        try{
885            if( parsedField.source == "GSCF" ) {
886                if(parsedField.id.isNumber()){
887                        return determineCategoryFromTemplateField(parsedField.id)
888                } else { // Domainfield or memberclass
889                    def field = domainObjectCallback( parsedField.type )?.declaredFields.find { it.name == parsedField.name };
890                    if( field ) {
891                        return determineCategoryFromClass( field.getType() )
892                    } else {
893                        // TODO: how do we communicate this to the user? Do we allow the process to proceed?
894                        log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
895                    }
896                }
897            } else {
898                data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
899                def cat = determineCategoryFromData(data)
900                return cat
901            }
902        } catch(Exception e){
903            log.error("VisualizationController: determineFieldType: "+e)
904            e.printStackTrace()
905            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
906            return CATEGORICALDATA
907        }
908    }
909
910    /**
911     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
912     * @param classObject
913     * @return Either CATEGORICALDATA of NUMERICALDATA
914     */
915    protected int determineCategoryFromClass(classObject){
916        println "classObject: "+classObject+", of class: "+classObject.class
917        if(classObject==java.lang.String){
918            return CATEGORICALDATA
919        } else {
920            return NUMERICALDATA
921        }
922    }
923
924    /**
925     * 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.
926     * @param inputObject Either a single item, or a collection of items
927     * @return Either CATEGORICALDATA of NUMERICALDATA
928     */
929    protected int determineCategoryFromData(inputObject){
930        def results = []
931        if(inputObject instanceof Collection){
932            // This data is more complex than a single value, so we will call ourselves again so we c
933            inputObject.each {
934                                if( it != null )
935                        results << determineCategoryFromData(it)
936            }
937        } else {
938            if(inputObject.toString().isDouble()){
939                results << NUMERICALDATA
940            } else {
941                results << CATEGORICALDATA
942            }
943        }
944
945        results.unique()
946
947        if(results.size()>1){
948            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
949            results[0] = CATEGORICALDATA
950        }
951
952        return results[0]
953    }
954
955    /**
956     * Determines a field category, based on the TemplateFieldType of a Templatefield
957     * @param id A database ID for a TemplateField
958     * @return Either CATEGORICALDATA of NUMERICALDATA
959     */
960    protected int determineCategoryFromTemplateField(id){
961        TemplateField tf = TemplateField.get(id)
962        if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG || tf.type==TemplateFieldType.DATE || tf.type==TemplateFieldType.RELTIME){
963            println "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
964            return NUMERICALDATA
965        } else {
966            println "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
967            return CATEGORICALDATA
968        }
969    }
970    /**
971     * 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.
972     * @param returnData The object containing the data
973     * @return results A JSON object
974     */
975    protected void sendResults(returnData){
976        def results = [:]
977        if(infoMessage.size()!=0){
978            results.put("infoMessage", infoMessage)
979            infoMessage = []
980        }
981        results.put("returnData", returnData)
982        render results as JSON
983    }
984
985    /**
986     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
987     * @param returnData The object containing the data
988     * @return results A JSON object
989     */
990    protected void sendInfoMessage(){
991        def results = [:]
992        results.put("infoMessage", infoMessage)
993        infoMessage = []
994        render results as JSON
995    }
996
997    /**
998     * Adds a new message to the infoMessage
999     * @param message The information that needs to be added to the infoMessage
1000     */
1001    protected void setInfoMessage(message){
1002        infoMessage.add(message)
1003        println "setInfoMessage: "+infoMessage
1004    }
1005
1006    /**
1007     * Adds a message to the infoMessage that gives the client information about offline modules
1008     */
1009    protected void setInfoMessageOfflineModules(){
1010        infoMessageOfflineModules.unique()
1011        if(infoMessageOfflineModules.size()>0){
1012            String message = "Unfortunately"
1013            infoMessageOfflineModules.eachWithIndex{ it, index ->
1014                if(index==(infoMessageOfflineModules.size()-2)){
1015                    message += ', the '+it+' and '
1016                } else {
1017                    if(index==(infoMessageOfflineModules.size()-1)){
1018                        message += ' the '+it
1019                    } else {
1020                        message += ', the '+it
1021                    }
1022                }
1023            }
1024            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
1025            if(infoMessageOfflineModules.size()>1){
1026                message += "these modules."
1027            } else {
1028                message += "this module."
1029            }
1030            setInfoMessage(message)
1031        }
1032        infoMessageOfflineModules = []
1033    }
1034
1035    /**
1036     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1037     * @param inputData Contains a list of maps, of the following format
1038     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1039     *            - a key 'name', containing, for example, a feature name or field name
1040     *            - a key 'y', containing a list of y-values
1041     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1042     */
1043    protected def formatCategoryData(inputData){
1044        // NOTE: This function is no longer up to date with the current inputData layout.
1045        def series = []
1046        inputData.eachWithIndex { it, i ->
1047            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1048        }
1049        def ret = [:]
1050        ret.put('type', inputData[0]['type'])
1051        ret.put('x', inputData[0]['x'])
1052        ret.put('yaxis',['title': 'title', 'unit': ''])
1053        ret.put('xaxis', inputData[0]['xaxis'])
1054        ret.put('series', series)
1055        return ret
1056    }
1057
1058    /**
1059     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1060     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1061     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1062     * @return
1063     */
1064    protected def determineVisualizationTypes(rowType, columnType){
1065         def types = []
1066        if(rowType==CATEGORICALDATA){
1067            if(columnType==CATEGORICALDATA){
1068                types = [ [ "id": "table", "name": "Table"] ];
1069            }
1070            if(columnType==NUMERICALDATA){
1071                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1072            }
1073        }
1074        if(rowType==NUMERICALDATA){
1075            if(columnType==CATEGORICALDATA){
1076                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
1077            }
1078            if(columnType==NUMERICALDATA){
1079                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1080            }
1081        }
1082        return types
1083    }
1084}
Note: See TracBrowser for help on using the repository browser.