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

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

visualization/VisualizeController.groovy, modules that appear to be offline or non-responsive are now checked only once even if another assay uses that module. These assays are ignored. This way, we prevent unnecessary delays for the client.

File size: 42.1 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        // Handle the case that we are gathering data for a horizontal barchart
291        if(inputData.visualizationType=='horizontal_barchart'){
292            def tmp = inputData.columnIds
293            inputData.columnIds = inputData.rowIds
294            inputData.rowIds = tmp
295        }
296               
297                // TODO: handle the case that we have multiple studies
298                def studyId = inputData.studyIds[ 0 ];
299                def study = Study.get( studyId as Integer );
300
301                // Find out what samples are involved
302                def samples = study.samples
303
304                // Retrieve the data for both axes for all samples
305                // TODO: handle the case of multiple fields on an axis
306                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ];
307                def data = getAllFieldData( study, samples, fields );
308
309                // Group data based on the y-axis if categorical axis is selected
310        def groupedData = groupFieldData( data );
311   
312        // Format data so it can be rendered as JSON
313        def returnData = formatData( inputData.visualizationType, groupedData, fields );
314        return sendResults(returnData)
315        }
316
317        /**
318         * Parses the parameters given by the user into a proper list
319         * @return Map with 4 keys:
320         *              studyIds:       list with studyIds selected
321         *              rowIds:         list with fieldIds selected for the rows
322         *              columnIds:      list with fieldIds selected for the columns
323         *              visualizationType:      String with the type of visualization required
324         * @see getFields
325         * @see getVisualizationTypes
326         */
327        def parseGetDataParams() {
328                def studyIds, rowIds, columnIds, visualizationType;
329               
330                studyIds = params.list( 'study' );
331                rowIds = params.list( 'rows' );
332                columnIds = params.list( 'columns' );
333                visualizationType = params.get( 'types')
334
335                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ];
336        }
337
338        /**
339         * Retrieve the field data for the selected fields
340         * @param study         Study for which the data should be retrieved
341         * @param samples       Samples for which the data should be retrieved
342         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
343         *                                              [ "x": "field-id-1", "y": "field-id-3" ]
344         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the
345         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example:
346         *                                              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ]
347         */
348        def getAllFieldData( study, samples, fields ) {
349                def fieldData = [:]
350                fields.each{ field ->
351                        fieldData[ field.key ] = getFieldData( study, samples, field.value );
352                }
353               
354                return fieldData;
355        }
356       
357        /**
358        * Retrieve the field data for the selected field
359        * @param study          Study for which the data should be retrieved
360        * @param samples        Samples for which the data should be retrieved
361        * @param fieldId        ID of the field to return data for
362        * @return                       A list of values of the selected field for all samples. If a value
363        *                                       could not be retrieved for a sample, null is returned. Examples:
364        *                                               [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
365        */
366        def getFieldData( study, samples, fieldId ) {
367                // Parse the fieldId as given by the user
368                def parsedField = parseFieldId( fieldId );
369               
370                def data = []
371               
372                if( parsedField.source == "GSCF" ) {
373                        // Retrieve data from GSCF itself
374                        def closure = valueCallback( parsedField.type )
375                       
376                        if( closure ) {
377                                samples.each { sample ->
378                                        // Retrieve the value for the selected field for this sample
379                                        def value = closure( sample, parsedField.name );
380                                       
381                                        if( value ) {
382                                                data << value;
383                                        } else {
384                                                // Return null if the value is not found
385                                                data << null
386                                        }
387                                }
388                        } else {
389                                // TODO: Handle error properly
390                                // Closure could not be retrieved, probably because the type is incorrect
391                                data = samples.collect { return null }
392                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
393                        }
394                } else {
395                        // Data must be retrieved from a module
396                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
397                }
398               
399                return data
400        }
401       
402        /**
403         * Retrieve data for a given field from a data module
404         * @param study                 Study to retrieve data for
405         * @param samples               Samples to retrieve data for
406         * @param source_module Name of the module to retrieve data from
407         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
408         * @return                              A list of values of the selected field for all samples. If a value
409         *                                              could not be retrieved for a sample, null is returned. Examples:
410         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
411         */
412        def getModuleData( study, samples, assay_id, fieldName ) {
413                def data = []
414                //println "assay_id: "+assay_id+", fieldName: "+fieldName
415                // TODO: Handle values that should be retrieved from multiple assays
416        def assay = Assay.get(assay_id);
417
418        if( assay ) {
419            // Request for a particular assay and a particular feature
420            def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName
421            urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
422
423            def callUrl
424            try {
425                callUrl = assay.module.url + "/rest/getMeasurementData"
426                def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
427
428                if( json ) {
429                    // First element contains sampletokens
430                    // Second element contains the featurename
431                    // Third element contains the measurement value
432                    def sampleTokens = json[ 0 ]
433                    def measurements = json[ 2 ]
434
435                    // Loop through the samples
436                    samples.each { sample ->
437                        // Search for this sampletoken
438                        def sampleToken = sample.sampleUUID;
439                        def index = sampleTokens.findIndexOf { it == sampleToken }
440
441                        if( index > -1 ) {
442                            data << measurements[ index ];
443                        } else {
444                            data << null
445                        }
446                    }
447                } else {
448                    // TODO: handle error
449                    // Returns an empty list with as many elements as there are samples
450                    data = samples.collect { return null }
451                }
452
453            } catch(Exception e){
454                log.error("VisualizationController: getFields: "+e)
455                //return returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
456                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.")
457            }
458        } else {
459            // TODO: Handle error correctly
460            // Returns an empty list with as many elements as there are samples
461            data = samples.collect { return null }
462        }
463
464        //println "\t data request: "+data
465                return data
466        }
467
468        /**
469         * Group the field data on the values of the specified axis. For example, for a bar chart, the values
470         * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard
471         * error of the mean is returned in the 'error' property
472         * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input
473         * @param groupAxis     Name of the axis to group on. Defaults to "x"
474         * @param valueAxis     Name of the axis where the values are. Defaults to "y"
475         * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"
476         * @param unknownName   Name of the group for all null groups. Defaults to "unknown"
477         * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the
478         *                                      selected field for all groups. For example, if the input is
479         *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ]
480         *                                      the output will be:
481         *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ]
482         *
483         *                                      As you can see: null values in the valueAxis are ignored. Null values in the
484         *                                      group axis are combined into a 'unknown' category.
485         */
486        def groupFieldData( data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) {
487                // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a
488                // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list
489                // elements. A few lines below, this case is handled again by checking whether a specific sample belongs
490                // to this group.
491                // After flattening, the list is uniqued. The closure makes sure that values with different classes are
492                // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator)
493                def groups = data[ groupAxis ]
494                                                .flatten()
495                                                .unique { it == null ? "null" : it.class.name + it.toString() }
496                // Make sure the null category is last
497                groups = groups.findAll { it != null } + groups.findAll { it == null }
498                // Gather names for the groups. Most of the times, the group names are just the names, only with
499                // a null value, the unknownName must be used
500                def groupNames = groups.collect { it != null ? it : unknownName }
501                // Generate the output object
502                def outputData = [:]
503                outputData[ valueAxis ] = [];
504                outputData[ errorName ] = [];
505                outputData[ groupAxis ] = groupNames;
506               
507                // Loop through all groups, and gather the values for this group
508                groups.each { group ->
509                        // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
510                        // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
511                        def indices= data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group };
512                        def values = data[ valueAxis ][ indices ]
513                       
514                        def dataForGroup = computeMeanAndError( values );
515                       
516                        outputData[ valueAxis ] << dataForGroup.value
517                        outputData[ errorName ] << dataForGroup.error 
518                }
519                return outputData
520        }
521       
522        /**
523         * Formats the grouped data in such a way that the clientside visualization method
524         * can handle the data correctly.
525         * @param groupedData   Data that has been grouped using the groupFields method
526         * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
527         *                                                      [ "x": "field-id-1", "y": "field-id-3" ]
528         * @param groupAxis             Name of the axis to with group data. Defaults to "x"
529         * @param valueAxis             Name of the axis where the values are stored. Defaults to "y"
530         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
531         * @return                              A map like the following:
532         *
533                        {
534                                "type": "barchart",
535                                "xaxis": { "title": "quarter 2011", "unit": "" },
536                                "yaxis": { "title": "temperature", "unit": "degrees C" },
537                                "series": [
538                                        {
539                                                "name": "series name",
540                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
541                        "x": [ "Q1", "Q2", "Q3", "Q4" ],
542                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
543                                        },
544                                ]
545                        }
546         *
547         */
548        def formatData( type, groupedData, fields, groupAxis = "x", valueAxis = "y", errorName = "error" ) {
549        // TODO: Handle name and unit of fields correctly
550        if(type=="table"){
551            def xAxis = groupedData[ groupAxis ].collect { it.toString() };
552            def yName = parseFieldId( fields[ valueAxis ] ).name;
553
554            def return_data = [:]
555            return_data[ "type" ] = type
556            return_data.put("yaxis", ["title" : yName, "unit" : "" ])
557            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ])
558            return_data.put("x", [])
559            return_data.put("y", [])
560            return_data.put("data", [])
561            return_data.put("error", [])
562
563            return return_data;
564        } else {
565            def xAxis = groupedData[ groupAxis ].collect { it.toString() };
566            def yName = parseFieldId( fields[ valueAxis ] ).name;
567
568            def return_data = [:]
569            return_data[ "type" ] = type
570            return_data.put("yaxis", ["title" : yName, "unit" : "" ])
571            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ])
572            return_data.put("series", [[
573                "name": yName,
574                "x": xAxis,
575                "y": groupedData[ valueAxis ],
576                "error": groupedData[ errorName ]
577            ]])
578
579            return return_data;
580        }
581        }
582
583        /**
584         * Returns a closure for the given entitytype that determines the value for a criterion
585         * on the given object. The closure receives two parameters: the sample and a field.
586         *
587         * For example:
588         *              How can one retrieve the value for subject.name, given a sample? This can be done by
589         *              returning the field values sample.parentSubject:
590         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
591         * @return      Closure that retrieves the value for a field and the given field
592         */
593        protected Closure valueCallback( String entity ) {
594                switch( entity ) {
595                        case "Study":
596                        case "studies":
597                                return { sample, field -> return getFieldValue( sample.parent, field ) }
598                        case "Subject":
599                        case "subjects":
600                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
601                        case "Sample":
602                        case "samples":
603                                return { sample, field -> return getFieldValue( sample, field ) }
604                        case "Event":
605                        case "events":
606                                return { sample, field ->
607                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
608                                                return null
609
610                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
611                                }
612                        case "SamplingEvent":
613                        case "samplingEvents":
614                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
615                        case "Assay":
616                        case "assays":
617                                return { sample, field ->
618                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
619                                        if( sampleAssays && sampleAssays.size() > 0 )
620                                                return sampleAssays.collect { getFieldValue( it, field ) }
621                                        else
622                                                return null
623                                }
624                }
625        }
626       
627        /**
628        * Returns the domain object that should be used with the given entity string
629        *
630        * For example:
631        *               What object should be consulted if the user asks for "studies"
632        *               Response: Study
633        * @return       Domain object that should be used with the given entity string
634        */
635   protected def domainObjectCallback( String entity ) {
636           switch( entity ) {
637                   case "Study":
638                   case "studies":
639                           return Study
640                   case "Subject":
641                   case "subjects":
642                           return Subject
643                   case "Sample":
644                   case "samples":
645                           return Sample
646                   case "Event":
647                   case "events":
648                        return Event
649                   case "SamplingEvent":
650                   case "samplingEvents":
651                           return SamplingEvent
652                   case "Assay":
653                   case "assays":
654                                return Assay
655           }
656   }
657
658    /**
659    * Returns the objects within the given study that should be used with the given entity string
660    *
661    * For example:
662    *           What object should be consulted if the user asks for "samples"
663    *           Response: study.samples
664    * @return   List of domain objects that should be used with the given entity string
665    */
666    protected def templateObjectCallback( String entity, Study study ) {
667      switch( entity ) {
668          case "Study":
669          case "studies":
670              return study
671          case "Subject":
672          case "subjects":
673              return study?.subjects
674          case "Sample":
675          case "samples":
676              return study?.samples
677          case "Event":
678          case "events":
679               return study?.events
680          case "SamplingEvent":
681          case "samplingEvents":
682              return study?.samplingEvents
683          case "Assay":
684          case "assays":
685                  return study?.assays
686      }
687    }
688       
689        /**
690         * Computes the mean value and Standard Error of the mean (SEM) for the given values
691         * @param values        List of values to compute the mean and SEM for. Strings and null
692         *                                      values are ignored
693         * @return                      Map with two keys: 'value' and 'error'
694         */
695        protected Map computeMeanAndError( values ) {
696                // TODO: Handle the case that one of the values is a list. In that case,
697                // all values should be taken into account.     
698                def mean = computeMean( values );
699                def error = computeSEM( values, mean );
700               
701                return [ 
702                        "value": mean,
703                        "error": error
704                ]
705        }
706       
707        /**
708         * Computes the mean of the given values. Values that can not be parsed to a number
709         * are ignored. If no values are given, the mean of 0 is returned.
710         * @param values        List of values to compute the mean for
711         * @return                      Arithmetic mean of the values
712         */
713        protected def computeMean( List values ) {
714                def sumOfValues = 0;
715                def sizeOfValues = 0;
716                values.each { value ->
717                        def num = getNumericValue( value );
718                        if( num != null ) {
719                                sumOfValues += num;
720                                sizeOfValues++
721                        } 
722                }
723
724                if( sizeOfValues > 0 )
725                        return sumOfValues / sizeOfValues;
726                else
727                        return 0;
728        }
729
730        /**
731        * Computes the standard error of mean of the given values. 
732        * Values that can not be parsed to a number are ignored. 
733        * If no values are given, the standard deviation of 0 is returned.
734        * @param values         List of values to compute the standard deviation for
735        * @param mean           Mean of the list (if already computed). If not given, the mean
736        *                                       will be computed using the computeMean method
737        * @return                       Standard error of the mean of the values or 0 if no values can be used.
738        */
739    protected def computeSEM( List values, def mean = null ) {
740       if( mean == null )
741            mean = computeMean( values )
742
743       def sumOfDifferences = 0;
744       def sizeOfValues = 0;
745       values.each { value ->
746           def num = getNumericValue( value );
747           if( num != null ) {
748               sumOfDifferences += Math.pow( num - mean, 2 );
749               sizeOfValues++
750           }
751       }
752
753       if( sizeOfValues > 0 ) {
754           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
755           return std / Math.sqrt( sizeOfValues );
756       } else {
757           return 0;
758       }
759    }
760   
761        /**
762         * Return the numeric value of the given object, or null if no numeric value could be returned
763         * @param       value   Object to return the value for
764         * @return                      Number that represents the given value
765         */
766        protected Number getNumericValue( value ) {
767                // TODO: handle special types of values
768                if( value instanceof Number ) {
769                        return value;
770                } else if( value instanceof RelTime ) {
771                        return value.value;
772                }
773               
774                return null
775        }
776
777        /** 
778         * Returns a field for a given templateentity
779         * @param object        TemplateEntity (or subclass) to retrieve data for
780         * @param fieldName     Name of the field to return data for.
781         * @return                      Value of the field or null if the value could not be retrieved
782         */
783        protected def getFieldValue( TemplateEntity object, String fieldName ) {
784                if( !object || !fieldName )
785                        return null;
786               
787                try {
788                        return object.getFieldValue( fieldName );
789                } catch( Exception e ) {
790                        return null;
791                }
792        }
793
794        /**
795         * Parses a fieldId that has been created earlier by createFieldId
796         * @param fieldId       FieldId to parse
797         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
798         * @see createFieldId
799         */
800        protected Map parseFieldId( String fieldId ) {
801                def attrs = [:]
802               
803                def parts = fieldId.split(",")
804               
805                attrs = [
806                        "id": parts[ 0 ],
807                        "name": parts[ 1 ],
808                        "source": parts[ 2 ],
809                        "type": parts[ 3 ]
810                ]
811        }
812       
813        /**
814         * Create a fieldId based on the given attributes
815         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
816         * @return                      Unique field ID for these parameters
817         * @see parseFieldId
818         */
819        protected String createFieldId( Map attrs ) {
820                // TODO: What if one of the attributes contains a comma?
821                def name = attrs.name;
822                def id = attrs.id ?: name;
823                def source = attrs.source;
824                def type = attrs.type ?: ""
825               
826                return id + "," + name + "," + source + "," + type;
827        }
828
829    /**
830     * Set the response code and an error message
831     * @param code HTTP status code
832     * @param msg Error message, string
833     */
834    protected void returnError(code, msg){
835        response.sendError(code , msg)
836    }
837
838    /**
839     * Determines what type of data a field contains
840     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
841     * @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
842     * @return Either CATEGORICALDATA of NUMERICALDATA
843     */
844    protected int determineFieldType(studyId, fieldId){
845        // Parse the fieldId as given by the user
846                def parsedField = parseFieldId( fieldId );
847
848        def study = Study.get(studyId)
849
850                def data = []
851
852                if( parsedField.source == "GSCF" ) {
853            if(parsedField.id.isNumber()){
854                // Templatefield
855                // ask for tf by id, ask for .type
856                try{
857                    TemplateField tf = TemplateField.get(parsedField.id)
858                    if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG || tf.type==TemplateFieldType.DATE || tf.type==TemplateFieldType.RELTIME){
859                        println "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
860                        return NUMERICALDATA
861                    } else {
862                        println "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
863                        return CATEGORICALDATA
864                    }
865                } catch(Exception e){
866                    log.error("VisualizationController: determineFieldType: "+e)
867                    // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
868                    return CATEGORICALDATA
869                }
870            } else {
871                // Domainfield or memberclass
872                try{
873                                        def field = domainObjectCallback( parsedField.type )?.declaredFields.find { it.name == parsedField.name };
874                                        if( field ) {
875                                                return determineCategoryFromClass( field.getType() )
876                                        } else {
877                                                log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
878                                        }
879                } catch(Exception e){
880                    log.error("VisualizationController: determineFieldType: "+e)
881                    e.printStackTrace()
882                    // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
883                    return CATEGORICALDATA
884                }
885            }
886                } else {
887            data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
888            println "Data: " + data
889                        def cat = determineCategoryFromData(data)
890            return cat
891                }
892    }
893
894    /**
895     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
896     * @param classObject
897     * @return Either CATEGORICALDATA of NUMERICALDATA
898     */
899    protected int determineCategoryFromClass(classObject){
900        println "classObject: "+classObject+", of class: "+classObject.class
901        if(classObject==java.lang.String){
902            return CATEGORICALDATA
903        } else {
904            return NUMERICALDATA
905        }
906    }
907
908    /**
909     * 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.
910     * @param inputObject Either a single item, or a collection of items
911     * @return Either CATEGORICALDATA of NUMERICALDATA
912     */
913    protected int determineCategoryFromData(inputObject){
914        def results = []
915        if(inputObject instanceof Collection){
916            // This data is more complex than a single value, so we will call ourselves again so we c
917            inputObject.each {
918                                if( it != null )
919                        results << determineCategoryFromData(it)
920            }
921        } else {
922            if(inputObject.toString().isDouble()){
923                results << NUMERICALDATA
924            } else {
925                results << CATEGORICALDATA
926            }
927        }
928
929        results.unique()
930
931        if(results.size()>1){
932            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
933            results[0] = CATEGORICALDATA
934        }
935
936        return results[0]
937    }
938
939    /**
940     * 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.
941     * @param returnData The object containing the data
942     * @return results A JSON object
943     */
944    protected void sendResults(returnData){
945        def results = [:]
946        if(infoMessage.size()!=0){
947            results.put("infoMessage", infoMessage)
948            infoMessage = []
949        }
950        results.put("returnData", returnData)
951        render results as JSON
952    }
953
954    /**
955     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
956     * @param returnData The object containing the data
957     * @return results A JSON object
958     */
959    protected void sendInfoMessage(){
960        def results = [:]
961        results.put("infoMessage", infoMessage)
962        infoMessage = []
963        render results as JSON
964    }
965
966    /**
967     * Adds a new message to the infoMessage
968     * @param message The information that needs to be added to the infoMessage
969     */
970    protected void setInfoMessage(message){
971        infoMessage.add(message)
972        println "setInfoMessage: "+infoMessage
973    }
974
975    /**
976     * Adds a message to the infoMessage that gives the client information about offline modules
977     */
978    protected void setInfoMessageOfflineModules(){
979        infoMessageOfflineModules.unique()
980        if(infoMessageOfflineModules.size()>0){
981            String message = "Unfortunately"
982            infoMessageOfflineModules.eachWithIndex{ it, index ->
983                if(index==(infoMessageOfflineModules.size()-2)){
984                    message += ', the '+it+' and '
985                } else {
986                    if(index==(infoMessageOfflineModules.size()-1)){
987                        message += 'the '+it
988                    } else {
989                        message += ', the '+it
990                    }
991                }
992            }
993            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
994            if(infoMessageOfflineModules.size()>1){
995                message += "these modules."
996            } else {
997                message += "this module."
998            }
999            setInfoMessage(message)
1000        }
1001        infoMessageOfflineModules = []
1002    }
1003
1004    /**
1005     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1006     * @param inputData Contains a list of maps, of the following format
1007     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1008     *            - a key 'name', containing, for example, a feature name or field name
1009     *            - a key 'y', containing a list of y-values
1010     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1011     */
1012    protected def formatCategoryData(inputData){
1013        // NOTE: This function is no longer up to date with the current inputData layout.
1014        def series = []
1015        inputData.eachWithIndex { it, i ->
1016            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1017        }
1018        def ret = [:]
1019        ret.put('type', inputData[0]['type'])
1020        ret.put('x', inputData[0]['x'])
1021        ret.put('yaxis',['title': 'title', 'unit': ''])
1022        ret.put('xaxis', inputData[0]['xaxis'])
1023        ret.put('series', series)
1024        return ret
1025    }
1026
1027    /**
1028     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1029     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1030     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1031     * @return
1032     */
1033    protected def determineVisualizationTypes(rowType, columnType){
1034         def types = []
1035        if(rowType==CATEGORICALDATA){
1036            if(columnType==CATEGORICALDATA){
1037                types = [ [ "id": "table", "name": "Table"] ];
1038            }
1039            if(columnType==NUMERICALDATA){
1040                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1041            }
1042        }
1043        if(rowType==NUMERICALDATA){
1044            if(columnType==CATEGORICALDATA){
1045                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
1046            }
1047            if(columnType==NUMERICALDATA){
1048                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1049            }
1050        }
1051        return types
1052    }
1053}
Note: See TracBrowser for help on using the repository browser.