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

Revision 2002, 33.6 KB (checked in by robert@…, 3 years ago)

Updates in visualization with type detection and removal of (for now) unnecessary code for multiple values on one axis

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