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

Revision 2009, 38.8 KB (checked in by taco@…, 3 years ago)

Update for the visualization controller, primarily more commenting

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