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

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

visualization/VisualizeController.groovy, VIS-9 'Sort response getFields ', VIS-28 'Table needs axis names and unit', VIS-29 'Add axis types to getData-response'

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