1 package jalview.ws2.operations;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
9 import java.util.Objects;
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;
45 import static java.lang.String.format;
47 public class AnnotationServiceWorker implements PollableAlignCalcWorkerI
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;
60 * indicates columns consisting of gaps only
62 boolean[] gapMap = new boolean[0];
64 boolean transferSequenceFeatures = false;
66 private List<AlignmentAnnotation> ourAnnots;
68 private int exceptionCount = MAX_RETRY;
69 private static final int MAX_RETRY = 5;
71 public AnnotationServiceWorker(AnnotationOperation operation,
72 List<ArgumentI> args, AlignViewport viewport, AlignmentViewPanel alignPanel,
73 IProgressIndicator progressIndicator, AlignFrame frame,
74 AlignCalcManagerI2 calcMan)
76 this.operation = operation;
77 this.service = operation.service;
79 this.viewport = viewport;
80 this.alignPanel = alignPanel;
81 this.progressIndicator = progressIndicator;
83 this.calcMan = calcMan;
87 public String getCalcName()
89 return service.getName();
93 public boolean involves(AlignmentAnnotation annot)
95 return ourAnnots != null && ourAnnots.contains(annot);
99 public void updateAnnotation()
101 if (!calcMan.isWorking(this) && job != null && !job.getStatus().isCompleted())
103 // is it correct to store annotations in a field and use them here?
104 updateResultAnnotation(ourAnnots);
109 public void removeAnnotation()
111 if (ourAnnots != null && viewport != null)
113 AlignmentI alignment = viewport.getAlignment();
114 synchronized (ourAnnots)
116 for (AlignmentAnnotation aa : ourAnnots)
118 alignment.deleteAnnotation(aa, true);
125 public boolean isDeletable()
131 public void startUp() throws IOException
133 if (viewport.isClosed())
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)
145 Cache.log.info("Sequences for analysis service were null");
148 if (!checkInputSequencesValid(sequences))
150 Cache.log.info("Sequences for analysis service were not valid");
152 Cache.log.debug(format("submitting %d sequences to %s", sequences.size(),
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
158 if (progressIndicator != null)
160 job.addPropertyChangeListener("status", new ProgressBarUpdater(progressIndicator));
161 progressIndicator.registerHandler(job.getUid(), new IProgressIndicatorHandler()
164 public boolean cancelActivity(long id)
166 calcMan.cancelWorker(AnnotationServiceWorker.this);
171 public boolean canCancel()
173 return isDeletable();
177 String jobId = service.submit(sequences, args);
179 Cache.log.debug(format("Service %s: submitted job id %s",
180 service.getHostName(), jobId));
183 private List<SequenceI> prepareInput(AlignmentI alignment,
184 AnnotatedCollectionI inputSeqs)
186 if (alignment == null || alignment.getWidth() <= 0 ||
187 alignment.getSequences() == null)
189 if (alignment.isNucleotide() && !operation.isNucleotideOperation())
191 if (!alignment.isNucleotide() && !operation.isProteinOperation())
193 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
194 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
195 inputSeqs = alignment;
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
207 // TODO: push attributes into WsJob instance (so they can be safely
208 // persisted/restored
209 for (SequenceI sq : inputSeqs.getSequences())
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);
217 sqlen = sq.getEnd() - sq.getStart();
220 String newName = SeqsetUtils.unique_name(seqs.size());
221 if (seqNames != null)
223 seqNames.put(newName, sq);
228 seq = new Sequence(newName, sq.getSequenceAsString());
230 if (gapMap == null || gapMap.length < seq.getLength())
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++)
237 gapMap[p] = false; // init as a gap
240 for (int apos : sq.gapMap())
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)
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));
258 // for annotation need to also record map to sequence start/end
260 // then transfer back to original sequence on return.
262 ln = Integer.max(seq.getLength(), ln);
265 if (operation.getNeedsAlignedSequences() && submitGaps)
268 for (int i = 0; i < gapMap.length; i++)
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++)
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++)
288 if (orig.length > pp)
290 padded[i++] = orig[pp];
298 seqs.set(p, new Sequence(sq.getName(), new String(padded)));
304 private boolean checkInputSequencesValid(List<SequenceI> sequences)
307 boolean allowProtein = operation.isProteinOperation(),
308 allowNucleotides = operation.isNucleotideOperation();
309 for (SequenceI sq : sequences)
311 if (sq.getStart() <= sq.getEnd() &&
312 (sq.isProtein() ? allowProtein : allowNucleotides))
317 return nvalid >= operation.getMinSequences();
321 public boolean poll() throws IOException
323 if (!job.getStatus().isDone() && !job.getStatus().isFailed())
325 Cache.log.debug(format("Polling job %s", job));
328 service.updateProgress(job);
329 exceptionCount = MAX_RETRY;
330 } catch (IOException e)
332 Cache.log.error(format("Polling job %s failed.", job), e);
333 if (--exceptionCount <= 0)
335 job.setStatus(WSJobStatus.SERVER_ERROR);
336 Cache.log.warn(format("Attempts limit exceeded. Dropping job %s.", job));
338 } catch (OutOfMemoryError e)
340 job.setStatus(WSJobStatus.BROKEN);
341 Cache.log.error(format("Out of memory when retrieving job %s", job), e);
344 return job.getStatus().isDone() || job.getStatus().isFailed();
353 } catch (IOException e)
355 Cache.log.error(format("Failed to cancel job %s.", job), e);
362 Cache.log.debug(format("Polling loop exited, job %s is %s", job, job.getStatus()));
363 if (!job.getStatus().isCompleted())
367 List<AlignmentAnnotation> outputAnnotations = null;
370 outputAnnotations = operation.annotationSupplier
371 .getResult(job, sequences, viewport);
372 } catch (IOException e)
374 Cache.log.error(format("Couldn't retrieve features for job %s.", job), e);
376 if (outputAnnotations != null)
377 Cache.log.debug(format("Obtained %d annotation rows.", outputAnnotations.size()));
379 Cache.log.debug("Obtained no annotations.");
380 Map<String, FeatureColourI> featureColours = new HashMap<>();
381 Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
382 FeaturesFile featuresFile;
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)
390 Alignment aln = new Alignment(sequences.toArray(new SequenceI[0]));
391 // I do nothing with the featureFilters object
392 featuresFile.parse(aln, featureColours, true);
394 } catch (IOException e)
396 Cache.log.error(format("Couldn't retrieve features for job %s", job), e);
398 Cache.log.debug(format("There are %d feature colours and %d filters.",
399 featureColours.size(), featureFilters.size()));
400 if (outputAnnotations != null)
402 for (AlignmentAnnotation aa : outputAnnotations)
404 if (aa.getCalcId() == null || aa.getCalcId().equals(""))
406 aa.setCalcId(service.getName());
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();
412 updateResultAnnotation(outputAnnotations);
413 if (transferSequenceFeatures)
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()
420 public FeatureColourI getFeatureColour(String type)
422 return featureColours.get(type);
426 public FeatureMatcherSetI getFeatureFilters(String type)
428 return featureFilters.get(type);
432 public boolean isFeatureDisplayed(String type)
434 return featureColours.containsKey(type);
437 if (frame.alignPanel == alignPanel)
439 viewport.setShowSequenceFeatures(true);
440 frame.setMenusForViewport();
444 Cache.log.debug("Annotation service task finished.");
447 // What is the purpose of this method?
448 // When is it called (apart from the above)?
449 private void updateResultAnnotation(List<AlignmentAnnotation> annotations)
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?
457 for (AlignmentAnnotation alna : currentAnnotations)
459 graphGroup = Integer.max(graphGroup, alna.graphGroup);
461 for (AlignmentAnnotation ala : annotations)
463 if (ala.graphGroup > 0)
465 ala.graphGroup += graphGroup;
468 // stores original sequence, in what case it ends up as null?
469 SequenceI aseq = null;
470 if (ala.sequenceRef != null)
472 SequenceI seq = seqNames.get(ala.sequenceRef.getName());
474 while (seq.getDatasetSequence() != null)
476 seq = seq.getDatasetSequence();
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++)
485 if (gapMap != null && gapMap.length > ap && !gapMap[ap])
487 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
489 else if (p < resAnnot.length)
491 gappedAnnot[ap] = resAnnot[p++];
494 // replacing sequence with the original one?
495 ala.sequenceRef = aseq;
496 ala.annotations = gappedAnnot;
497 AlignmentAnnotation newAnnot = viewport.getAlignment()
498 .updateFromOrCopyAnnotation(ala);
501 aseq.addAlignmentAnnotation(newAnnot);
502 newAnnot.adjustForAlignment();
503 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(newAnnot,
504 newAnnot.label, newAnnot.getCalcId());
506 newAnnots.add(newAnnot);
509 for (SequenceI sq : sequences)
511 // what are DBRefs? why are they relevant here?
512 if (!sq.getFeatures().hasFeatures() &&
513 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
517 transferSequenceFeatures = true;
518 SequenceI seq = seqNames.get(sq.getName());
520 ContiguousI seqRange = seq.findPositions(start, end);
522 while ((dseq = seq).getDatasetSequence() != null)
524 seq = seq.getDatasetSequence();
526 List<ContiguousI> sourceRange = new ArrayList<>();
527 if (gapMap != null && gapMap.length > end)
529 int lastcol = start, col = start;
532 if (col == end || !gapMap[col])
534 if (lastcol <= col - 1)
536 seqRange = seq.findPositions(lastcol, col);
537 sourceRange.add(seqRange);
541 } while (++col < end);
545 sourceRange.add(seq.findPositions(start, end));
548 int sourceStartEnd[] = new int[sourceRange.size() * 2];
549 for (ContiguousI range : sourceRange)
551 sourceStartEnd[i++] = range.getBegin();
552 sourceStartEnd[i++] = range.getEnd();
554 Mapping mp = new Mapping(new MapList(sourceStartEnd,
555 new int[] { seq.getStart(), seq.getEnd() }, 1, 1));
556 dseq.transferAnnotation(sq, mp);
558 updateOurAnnots(newAnnots);
561 protected void updateOurAnnots(List<AlignmentAnnotation> annots)
563 List<AlignmentAnnotation> our = ourAnnots;
564 ourAnnots = Collections.synchronizedList(annots);
565 AlignmentI alignment = viewport.getAlignment();
570 for (AlignmentAnnotation an : our)
572 if (!ourAnnots.contains(an))
574 // remove the old annotation
575 alignment.deleteAnnotation(an);
581 // validate rows and update Alignment state
582 synchronized (ourAnnots)
584 for (AlignmentAnnotation an : ourAnnots)
586 viewport.getAlignment().validateAnnotation(an);
589 // TODO: may need a menu refresh after this
590 // af.setMenusForViewport();
591 alignPanel.adjustAnnotationHeight();