JAL-3878 Refactoring SeqAnnotationServiceCalcWorker.
[jalview.git] / src / jalview / ws2 / operations / AnnotationServiceWorker.java
1 package jalview.ws2.operations;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Objects;
10
11 import jalview.analysis.AlignSeq;
12 import jalview.analysis.AlignmentAnnotationUtils;
13 import jalview.analysis.SeqsetUtils;
14 import jalview.api.AlignCalcManagerI2;
15 import jalview.api.AlignViewportI;
16 import jalview.api.AlignmentViewPanel;
17 import jalview.api.FeatureColourI;
18 import jalview.api.PollableAlignCalcWorkerI;
19 import jalview.bin.Cache;
20 import jalview.datamodel.Alignment;
21 import jalview.datamodel.AlignmentAnnotation;
22 import jalview.datamodel.AlignmentI;
23 import jalview.datamodel.AnnotatedCollectionI;
24 import jalview.datamodel.Annotation;
25 import jalview.datamodel.ContiguousI;
26 import jalview.datamodel.Mapping;
27 import jalview.datamodel.Sequence;
28 import jalview.datamodel.SequenceI;
29 import jalview.datamodel.features.FeatureMatcherSetI;
30 import jalview.gui.AlignFrame;
31 import jalview.gui.AlignViewport;
32 import jalview.gui.IProgressIndicator;
33 import jalview.gui.IProgressIndicatorHandler;
34 import jalview.io.FeaturesFile;
35 import jalview.schemes.FeatureSettingsAdapter;
36 import jalview.schemes.ResidueProperties;
37 import jalview.util.MapList;
38 import jalview.workers.AlignCalcManager2;
39 import jalview.ws.params.ArgumentI;
40 import jalview.ws2.WSJob;
41 import jalview.ws2.WSJobStatus;
42 import jalview.ws2.WebServiceI;
43 import jalview.ws2.gui.ProgressBarUpdater;
44
45 import static java.lang.String.format;
46
47 public class AnnotationServiceWorker implements PollableAlignCalcWorkerI
48 {
49   private AnnotationOperation operation;
50   private WebServiceI service;
51   private List<ArgumentI> args;
52   private AlignViewport viewport;
53   private AlignmentViewPanel alignPanel;
54   List<SequenceI> sequences;
55   private IProgressIndicator progressIndicator;
56   private AlignFrame frame;
57   private final AlignCalcManagerI2 calcMan;
58   private Map<String, SequenceI> seqNames;
59   boolean[] gapMap = new boolean[0];
60   int start, end;
61   boolean transferSequenceFeatures = false;
62   private WSJob job;
63   private List<AlignmentAnnotation> ourAnnots;
64   
65   private int exceptionCount = MAX_RETRY;
66   private static final int MAX_RETRY = 5;
67   
68   AnnotationServiceWorker(AnnotationOperation operation, WebServiceI service,
69       List<ArgumentI> args, AlignViewport viewport, AlignmentViewPanel alignPanel,
70       IProgressIndicator progressIndicator, AlignFrame frame, AlignCalcManagerI2 calcMan)
71   {
72     this.operation = operation;
73     this.service = service;
74     this.args = args;
75     this.viewport = viewport;
76     this.alignPanel = alignPanel;
77     this.progressIndicator = progressIndicator;
78     this.frame = frame;
79     this.calcMan = calcMan;
80   }
81   
82   @Override
83   public String getCalcName()
84   {
85     return service.getName();
86   }
87
88   @Override
89   public boolean involves(AlignmentAnnotation annot)
90   {
91     return ourAnnots != null && ourAnnots.contains(annot);
92   }
93
94   @Override
95   public void updateAnnotation()
96   {
97     if (!calcMan.isWorking(this) && job != null && !job.getStatus().isCompleted())
98     {
99       updateResultAnnotation(ourAnnots);
100     }
101   }
102
103   @Override
104   public void removeAnnotation()
105   {
106     if (ourAnnots != null && viewport != null)
107     {
108       AlignmentI alignment = viewport.getAlignment();
109       synchronized (ourAnnots)
110       {
111         for (AlignmentAnnotation aa : ourAnnots)
112         {
113           alignment.deleteAnnotation(aa, true);
114         }
115       }
116     }
117   }
118
119   @Override
120   public boolean isDeletable()
121   {
122     return false;
123   }
124
125   @Override
126   public void startUp() throws IOException
127   {
128     if (viewport.isClosed())
129     {
130       return;
131     }
132     var bySequence = !operation.isAlignmentAnalysis();
133     sequences = prepareInput(viewport.getAlignment(),
134         bySequence ? viewport.getSelectionGroup() : null);
135     if (sequences == null)
136     {
137       Cache.log.info("Sequences for analysis service were null");
138       return;
139     }
140     if (!checkInputSequencesValid(sequences))
141     {
142       Cache.log.info("Sequences for analysis service were not valid");
143     }
144     Cache.log.debug(format("submitting %d sequences to %s", sequences.size(),
145         service.getName()));
146     job = new WSJob(service.getProviderName(), service.getName(),
147         service.getHostName());
148     if (progressIndicator != null)
149     {
150       job.addPropertyChangeListener("status", new ProgressBarUpdater(progressIndicator));
151       progressIndicator.registerHandler(job.getUid(), new IProgressIndicatorHandler()
152       {
153         @Override
154         public boolean cancelActivity(long id)
155         {
156           calcMan.cancelWorker(AnnotationServiceWorker.this);
157           return true;
158         }
159
160         @Override
161         public boolean canCancel()
162         {
163           return isDeletable();
164         }
165       });
166     }
167     String jobId = service.submit(sequences, args);
168     job.setJobId(jobId);
169     Cache.log.debug(format("Service %s: submitted job id %s",
170         service.getHostName(), jobId));
171   }
172
173   private List<SequenceI> prepareInput(AlignmentI alignment,
174       AnnotatedCollectionI inputSeqs)
175   {
176     if (alignment == null || alignment.getWidth() <= 0 || 
177         alignment.getSequences() == null)
178       return null;
179     if (alignment.isNucleotide() && !operation.isNucleotideOperation())
180       return null;
181     if (!alignment.isNucleotide() && !operation.isProteinOperation())
182       return null;
183     if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
184         inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
185       inputSeqs = alignment;
186     
187     List<SequenceI> seqs = new ArrayList<>();
188     final boolean submitGaps = operation.isAlignmentAnalysis();
189     final int minlen = 10;
190     int ln = -1;
191     // FIXME don't return values by class parameters
192     if (!operation.isAlignmentAnalysis())
193       seqNames = new HashMap<>();
194     start = inputSeqs.getStartRes();
195     end = inputSeqs.getEndRes();
196     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
197     // correctly
198     // TODO: push attributes into WsJob instance (so they can be safely
199     // persisted/restored
200     for (SequenceI sq : inputSeqs.getSequences())
201     {
202       int sqlen;
203       if (!operation.isAlignmentAnalysis())
204         sqlen = sq.findPosition(end + 1) - sq.findPosition(start + 1);
205       else
206         sqlen = sq.getEnd() - sq.getStart();
207       if (sqlen >= minlen)
208       {
209         String newName = SeqsetUtils.unique_name(seqs.size());
210         if (seqNames != null)
211         {
212           seqNames.put(newName, sq);
213         }
214         SequenceI seq;
215         if (submitGaps)
216         {
217           seq = new Sequence(newName, sq.getSequenceAsString());
218           seqs.add(seq);
219           if (gapMap == null || gapMap.length < seq.getLength())
220           {
221             boolean[] tg = gapMap;
222             gapMap = new boolean[seq.getLength()];
223             System.arraycopy(tg, 0, gapMap, 0, tg.length);
224             for (int p = tg.length; p < gapMap.length; p++) 
225             {
226               gapMap[p] = false; // init as a gap
227             }
228           }
229           for (int apos : sq.gapMap())
230           {
231             char sqc = sq.getCharAt(apos);
232             boolean isStandard = sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
233                 : ResidueProperties.nucleotideIndex[sqc] < 5;
234             if (!operation.getFilterNonStandardSymbols() || isStandard)
235             {
236               gapMap[apos] = true;
237             }
238           }
239         }
240         else
241         {
242           // TODO: add ability to exclude hidden regions
243           String sqstring = sq.getSequenceAsString(start, end + 1);
244           seq = new Sequence(newName, 
245               AlignSeq.extractGaps(jalview.util.Comparison.GapChars, sqstring));
246           // for annotation need to also record map to sequence start/end
247           // position in range
248           // then transfer back to original sequence on return.
249         }
250         ln = Integer.max(seq.getLength(), ln);
251       }
252     }
253     if (operation.getNeedsAlignedSequences() && submitGaps)
254     {
255       int realw = 0;
256       for (int i = 0; i < gapMap.length; i++)
257       {
258         if (gapMap[i])
259         {
260           realw++;
261         }
262       }
263       // try real hard to return something submittable
264       // TODO: some of AAcon measures need a minimum of two or three amino
265       // acids at each position, and AAcon doesn't gracefully degrade.
266       for (int p = 0; p < seqs.size(); p++)
267       {
268         SequenceI sq = seqs.get(p);
269         // strip gapped columns
270         char[] padded = new char[realw];
271         char[] orig = sq.getSequence();
272         for (int i = 0, pp = 0; i < realw; pp++)
273         {
274           if (gapMap[pp])
275           {
276             if (orig.length > pp)
277             {
278               padded[i++] = orig[pp];
279             }
280             else
281             {
282               padded[i++] = '-';
283             }
284           }
285         }
286         seqs.set(p, new Sequence(sq.getName(), new String(padded)));
287       }
288     }
289     return seqs;
290   }
291   
292   private boolean checkInputSequencesValid(List<SequenceI> sequences)
293   {
294     int nvalid = 0;
295     boolean allowProtein = operation.isProteinOperation(),
296         allowNucleotides = operation.isNucleotideOperation();
297     for (SequenceI sq : sequences)
298     {
299       if (sq.getStart() <= sq.getEnd() &&
300           (sq.isProtein() ? allowProtein : allowNucleotides))
301       {
302         nvalid++;
303       }
304     }
305     return nvalid >= operation.getMinSequences();
306   }
307
308   @Override
309   public boolean poll() throws IOException
310   {
311     if (!job.getStatus().isDone() && !job.getStatus().isFailed())
312     {
313       Cache.log.debug(format("Polling job %s", job));
314       try
315       {
316         service.updateProgress(job);
317         exceptionCount = MAX_RETRY;
318       } catch (IOException e)
319       {
320         Cache.log.error(format("Polling job %s failed.", job), e);
321         if (--exceptionCount <= 0)
322         {
323           job.setStatus(WSJobStatus.SERVER_ERROR);
324           Cache.log.warn(format("Attempts limit exceeded. Dropping job %s.", job));
325         }
326       } catch (OutOfMemoryError e)
327       {
328         job.setStatus(WSJobStatus.BROKEN);
329         Cache.log.error(format("Out of memory when retrieving job %s", job), e);
330       }
331     }
332     return job.getStatus().isDone() || job.getStatus().isFailed();
333   }
334
335   @Override
336   public void cancel()
337   {
338     try
339     {
340       service.cancel(job);
341     } catch (IOException e)
342     {
343       Cache.log.error(format("Failed to cancel job %s.", job), e);
344     }
345   }
346
347   @Override
348   public void done()
349   {
350     Cache.log.debug(format("Polling loop exited, job %s is %s", job, job.getStatus()));
351     if (!job.getStatus().isCompleted())
352     {
353       return;
354     }
355     List<AlignmentAnnotation> outputAnnotations = null;
356     try
357     {
358       outputAnnotations = operation.annotationSupplier
359           .getResult(job, sequences, viewport);
360     } catch (IOException e)
361     {
362       Cache.log.error(format("Couldn't retrieve features for job %s.", job), e);
363     }
364     if (outputAnnotations != null)
365       Cache.log.debug(format("Obtained %d annotation rows.", outputAnnotations.size()));
366     else
367       Cache.log.debug("Obtained no annotations.");
368     Map<String, FeatureColourI> featureColours = new HashMap<>();
369     Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
370     FeaturesFile featuresFile;
371     try
372     {
373       featuresFile = operation.featuresSupplier.getResult(job, sequences, viewport);
374       if (featuresFile != null)
375       {
376         Alignment aln = new Alignment(sequences.toArray(new SequenceI[0]));
377         featuresFile.parse(aln, featureColours, true);
378       }
379     } catch (IOException e)
380     {
381       Cache.log.error(format("Couldn't retrieve features for job %s", job), e);
382     }
383     Cache.log.debug(format("There are %d feature colours and %d filters.",
384         featureColours.size(), featureFilters.size()));
385     if (outputAnnotations != null)
386     {
387       for (AlignmentAnnotation aa : outputAnnotations)
388       {
389         if (aa.getCalcId() == null || aa.getCalcId().equals(""))
390         {
391           aa.setCalcId(service.getName());
392         }
393         aa.autoCalculated = operation.isAlignmentAnalysis() && operation.isInteractive();
394       }
395       updateResultAnnotation(outputAnnotations);
396       if (transferSequenceFeatures)
397       {
398         Cache.log.debug(format("Updating feature display settings and transferring"
399             + "features fron job %s at %s", job, service.getHostName()));
400         viewport.applyFeaturesStyle(new FeatureSettingsAdapter()
401         {
402           @Override
403           public FeatureColourI getFeatureColour(String type)
404           {
405             return featureColours.get(type);
406           }
407           
408           @Override
409           public FeatureMatcherSetI getFeatureFilters(String type)
410           {
411             return featureFilters.get(type);
412           }
413           
414           @Override
415           public boolean isFeatureDisplayed(String type)
416           {
417             return featureColours.containsKey(type);
418           }
419         });
420         if (frame.alignPanel == alignPanel)
421         {
422           viewport.setShowSequenceFeatures(true);
423           frame.setMenusForViewport();
424         }
425       }
426     }
427     Cache.log.debug("Annotation service task finished.");
428   }
429   
430   private void updateResultAnnotation(List<AlignmentAnnotation> annotations)
431   {
432     var currentAnnotations = Objects.requireNonNullElse(
433         viewport.getAlignment().getAlignmentAnnotation(),
434         new AlignmentAnnotation[0]);
435     List<AlignmentAnnotation> newAnnots = new ArrayList<>();
436     int graphGroup = 1;
437     for (AlignmentAnnotation alna : currentAnnotations)
438     {
439       graphGroup = Integer.max(graphGroup, alna.graphGroup);
440     }
441     for (AlignmentAnnotation ala : annotations)
442     {
443       if (ala.graphGroup > 0)
444       {
445         ala.graphGroup += graphGroup;
446       }
447
448       SequenceI aseq = null;
449       if (ala.sequenceRef != null)
450       {
451         SequenceI seq = seqNames.get(ala.sequenceRef.getName());
452         aseq = seq;
453         while (seq.getDatasetSequence() != null)
454         {
455           seq = seq.getDatasetSequence();
456         }
457       }
458       Annotation[] resAnnot = ala.annotations;
459       Annotation[] gappedAnnot = new Annotation[Math
460           .max(viewport.getAlignment().getWidth(), gapMap.length)];
461       for (int p = 0, ap = start; ap < gappedAnnot.length; ap++)
462       {
463         if (gapMap != null && gapMap.length > ap && !gapMap[ap])
464         {
465           gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
466         }
467         else if (p < resAnnot.length)
468         {
469           gappedAnnot[ap] = resAnnot[p++];
470         }
471       }
472       ala.sequenceRef = aseq;
473       ala.annotations = gappedAnnot;
474       AlignmentAnnotation newAnnot = viewport.getAlignment()
475           .updateFromOrCopyAnnotation(ala);
476       if (aseq != null)
477       {
478         aseq.addAlignmentAnnotation(newAnnot);
479         newAnnot.adjustForAlignment();
480         AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(newAnnot,
481             newAnnot.label, newAnnot.getCalcId());
482       }
483       newAnnots.add(newAnnot);
484     }
485
486     for (SequenceI sq : sequences)
487     {
488       if (!sq.getFeatures().hasFeatures() &&
489           (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
490       {
491         continue;
492       }
493       transferSequenceFeatures = true;
494       SequenceI seq = seqNames.get(sq.getName());
495       SequenceI dseq;
496       ContiguousI seqRange = seq.findPositions(start, end);
497       
498       while ((dseq = seq).getDatasetSequence() != null)
499       {
500         seq = seq.getDatasetSequence();
501       }
502       List<ContiguousI> sourceRange = new ArrayList<>();
503       if (gapMap != null && gapMap.length > end)
504       {
505         int lastcol = start, col = start;
506         do
507         {
508           if (col == end || !gapMap[col])
509           {
510             if (lastcol <= col - 1)
511             {
512               seqRange = seq.findPositions(lastcol, col);
513               sourceRange.add(seqRange);
514             }
515             lastcol = col + 1;
516           }
517         } while (++col < end);
518       }
519       else
520       {
521         sourceRange.add(seq.findPositions(start, end));
522       }
523       int i = 0;
524       int sourceStartEnd[] = new int[sourceRange.size() * 2];
525       for (ContiguousI range : sourceRange)
526       {
527         sourceStartEnd[i++] = range.getBegin();
528         sourceStartEnd[i++] = range.getEnd();
529       }
530       Mapping mp = new Mapping(new MapList(sourceStartEnd,
531           new int[] { seq.getStart(), seq.getEnd() }, 1, 1));
532       dseq.transferAnnotation(sq, mp);
533     }
534     updateOurAnnots(newAnnots);
535   }
536   
537   protected void updateOurAnnots(List<AlignmentAnnotation> annots)
538   {
539     List<AlignmentAnnotation> our = ourAnnots;
540     ourAnnots = Collections.synchronizedList(annots);
541     AlignmentI alignment = viewport.getAlignment();
542     if (our != null)
543     {
544       if (our.size() > 0)
545       {
546         for (AlignmentAnnotation an : our)
547         {
548           if (!ourAnnots.contains(an))
549           {
550             // remove the old annotation
551             alignment.deleteAnnotation(an);
552           }
553         }
554       }
555       our.clear();
556     }
557     // validate rows and update Alignment state
558     synchronized (ourAnnots)
559     {
560       for (AlignmentAnnotation an : ourAnnots)
561       {
562         viewport.getAlignment().validateAnnotation(an);
563       }
564     }
565     // TODO: may need a menu refresh after this
566     // af.setMenusForViewport();
567     alignPanel.adjustAnnotationHeight();
568   }
569 }