root/grails-app/controllers/importer/ImporterController.groovy @ 154

Revision 154, 32.9 KB (checked in by j.saito@…, 3 years ago)

Ticket # 57
AssayModule?.url has been renamed to AssayModule?.consumer in the GSCF.
Accordingly updated SAM.

Line 
1package importer
2
3import data.SimpleAssay
4import data.SimpleAssayMeasurement
5import data.SimpleAssayMeasurementType
6import data.SimpleAssaySample
7
8/**
9 * @author Adem and Jahn
10 * @since 20100602
11 *
12 *
13 * This Controller provides controler functionality for three views.
14 *
15 * (1) A workflow for creating new Assays from a string of text given as XLS sheet or as pasted, tab-delimited plain text.
16 *
17 * (2) A simple preview for all Assay data within a Study.
18 *
19 * (3) A simple editor for editing Assay measurements.
20 *
21 */
22
23
24
25import org.codehaus.groovy.grails.web.json.*
26import org.apache.poi.hssf.usermodel.*
27import org.apache.poi.poifs.filesystem.POIFSFileSystem
28import org.apache.poi.ss.usermodel.DataFormatter
29import grails.converters.*
30import org.apache.poi.hssf.usermodel.HSSFWorkbook
31import dbnp.rest.common.CommunicationManager
32import org.codehaus.groovy.grails.commons.GrailsApplication
33import org.apache.commons.codec.*
34
35
36class ImporterController {
37
38    def ImporterService
39    def searchableService
40    def fileService
41
42    def index = {
43        String.metaClass.mostSimilarTo = { mostSimilar(delegate, it) }
44        redirect(action: 'pages')
45    }
46   
47    /**
48     *   This flow is provides the user with a means for importing Assay data into the Simple Assay Module (SAM).
49     *
50     *   @param string externalStudyID          The code member of a Study domain object in GSCF.
51     *   @param string externalAssayID          The externalAssayID of member of a Assay domain object in GSCF.
52     *
53     *
54     *
55     *   The flow performs the following view actions.
56     *
57     *   (1) The user pastes a tab-delimted string into a text area containing a table to import.
58     *
59     *   (2) The user selects the columns of the table that he/she wants to import.
60     *
61     *   (3) The user matches the subjects in the table to those in the system.
62     *
63     *   (4) The data is validated at the GSCF and saved in SAM.
64     *
65     *   (5) An overvie of the imported data is shown to the user.
66     *
67     *
68     *
69     *   Variables stored in flow scope:
70     *
71     *   flow.selectedAssayID   The externalAssayID of a GSCF assay.
72     *   flow.studies                   list of all studies. each element e is a map with two entires. e['name'] gives
73     *                                                  the name of the study, e['id'] gives the corresponding id.
74     *
75     *   flow.selectedStudy             name of the selected study as string
76     *
77     *   flow.selectedStudyID   external ID of the selected study
78     *
79     *   flow.selectedAsasyID   external ID of the selected assay
80     *
81     *   flow.data                              2-dimensional map containing information to be stored as read in from the user
82     *                          The format of this map is as follows.
83     *                          The first level map has GSCF-subject names as keys. The values are level-2 maps.
84     *                          Level-2 maps have GSCF start time as keys which are mapped to string values.
85     *                          These string values are at some point stored as SimpleAssayMeasurements.
86     *
87     *   flow.samples                   list of SimpleAssaySamples that belong to this import's assay
88     *   
89     *   flow.tabDelimitedString  tab-delimited string representing a table. given by user.
90     */
91
92    def test = { render params }
93
94    def startError = {
95        render (view:'/importer/pages/_startError')
96    }
97
98    def pagesFlow = {
99
100        String.metaClass.mostSimilarTo = { mostSimilar(delegate, it) }
101
102        onStart {
103            flow.layouts = [ 'Sample Layout','Subject Layout' ]
104
105            try {
106                def list = CommunicationManager.getStudies()
107                def studies = list.collect{ it['studyToken'] }
108                flow.studies = studies
109            }
110            catch (Exception e){
111                redirect(action: 'startError')
112            }
113            flow.warnings = []
114
115            flow.page = 0
116            flow.pages = [
117                [title: 'Select Study'],
118                [title: 'Select Assay'],
119                [title: 'Select Layout'],
120                [title: 'Input Assay Data'],
121                [title: 'Select Data'],
122                [title: 'Show  Data'],
123                [title: 'Done']
124            ]
125        }
126
127        mainPage {
128            render(view: '/importer/importMain')
129            onRender {
130                flow.page = 0
131                next()
132            }
133            on("next") {
134                success()
135            }.to "selectStudy"
136        }
137
138        selectStudy {
139            render(view: '_selectStudy')
140            onRender {
141                flow.page = 1
142            }
143            on("next") {
144                flow.selectedStudyID = params['studyId']
145                def assays = []
146                def assayNames = []
147                CommunicationManager.getAssays( flow.selectedStudyID, grailsApplication.config.grails.serverURL ).each{
148                    assays.push( ['name':it['name'],'externalAssayId':it['assayToken']] )
149                    assayNames.push( it['name'] )
150                }
151                flow.assays     = assays
152                flow.assayNames = assayNames
153                success()
154            }.to "selectAssay"
155        }
156
157        selectAssay {
158            render( view: '_selectAssay' )
159            onRender {
160                flow.page = 2
161            }
162            on("next") {
163                def selectedName = params['assayName']
164                def data = flow.assays.find{ it['name'] == selectedName }
165                flow.selectedAssayID = data['externalAssayId']
166                flow.selectedAssayName = selectedName
167                // Save the local cache of the selected assay
168                flow.assay = saveAssay(flow.selectedAssayID, flow.selectedStudyID, selectedName )
169                success()
170            }.to "selectLayout"
171            on('previous').to('selectStudy')
172        }
173
174
175        // get table data from user
176        getTable {                                                      // user gives text input as file or by copy-and-paste
177            render(view: '_getTable')
178            onRender {
179                flow.page = 3
180            }
181            on("next"){
182                def tabbedTable = params['assayData']
183                if( (tabbedTable.replaceAll("^\\s+", "").size() > 0) && (params['file'] == "null" ) ) {
184                    flow.tabDelimitedString = tabbedTable
185                    flow.file = params['file']
186                    success()
187                }
188                // there is a file
189                else if ((tabbedTable.replaceAll("^\\s+", "").size() == 0) && (params['file'] != "null" )) {
190                    flow.tabDelimitedString = fileService.get(params['file'])
191                    flow.file = params['file']
192                    success()
193                }
194                // catch error if there is nothing to do
195                else if ((tabbedTable.replaceAll("^\\s+", "").size() == 0) && (params['file'] == "null" ))
196                {
197                    flash.errors = 'No user input : Please paste your data or select a file'
198                    error()
199                }
200                //catch error if there is too much information
201                else if ((tabbedTable.replaceAll("^\\s+", "").size() > 0) && (params['file'] != "null" ))
202                {
203                    flash.errors = 'Too much information : Please paste your data OR select a file'
204                    error()
205                }
206            }.to "buildTableModel"
207            on("previous").to "selectAssay"
208        }
209
210
211        selectLayout{
212            render( view: '_selectLayout' )
213            onRender {
214                flow.page = 4
215            }
216            on("next") {
217                def layout = params['layout']
218                flow.hasSubjectLayout =  (layout==flow.layouts[1]) ? true : false ;
219            }.to "getTable"
220            on('previous').to('selectAssay')
221        }
222
223
224        // methods for parsing the file
225        // only for Subject Layout
226        buildTableModel{
227            action {
228                // define the list of samples into the study
229                flow.samples     = getSamples( flow.selectedAssayID )
230                flow.sampleTimes = flow.samples.collect{ it.getStartTime() }.unique()
231                buildModel( flow )   // add all table data to the flow
232                if( flow.hasSubjectLayout ) { return subjectLayout() }
233                else { return sampleLayout() }
234            }
235            on("subjectLayout"){}.to "selectTypesForSubjectLayout"
236            on( "sampleLayout"){}.to "selectTypesForSampleLayout"
237        }
238
239
240        // only for sample layout
241        selectTypesForSampleLayout{                                     // match columns to assays and samples (i.e., date-subject pair)
242            render(view: '_selectTypesForSampleLayout')
243            onRender {
244                flow.page = 5
245            }
246            on('refresh'){
247                def measurementTypes = []
248                for(i in data.SimpleAssayMeasurementType.list()){
249                    measurementTypes.add(i.name)
250                    i.discard()
251                }
252                flow.measurementTypes = measurementTypes
253            }.to('selectTypesForSampleLayout')
254            on("next"){
255                // return the list of subjects and assays
256                try{
257                    addMeasurementsToFlow(flow, params)  // set up-to-date measurements in flow.measurements
258                }
259                catch (Exception e) {
260                    flash.erors = "Error"
261                    error()
262                }
263            }.to "showDataForSampleLayout"
264            on("previous").to('selectLayout')
265        }
266
267        // only for subject layout
268        selectTypesForSubjectLayout {           // match columns to assays and samples (i.e., date-subject pair)
269            render(view: '_selectTypesForSubjectLayout' )
270            onRender {
271                flow.page = 5
272            }
273            on("next"){
274                // return the list of subjects and assays
275                getAttributes(flow, params)
276            }.to "fetchSubjectsForSubjectLayout"
277            on("previous").to('selectLayout')
278        }
279
280
281        // return the list of the subjects into the study from GSCF
282        // only for subject layout
283        fetchSubjectsForSubjectLayout {
284            action {
285                def studySubjects = []
286                for (subject in CommunicationManager.getSubjects(flow.selectedStudyID.toString())){
287                    studySubjects.add(subject)
288                }
289
290                if (studySubjects.size() < flow.importedSubjects.size() ) {
291                    flash.errors = 'Error : wrong number of subjects'
292                    error()
293                }
294
295                flow.studySubjects = studySubjects
296            }
297            on("success").to("validateSubjects")
298        }
299
300
301        // only for subject layout
302        validateSubjects {
303            render(view: '_validateSubjects')                           // probably this requires more than one state
304            onRender {
305                flow.page = 5
306            }
307            on("next"){
308                // get the list of matching subjects and the data
309                getSubjects(flow, params)
310            }.to "showDataForSubjectLayout"
311            on("previous").to "selectTypesForSubjectLayout"
312        }
313
314
315        // for sample layout
316        showDataForSampleLayout {                                                       // show summary of assay to user
317            render(view:'_showSampleLayout')
318            onRender {
319                flow.page = 6
320            }
321            on("next").to "saveData"
322            on('previous').to('selectTypesForSampleLayout')
323        }
324       
325
326       
327
328        // for subject layout and sample layout
329        showDataForSubjectLayout {                                                      // show summary of assay to user
330            render(view:'_showSubjectLayout')
331            onRender {
332                flow.page = 6
333            }
334            on("next").to "saveData"
335            on('previous').to('validateSubject')
336        }
337       
338
339
340        // try to save data
341        // for subject layout and sample layout
342        saveData {                                                     
343            action{
344                if( flow.hasSubjectLayout ) {                                   // save subject layout
345                    flow.warnings = saveSAMforSubjectLayout( flow.selectedStudyID, flow.selectedAssayID,
346                        flow.selectedAssayName, flow.subjects, flow.times, flow.data )
347                }
348                else {                                                                                  // save sample
349                    flow.measurements.each {
350                        it.save(flush:true)
351                    }
352                }
353                fileService.delete(flow.file)
354                return success()
355            }
356            on("success").to "done"
357        }
358
359
360        done {                                                          // display success or error message,
361            render(view: '_done')                       // go back to first step or exit
362            onRender {
363                flow.page = 7
364            }
365            on("next").to "done"
366        }
367
368    }
369
370    /* what is this function */
371    private getAttributes(flow, params) {
372        // matchingAssays contains the ID for each assays into the file and into the study
373        def matchingAssays = []
374        // importedSubjects contains the list of subjects into the file
375        def importedSubjects = []
376        // assayValuesMap contains the list of data for each assay
377        def assayValuesMap = []
378        def typeList = []
379
380        for (entity in 0..params['entity'].size()-1){
381            if( params['entity'].getAt(entity).equals("Subjects")){
382                for (line in flow.datamatrix){
383                    importedSubjects.add(line.getAt(entity))
384                }
385            }
386            else if (params['entity'].getAt(entity).equals("Not Used")){}
387
388            else {
389                def assayID = []
390
391                assayID.add(flow.headersList.getAt(entity))
392                assayID.add(params['entity'].getAt(entity))
393                matchingAssays.add(assayID)
394
395                def assayValues = []
396                for (line in flow.datamatrix){
397                    assayValues.add(line.getAt(entity))
398                }
399                def assayMap = [:]
400                assayMap[params['entity'].getAt(entity)]= assayValues
401                assayValuesMap.add assayMap
402                typeList.add(params['type'].getAt(entity))
403            }
404        }
405        flow.importedSubjects = importedSubjects
406        flow.assayValuesMap   = assayValuesMap
407        flow.typeList = typeList
408    }
409
410
411    private getSubjects(flow, params){
412
413        def rankedImportedSubjects = []
414        def rankedStudySubjects = []
415        for (importedSubjects in params['importedSubject']){
416            rankedImportedSubjects.add(importedSubjects)
417        }
418        for (studySubjects in params['studySubject']){
419            rankedStudySubjects.add(studySubjects)
420        }
421        flow.rankedImportedSubjects = rankedImportedSubjects
422        flow.rankedStudySubjects = rankedStudySubjects  //simpleAssayMeasurementAssayType/create
423
424
425        // the list of the subjects [importedID, studyID]
426        def matchingSubjects = []
427        def mapToSave = [:]
428
429        // creation of a map to save into the SAM database
430        // exemple data = [subject1 : [time1 : (type1 , 1.2), time2 : 2.3], subject2 : [time1 : 3.4, time2 : 7.4]]
431        for (i in 0..flow.rankedStudySubjects.size()-1){
432            def matchingSubjectstmp = []
433            matchingSubjectstmp.add(flow.rankedStudySubjects.getAt(i))
434            matchingSubjectstmp.add(flow.rankedImportedSubjects.getAt(i))
435            matchingSubjects.add(matchingSubjectstmp)
436
437            def subjectAssaysList = []
438            int pos = 0
439            for (j in flow.assayValuesMap) {
440                def studyAssayMap = [:]
441                j.each { key,value ->
442                    def valuesList = []
443                    valuesList.add(flow.typeList.getAt(pos))
444                    valuesList.add(value.getAt(i))
445                    studyAssayMap[key]= valuesList
446                    pos = pos + 1
447                }
448                subjectAssaysList.add(studyAssayMap)
449            }
450            mapToSave[flow.rankedStudySubjects.getAt(i)]= subjectAssaysList
451        }
452        flow.subjects = matchingSubjects
453        flow.times    = flow.assayValuesMap
454        flow.data     = mapToSave
455    }
456
457
458    /**
459     * This method handles a file being uploaded and storing it in a temporary directory
460     * and returning a workbook
461     *
462     * @param formfilename name used for the file field in the form
463     * @return workbook object reference
464     */
465    private HSSFWorkbook handleUpload() {
466        def tempfile = null // new File("/home/lupin/projects/eclipse/sam/testTjeerdCode.xls")
467        //tempfile.leftShift(pastedData)
468        //def tempfile = new File("/home/ademcan/Desktop/testSAM.xls")
469        POIFSFileSystem fs = new POIFSFileSystem(new FileInputStream(tempfile))
470        HSSFWorkbook    wb = new HSSFWorkbook(fs);
471        return wb
472        //return ImporterService.getWorkbook(new FileInputStream(tempfile))
473    }
474
475
476
477
478
479
480    /* Create a Horrible Style Sheet Framework Workbook from tab-delimited string.
481     *
482     * @param  tabbed:  A tab-delimited string representing a table as by copy and paste from 
483     *                  an Excell sheet.
484     * @return HSSFWorkbook containing the same table.
485     *       
486     */
487    private HSSFWorkbook getWorkBook( tabbed ) {
488
489        //tabbed=tabbed.trim()
490        //def wb = new HSSFWorkbook()
491
492        def wb
493        if (tabbed instanceof File){
494            def fs = new POIFSFileSystem(new FileInputStream(tabbed))
495            wb = new HSSFWorkbook(fs)
496        }
497        else {
498            wb = new HSSFWorkbook()
499        }
500
501        def createHelper = wb.getCreationHelper()
502        def sheet = wb.createSheet("sheet")
503
504        def rowIndex = 0
505        tabbed.splitEachLine("\t") { line ->
506            def row = sheet.createRow((short)rowIndex++)
507            def colIndex = 0
508            line.each{ entry ->
509                def cell = row.createCell(colIndex++).setCellValue(entry)
510            }
511        }
512
513        return wb
514    }
515
516
517
518    /* Convenience method for isConsistentWithGSCF()
519     * see there for documentation.
520     *
521     * @param  subject: a GSCF subject.name string
522     * @param  time:    a GSCF time string for the event that belongs to a sample in GSCF
523     * @param  samples: a list of sample related strings as given by the CommunicationManager
524     * @return true, if the pair (subject, time) exists as a sample in GSCF (for the current
525     *         assay)
526     */
527    private boolean sampleExists( subject, time, samples )
528    {
529        for( sample in samples ) {
530            def sampleSubject = sample['subject'].toString()
531            def sampleTime    = sample['startTime'].toString()
532            if( subject.toString()==sampleSubject && time.toString()==sampleTime ) {
533                return true
534            }
535        }
536
537        return false
538    }
539
540
541
542    /**
543     * This method checks whether the data imported and matched by the user is
544     * still consistent with the subject names and sample times found in GSCF.
545     *
546     * We veryfy that:
547     * (1) All subject names (i.e. names in GSCF) with non-null measurement occur in GSCF.
548     * (2) All pairs (time, subject name) selected by the user correspond to a sample existing in GSCF.
549     *
550     * @param  externalAssayID Id of an assay to be tested
551     * @param  subjects:  Map of subjects, mapping GSCF subject names to local names given by the user as column names
552     * @param  ttimes:    Map of times, mapping GSCF times names to local times given by the user as column names
553     * @param  data:      Big data map containing all meassurements. GSCF subject names are mapped to a second-level
554     *                    map M2. M2 maps GSCF times to list-pairs of measurement type and value. Example:
555     *
556     *                    [ subject1 : [ time1 : [type1, 1.2], time2 : [type2, 2.3] ],
557     *                      subject2 : [ time1 : [type1, 3.4], time2 : [type2, 7.4] ]  ]
558     *
559     *                    Here, we ignore the actual values pairs and only look at data like this:
560     *
561     *                    [ subject1 : [ time1 : XXX, time2 : XXX ],
562     *                      subject2 : [ time1 : XXX, time2 : XXX ]  ]
563     *
564     *                    where XXX is ignored.
565     *
566     * @return true if data is still in sync with GSCF
567     */
568    public boolean isConsistentWithGSCF( externalAssayID, subjects, ttimes, data, samples ) {
569 
570        // Loop through all pairs ( GSCF subject names,  Time) selected by the user.
571        // Check whether the corresponding sample exists in GSCF.
572
573        def returnValue = true
574        data.each{ subject, list ->       
575            if( returnValue )  list.each{ pair ->
576                if( returnValue )  pair.keySet().each { time ->
577                    if( !sampleExists( subject, time, samples) ) {
578                        returnValue = false
579                    }
580                }
581            }
582        }                                                      // remark: calling break in a closure does not work
583
584        return returnValue
585    }
586
587
588
589    /** Save user-imported data on SAM. Before saving, check if subjects and times
590     *  given by the user are still consistent with the GSCF. Then save the data.
591     *  Then check for inconsistencies again. If inconsistencies arise at any point
592     *  don't save changes, or even roll back changes made, and return false.
593     *
594     *
595     * @param  externalAssayID Id of an assay to be tested
596     * @param  subjects:  Map of subjects, mapping GSCF subject names to local names given by the user as column names
597     * @param  ttimes:    Map of times, mapping GSCF times names to local times given by the user as column names
598     * @param  data:      Big data map containing all meassurements. GSCF subject names are mapped to a second-level
599     *                    map M2. M2 maps GSCF times to list-pairs of measurement type and value. Example:
600     *
601     *                    [ subject1 : [ time1 : [type1, 1.2], time2 : [type2, 2.3] ],
602     *                      subject2 : [ time1 : [type1, 3.4], time2 : [type2, 7.4] ]  ]
603     *
604     * @return true if data is still in sync with GSCF
605     */
606    private Object saveSAMforSubjectLayout( selectedStudyID, externalAssayID, assayName, subjects, ttimes, data ) {
607        def warnings = saveMeasurements( selectedStudyID, externalAssayID, subjects, ttimes, data )
608        saveAssay( externalAssayID, selectedStudyID, assayName )
609        return warnings
610    }
611
612
613
614    /* convenience method for saveMeasurements()
615     *
616     * There should be error handling added here.
617     *
618     * This method saves measurement data to the database by
619     * creating new instances of domain objects classes.
620     */
621    private String saveMeasurement( assaySamples, subject, time, list, type, value, assay ) {
622
623        def measurementType =
624        SimpleAssayMeasurementType.find( "from SimpleAssayMeasurementType as s where s.name = '${type}'" )
625
626        // TODO: check here whether a measurementType was actually found (and handle '-' )
627        if (!measurementType) {
628            return "Could not find measurement type ${type}"
629        }
630
631        def sample =  assaySamples.find{ it.startTime==time && it.subject==subject && it.assay==assay }
632        if( !sample ) {                 // this is only a work-around; if a sample cannot be found, this is actually an error; gscf should supply it.
633            return    "Could not save Measurement. Sample not found in GSCF for time ${time}, subject ${subject}"
634        }
635
636        // possible error: type mismatch
637        // possible error: sample not found
638
639        def measurementQuery = "from SimpleAssayMeasurement as m where m.assay= ${assay.id} " +
640                                "AND m.type= ${measurementType.id} AND m.sample = ${sample.id} "
641        def measurement = SimpleAssayMeasurementType.find( measurementQuery )
642
643        if(measurement) {
644            measurement.value  = value.toDouble()
645            measurement.sample = sample
646            measurement.type   = measurementType
647            measurement.value  = value.toDouble()
648        }
649        else {
650            measurement = new SimpleAssayMeasurement(
651                startTime:time, value:value, type:measurementType, assay:assay, sample:sample )
652        }
653
654        // save(flush:true) and discard() are neccessary here.
655        // Without, grails tries to serialise the object (measurement) to
656        // store it in the 'persistenceContext' of the flow object.
657        // This in turn leads to an error, as persistent domain objects are
658        // usually not serializable. Grails strange behaviour is documented.
659        // discard() tells hibernate to 'forget' a domain object.
660        measurement.type.discard()
661        measurement.save(flush:true)
662        sample.discard()           // needed at all?
663        sample.save(flush:true)    // needed at all?
664        return null
665    }
666
667
668
669
670    /* convenience method for saveSAM()
671     *
672     * @return  list of SimpleAssayMeasurements that have been saved in SAM. The list is mixed:
673     *          it contains measurements that are new and also measurements that have been modified.
674     *          The modified measurements contain their old values in the transient member oldValue.
675     */
676    private List saveMeasurements( externalStudyID, externalAssayID, subjects, ttimes, data ) {
677
678        def warnings = []
679        def assaySamples = getSamples( externalAssayID )
680        def assay = SimpleAssay.find("from SimpleAssay s where s.externalAssayID = '${externalAssayID}'")
681
682        data.each{ subject, list ->
683            list.each{ pair ->
684                pair.each { time, typeValuePair ->
685                    def type  = typeValuePair[0]
686                    def value = typeValuePair[1]
687                    def warning = saveMeasurement( assaySamples, subject, time, list, type, value, assay )
688                    if(warning) { warnings.add warning }
689                }
690            }
691        }
692
693        return warnings
694    }
695
696
697
698
699    /* convenience method for saveSAM() */
700    private SimpleAssay saveAssay( externalAssayID, externalStudyID, assayName ) {
701        def assay = SimpleAssayMeasurement.find( "from SimpleAssay as s where s.externalAssayID='${externalAssayID}'" )
702        if(!assay) { assay = new SimpleAssay(
703                externalAssayID:externalAssayID,
704                externalStudyID:externalStudyID,
705                isNew:true,
706                name:assayName ) }
707        else       { assay.isNew = false }
708        assay.save(flush:true)
709        return assay
710    }
711
712
713
714    /* convenience method for importer flow)
715    in future, use samples from cache!    */
716    private getSamples( externalAssayID )
717    {
718        def assay = SimpleAssay.find( "from SimpleAssay s where s.externalAssayID = '${externalAssayID}'" )
719        def samples = []
720        for ( i in CommunicationManager.getSamples(externalAssayID) ) {
721            samples.add( getSample(assay,i) )
722        }
723        assay.save(flush:true)
724        return samples.unique()
725    }
726
727
728
729    /** convenience method for get samples
730     *  Get all Samples belonging to map.
731     *  If Sample does no occure in SAM yet, then create a new Sample object in SAM
732     *  based on Sample domain object from GSCF. */
733    private getSample(assay,map)
734    {
735        def subject  = map[  'subject']
736        def time     = map['startTime']
737        def material = map[ 'material']
738        def name     = map[     'event']
739        def externalSampleId = map['sampleToken']
740        def query  = "from SimpleAssaySample as s where s.assay= '${assay.id}' and s.subject = '${subject}' AND s.startTime = '${time}' "
741        def sample = SimpleAssaySample.find(query)
742        if(!sample)
743        {
744            sample = new SimpleAssaySample( assay:assay, startTime:time, name:name, material:material,
745                subject:subject, externalSampleId:externalSampleId )
746            sample.save(flush:true)
747        }
748        assay.addToSamples(sample)
749        return sample
750    }
751
752
753
754    /* add to flow scope all data need for building the table view */
755    private buildModel(flow)
756    {
757   
758        def datamatrix = []         // the values containes into the lines
759        def headersList =  []       // the list of all headers in the file to import
760        //def wb = (flow.tabDelimitedString==null) ? handleUpload() : getWorkBook(flow.tabDelimitedString)
761
762        def wb = getWorkBook(flow.tabDelimitedString)
763
764        // getHeader contains information about headers
765        for (header in ImporterService.getHeader(wb, 0)){
766            headersList.add(header.name)
767        }
768        // getDatamatrix contains the data for each line
769        for (line in importerService.getDatamatrix(wb,0,5,1)){
770            def lines = []
771            for (box in line ){
772                lines.add(box.toString())
773            }
774            datamatrix.add(lines)
775        }
776
777        def typeList = data.SimpleAssayMeasurementType.list()
778        def measurementTypes = []
779
780        for(i in typeList){
781            measurementTypes.add(i.name)
782            i.discard()
783        }
784
785        flow.headersList      = headersList
786        flow.datamatrix       = datamatrix
787        flow.measurementTypes = measurementTypes
788    }
789
790
791
792    /**   Make a list of updated and new SimpleAssayMeasurements containing
793     *   the values provided by the user. Store this list in flow.measurements
794     *
795     *   This method fills three flow variables:
796     *
797     *   (1) flow.measurements     list of measurements changed
798     *   (2) flow.usedSamples     samples effected
799     *   (3) flow.usedTypes       types effected
800     *
801     */
802    private addMeasurementsToFlow(flow, params) {
803        def samples = params['samples'] // get data from form
804        def types   = params['types']
805        def cells   = params['cells']
806        def typeObjects = SimpleAssayMeasurementType.findAll()
807        def sampleObjects = flow.samples
808        def measurements = []
809        def assay = SimpleAssay.find("from SimpleAssay as a where a.externalAssayID = '${flow.selectedAssayID}'")
810        flow.usedSamples = []
811        flow.usedTypes   = []
812
813        samples.each{ row,sampleName ->
814            types.each{ col,typeName ->
815                if( !sampleName.equals('') && !typeName.equals('-') ) {
816                    def ck = col+'.' + row
817                    def value = cells[ck].replace(",",".").toDouble()
818                    measurements.add setMeasurement(flow,sampleObjects,typeObjects,sampleName,typeName,value,assay)
819                }
820            }
821        }
822
823        flow.measurements = measurements
824    }
825
826    /**   Convenience method for fillSamples()
827     *   Make a list of updated and new SimpleAssayMeasurements containing
828     *   the values provided by the user.
829     *
830     *   @return updated or new SimpleAssayMeasurements which is transient.
831     *           The values of the new measurment are set according to the arguments given.
832     */
833    private setMeasurement( flow,samples,types,sampleName,typeName,value,assay ) {
834        def sample = samples.find{ it.externalSampleId==sampleName }
835        def type   = types.find{ it.name==typeName}
836
837        if( !flow.usedSamples.contains(sample) ) { flow.usedSamples.add(sample) }
838        if( !flow.usedTypes.contains(type)     ) { flow.usedTypes.add(type)     }
839
840        def query = "from SimpleAssayMeasurement as m where m.type= '${type.id}' AND m.sample= '${sample.id}'"
841        def measurement =
842        SimpleAssayMeasurement.find(query) ?: new SimpleAssayMeasurement()
843        measurement.assay  = assay
844        measurement.sample = sample
845        measurement.type   = type
846        measurement.value  = value
847        return measurement
848    }
849
850    // Quickly add subAssay method
851    // Used for adding subAssays form the assay importer webflow
852    private addSubAssays(params,flow){
853        for (i in 0..params['name'].size()-1){
854            def subAssay = new SimpleAssayMeasurementType(
855                    name: params['name'].getAt(i),
856                    unit: params['unit'].getAt(i),
857                    detectableLimit: params['limit'].getAt(i).toDouble()
858                )
859                subAssay.save(flush:true)
860                flow.measurementTypes.add(subAssay.name)
861        }
862    }
863
864
865    // classes for fuzzy string matching
866    // <FUZZY MATCHING>
867    static def similarity(l_seq, r_seq, degree=2) {
868        def l_histo = countNgramFrequency(l_seq, degree)
869        def r_histo = countNgramFrequency(r_seq, degree)
870
871        dotProduct(l_histo, r_histo) /
872        Math.sqrt(dotProduct(l_histo, l_histo) *
873            dotProduct(r_histo, r_histo))
874    }
875
876    static def countNgramFrequency(sequence, degree) {
877        def histo = [:]
878        def items = sequence.size()
879
880        for (int i = 0; i + degree <= items; i++)
881        {
882            def gram = sequence[i..<(i + degree)]
883            histo[gram] = 1 + histo.get(gram, 0)
884        }
885        histo
886    }
887
888    static def dotProduct(l_histo, r_histo) {
889        def sum = 0
890        l_histo.each { key, value ->
891            sum = sum + l_histo[key] * r_histo.get(key, 0)
892        }
893        sum
894    }
895
896    static def stringSimilarity (l_str, r_str, degree=2) {
897        similarity(l_str.toLowerCase().toCharArray(),
898            r_str.toLowerCase().toCharArray(),
899            degree)
900    }
901
902    static def mostSimilar(pattern, candidates, threshold=0) {
903        def topScore = 0
904        def bestFit = null
905
906        candidates.each { candidate ->
907            def score = stringSimilarity(pattern, candidate)
908            if (score > topScore) {
909                topScore = score
910                bestFit = candidate
911            }
912        }
913
914        if (topScore < threshold)
915        bestFit = null
916
917        bestFit
918    }
919    // </FUZZY MATCHING>
920
921}
Note: See TracBrowser for help on using the browser.