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

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

Added x-values for all series in visualization controller

File size: 40.2 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(['studyIds': studies, 'fields': 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+", determined this based on "+rowType+" and "+columnType
155        return sendResults(['types':types,'rowIds':inputData.rowIds[0],'columnIds':inputData.columnIds[0]])
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                def xAxis = groupedData[ groupAxis ].collect { it.toString() };
537                def yName = parseFieldId( fields[ valueAxis ] ).name;
538               
539                def return_data = [:]
540                return_data[ "type" ] = "barchart"
541                return_data[ "x" ] = xAxis
542                return_data.put("yaxis", ["title" : yName, "unit" : "" ])
543                return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ])
544                return_data.put("series", [[
545                        "name": yName,
546                        "x": xAxis,
547                        "y": groupedData[ valueAxis ],
548                        "error": groupedData[ errorName ]
549                ]])
550               
551                return return_data;
552        }
553
554        /**
555         * Returns a closure for the given entitytype that determines the value for a criterion
556         * on the given object. The closure receives two parameters: the sample and a field.
557         *
558         * For example:
559         *              How can one retrieve the value for subject.name, given a sample? This can be done by
560         *              returning the field values sample.parentSubject:
561         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
562         * @return      Closure that retrieves the value for a field and the given field
563         */
564        protected Closure valueCallback( String entity ) {
565                switch( entity ) {
566                        case "Study":
567                        case "studies":
568                                return { sample, field -> return getFieldValue( sample.parent, field ) }
569                        case "Subject":
570                        case "subjects":
571                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
572                        case "Sample":
573                        case "samples":
574                                return { sample, field -> return getFieldValue( sample, field ) }
575                        case "Event":
576                        case "events":
577                                return { sample, field ->
578                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
579                                                return null
580
581                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
582                                }
583                        case "SamplingEvent":
584                        case "samplingEvents":
585                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
586                        case "Assay":
587                        case "assays":
588                                return { sample, field ->
589                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
590                                        if( sampleAssays && sampleAssays.size() > 0 )
591                                                return sampleAssays.collect { getFieldValue( it, field ) }
592                                        else
593                                                return null
594                                }
595                }
596        }
597       
598        /**
599        * Returns the domain object that should be used with the given entity string
600        *
601        * For example:
602        *               What object should be consulted if the user asks for "studies"
603        *               Response: Study
604        * @return       Domain object that should be used with the given entity string
605        */
606   protected def domainObjectCallback( String entity ) {
607           switch( entity ) {
608                   case "Study":
609                   case "studies":
610                           return Study
611                   case "Subject":
612                   case "subjects":
613                           return Subject
614                   case "Sample":
615                   case "samples":
616                           return Sample
617                   case "Event":
618                   case "events":
619                        return Event
620                   case "SamplingEvent":
621                   case "samplingEvents":
622                           return SamplingEvent
623                   case "Assay":
624                   case "assays":
625                                return Assay
626           }
627   }
628
629    /**
630    * Returns the objects within the given study that should be used with the given entity string
631    *
632    * For example:
633    *           What object should be consulted if the user asks for "samples"
634    *           Response: study.samples
635    * @return   List of domain objects that should be used with the given entity string
636    */
637    protected def templateObjectCallback( String entity, Study study ) {
638      switch( entity ) {
639          case "Study":
640          case "studies":
641              return study
642          case "Subject":
643          case "subjects":
644              return study?.samples?.parentSubject
645          case "Sample":
646          case "samples":
647              return study?.samples
648          case "Event":
649          case "events":
650               return study?.samples?.parentEventGroup?.events?.flatten()
651          case "SamplingEvent":
652          case "samplingEvents":
653              return study?.samples?.parentEvent
654          case "Assay":
655          case "assays":
656                  return study?.assays
657      }
658    }
659       
660        /**
661         * Computes the mean value and Standard Error of the mean (SEM) for the given values
662         * @param values        List of values to compute the mean and SEM for. Strings and null
663         *                                      values are ignored
664         * @return                      Map with two keys: 'value' and 'error'
665         */
666        protected Map computeMeanAndError( values ) {
667                // TODO: Handle the case that one of the values is a list. In that case,
668                // all values should be taken into account.     
669                def mean = computeMean( values );
670                def error = computeSEM( values, mean );
671               
672                return [ 
673                        "value": mean,
674                        "error": error
675                ]
676        }
677       
678        /**
679         * Computes the mean of the given values. Values that can not be parsed to a number
680         * are ignored. If no values are given, the mean of 0 is returned.
681         * @param values        List of values to compute the mean for
682         * @return                      Arithmetic mean of the values
683         */
684        protected def computeMean( List values ) {
685                def sumOfValues = 0;
686                def sizeOfValues = 0;
687                values.each { value ->
688                        def num = getNumericValue( value );
689                        if( num != null ) {
690                                sumOfValues += num;
691                                sizeOfValues++
692                        } 
693                }
694
695                if( sizeOfValues > 0 )
696                        return sumOfValues / sizeOfValues;
697                else
698                        return 0;
699        }
700
701        /**
702        * Computes the standard error of mean of the given values. 
703        * Values that can not be parsed to a number are ignored. 
704        * If no values are given, the standard deviation of 0 is returned.
705        * @param values         List of values to compute the standard deviation for
706        * @param mean           Mean of the list (if already computed). If not given, the mean
707        *                                       will be computed using the computeMean method
708        * @return                       Standard error of the mean of the values or 0 if no values can be used.
709        */
710    protected def computeSEM( List values, def mean = null ) {
711       if( mean == null )
712            mean = computeMean( values )
713
714       def sumOfDifferences = 0;
715       def sizeOfValues = 0;
716       values.each { value ->
717           def num = getNumericValue( value );
718           if( num != null ) {
719               sumOfDifferences += Math.pow( num - mean, 2 );
720               sizeOfValues++
721           }
722       }
723
724       if( sizeOfValues > 0 ) {
725           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
726           return std / Math.sqrt( sizeOfValues );
727       } else {
728           return 0;
729       }
730    }
731    Exception e
732        /**
733         * Return the numeric value of the given object, or null if no numeric value could be returned
734         * @param       value   Object to return the value for
735         * @return                      Number that represents the given value
736         */
737        protected Number getNumericValue( value ) {
738                // TODO: handle special types of values
739                if( value instanceof Number ) {
740                        return value;
741                } else if( value instanceof RelTime ) {
742                        return value.value;
743                }
744               
745                return null
746        }
747
748        /** 
749         * Returns a field for a given templateentity
750         * @param object        TemplateEntity (or subclass) to retrieve data for
751         * @param fieldName     Name of the field to return data for.
752         * @return                      Value of the field or null if the value could not be retrieved
753         */
754        protected def getFieldValue( TemplateEntity object, String fieldName ) {
755                if( !object || !fieldName )
756                        return null;
757               
758                try {
759                        return object.getFieldValue( fieldName );
760                } catch( Exception e ) {
761                        return null;
762                }
763        }
764
765        /**
766         * Parses a fieldId that has been created earlier by createFieldId
767         * @param fieldId       FieldId to parse
768         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
769         * @see createFieldId
770         */
771        protected Map parseFieldId( String fieldId ) {
772                def attrs = [:]
773               
774                def parts = fieldId.split(",")
775               
776                attrs = [
777                        "id": parts[ 0 ],
778                        "name": parts[ 1 ],
779                        "source": parts[ 2 ],
780                        "type": parts[ 3 ]
781                ]
782        }
783       
784        /**
785         * Create a fieldId based on the given attributes
786         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
787         * @return                      Unique field ID for these parameters
788         * @see parseFieldId
789         */
790        protected String createFieldId( Map attrs ) {
791                // TODO: What if one of the attributes contains a comma?
792                def name = attrs.name;
793                def id = attrs.id ?: name;
794                def source = attrs.source;
795                def type = attrs.type ?: ""
796               
797                return id + "," + name + "," + source + "," + type;
798        }
799
800    /**
801     * Set the response code and an error message
802     * @param code HTTP status code
803     * @param msg Error message, string
804     */
805    protected void returnError(code, msg){
806        response.sendError(code , msg)
807    }
808
809    /**
810     * Determines what type of data a field contains
811     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
812     * @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
813     * @return Either CATEGORICALDATA of NUMERICALDATA
814     */
815    protected int determineFieldType(studyId, fieldId){
816        // Parse the fieldId as given by the user
817                def parsedField = parseFieldId( fieldId );
818
819        def study = Study.get(studyId)
820
821                def data = []
822
823                if( parsedField.source == "GSCF" ) {
824            if(parsedField.id.isNumber()){
825                // Templatefield
826                // ask for tf by id, ask for .type
827                try{
828                    TemplateField tf = TemplateField.get(parsedField.id)
829                    if(tf.type=="DOUBLE" || tf.type=="LONG" || tf.type=="DATE" || tf.type=="RELTIME"){
830                        return NUMERICALDATA
831                    } else {
832                        return CATEGORICALDATA
833                    }
834                } catch(Exception e){
835                    log.error("VisualizationController: determineFieldType: "+e)
836                    // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
837                    return CATEGORICALDATA
838                }
839            } else {
840                // Domainfield or memberclass
841                try{
842                                        return determineCategoryFromClass(domainObjectCallback( parsedField.type )?.fields[parsedField.name].type)
843                } catch(Exception e){
844                    log.error("VisualizationController: determineFieldType: "+e)
845                    e.printStackTrace()
846                    // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
847                    return CATEGORICALDATA
848                }
849            }
850                } else {
851            data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
852            println "Data: " + data
853                        def cat = determineCategoryFromData(data)
854            return cat
855                }
856    }
857
858    /**
859     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
860     * @param classObject
861     * @return Either CATEGORICALDATA of NUMERICALDATA
862     */
863    protected int determineCategoryFromClass(classObject){
864        println "classObject: "+classObject+", of class: "+classObject.class
865        if(classObject==java.lang.String){
866            return CATEGORICALDATA
867        } else {
868            return NUMERICALDATA
869        }
870    }
871
872    /**
873     * 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.
874     * @param inputObject Either a single item, or a collection of items
875     * @return Either CATEGORICALDATA of NUMERICALDATA
876     */
877    protected int determineCategoryFromData(inputObject){
878        def results = []
879        if(inputObject instanceof Collection){
880            // This data is more complex than a single value, so we will call ourselves again so we c
881            inputObject.each {
882                                if( it != null )
883                        results << determineCategoryFromData(it)
884            }
885        } else {
886            if(inputObject.toString().isDouble()){
887                results << NUMERICALDATA
888            } else {
889                results << CATEGORICALDATA
890            }
891        }
892
893        results.unique()
894
895        if(results.size()>1){
896            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
897            results[0] = CATEGORICALDATA
898        }
899
900        return results[0]
901    }
902
903
904    /**
905     * 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.
906     * @param returnData The object containing the data
907     * @return results A JSON object
908     */
909    protected void sendResults(returnData){
910        def results = [:]
911        if(infoMessage.size()!=0){
912            results.put("infoMessage", infoMessage)
913            infoMessage = []
914        }
915        results.put("returnData", returnData)
916        render results as JSON
917    }
918
919    /**
920     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
921     * @param returnData The object containing the data
922     * @return results A JSON object
923     */
924    protected void sendInfoMessage(){
925        def results = [:]
926        results.put("infoMessage", infoMessage)
927        infoMessage = []
928        render results as JSON
929    }
930
931    /**
932     * Adds a new message to the infoMessage
933     * @param message The information that needs to be added to the infoMessage
934     */
935    protected void setInfoMessage(message){
936        infoMessage.add(message)
937        println "setInfoMessage: "+infoMessage
938    }
939
940    /**
941     * Adds a message to the infoMessage that gives the client information about offline modules
942     */
943    protected void setInfoMessageOfflineModules(){
944        infoMessageOfflineModules.unique()
945        if(infoMessageOfflineModules.size()>0){
946            String message = "Unfortunately "
947            infoMessageOfflineModules.eachWithIndex{ it, index ->
948                if(index==(infoMessageOfflineModules.size()-2)){
949                    message += ', the '+it+' and '
950                } else {
951                    if(index==(infoMessageOfflineModules.size()-1)){
952                        message += 'the '+it
953                    } else {
954                        message += ', the '+it
955                    }
956                }
957            }
958            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
959            if(infoMessageOfflineModules.size()>1){
960                message += "these modules."
961            } else {
962                message += "this module."
963            }
964            setInfoMessage(message)
965        }
966        infoMessageOfflineModules = []
967    }
968
969    /**
970     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
971     * @param inputData Contains a list of maps, of the following format
972     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
973     *            - a key 'name', containing, for example, a feature name or field name
974     *            - a key 'y', containing a list of y-values
975     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
976     */
977    protected def formatCategoryData(inputData){
978        def series = []
979        inputData.eachWithIndex { it, i ->
980            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
981        }
982        def ret = [:]
983        ret.put('type', inputData[0]['type'])
984        ret.put('x', inputData[0]['x'])
985        ret.put('yaxis',['title': 'title', 'unit': ''])
986        ret.put('xaxis', inputData[0]['xaxis'])
987        ret.put('series', series)
988        return ret
989    }
990
991    /**
992     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
993     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
994     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
995     * @return
996     */
997    protected def determineVisualizationTypes(rowType, columnType){
998         def types = []
999        if(rowType==CATEGORICALDATA){
1000            if(columnType==CATEGORICALDATA){
1001                types = [ [ "id": "table", "name": "Table"] ];
1002            }
1003            if(columnType==NUMERICALDATA){
1004                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1005            }
1006        }
1007        if(rowType==NUMERICALDATA){
1008            if(columnType==CATEGORICALDATA){
1009                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
1010            }
1011            if(columnType==NUMERICALDATA){
1012                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1013            }
1014        }
1015        return types
1016    }
1017}
Note: See TracBrowser for help on using the repository browser.