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