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