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

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

visualization/VisualizeController.groovy, VIS-27 'Handle dates and times in a more sustainable way (e.g. check for fieldtype)'

File size: 50.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    def offlineModules = []
27    def infoMessageOfflineModules = []
28    final int CATEGORICALDATA = 0
29    final int NUMERICALDATA = 1
30    final int RELTIME = 2
31    final int DATE = 3
32
33        /**
34         * Shows the visualization screen
35         */
36        def index = {
37                [ studies: Study.giveReadableStudies( authenticationService.getLoggedInUser() )]
38        }
39
40        def getStudies = {
41                def studies = Study.giveReadableStudies( authenticationService.getLoggedInUser() );
42        return sendResults(studies)
43        }
44
45        /**
46         * 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
47         * @return List containing fields
48     * @see parseGetDataParams
49         * @see getFields
50         */
51    def getFields = {
52                def input_object
53                def studies
54
55                try{
56                        input_object = parseGetDataParams();
57                } catch(Exception e) {
58                        log.error("VisualizationController: getFields: "+e)
59            return returnError(400, "An error occured while retrieving the user input.")
60                }
61
62        // Check to see if we have enough information
63        if(input_object==null || input_object?.studyIds==null){
64            setInfoMessage("Please select a study.")
65            return sendInfoMessage()
66        } else {
67            studies = input_object.studyIds[0]
68        }
69
70                def fields = [];
71
72        /*
73         Gather fields related to this study from GSCF.
74         This requires:
75         - a study.
76         - a category variable, e.g. "events".
77         - a type variable, either "domainfields" or "templatefields".
78         */
79        // TODO: Handle multiple studies
80        def study = Study.get(studies)
81
82        if(study!=null){
83            fields += getFields(study, "subjects", "domainfields")
84            fields += getFields(study, "subjects", "templatefields")
85            fields += getFields(study, "events", "domainfields")
86            fields += getFields(study, "events", "templatefields")
87            fields += getFields(study, "samplingEvents", "domainfields")
88            fields += getFields(study, "samplingEvents", "templatefields")
89            fields += getFields(study, "assays", "domainfields")
90            fields += getFields(study, "assays", "templatefields")
91            fields += getFields(study, "samples", "domainfields")
92            fields += getFields(study, "samples", "templatefields")
93
94            /*
95            Gather fields related to this study from modules.
96            This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
97            It does not actually return measurements (the getMeasurementData call does).
98            The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
99            types related to these assays.
100            So, the required variables for such a call are:
101              - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
102              - an assay, which can be obtained with study.getAssays()
103             */
104            study.getAssays().each { assay ->
105                def list = []
106                if(!offlineModules.contains(assay.module.id)){
107                    list = getFields(assay.module.id, assay)
108                    if(list!=null){
109                        if(list.size()!=0){
110                            fields += list
111                        }
112                    }
113                }
114            }
115            offlineModules = []
116
117            // Make sure any informational messages regarding offline modules are submitted to the client
118            setInfoMessageOfflineModules()
119
120
121            // TODO: Maybe we should add study's own fields
122        } else {
123            log.error("VisualizationController: getFields: The requested study could not be found. Id: "+studies)
124            return returnError(404, "The requested study could not be found.")
125        }
126
127        fields.unique() // Todo: find out root cause of why some fields occur more than once
128        fields.sort { a, b ->
129            def sourceEquality = a.source.toString().toLowerCase().compareTo(b.source.toString().toLowerCase())
130            if( sourceEquality == 0 ) {
131                def categoryEquality = a.category.toString().toLowerCase().compareTo(b.category.toString().toLowerCase())
132                if( categoryEquality == 0 ){
133                    a.name.toString().toLowerCase().compareTo(b.name.toString().toLowerCase())
134                } else return categoryEquality
135            } else return sourceEquality
136        }
137                return sendResults(['studyIds': studies, 'fields': fields])
138        }
139
140        /**
141         * 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.
142         * @return List containing the possible visualization types, with each element containing
143     *          - a unique id
144     *          - a unique name
145     *         For example: ["id": "barchart", "name": "Barchart"]
146     * @see parseGetDataParams
147         * @see determineFieldType
148     * @see determineVisualizationTypes
149         */
150        def getVisualizationTypes = {
151        def inputData = parseGetDataParams();
152
153        if(inputData.columnIds == null || inputData.columnIds == [] || inputData.columnIds[0] == null || inputData.columnIds[0] == ""){
154            setInfoMessage("Please select a data source for the x-axis.")
155            return sendInfoMessage()
156        }
157        if(inputData.rowIds == null || inputData.rowIds == [] ||  inputData.rowIds[0] == null ||   inputData.rowIds[0] == ""){
158            setInfoMessage("Please select a data source for the y-axis.")
159            return sendInfoMessage()
160        }
161
162        // TODO: handle the case of multiple fields on an axis
163        // Determine data types
164        println "Determining rowType: "+inputData.rowIds[0]
165        def rowType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0])
166        println "Determining columnType: "+inputData.columnIds[0]
167        def columnType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0])
168
169        // Determine possible visualization types
170        def types = determineVisualizationTypes(rowType, columnType)
171
172        println "types: "+types+", determined this based on "+rowType+" and "+columnType
173        return sendResults(['types':types,'rowIds':inputData.rowIds[0],'columnIds':inputData.columnIds[0]])
174        }
175
176    /**
177     * Gather fields related to this study from modules.
178        This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
179        getMeasurements does not actually return measurements (the getMeasurementData call does).
180     * @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)
181     * @param assay     The assay that the source module and the requested fields belong to
182     * @return  A list of map objects, containing the following:
183     *           - a key 'id' with a value formatted by the createFieldId function
184     *           - a key 'source' with a value equal to the input parameter 'source'
185     *           - a key 'category' with a value equal to the 'name' field of the input paramater 'assay'
186     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
187     */
188    def getFields(source, assay){
189        def fields = []
190        def callUrl = ""
191
192        // Making a different call for each assay
193        def urlVars = "assayToken="+assay.assayUUID
194        try {
195            callUrl = ""+assay.module.url + "/rest/getMeasurements/query?"+urlVars
196            def json = moduleCommunicationService.callModuleRestMethodJSON( assay.module.url /* consumer */, callUrl );
197            def collection = []
198            json.each{ jason ->
199                collection.add(jason)
200            }
201            // Formatting the data
202            collection.each { field ->
203                // For getting this field from this assay
204                fields << [ "id": createFieldId( id: field, name: field, source: assay.id, type: ""+assay.name), "source": source, "category": ""+assay.name, "name": field ]
205            }
206        } catch(Exception e){
207            //returnError(404, "An error occured while trying to collect field data from a module. Most likely, this module is offline.")
208            offlineModules.add(assay.module.id)
209            infoMessageOfflineModules.add(assay.module.name)
210            log.error("VisualizationController: getFields: "+e)
211        }
212
213        return fields
214    }
215
216    /**
217     * Gather fields related to this study from GSCF.
218     * @param study The study that is the source of the requested fields
219     * @param category  The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
220     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
221     * @return A list of map objects, formatted by the formatGSCFFields function
222     */
223    def getFields(study, category, type){
224        // Collecting the data from it's source
225        def collection = []
226        def fields = []
227        def source = "GSCF"
228               
229                if( type == "domainfields" ) 
230                        collection = domainObjectCallback( category )?.giveDomainFields();
231                else 
232                        collection = templateObjectCallback( category, study )?.template?.fields
233
234        collection?.unique()
235
236        // Formatting the data
237        fields += formatGSCFFields(type, collection, source, category)
238
239        return fields
240    }
241
242    /**
243     * 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
244     * @param type A string that indicates the type of field, either "domainfields" or "templatefields".
245     * @param collectionOfFields A collection of fields, which could also contain only one item
246     * @param source Likely to be "GSCF"
247     * @param category The domain that a field (a property in this case) belongs to, e.g. "subjects", "samplingEvents"
248     * @return A list containing list objects, containing the following:
249     *           - a key 'id' with a value formatted by the createFieldId function
250     *           - a key 'source' with a value equal to the input parameter 'source'
251     *           - a key 'category' with a value equal to the input parameter 'category'
252     *           - a key 'name' with a value equal to the name of the field in question, as determined by the source value
253     */
254    def formatGSCFFields(type, collectionOfFields, source, category){
255
256        if(collectionOfFields==null || collectionOfFields == []){
257            return []
258        }
259        def fields = []
260        if(collectionOfFields instanceof Collection){
261            // Apparently this field is actually a list of fields.
262            // We will call ourselves again with the list's elements as input.
263            // These list elements will themselves go through this check again, effectively flattening the original input
264            for(int i = 0; i < collectionOfFields.size(); i++){
265                fields += formatGSCFFields(type, collectionOfFields[i], source, category)
266            }
267            return fields
268        } else {
269            // This is a single field. Format it and return the result.
270            if(type=="domainfields"){
271                fields << [ "id": createFieldId( id: collectionOfFields.name, name: collectionOfFields.name, source: source, type: category ), "source": source, "category": category, "name": collectionOfFields.name ]
272            }
273            if(type=="templatefields"){
274                fields << [ "id": createFieldId( id: collectionOfFields.id, name: collectionOfFields.name, source: source, type: category ), "source": source, "category": category, "name": collectionOfFields.name ]
275            }
276            return fields
277        }
278    }
279
280        /**
281         * Retrieves data for the visualization itself.
282     * 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.
283     * @return A map containing containing (at least, in the case of a barchart) the following:
284     *           - a key 'type' containing the type of chart that will be visualized
285     *           - a key 'xaxis' containing the title and unit that should be displayed for the x-axis
286     *           - a key 'yaxis' containing the title and unit that should be displayed for the y-axis*
287     *           - a key 'series' containing a list, that contains one or more maps, which contain the following:
288     *                - a key 'name', containing, for example, a feature name or field name
289     *                - a key 'y', containing a list of y-values
290     *                - 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
291         */
292        def getData = {
293                // Extract parameters
294                // TODO: handle erroneous input data
295                def inputData = parseGetDataParams();
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 || valueAxisType==DATE || valueAxisType==RELTIME ? "categorical" : "numerical")
606        def groupAxisTypeString = (groupAxisType==CATEGORICALDATA || groupAxisType==DATE || groupAxisType==RELTIME ? "categorical" : "numerical")
607        groupedData[groupAxis] = renderTimesAndDatesHumanReadable(groupedData[groupAxis], groupAxisType)
608        groupedData[valueAxis] = renderTimesAndDatesHumanReadable(groupedData[valueAxis], valueAxisType)
609
610        if(type=="table"){
611            def return_data = [:]
612            return_data[ "type" ] = type
613            return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : "", "type":valueAxisTypeString ])
614            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "", "type":groupAxisTypeString ])
615            return_data.put("series", [[
616                    "x": groupedData[ groupAxis ].collect { it.toString() },
617                    "y": groupedData[ valueAxis ].collect { it.toString() },
618                    "data": groupedData["data"]
619            ]])
620            return return_data;
621        } else {
622            def xAxis = groupedData[ groupAxis ].collect { it.toString() };
623            def yName = parseFieldId( fields[ valueAxis ] ).name;
624
625            def return_data = [:]
626            return_data[ "type" ] = type
627            return_data.put("yaxis", ["title" : yName, "unit" : "", "type":valueAxisTypeString ])
628            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "", "type":groupAxisTypeString  ])
629            return_data.put("series", [[
630                "name": yName,
631                "x": xAxis,
632                "y": groupedData[ valueAxis ],
633                "error": groupedData[ errorName ]
634            ]])
635
636            return return_data;
637        }
638        }
639
640    /**
641     * 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.
642     * @param data  The list of items that needs to be checked/converted
643     * @param axisType As determined by determineFieldType
644     * @return The input variable 'data', with it's date and time elements converted.
645     * @see determineFieldType
646     */
647    def renderTimesAndDatesHumanReadable(data, axisType){
648        if(axisType==RELTIME){
649            data = renderTimesHumanReadable(data)
650        }
651        if(axisType==DATE){
652           data = renderDatesHumanReadable(data)
653        }
654        return data
655    }
656
657    /**
658     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
659     * @param data
660     * @return
661     */
662    def renderTimesHumanReadable(data){
663        def tmpTimeContainer = []
664        data. each {
665            if(it instanceof Number) {
666                try{
667                    tmpTimeContainer << new RelTime( it ).toPrettyString()
668                } catch(IllegalArgumentException e){
669                    tmpTimeContainer << it
670                }
671            } else {
672                tmpTimeContainer << it // To handle items such as 'unknown'
673            }
674        }
675        return tmpTimeContainer
676    }
677
678    /**
679     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
680     * @param data
681     * @return
682     */
683    def renderDatesHumanReadable(data) {
684        def tmpDateContainer = []
685        data. each {
686            if(it instanceof Number) {
687                try{
688                    tmpDateContainer << new java.util.Date( it ).toString()
689                } catch(IllegalArgumentException e){
690                    tmpDateContainer << it
691                }
692            } else {
693                tmpDateContainer << it // To handle items such as 'unknown'
694            }
695        }
696        return tmpDateContainer
697    }
698        /**
699         * Returns a closure for the given entitytype that determines the value for a criterion
700         * on the given object. The closure receives two parameters: the sample and a field.
701         *
702         * For example:
703         *              How can one retrieve the value for subject.name, given a sample? This can be done by
704         *              returning the field values sample.parentSubject:
705         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
706         * @return      Closure that retrieves the value for a field and the given field
707         */
708        protected Closure valueCallback( String entity ) {
709                switch( entity ) {
710                        case "Study":
711                        case "studies":
712                                return { sample, field -> return getFieldValue( sample.parent, field ) }
713                        case "Subject":
714                        case "subjects":
715                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
716                        case "Sample":
717                        case "samples":
718                                return { sample, field -> return getFieldValue( sample, field ) }
719                        case "Event":
720                        case "events":
721                                return { sample, field ->
722                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
723                                                return null
724
725                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
726                                }
727                        case "SamplingEvent":
728                        case "samplingEvents":
729                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
730                        case "Assay":
731                        case "assays":
732                                return { sample, field ->
733                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
734                                        if( sampleAssays && sampleAssays.size() > 0 )
735                                                return sampleAssays.collect { getFieldValue( it, field ) }
736                                        else
737                                                return null
738                                }
739                }
740        }
741       
742        /**
743        * Returns the domain object that should be used with the given entity string
744        *
745        * For example:
746        *               What object should be consulted if the user asks for "studies"
747        *               Response: Study
748        * @return       Domain object that should be used with the given entity string
749        */
750   protected def domainObjectCallback( String entity ) {
751           switch( entity ) {
752                   case "Study":
753                   case "studies":
754                           return Study
755                   case "Subject":
756                   case "subjects":
757                           return Subject
758                   case "Sample":
759                   case "samples":
760                           return Sample
761                   case "Event":
762                   case "events":
763                        return Event
764                   case "SamplingEvent":
765                   case "samplingEvents":
766                           return SamplingEvent
767                   case "Assay":
768                   case "assays":
769                                return Assay
770           }
771   }
772
773    /**
774    * Returns the objects within the given study that should be used with the given entity string
775    *
776    * For example:
777    *           What object should be consulted if the user asks for "samples"
778    *           Response: study.samples
779    * @return   List of domain objects that should be used with the given entity string
780    */
781    protected def templateObjectCallback( String entity, Study study ) {
782      switch( entity ) {
783          case "Study":
784          case "studies":
785              return study
786          case "Subject":
787          case "subjects":
788              return study?.subjects
789          case "Sample":
790          case "samples":
791              return study?.samples
792          case "Event":
793          case "events":
794               return study?.events
795          case "SamplingEvent":
796          case "samplingEvents":
797              return study?.samplingEvents
798          case "Assay":
799          case "assays":
800                  return study?.assays
801      }
802    }
803       
804        /**
805         * Computes the mean value and Standard Error of the mean (SEM) for the given values
806         * @param values        List of values to compute the mean and SEM for. Strings and null
807         *                                      values are ignored
808         * @return                      Map with two keys: 'value' and 'error'
809         */
810        protected Map computeMeanAndError( values ) {
811                // TODO: Handle the case that one of the values is a list. In that case,
812                // all values should be taken into account.     
813                def mean = computeMean( values );
814                def error = computeSEM( values, mean );
815               
816                return [ 
817                        "value": mean,
818                        "error": error
819                ]
820        }
821       
822        /**
823         * Computes the mean of the given values. Values that can not be parsed to a number
824         * are ignored. If no values are given, the mean of 0 is returned.
825         * @param values        List of values to compute the mean for
826         * @return                      Arithmetic mean of the values
827         */
828        protected def computeMean( List values ) {
829                def sumOfValues = 0;
830                def sizeOfValues = 0;
831                values.each { value ->
832                        def num = getNumericValue( value );
833                        if( num != null ) {
834                                sumOfValues += num;
835                                sizeOfValues++
836                        } 
837                }
838
839                if( sizeOfValues > 0 )
840                        return sumOfValues / sizeOfValues;
841                else
842                        return 0;
843        }
844
845        /**
846        * Computes the standard error of mean of the given values. 
847        * Values that can not be parsed to a number are ignored. 
848        * If no values are given, the standard deviation of 0 is returned.
849        * @param values         List of values to compute the standard deviation for
850        * @param mean           Mean of the list (if already computed). If not given, the mean
851        *                                       will be computed using the computeMean method
852        * @return                       Standard error of the mean of the values or 0 if no values can be used.
853        */
854    protected def computeSEM( List values, def mean = null ) {
855       if( mean == null )
856            mean = computeMean( values )
857
858       def sumOfDifferences = 0;
859       def sizeOfValues = 0;
860       values.each { value ->
861           def num = getNumericValue( value );
862           if( num != null ) {
863               sumOfDifferences += Math.pow( num - mean, 2 );
864               sizeOfValues++
865           }
866       }
867
868       if( sizeOfValues > 0 ) {
869           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
870           return std / Math.sqrt( sizeOfValues );
871       } else {
872           return 0;
873       }
874    }
875   
876        /**
877         * Return the numeric value of the given object, or null if no numeric value could be returned
878         * @param       value   Object to return the value for
879         * @return                      Number that represents the given value
880         */
881        protected Number getNumericValue( value ) {
882                // TODO: handle special types of values
883                if( value instanceof Number ) {
884                        return value;
885                } else if( value instanceof RelTime ) {
886                        return value.value;
887                }
888               
889                return null
890        }
891
892        /** 
893         * Returns a field for a given templateentity
894         * @param object        TemplateEntity (or subclass) to retrieve data for
895         * @param fieldName     Name of the field to return data for.
896         * @return                      Value of the field or null if the value could not be retrieved
897         */
898        protected def getFieldValue( TemplateEntity object, String fieldName ) {
899                if( !object || !fieldName )
900                        return null;
901               
902                try {
903                        return object.getFieldValue( fieldName );
904                } catch( Exception e ) {
905                        return null;
906                }
907        }
908
909        /**
910         * Parses a fieldId that has been created earlier by createFieldId
911         * @param fieldId       FieldId to parse
912         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
913         * @see createFieldId
914         */
915        protected Map parseFieldId( String fieldId ) {
916                def attrs = [:]
917               
918                def parts = fieldId.split(",")
919               
920                attrs = [
921                        "id": parts[ 0 ],
922                        "name": parts[ 1 ],
923                        "source": parts[ 2 ],
924                        "type": parts[ 3 ]
925                ]
926        }
927       
928        /**
929         * Create a fieldId based on the given attributes
930         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
931         * @return                      Unique field ID for these parameters
932         * @see parseFieldId
933         */
934        protected String createFieldId( Map attrs ) {
935                // TODO: What if one of the attributes contains a comma?
936                def name = attrs.name;
937                def id = attrs.id ?: name;
938                def source = attrs.source;
939                def type = attrs.type ?: ""
940               
941                return id + "," + name + "," + source + "," + type;
942        }
943
944    /**
945     * Set the response code and an error message
946     * @param code HTTP status code
947     * @param msg Error message, string
948     */
949    protected void returnError(code, msg){
950        response.sendError(code , msg)
951    }
952
953    /**
954     * Determines what type of data a field contains
955     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
956     * @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
957     * @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
958     * @return Either CATEGORICALDATA, NUMERICALDATA, DATE or RELTIME
959     */
960    protected int determineFieldType(studyId, fieldId, inputData = null){
961                def parsedField = parseFieldId( fieldId );
962        def study = Study.get(studyId)
963                def data = []
964
965        try{
966            if( parsedField.source == "GSCF" ) {
967                if(parsedField.id.isNumber()){
968                        return determineCategoryFromTemplateField(parsedField.id)
969                } else { // Domainfield or memberclass
970                    def callback = domainObjectCallback( parsedField.type )
971
972                    // Can the field be found in the domainFields as well? If so, treat it as a template field, so that dates and times can be properly rendered in a human-readable fashion
973                    if(callback?.giveDomainFields().name.contains(parsedField.name.toString())){
974                        // Use the associated templateField to determine the field type
975                        return determineCategoryFromTemplateField(
976                                callback?.giveDomainFields()[
977                                    callback?.giveDomainFields().name.indexOf(parsedField.name.toString())
978                                ]
979                        )
980                    }
981                    // Apparently it is not a templatefield as well as a memberclass
982
983                    def field = callback?.declaredFields.find { it.name == parsedField.name };
984                    if( field ) {
985                        return determineCategoryFromClass( field.getType() )
986                    } else {
987                        // TODO: how do we communicate this to the user? Do we allow the process to proceed?
988                        log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
989                    }
990                }
991            } else {
992                if(inputData == null){ // If we did not get data, we need to request it from the module first
993                    data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
994                    return determineCategoryFromData(data)
995                } else {
996                    return determineCategoryFromData(inputData)
997                }
998            }
999        } catch(Exception e){
1000            log.error("VisualizationController: determineFieldType: "+e)
1001            e.printStackTrace()
1002            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1003            return CATEGORICALDATA
1004        }
1005    }
1006
1007    /**
1008     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
1009     * @param classObject
1010     * @return Either CATEGORICALDATA of NUMERICALDATA
1011     */
1012    protected int determineCategoryFromClass(classObject){
1013        println "classObject: "+classObject+", of class: "+classObject.class
1014        if(classObject==java.lang.String){
1015            return CATEGORICALDATA
1016        } else {
1017            return NUMERICALDATA
1018        }
1019    }
1020
1021    /**
1022     * 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.
1023     * @param inputObject Either a single item, or a collection of items
1024     * @return Either CATEGORICALDATA of NUMERICALDATA
1025     */
1026    protected int determineCategoryFromData(inputObject){
1027        def results = []
1028        if(inputObject instanceof Collection){
1029            // This data is more complex than a single value, so we will call ourselves again so we c
1030            inputObject.each {
1031                                if( it != null )
1032                        results << determineCategoryFromData(it)
1033            }
1034        } else {
1035            if(inputObject.toString().isDouble()){
1036                results << NUMERICALDATA
1037            } else {
1038                results << CATEGORICALDATA
1039            }
1040        }
1041
1042        results.unique()
1043
1044        if(results.size()>1){
1045            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1046            results[0] = CATEGORICALDATA
1047        }
1048
1049        return results[0]
1050    }
1051
1052    /**
1053     * Determines a field category, based on the TemplateFieldId of a Templatefield
1054     * @param id A database ID for a TemplateField
1055     * @return Either CATEGORICALDATA of NUMERICALDATA
1056     */
1057    protected int determineCategoryFromTemplateFieldId(id){
1058        TemplateField tf = TemplateField.get(id)
1059        return determineCategoryFromTemplateField(tf)
1060    }
1061
1062    /**
1063     * Determines a field category, based on the TemplateFieldType of a Templatefield
1064     * @param id A database ID for a TemplateField
1065     * @return Either CATEGORICALDATA of NUMERICALDATA
1066     */
1067    protected int determineCategoryFromTemplateField(tf){
1068        if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG){
1069            println "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
1070            return NUMERICALDATA
1071        }
1072        if(tf.type==TemplateFieldType.DATE){
1073            println "GSCF templatefield: DATE ("+DATE+") (based on "+tf.type+")"
1074            return DATE
1075        }
1076        if(tf.type==TemplateFieldType.RELTIME){
1077            println "GSCF templatefield: RELTIME ("+RELTIME+") (based on "+tf.type+")"
1078            return RELTIME
1079        }
1080        println "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
1081        return CATEGORICALDATA
1082    }
1083    /**
1084     * 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.
1085     * @param returnData The object containing the data
1086     * @return results A JSON object
1087     */
1088    protected void sendResults(returnData){
1089        def results = [:]
1090        if(infoMessage.size()!=0){
1091            results.put("infoMessage", infoMessage)
1092            infoMessage = []
1093        }
1094        results.put("returnData", returnData)
1095        render results as JSON
1096    }
1097
1098    /**
1099     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
1100     * @param returnData The object containing the data
1101     * @return results A JSON object
1102     */
1103    protected void sendInfoMessage(){
1104        def results = [:]
1105        results.put("infoMessage", infoMessage)
1106        infoMessage = []
1107        render results as JSON
1108    }
1109
1110    /**
1111     * Adds a new message to the infoMessage
1112     * @param message The information that needs to be added to the infoMessage
1113     */
1114    protected void setInfoMessage(message){
1115        infoMessage.add(message)
1116        println "setInfoMessage: "+infoMessage
1117    }
1118
1119    /**
1120     * Adds a message to the infoMessage that gives the client information about offline modules
1121     */
1122    protected void setInfoMessageOfflineModules(){
1123        infoMessageOfflineModules.unique()
1124        if(infoMessageOfflineModules.size()>0){
1125            String message = "Unfortunately"
1126            infoMessageOfflineModules.eachWithIndex{ it, index ->
1127                if(index==(infoMessageOfflineModules.size()-2)){
1128                    message += ', the '+it+' and '
1129                } else {
1130                    if(index==(infoMessageOfflineModules.size()-1)){
1131                        message += ' the '+it
1132                    } else {
1133                        message += ', the '+it
1134                    }
1135                }
1136            }
1137            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
1138            if(infoMessageOfflineModules.size()>1){
1139                message += "these modules."
1140            } else {
1141                message += "this module."
1142            }
1143            setInfoMessage(message)
1144        }
1145        infoMessageOfflineModules = []
1146    }
1147
1148    /**
1149     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1150     * @param inputData Contains a list of maps, of the following format
1151     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1152     *            - a key 'name', containing, for example, a feature name or field name
1153     *            - a key 'y', containing a list of y-values
1154     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1155     */
1156    protected def formatCategoryData(inputData){
1157        // NOTE: This function is no longer up to date with the current inputData layout.
1158        def series = []
1159        inputData.eachWithIndex { it, i ->
1160            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1161        }
1162        def ret = [:]
1163        ret.put('type', inputData[0]['type'])
1164        ret.put('x', inputData[0]['x'])
1165        ret.put('yaxis',['title': 'title', 'unit': ''])
1166        ret.put('xaxis', inputData[0]['xaxis'])
1167        ret.put('series', series)
1168        return ret
1169    }
1170
1171    /**
1172     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1173     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1174     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1175     * @return
1176     */
1177    protected def determineVisualizationTypes(rowType, columnType){
1178         def types = []
1179        if(rowType==CATEGORICALDATA || DATE || RELTIME){
1180            if(columnType==CATEGORICALDATA || DATE || RELTIME){
1181                types = [ [ "id": "table", "name": "Table"] ];
1182            }
1183            if(columnType==NUMERICALDATA){
1184                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1185            }
1186        }
1187        if(rowType==NUMERICALDATA){
1188            if(columnType==CATEGORICALDATA || DATE || RELTIME){
1189                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
1190            }
1191            if(columnType==NUMERICALDATA){
1192                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1193            }
1194        }
1195        return types
1196    }
1197}
Note: See TracBrowser for help on using the repository browser.