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

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

VisualizeController?.groovy, improved infoMessage system.

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