1 package jalview.ws2.operations;
3 import java.util.Collections;
5 import java.util.Objects;
6 import java.io.IOException;
7 import java.util.ArrayList;
8 import java.util.HashMap;
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;
39 import static java.lang.String.format;
41 public class AnnotationWorker extends AbstractWorker
42 implements PollableAlignCalcWorkerI
44 AnnotationOperation operation;
46 private WSJobList<AnnotationJob> jobs = new WSJobList<>();
50 private List<ArgumentI> args = Collections.emptyList();
52 private AlignViewport viewport;
54 private AlignmentViewPanel alignPanel;
56 private IProgressIndicator progressIndicator;
58 private AlignFrame frame;
60 private AlignCalcManagerI2 calcMan;
62 protected List<AlignmentAnnotation> ourAnnots;
64 // TODO: convert to bitset
65 private boolean[] gapMap = new boolean[0];
67 private class JobInput
69 List<SequenceI> sequences;
71 Map<String, SequenceI> seqNames;
76 public class AnnotationJob extends WSJob
78 private List<SequenceI> sequences;
80 private int start, end;
82 private Map<String, SequenceI> seqNames;
84 private boolean transferSequenceFeatures = false;
86 private AnnotationJob(String serviceProvider, String serviceName,
89 super(serviceProvider, serviceName, hostName);
92 private void setInput(JobInput input)
94 this.sequences = input.sequences;
95 this.start = input.start;
97 this.seqNames = input.seqNames;
101 public AnnotationWorker(AnnotationOperation operation,
102 List<ArgumentI> args, AlignFrame frame,
103 IProgressIndicator progressIndicator)
105 this.operation = operation;
107 this.viewport = frame.getCurrentView();
108 this.alignPanel = frame.alignPanel;
109 this.progressIndicator = progressIndicator;
111 this.calcMan = viewport.getCalcManager();
115 public String getCalcName()
117 return operation.getName();
121 public Operation getOperation()
127 public WSJobList<? extends WSJob> getJobs()
133 public boolean involves(AlignmentAnnotation annot)
135 return ourAnnots != null && ourAnnots.contains(annot);
139 public void updateAnnotation()
141 updateResultAnnotation(ourAnnots);
145 public void removeAnnotation()
147 if (ourAnnots != null && viewport != null)
149 AlignmentI alignment = viewport.getAlignment();
150 synchronized (ourAnnots)
152 for (AlignmentAnnotation aa : ourAnnots)
154 alignment.deleteAnnotation(aa, true);
162 public boolean isDeletable()
168 public void startUp() throws IOException
170 if (viewport.isClosed())
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))
183 Cache.log.info("Sequences for analysis service were null");
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());
191 listeners.fireJobCreated(job);
193 // Should this part be moved out of this class to one of the gui
195 if (progressIndicator != null)
197 job.addPropertyChangeListener("status", new ProgressBarUpdater(progressIndicator));
198 progressIndicator.registerHandler(job.getUid(), new IProgressIndicatorHandler()
201 public boolean cancelActivity(long id)
203 calcMan.cancelWorker(AnnotationWorker.this);
208 public boolean canCancel()
210 return isDeletable();
214 String jobId = operation.getWebService().submit(input.sequences, args);
216 Cache.log.debug(format("Service %s: submitted job id %s",
217 operation.getHostName(), jobId));
218 listeners.fireWorkerStarted();
221 private JobInput prepareInput(AlignmentI alignment,
222 AnnotatedCollectionI inputSeqs)
224 if (alignment == null || alignment.getWidth() <= 0 ||
225 alignment.getSequences() == null)
227 if (alignment.isNucleotide() && !operation.isNucleotideOperation())
229 if (!alignment.isNucleotide() && !operation.isProteinOperation())
231 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
232 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
233 inputSeqs = alignment;
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
246 // TODO: push attributes into WsJob instance (so they can be safely
247 // persisted/restored
248 for (SequenceI sq : inputSeqs.getSequences())
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);
256 sqlen = sq.getEnd() - sq.getStart();
259 String newName = SeqsetUtils.unique_name(seqs.size());
260 if (seqNames != null)
262 seqNames.put(newName, sq);
267 seq = new Sequence(newName, sq.getSequenceAsString());
269 if (gapMap == null || gapMap.length < seq.getLength())
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++)
276 gapMap[p] = false; // init as a gap
279 for (int apos : sq.gapMap())
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)
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));
297 // for annotation need to also record map to sequence start/end
299 // then transfer back to original sequence on return.
301 ln = Integer.max(seq.getLength(), ln);
304 if (operation.getNeedsAlignedSequences() && submitGaps)
307 for (int i = 0; i < gapMap.length; i++)
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++)
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++)
327 if (orig.length > pp)
329 padded[i++] = orig[pp];
337 seqs.set(p, new Sequence(sq.getName(), new String(padded)));
340 var inp = new JobInput();
341 inp.sequences = seqs;
342 inp.seqNames = seqNames;
348 private boolean checkInputSequencesValid(List<SequenceI> sequences)
351 boolean allowProtein = operation.isProteinOperation(),
352 allowNucleotides = operation.isNucleotideOperation();
353 for (SequenceI sq : sequences)
355 if (sq.getStart() <= sq.getEnd() &&
356 (sq.isProtein() ? allowProtein : allowNucleotides))
361 return nvalid >= operation.getMinSequences();
369 operation.getWebService().cancel(job);
370 } catch (IOException e)
372 Cache.log.error(format("Failed to cancel job %s.", job), e);
379 Cache.log.debug(format("Polling loop exited, job %s is %s", job, job.getStatus()));
380 if (!job.getStatus().isCompleted())
384 var featureRenderer = alignPanel.cloneFeatureRenderer();
385 Map<String, FeatureColourI> featureColours = new HashMap<>();
386 Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
387 List<AlignmentAnnotation> returnedAnnot = null;
390 returnedAnnot = operation.annotationSupplier.attachAnnotations(
391 job, job.sequences, featureColours, featureFilters);
392 } catch (Exception e)
394 if (!operation.getWebService().handleCollectionError(job, e))
396 Cache.log.error("Couldn't get annotations for job.", e);
397 job.setStatus(WSJobStatus.SERVER_ERROR);
398 listeners.firePollException(job, e);
402 Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
403 : ("" + returnedAnnot.size())));
405 String.format("There were %s feature colours and %s filters defined",
406 featureColours.size(), featureFilters.size()));
407 if (returnedAnnot != null)
409 for (AlignmentAnnotation aa : returnedAnnot)
411 // assume that any CalcIds already set
412 if (aa.getCalcId() == null || aa.getCalcId().equals(""))
414 aa.setCalcId(operation.getName());
416 // autocalculated annotation are created by interactive alignment
418 aa.autoCalculated = operation.isAlignmentAnalysis()
419 && operation.isInteractive();
422 updateResultAnnotation(returnedAnnot);
423 if (job.transferSequenceFeatures)
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()
430 public FeatureColourI getFeatureColour(String type)
432 return featureColours.get(type);
436 public FeatureMatcherSetI getFeatureFilters(String type)
438 return featureFilters.get(type);
442 public boolean isFeatureDisplayed(String type)
444 return featureColours.containsKey(type);
447 if (frame.alignPanel == alignPanel)
449 viewport.setShowSequenceFeatures(true);
450 frame.setMenusForViewport();
453 Cache.log.debug("Annotation service task finished.");
456 // What is the purpose of this method?
457 // When is it called (apart from the above)?
458 private void updateResultAnnotation(List<AlignmentAnnotation> annotations)
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?
466 for (AlignmentAnnotation alna : currentAnnotations)
468 graphGroup = Integer.max(graphGroup, alna.graphGroup);
470 for (AlignmentAnnotation ala : annotations)
472 if (ala.graphGroup > 0)
474 ala.graphGroup += graphGroup;
477 // stores original sequence, in what case it ends up as null?
478 SequenceI aseq = null;
479 if (ala.sequenceRef != null)
481 SequenceI seq = job.seqNames.get(ala.sequenceRef.getName());
483 while (seq.getDatasetSequence() != null)
485 seq = seq.getDatasetSequence();
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++)
494 if (gapMap != null && gapMap.length > ap && !gapMap[ap])
496 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
498 else if (p < resAnnot.length)
500 gappedAnnot[ap] = resAnnot[p++];
503 // replacing sequence with the original one?
504 ala.sequenceRef = aseq;
505 ala.annotations = gappedAnnot;
506 AlignmentAnnotation newAnnot = viewport.getAlignment()
507 .updateFromOrCopyAnnotation(ala);
510 aseq.addAlignmentAnnotation(newAnnot);
511 newAnnot.adjustForAlignment();
512 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(newAnnot,
513 newAnnot.label, newAnnot.getCalcId());
515 newAnnots.add(newAnnot);
518 for (SequenceI sq : job.sequences)
520 // what are DBRefs? why are they relevant here?
521 if (!sq.getFeatures().hasFeatures() &&
522 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
526 job.transferSequenceFeatures = true;
527 SequenceI seq = job.seqNames.get(sq.getName());
529 ContiguousI seqRange = seq.findPositions(job.start, job.end);
531 while ((dseq = seq).getDatasetSequence() != null)
533 seq = seq.getDatasetSequence();
535 List<ContiguousI> sourceRange = new ArrayList<>();
536 if (gapMap != null && gapMap.length > job.end)
538 int lastcol = job.start, col = job.start;
541 if (col == job.end || !gapMap[col])
543 if (lastcol <= col - 1)
545 seqRange = seq.findPositions(lastcol, col);
546 sourceRange.add(seqRange);
550 } while (++col < job.end);
554 sourceRange.add(seq.findPositions(job.start, job.end));
557 int sourceStartEnd[] = new int[sourceRange.size() * 2];
558 for (ContiguousI range : sourceRange)
560 sourceStartEnd[i++] = range.getBegin();
561 sourceStartEnd[i++] = range.getEnd();
563 Mapping mp = new Mapping(new MapList(sourceStartEnd,
564 new int[] { seq.getStart(), seq.getEnd() }, 1, 1));
565 dseq.transferAnnotation(sq, mp);
567 updateOurAnnots(newAnnots);
570 protected void updateOurAnnots(List<AlignmentAnnotation> annots)
572 List<AlignmentAnnotation> our = ourAnnots;
573 ourAnnots = Collections.synchronizedList(annots);
574 AlignmentI alignment = viewport.getAlignment();
579 for (AlignmentAnnotation an : our)
581 if (!ourAnnots.contains(an))
583 // remove the old annotation
584 alignment.deleteAnnotation(an);
590 // validate rows and update Alignment state
591 synchronized (ourAnnots)
593 for (AlignmentAnnotation an : ourAnnots)
595 viewport.getAlignment().validateAnnotation(an);
598 // TODO: may need a menu refresh after this
599 // af.setMenusForViewport();
600 alignPanel.adjustAnnotationHeight();