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