JAL-3070 tidying
[jalview.git] / src / jalview / ws / jws2 / SeqAnnotationServiceCalcWorker.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.jws2;
22
23 import jalview.analysis.AlignSeq;
24 import jalview.analysis.SeqsetUtils;
25 import jalview.api.AlignViewportI;
26 import jalview.api.AlignmentViewPanel;
27 import jalview.bin.Cache;
28 import jalview.datamodel.AlignmentAnnotation;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.AnnotatedCollectionI;
31 import jalview.datamodel.SequenceI;
32 import jalview.gui.AlignFrame;
33 import jalview.gui.Desktop;
34 import jalview.gui.IProgressIndicator;
35 import jalview.gui.IProgressIndicatorHandler;
36 import jalview.gui.JvOptionPane;
37 import jalview.schemes.ResidueProperties;
38 import jalview.util.MessageManager;
39 import jalview.workers.AlignCalcWorker;
40 import jalview.ws.api.CancellableI;
41 import jalview.ws.api.JalviewServiceEndpointProviderI;
42 import jalview.ws.api.JobId;
43 import jalview.ws.api.SequenceAnnotationServiceI;
44 import jalview.ws.api.ServiceWithParameters;
45 import jalview.ws.api.WSAnnotationCalcManagerI;
46 import jalview.ws.gui.AnnotationWsJob;
47 import jalview.ws.jws2.dm.AAConSettings;
48 import jalview.ws.params.ArgumentI;
49 import jalview.ws.params.WsParamSetI;
50
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55
56 public class SeqAnnotationServiceCalcWorker extends AlignCalcWorker
57         implements WSAnnotationCalcManagerI
58 {
59
60   protected ServiceWithParameters service;
61
62   protected WsParamSetI preset;
63
64   protected List<ArgumentI> arguments;
65
66   protected IProgressIndicator guiProgress;
67
68   protected boolean submitGaps = true;
69
70   /**
71    * by default, we filter out non-standard residues before submission
72    */
73   protected boolean filterNonStandardResidues = true;
74
75   /**
76    * Recover any existing parameters for this service
77    */
78   protected void initViewportParams()
79   {
80     if (getCalcId() != null)
81     {
82       ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
83               getCalcId(),
84               new AAConSettings(true, service, this.preset, arguments),
85               true);
86     }
87   }
88
89   /**
90    * 
91    * @return null or a string used to recover all annotation generated by this
92    *         worker
93    */
94   public String getCalcId()
95   {
96     return service.getAlignAnalysisUI() == null ? null
97             : service.getAlignAnalysisUI().getCalcId();
98   }
99
100   public WsParamSetI getPreset()
101   {
102     return preset;
103   }
104
105   public List<ArgumentI> getArguments()
106   {
107     return arguments;
108   }
109
110   /**
111    * reconfigure and restart the AAConClient. This method will spawn a new
112    * thread that will wait until any current jobs are finished, modify the
113    * parameters and restart the conservation calculation with the new values.
114    * 
115    * @param newpreset
116    * @param newarguments
117    */
118   public void updateParameters(final WsParamSetI newpreset,
119           final List<ArgumentI> newarguments)
120   {
121     preset = newpreset;
122     arguments = newarguments;
123     calcMan.startWorker(this);
124     initViewportParams();
125   }
126   protected boolean alignedSeqs = true;
127
128   protected boolean nucleotidesAllowed = false;
129
130   protected boolean proteinAllowed = false;
131
132   /**
133    * record sequences for mapping result back to afterwards
134    */
135   protected boolean bySequence = false;
136
137   protected Map<String, SequenceI> seqNames;
138
139   // TODO: convert to bitset
140   protected boolean[] gapMap;
141
142   int realw;
143
144   protected int start;
145
146   int end;
147
148   private AlignFrame alignFrame;
149
150   public boolean[] getGapMap()
151   {
152     return gapMap;
153   }
154
155   public SeqAnnotationServiceCalcWorker(AlignViewportI alignViewport,
156           AlignmentViewPanel alignPanel)
157   {
158     super(alignViewport, alignPanel);
159   }
160
161   public SeqAnnotationServiceCalcWorker(ServiceWithParameters service,
162           AlignFrame alignFrame,
163           WsParamSetI preset, List<ArgumentI> paramset)
164   {
165     this(alignFrame.getCurrentView(), alignFrame.alignPanel);
166     // TODO: both these fields needed ?
167     this.alignFrame = alignFrame;
168     this.guiProgress = alignFrame;
169     this.preset = preset;
170     this.arguments = paramset;
171     this.service = service;
172     try
173     {
174       annotService = (jalview.ws.api.SequenceAnnotationServiceI) ((JalviewServiceEndpointProviderI) service)
175               .getEndpoint();
176     } catch (ClassCastException cce)
177     {
178       JvOptionPane.showMessageDialog(Desktop.desktop,
179               MessageManager.formatMessage(
180                       "label.service_called_is_not_an_annotation_service",
181                       new String[]
182                       { service.getName() }),
183               MessageManager.getString("label.internal_jalview_error"),
184               JvOptionPane.WARNING_MESSAGE);
185
186     }
187     // configure submission flags
188     proteinAllowed = service.isProteinService();
189     nucleotidesAllowed = service.isNucleotideService();
190     alignedSeqs = service.isNeedsAlignedSequences();
191     bySequence = !service.isAlignmentAnalysis();
192     filterNonStandardResidues = service.isFilterSymbols();
193     min_valid_seqs = service.getMinimumInputSequences();
194
195     if (service.isInteractiveUpdate())
196     {
197       initViewportParams();
198     }
199   }
200
201   /**
202    * 
203    * @return true if the submission thread should attempt to submit data
204    */
205   public boolean hasService()
206   {
207     return annotService != null;
208   }
209
210   protected jalview.ws.api.SequenceAnnotationServiceI annotService = null;
211
212   volatile JobId rslt = null;
213
214   AnnotationWsJob running = null;
215
216   private int min_valid_seqs;
217
218   @Override
219   public void run()
220   {
221     if (!hasService())
222     {
223       return;
224     }
225
226     long progressId = -1;
227
228     int serverErrorsLeft = 3;
229     final boolean cancellable = CancellableI.class
230             .isAssignableFrom(annotService.getClass());
231     StringBuffer msg = new StringBuffer();
232     try
233     {
234       if (checkDone())
235       {
236         return;
237       }
238       List<SequenceI> seqs = getInputSequences(
239               alignViewport.getAlignment(),
240               bySequence ? alignViewport.getSelectionGroup() : null);
241
242       if (seqs == null || !checkValidInputSeqs(seqs))
243       {
244         jalview.bin.Cache.log.debug(
245                 "Sequences for analysis service were null or not valid");
246         calcMan.workerComplete(this);
247         return;
248       }
249
250       if (guiProgress != null)
251       {
252         guiProgress.setProgressBar(service.getActionText(),
253                 progressId = System.currentTimeMillis());
254       }
255       jalview.bin.Cache.log.debug("submitted " + seqs.size()
256               + " sequences to " + service.getActionText());
257
258       rslt = annotService.submitToService(seqs, getPreset(),
259               getArguments());
260       if (rslt == null)
261       {
262         return;
263       }
264       // TODO: handle job submission error reporting here.
265       
266       // ///
267       // otherwise, construct WsJob and any UI handlers
268       running = new AnnotationWsJob();
269       running.setJobHandle(rslt);
270       running.setSeqNames(seqNames);
271       running.setStartPos(start);
272       running.setSeqs(seqs);
273
274       if (guiProgress != null)
275       {
276         guiProgress.registerHandler(progressId,
277                 new IProgressIndicatorHandler()
278                 {
279
280                   @Override
281                   public boolean cancelActivity(long id)
282                   {
283                     ((CancellableI) annotService).cancel(running);
284                     return true;
285                   }
286
287                   @Override
288                   public boolean canCancel()
289                   {
290                     return cancellable;
291                   }
292                 });
293       }
294       
295       // ///
296       // and poll for updates until job finishes, fails or becomes stale
297       
298       boolean finished = false;
299       do
300       {
301         Cache.log.debug("Updating status for annotation service.");
302         annotService.updateStatus(running);
303
304         if (running.isFinished())
305         {
306           Cache.log.debug("Analysis service job reported finished.");
307           finished = true;
308         }
309         else
310         {
311           Cache.log.debug("Status now " + running.getState());
312         }
313
314         if (calcMan.isPending(this) && isInteractiveUpdate())
315         {
316           Cache.log.debug("Analysis service job is stale. aborting.");
317           // job has become stale.
318           if (!finished) {
319             finished = true;
320             // cancel this job and yield to the new job
321             try
322             {
323               if (cancellable
324                         && ((CancellableI) annotService).cancel(running))
325               {
326                 System.err.println("Cancelled job: " + rslt);
327               }
328               else
329               {
330                 System.err.println("FAILED TO CANCEL job: " + rslt);
331               }
332   
333             } catch (Exception x)
334             {
335   
336             }
337           }
338           rslt = running.getJobHandle();
339           return;
340         }
341
342         // pull any stats - some services need to flush log output before
343         // results are available
344         Cache.log.debug("Updating progress log for annotation service.");
345
346         try
347         {
348         annotService.updateJobProgress(running);
349         } catch (Throwable thr)
350         {
351           Cache.log.debug("Ignoring exception during progress update.",
352                   thr);
353         }
354         Cache.log.trace("Result of poll: " + running.getStatus());
355
356         if (!finished && !running.isFailed())
357         {
358           try
359           {
360             Cache.log.debug("Analysis service job thread sleeping.");
361             Thread.sleep(200);
362             Cache.log.debug("Analysis service job thread woke.");
363           } catch (InterruptedException x)
364           {
365           }
366           ;
367         }
368       } while (!finished);
369
370       // TODO: need to poll/retry
371       if (serverErrorsLeft > 0)
372       {
373         try
374         {
375           Thread.sleep(200);
376         } catch (InterruptedException x)
377         {
378         }
379       }
380       // configure job with the associated view's feature renderer, if one
381       // exists.
382       // TODO: here one would also grab the 'master feature renderer' in order
383       // to enable/disable
384       // features automatically according to user preferences
385       running.setFeatureRenderer(
386               ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
387       Cache.log.debug("retrieving job results.");
388       List<AlignmentAnnotation> returnedAnnot = annotService
389               .getAlignmentAnnotation(running, this);
390       Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
391               : ("" + returnedAnnot.size())));
392       running.setAnnotation(returnedAnnot);
393
394       if (running.hasResults())
395       {
396         jalview.bin.Cache.log.debug("Updating result annotation from Job "
397                 + rslt + " at " + service.getUri());
398         updateResultAnnotation(true);
399         if (running.isTransferSequenceFeatures())
400         {
401           jalview.bin.Cache.log.debug(
402                   "Updating feature display settings and transferring features from Job "
403                           + rslt + " at " + service.getUri());
404           ((jalview.gui.AlignmentPanel) ap)
405                   .updateFeatureRendererFrom(running.getFeatureRenderer());
406           // TODO: JAL-1150 - create sequence feature settings API for defining
407           // styles and enabling/disabling feature overlay on alignment panel
408
409           if (alignFrame.alignPanel == ap)
410           {
411             // only do this if the alignFrame is currently showing this view.
412             Desktop.getAlignFrameFor(alignViewport)
413                     .setShowSeqFeatures(true);
414           }
415         }
416         ap.adjustAnnotationHeight();
417       }
418       Cache.log.debug("Annotation Service Worker thread finished.");
419     }
420 // TODO: use service specitic exception handlers
421 //    catch (JobSubmissionException x)
422 //    {
423 //
424 //      System.err.println(
425 //              "submission error with " + getServiceActionText() + " :");
426 //      x.printStackTrace();
427 //      calcMan.disableWorker(this);
428 //    } catch (ResultNotAvailableException x)
429 //    {
430 //      System.err.println("collection error:\nJob ID: " + rslt);
431 //      x.printStackTrace();
432 //      calcMan.disableWorker(this);
433 //
434 //    } catch (OutOfMemoryError error)
435 //    {
436 //      calcMan.disableWorker(this);
437 //
438 //      ap.raiseOOMWarning(getServiceActionText(), error);
439 //    } 
440     catch (Throwable x)
441     {
442       calcMan.disableWorker(this);
443
444       System.err
445               .println("Blacklisting worker due to unexpected exception:");
446       x.printStackTrace();
447     } finally
448     {
449
450       calcMan.workerComplete(this);
451       if (ap != null)
452       {
453         calcMan.workerComplete(this);
454         if (guiProgress != null && progressId != -1)
455         {
456           guiProgress.setProgressBar("", progressId);
457         }
458         // TODO: may not need to paintAlignment again !
459         ap.paintAlignment(false, false);
460       }
461       if (msg.length() > 0)
462       {
463         // TODO: stash message somewhere in annotation or alignment view.
464         // code below shows result in a text box popup
465         /*
466          * jalview.gui.CutAndPasteTransfer cap = new
467          * jalview.gui.CutAndPasteTransfer(); cap.setText(msg.toString());
468          * jalview.gui.Desktop.addInternalFrame(cap,
469          * "Job Status for "+getServiceActionText(), 600, 400);
470          */
471       }
472     }
473
474   }
475
476   /**
477    * validate input for dynamic/non-dynamic update context TODO: move to
478    * analysis interface ?
479    * @param seqs
480    * 
481    * @return true if input is valid
482    */
483   boolean checkValidInputSeqs(List<SequenceI> seqs)
484   {
485     int nvalid = 0;
486     for (SequenceI sq : seqs)
487     {
488       if (sq.getStart() <= sq.getEnd()
489               && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
490       {
491         if (submitGaps
492                 || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
493         {
494           nvalid++;
495         }
496       }
497     }
498     return nvalid >= min_valid_seqs;
499   }
500
501   public void cancelCurrentJob()
502   {
503     try
504     {
505       String id = running.getJobId();
506       if (((CancellableI) annotService).cancel(running))
507       {
508         System.err.println("Cancelled job " + id);
509       }
510       else
511       {
512         System.err.println("Job " + id + " couldn't be cancelled.");
513       }
514     } catch (Exception q)
515     {
516       q.printStackTrace();
517     }
518   }
519
520   /**
521    * Interactive updating. Analysis calculations that work on the currently
522    * displayed alignment data should cancel existing jobs when the input data
523    * has changed.
524    * 
525    * @return true if a running job should be cancelled because new input data is
526    *         available for analysis
527    */
528   boolean isInteractiveUpdate()
529   {
530     return service.isInteractiveUpdate();
531   }
532
533   /**
534    * decide what sequences will be analysed TODO: refactor to generate
535    * List<SequenceI> for submission to service interface
536    * 
537    * @param alignment
538    * @param inputSeqs
539    * @return
540    */
541   public List<SequenceI> getInputSequences(AlignmentI alignment,
542           AnnotatedCollectionI inputSeqs)
543   {
544     if (alignment == null || alignment.getWidth() <= 0
545             || alignment.getSequences() == null || alignment.isNucleotide()
546                     ? !nucleotidesAllowed
547                     : !proteinAllowed)
548     {
549       return null;
550     }
551     if (inputSeqs == null || inputSeqs.getWidth() <= 0
552             || inputSeqs.getSequences() == null
553             || inputSeqs.getSequences().size() < 1)
554     {
555       inputSeqs = alignment;
556     }
557
558     List<SequenceI> seqs = new ArrayList<>();
559
560     int minlen = 10;
561     int ln = -1;
562     if (bySequence)
563     {
564       seqNames = new HashMap<>();
565     }
566     gapMap = new boolean[0];
567     start = inputSeqs.getStartRes();
568     end = inputSeqs.getEndRes();
569     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
570     // correctly
571     // TODO: push attributes into WsJob instance (so they can be safely
572     // persisted/restored
573     for (SequenceI sq : (inputSeqs.getSequences()))
574     {
575       if (bySequence
576               ? sq.findPosition(end + 1)
577                       - sq.findPosition(start + 1) > minlen - 1
578               : sq.getEnd() - sq.getStart() > minlen - 1)
579       {
580         String newname = SeqsetUtils.unique_name(seqs.size() + 1);
581         // make new input sequence with or without gaps
582         if (seqNames != null)
583         {
584           seqNames.put(newname, sq);
585         }
586         SequenceI seq;
587         if (submitGaps)
588         {
589           seqs.add(seq = new jalview.datamodel.Sequence(newname,
590                   sq.getSequenceAsString()));
591           if (gapMap == null || gapMap.length < seq.getLength())
592           {
593             boolean[] tg = gapMap;
594             gapMap = new boolean[seq.getLength()];
595             System.arraycopy(tg, 0, gapMap, 0, tg.length);
596             for (int p = tg.length; p < gapMap.length; p++)
597             {
598               gapMap[p] = false; // init as a gap
599             }
600           }
601           for (int apos : sq.gapMap())
602           {
603             char sqc = sq.getCharAt(apos);
604             if (!filterNonStandardResidues
605                     || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
606                             : ResidueProperties.nucleotideIndex[sqc] < 5))
607             {
608               gapMap[apos] = true; // aligned and real amino acid residue
609             }
610             ;
611           }
612         }
613         else
614         {
615           // TODO: add ability to exclude hidden regions
616           seqs.add(seq = new jalview.datamodel.Sequence(newname,
617                   AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
618                           sq.getSequenceAsString(start, end + 1))));
619           // for annotation need to also record map to sequence start/end
620           // position in range
621           // then transfer back to original sequence on return.
622         }
623         if (seq.getLength() > ln)
624         {
625           ln = seq.getLength();
626         }
627       }
628     }
629     if (alignedSeqs && submitGaps)
630     {
631       realw = 0;
632       for (int i = 0; i < gapMap.length; i++)
633       {
634         if (gapMap[i])
635         {
636           realw++;
637         }
638       }
639       // try real hard to return something submittable
640       // TODO: some of AAcon measures need a minimum of two or three amino
641       // acids at each position, and AAcon doesn't gracefully degrade.
642       for (int p = 0; p < seqs.size(); p++)
643       {
644         SequenceI sq = seqs.get(p);
645         // strip gapped columns
646         char[] padded = new char[realw],
647                 orig = sq.getSequence();
648         for (int i = 0, pp = 0; i < realw; pp++)
649         {
650           if (gapMap[pp])
651           {
652             if (orig.length > pp)
653             {
654               padded[i++] = orig[pp];
655             }
656             else
657             {
658               padded[i++] = '-';
659             }
660           }
661         }
662         seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
663                 new String(padded)));
664       }
665     }
666     return seqs;
667   }
668
669   @Override
670   public void updateAnnotation()
671   {
672     updateResultAnnotation(false);
673   }
674
675   public void updateResultAnnotation(boolean immediate)
676   {
677     if ((immediate || !calcMan.isWorking(this)) && running != null
678             && running.hasResults())
679     {
680       List<AlignmentAnnotation> ourAnnot = running.getAnnotation();
681       updateOurAnnots(ourAnnot);
682     }
683   }
684
685   /**
686    * notify manager that we have started, and wait for a free calculation slot
687    * 
688    * @return true if slot is obtained and work still valid, false if another
689    *         thread has done our work for us.
690    */
691   protected boolean checkDone()
692   {
693     calcMan.notifyStart(this);
694     ap.paintAlignment(false, false);
695     while (!calcMan.notifyWorking(this))
696     {
697       if (calcMan.isWorking(this))
698       {
699         return true;
700       }
701       try
702       {
703         if (ap != null)
704         {
705           ap.paintAlignment(false, false);
706         }
707
708         Thread.sleep(200);
709       } catch (Exception ex)
710       {
711         ex.printStackTrace();
712       }
713     }
714     if (alignViewport.isClosed())
715     {
716       abortAndDestroy();
717       return true;
718     }
719     return false;
720   }
721
722   protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
723   {
724     List<AlignmentAnnotation> our = ourAnnots;
725     ourAnnots = ourAnnot;
726     AlignmentI alignment = alignViewport.getAlignment();
727     if (our != null)
728     {
729       if (our.size() > 0)
730       {
731         for (AlignmentAnnotation an : our)
732         {
733           if (!ourAnnots.contains(an))
734           {
735             // remove the old annotation
736             alignment.deleteAnnotation(an);
737           }
738         }
739       }
740       our.clear();
741       // validate rows and update Alignmment state
742       for (AlignmentAnnotation an : ourAnnots)
743       {
744         alignViewport.getAlignment().validateAnnotation(an);
745       }
746       // TODO: may need a menu refresh after this
747       // af.setMenusForViewport();
748       ap.adjustAnnotationHeight();
749     }
750   }
751
752   public SequenceAnnotationServiceI getService()
753   {
754     return annotService;
755   }
756
757 }