applied copyright 2008
[jalview.git] / src / jalview / ws / JPredThread.java
1 /*\r
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.4)\r
3  * Copyright (C) 2008 AM Waterhouse, J Procter, G Barton, M Clamp, S Searle\r
4  * \r
5  * This program is free software; you can redistribute it and/or\r
6  * modify it under the terms of the GNU General Public License\r
7  * as published by the Free Software Foundation; either version 2\r
8  * of the License, or (at your option) any later version.\r
9  * \r
10  * This program is distributed in the hope that it will be useful,\r
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
13  * GNU General Public License for more details.\r
14  * \r
15  * You should have received a copy of the GNU General Public License\r
16  * along with this program; if not, write to the Free Software\r
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA\r
18  */\r
19 package jalview.ws;\r
20 \r
21 import java.util.*;\r
22 \r
23 import jalview.analysis.*;\r
24 import jalview.bin.*;\r
25 import jalview.datamodel.*;\r
26 import jalview.datamodel.Alignment;\r
27 import jalview.gui.*;\r
28 import jalview.io.*;\r
29 import jalview.util.*;\r
30 import vamsas.objects.simple.JpredResult;\r
31 \r
32 class JPredThread\r
33     extends WSThread implements WSClientI\r
34 {\r
35   // TODO: put mapping between JPredJob input and input data here - JNetAnnotation adding is done after result parsing.\r
36   class JPredJob\r
37       extends WSThread.WSJob\r
38   {\r
39     // TODO: make JPredJob deal only with what was sent to and received from a JNet service\r
40     int[] predMap = null; // mapping from sequence(i) to the original sequence(predMap[i]) being predicted on\r
41     vamsas.objects.simple.Sequence sequence;\r
42     vamsas.objects.simple.Msfalignment msa;\r
43     java.util.Hashtable SequenceInfo = null;\r
44     int msaIndex = 0; // the position of the original sequence in the array of Sequences in the input object that this job holds a prediction for\r
45     /**\r
46      *\r
47      * @return true if getResultSet will return a valid alignment and prediction result.\r
48      */\r
49     public boolean hasResults()\r
50     {\r
51       if (subjobComplete && result != null && result.isFinished()\r
52           && ( (JpredResult) result).getPredfile() != null &&\r
53           ( (JpredResult) result).getAligfile() != null)\r
54       {\r
55         return true;\r
56       }\r
57       return false;\r
58     }\r
59 \r
60     boolean hasValidInput()\r
61     {\r
62       if (sequence != null)\r
63       {\r
64         return true;\r
65       }\r
66       return false;\r
67     }\r
68 \r
69     /**\r
70      *\r
71      * @return null or Object[] { annotated alignment for this prediction, ColumnSelection for this prediction} or null if no results available.\r
72      * @throws Exception\r
73      */\r
74     public Object[] getResultSet()\r
75         throws Exception\r
76     {\r
77       if (result == null || !result.isFinished())\r
78       {\r
79         return null;\r
80       }\r
81       Alignment al = null;\r
82       ColumnSelection alcsel = null;\r
83       int FirstSeq = -1; // the position of the query sequence in Alignment al\r
84 \r
85       JpredResult result = (JpredResult)this.result;\r
86 \r
87       jalview.bin.Cache.log.debug("Parsing output from JNet job.");\r
88       // JPredFile prediction = new JPredFile("C:/JalviewX/files/jpred.txt", "File");\r
89       jalview.io.JPredFile prediction = new jalview.io.JPredFile(result.\r
90           getPredfile(),\r
91           "Paste");\r
92       SequenceI[] preds = prediction.getSeqsAsArray();\r
93       jalview.bin.Cache.log.debug("Got prediction profile.");\r
94 \r
95       if ( (this.msa != null) && (result.getAligfile() != null))\r
96       {\r
97         jalview.bin.Cache.log.debug("Getting associated alignment.");\r
98         // we ignore the returned alignment if we only predicted on a single sequence\r
99         String format = new jalview.io.IdentifyFile().Identify(result.\r
100             getAligfile(),\r
101             "Paste");\r
102 \r
103         if (jalview.io.FormatAdapter.isValidFormat(format))\r
104         {\r
105           SequenceI sqs[];\r
106           if (predMap != null)\r
107           {\r
108             Object[] alandcolsel = input.getAlignmentAndColumnSelection(\r
109                 getGapChar());\r
110             sqs = (SequenceI[]) alandcolsel[0];\r
111             al = new Alignment(sqs);\r
112             alcsel = (ColumnSelection) alandcolsel[1];\r
113           }\r
114           else\r
115           {\r
116             al = new FormatAdapter().readFile(result.getAligfile(),\r
117                                               "Paste", format);\r
118             sqs = new SequenceI[al.getHeight()];\r
119 \r
120             for (int i = 0, j = al.getHeight(); i < j; i++)\r
121             {\r
122               sqs[i] = al.getSequenceAt(i);\r
123             }\r
124             if (!jalview.analysis.SeqsetUtils.deuniquify( (Hashtable)\r
125                 SequenceInfo, sqs))\r
126             {\r
127               throw (new Exception(\r
128                   "Couldn't recover sequence properties for alignment."));\r
129             }\r
130           }\r
131           FirstSeq = 0;\r
132           al.setDataset(null);\r
133 \r
134           jalview.io.JnetAnnotationMaker.add_annotation(prediction, al,\r
135               FirstSeq,\r
136               false, predMap);\r
137 \r
138         }\r
139         else\r
140         {\r
141           throw (new Exception(\r
142               "Unknown format " + format + " for file : \n" +\r
143               result.getAligfile()));\r
144         }\r
145       }\r
146       else\r
147       {\r
148         al = new Alignment(preds);\r
149         FirstSeq = prediction.getQuerySeqPosition();\r
150         if (predMap != null)\r
151         {\r
152           char gc = getGapChar();\r
153           SequenceI[] sqs = (SequenceI[]) ( (java.lang.Object[]) input.\r
154                                            getAlignmentAndColumnSelection(gc))[\r
155               0];\r
156           if (this.msaIndex >= sqs.length)\r
157           {\r
158             throw new Error("Implementation Error! Invalid msaIndex for JPredJob on parent MSA input object!");\r
159           }\r
160 \r
161           /////\r
162           //Uses RemoveGapsCommand\r
163           /////\r
164           new jalview.commands.RemoveGapsCommand("Remove Gaps",\r
165                                                  new SequenceI[]\r
166                                                  {sqs[msaIndex]},\r
167                                                  currentView);\r
168 \r
169           SequenceI profileseq = al.getSequenceAt(FirstSeq);\r
170           profileseq.setSequence(sqs[msaIndex].getSequenceAsString());\r
171         }\r
172 \r
173         if (!jalview.analysis.SeqsetUtils.SeqCharacterUnhash(\r
174             al.getSequenceAt(FirstSeq), SequenceInfo))\r
175         {\r
176           throw (new Exception(\r
177               "Couldn't recover sequence properties for JNet Query sequence!"));\r
178         }\r
179         else\r
180         {\r
181           al.setDataset(null);\r
182           jalview.io.JnetAnnotationMaker.add_annotation(prediction, al,\r
183               FirstSeq,\r
184               true, predMap);\r
185           SequenceI profileseq = al.getSequenceAt(0); // this includes any gaps.\r
186           alignToProfileSeq(al, profileseq);\r
187           if (predMap != null)\r
188           {\r
189             // Adjust input view for gaps\r
190             // propagate insertions into profile\r
191             alcsel = propagateInsertions(profileseq, al, input);\r
192           }\r
193         }\r
194       }\r
195       return new Object[]\r
196           {\r
197           al, alcsel}; // , FirstSeq, noMsa};\r
198     }\r
199 \r
200     /**\r
201      * Given an alignment where all other sequences except profileseq are aligned to the ungapped profileseq, insert gaps in the other sequences to realign them with the residues in profileseq\r
202      * @param al\r
203      * @param profileseq\r
204      */\r
205     private void alignToProfileSeq(Alignment al, SequenceI profileseq)\r
206     {\r
207       char gc = al.getGapCharacter();\r
208       int[] gapMap = profileseq.gapMap();\r
209       // insert gaps into profile\r
210       for (int lp = 0, r = 0; r < gapMap.length; r++)\r
211       {\r
212         if (gapMap[r] - lp > 1)\r
213         {\r
214           StringBuffer sb = new StringBuffer();\r
215           for (int s = 0, ns = gapMap[r] - lp; s < ns; s++)\r
216           {\r
217             sb.append(gc);\r
218           }\r
219           for (int s = 1, ns = al.getHeight(); s < ns; s++)\r
220           {\r
221             String sq = al.getSequenceAt(s).getSequenceAsString();\r
222             int diff = gapMap[r] - sq.length();\r
223             if (diff > 0)\r
224             {\r
225               // pad gaps\r
226               sq = sq + sb;\r
227               while ( (diff = gapMap[r] - sq.length()) > 0)\r
228               {\r
229                 sq = sq +\r
230                     ( (diff >= sb.length()) ? sb.toString() :\r
231                      sb.substring(0, diff));\r
232               }\r
233               al.getSequenceAt(s).setSequence(sq);\r
234             }\r
235             else\r
236             {\r
237               al.getSequenceAt(s).setSequence(sq.substring(0, gapMap[r]) +\r
238                                               sb.toString() +\r
239                                               sq.substring(gapMap[r]));\r
240             }\r
241           }\r
242         }\r
243         lp = gapMap[r];\r
244       }\r
245     }\r
246 \r
247     /**\r
248      * Add gaps into the sequences aligned to profileseq under the given AlignmentView\r
249      * @param profileseq\r
250      * @param al\r
251      * @param input\r
252      */\r
253     private ColumnSelection propagateInsertions(SequenceI profileseq,\r
254                                                 Alignment al,\r
255                                                 AlignmentView input)\r
256     {\r
257       char gc = al.getGapCharacter();\r
258       Object[] alandcolsel = input.getAlignmentAndColumnSelection(gc);\r
259       ColumnSelection nview = (ColumnSelection) alandcolsel[1];\r
260       SequenceI origseq;\r
261       nview.pruneDeletions(ShiftList.parseMap( (origseq = ( (SequenceI[])\r
262           alandcolsel[0])[0]).gapMap())); // recover original prediction sequence's mapping to view.\r
263       int[] viscontigs = nview.getVisibleContigs(0, profileseq.getLength());\r
264       int spos = 0;\r
265       int offset = 0;\r
266       //  input.pruneDeletions(ShiftList.parseMap(((SequenceI[]) alandcolsel[0])[0].gapMap()))\r
267       // add profile to visible contigs\r
268       for (int v = 0; v < viscontigs.length; v += 2)\r
269       {\r
270         if (viscontigs[v] > spos)\r
271         {\r
272           StringBuffer sb = new StringBuffer();\r
273           for (int s = 0, ns = viscontigs[v] - spos; s < ns; s++)\r
274           {\r
275             sb.append(gc);\r
276           }\r
277           for (int s = 0, ns = al.getHeight(); s < ns; s++)\r
278           {\r
279             SequenceI sqobj = al.getSequenceAt(s);\r
280             if (sqobj != profileseq)\r
281             {\r
282               String sq = al.getSequenceAt(s).getSequenceAsString();\r
283               if (sq.length() <= spos + offset)\r
284               {\r
285                 // pad sequence\r
286                 int diff = spos + offset - sq.length() - 1;\r
287                 if (diff > 0)\r
288                 {\r
289                   // pad gaps\r
290                   sq = sq + sb;\r
291                   while ( (diff = spos + offset - sq.length() - 1) > 0)\r
292                   {\r
293                     sq = sq +\r
294                         ( (diff >= sb.length()) ? sb.toString() :\r
295                          sb.substring(0, diff));\r
296                   }\r
297                 }\r
298                 sq += sb.toString();\r
299               }\r
300               else\r
301               {\r
302                 al.getSequenceAt(s).setSequence(sq.substring(0, spos + offset) +\r
303                                                 sb.toString() +\r
304                                                 sq.substring(spos + offset));\r
305               }\r
306             }\r
307           }\r
308           //offset+=sb.length();\r
309         }\r
310         spos = viscontigs[v + 1] + 1;\r
311       }\r
312       if ( (offset + spos) < profileseq.getLength())\r
313       {\r
314         StringBuffer sb = new StringBuffer();\r
315         for (int s = 0, ns = profileseq.getLength() - spos - offset; s < ns; s++)\r
316         {\r
317           sb.append(gc);\r
318         }\r
319         for (int s = 1, ns = al.getHeight(); s < ns; s++)\r
320         {\r
321           String sq = al.getSequenceAt(s).getSequenceAsString();\r
322           // pad sequence\r
323           int diff = origseq.getLength() - sq.length();\r
324           while (diff > 0)\r
325           {\r
326             sq = sq +\r
327                 ( (diff >= sb.length()) ? sb.toString() : sb.substring(0, diff));\r
328             diff = origseq.getLength() - sq.length();\r
329           }\r
330         }\r
331       }\r
332       return nview;\r
333     }\r
334 \r
335     public JPredJob(Hashtable SequenceInfo, SequenceI seq, int[] delMap)\r
336     {\r
337       super();\r
338       this.predMap = delMap;\r
339       String sq = AlignSeq.extractGaps(Comparison.GapChars,\r
340                                        seq.getSequenceAsString());\r
341       if (sq.length() >= 20)\r
342       {\r
343         this.SequenceInfo = SequenceInfo;\r
344         sequence = new vamsas.objects.simple.Sequence();\r
345         sequence.setId(seq.getName());\r
346         sequence.setSeq(sq);\r
347       }\r
348     }\r
349 \r
350     public JPredJob(Hashtable SequenceInfo, SequenceI[] msf, int[] delMap)\r
351     {\r
352       this(SequenceInfo, msf[0], delMap);\r
353       if (sequence != null)\r
354       {\r
355         if (msf.length > 1)\r
356         {\r
357           msa = new vamsas.objects.simple.Msfalignment();\r
358           jalview.io.PileUpfile pileup = new jalview.io.PileUpfile();\r
359           msa.setMsf(pileup.print(msf));\r
360         }\r
361       }\r
362     }\r
363   }\r
364 \r
365   ext.vamsas.Jpred server;\r
366   String altitle = "";\r
367   JPredThread(WebserviceInfo wsinfo, String altitle, ext.vamsas.Jpred server,\r
368               String wsurl, AlignmentView alview, AlignFrame alframe)\r
369   {\r
370     super(alframe, wsinfo, alview, wsurl);\r
371     this.altitle = altitle;\r
372     this.server = server;\r
373   }\r
374 \r
375   JPredThread(WebserviceInfo wsinfo, String altitle, ext.vamsas.Jpred server,\r
376               String wsurl, Hashtable SequenceInfo, SequenceI seq, int[] delMap,\r
377               AlignmentView alview, AlignFrame alframe)\r
378   {\r
379     this(wsinfo, altitle, server, wsurl, alview, alframe);\r
380     JPredJob job = new JPredJob(SequenceInfo, seq, delMap);\r
381     if (job.hasValidInput())\r
382     {\r
383       OutputHeader = wsInfo.getProgressText();\r
384       jobs = new WSJob[]\r
385           {\r
386           job};\r
387       job.jobnum = 0;\r
388     }\r
389   }\r
390 \r
391   JPredThread(WebserviceInfo wsinfo, String altitle, ext.vamsas.Jpred server,\r
392               Hashtable SequenceInfo, SequenceI[] msf, int[] delMap,\r
393               AlignmentView alview, AlignFrame alframe, String wsurl)\r
394   {\r
395     this(wsinfo, altitle, server, wsurl, alview, alframe);\r
396     JPredJob job = new JPredJob(SequenceInfo, msf, delMap);\r
397     if (job.hasValidInput())\r
398     {\r
399       jobs = new WSJob[]\r
400           {\r
401           job};\r
402       OutputHeader = wsInfo.getProgressText();\r
403       job.jobnum = 0;\r
404     }\r
405   }\r
406 \r
407   void StartJob(WSJob j)\r
408   {\r
409     if (! (j instanceof JPredJob))\r
410     {\r
411       throw new Error("Implementation error - StartJob(JpredJob) called on " +\r
412                       j.getClass());\r
413     }\r
414     try\r
415     {\r
416       JPredJob job = (JPredJob) j;\r
417       if (job.msa != null)\r
418       {\r
419         job.jobId = server.predictOnMsa(job.msa);\r
420       }\r
421       else\r
422       if (job.sequence != null)\r
423       {\r
424         job.jobId = server.predict(job.sequence); // debug like : job.jobId = "/jobs/www-jpred/jp_Yatat29";//\r
425       }\r
426 \r
427       if (job.jobId != null)\r
428       {\r
429         if (job.jobId.startsWith("Broken"))\r
430         {\r
431           job.result = (vamsas.objects.simple.Result)new JpredResult();\r
432           job.result.setInvalid(true);\r
433           job.result.setStatus("Submission " + job.jobId);\r
434           throw new Exception(job.jobId); \r
435         }\r
436         else\r
437         {\r
438           job.submitted = true;\r
439           job.subjobComplete = false;\r
440           Cache.log.info(WsUrl + " Job Id '" + job.jobId + "'");\r
441         }\r
442       }\r
443       else\r
444       {\r
445         throw new Exception("Server timed out - try again later\n");\r
446       }\r
447     }\r
448     catch (Exception e)\r
449     {\r
450       // kill the whole job.\r
451       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);\r
452       if (e.getMessage().indexOf("Exception") > -1)\r
453       {\r
454         wsInfo.setStatus(j.jobnum, WebserviceInfo.STATE_STOPPED_SERVERERROR);\r
455         wsInfo.setProgressText(j.jobnum,\r
456                                "Failed to submit the prediction. (Just close the window)\n"\r
457                                +\r
458                                "It is most likely that there is a problem with the server.\n");\r
459         System.err.println(\r
460             "JPredWS Client: Failed to submit the prediction. Quite possibly because of a server error - see below)\n" +\r
461             e.getMessage() + "\n");\r
462 \r
463         jalview.bin.Cache.log.warn("Server Exception", e);\r
464       }\r
465       else\r
466       {\r
467         wsInfo.setStatus(j.jobnum, WebserviceInfo.STATE_STOPPED_ERROR);\r
468         // JBPNote - this could be a popup informing the user of the problem.\r
469         wsInfo.appendProgressText(j.jobnum,\r
470                                   "Failed to submit the prediction:\n"\r
471                                   + e.getMessage() +\r
472                                   wsInfo.getProgressText());\r
473 \r
474         jalview.bin.Cache.log.debug("Failed Submission of job " + j.jobnum, e);\r
475 \r
476       }\r
477       j.allowedServerExceptions = -1;\r
478       j.subjobComplete = true;\r
479     }\r
480   }\r
481 \r
482   void parseResult()\r
483   {\r
484     int results = 0; // number of result sets received\r
485     JobStateSummary finalState = new JobStateSummary();\r
486     try\r
487     {\r
488       for (int j = 0; j < jobs.length; j++)\r
489       {\r
490         finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);\r
491         if (jobs[j].submitted && jobs[j].subjobComplete && jobs[j].hasResults())\r
492         {\r
493           results++;\r
494         }\r
495       }\r
496     }\r
497     catch (Exception ex)\r
498     {\r
499 \r
500       Cache.log.error("Unexpected exception when processing results for " +\r
501                       altitle, ex);\r
502       wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);\r
503     }\r
504     if (results > 0)\r
505     {\r
506       wsInfo.showResultsNewFrame\r
507           .addActionListener(new java.awt.event.ActionListener()\r
508       {\r
509         public void actionPerformed(\r
510             java.awt.event.ActionEvent evt)\r
511         {\r
512           displayResults(true);\r
513         }\r
514       });\r
515       wsInfo.mergeResults\r
516           .addActionListener(new java.awt.event.ActionListener()\r
517       {\r
518         public void actionPerformed(\r
519             java.awt.event.ActionEvent evt)\r
520         {\r
521           displayResults(false);\r
522         }\r
523       });\r
524       wsInfo.setResultsReady();\r
525     }\r
526     else\r
527     {\r
528       wsInfo.setFinishedNoResults();\r
529     }\r
530   }\r
531 \r
532   void displayResults(boolean newWindow)\r
533   {\r
534     // TODO: cope with multiple subjobs.\r
535     if (jobs != null)\r
536     {\r
537       Object[] res = null;\r
538       boolean msa = false;\r
539       for (int jn = 0; jn < jobs.length; jn++)\r
540       {\r
541         Object[] jobres = null;\r
542         JPredJob j = (JPredJob) jobs[jn];\r
543 \r
544         if (j.hasResults())\r
545         {\r
546           // hack - we only deal with all single seuqence predictions or all profile predictions\r
547           msa = (j.msa != null) ? true : msa;\r
548           try\r
549           {\r
550             jalview.bin.Cache.log.debug("Parsing output of job " + jn);\r
551             jobres = j.getResultSet();\r
552             jalview.bin.Cache.log.debug("Finished parsing output.");\r
553             if (jobs.length == 1)\r
554             {\r
555               res = jobres;\r
556             }\r
557             else\r
558             {\r
559               // do merge with other job results\r
560               throw new Error(\r
561                   "Multiple JNet subjob merging not yet implemented.");\r
562             }\r
563           }\r
564           catch (Exception e)\r
565           {\r
566             jalview.bin.Cache.log.error(\r
567                 "JNet Client: JPred Annotation Parse Error",\r
568                 e);\r
569             wsInfo.setStatus(j.jobnum, WebserviceInfo.STATE_STOPPED_ERROR);\r
570             wsInfo.appendProgressText(j.jobnum,\r
571                                       OutputHeader + "\n" +\r
572                                       j.result.getStatus() +\r
573                                       "\nInvalid JNet job result data!\n" +\r
574                                       e.getMessage());\r
575             j.result.setBroken(true);\r
576           }\r
577         }\r
578       }\r
579 \r
580       if (res != null)\r
581       {\r
582         if (newWindow)\r
583         {\r
584           AlignFrame af;\r
585           if (input == null)\r
586           {\r
587             if (res[1] != null)\r
588             {\r
589               af = new AlignFrame( (Alignment) res[0], (ColumnSelection) res[1],\r
590                                   AlignFrame.DEFAULT_WIDTH,\r
591                                   AlignFrame.DEFAULT_HEIGHT);\r
592             }\r
593             else\r
594             {\r
595               af = new AlignFrame( (Alignment) res[0], AlignFrame.DEFAULT_WIDTH,\r
596                                   AlignFrame.DEFAULT_HEIGHT);\r
597             }\r
598           }\r
599           else\r
600           {\r
601             /*java.lang.Object[] alandcolsel = input.getAlignmentAndColumnSelection(alignFrame.getViewport().getGapCharacter());\r
602              if (((SequenceI[])alandcolsel[0])[0].getLength()!=res.getWidth()) {\r
603               if (msa) {\r
604                 throw new Error("Implementation Error! ColumnSelection from input alignment will not map to result alignment!");\r
605               }\r
606                          }\r
607                          if (!msa) {\r
608               // update hidden regions to account for loss of gaps in profile. - if any\r
609               // gapMap returns insert list, interpreted as delete list by pruneDeletions\r
610               //((ColumnSelection) alandcolsel[1]).pruneDeletions(ShiftList.parseMap(((SequenceI[]) alandcolsel[0])[0].gapMap()));\r
611                          }*/\r
612 \r
613             af = new AlignFrame( (Alignment) res[0], (ColumnSelection) res[1],\r
614                                 AlignFrame.DEFAULT_WIDTH,\r
615                                 AlignFrame.DEFAULT_HEIGHT);\r
616           }\r
617           Desktop.addInternalFrame(af, altitle,\r
618                                    AlignFrame.DEFAULT_WIDTH,\r
619                                    AlignFrame.DEFAULT_HEIGHT);\r
620         }\r
621         else\r
622         {\r
623           Cache.log.info("Append results onto existing alignment.");\r
624         }\r
625       }\r
626     }\r
627   }\r
628 \r
629   void pollJob(WSJob job)\r
630       throws Exception\r
631   {\r
632     job.result = server.getresult(job.jobId);\r
633   }\r
634 \r
635   public boolean isCancellable()\r
636   {\r
637     return false;\r
638   }\r
639 \r
640   public void cancelJob()\r
641   {\r
642     throw new Error("Implementation error!");\r
643   }\r
644 \r
645   public boolean canMergeResults()\r
646   {\r
647     return false;\r
648   }\r
649 \r
650 }\r