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

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

VisualizeController?.groovy, changed some infoMessages to be more user friendly.

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