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