1 package jalview.ws2.actions.annotation;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Collections;
6 import java.util.HashMap;
10 import jalview.analysis.AlignmentAnnotationUtils;
11 import jalview.api.AlignCalcManagerI2;
12 import jalview.api.AlignCalcWorkerI;
13 import jalview.api.AlignViewportI;
14 import jalview.api.FeatureColourI;
15 import jalview.api.PollableAlignCalcWorkerI;
16 import jalview.bin.Cache;
17 import jalview.datamodel.AlignmentAnnotation;
18 import jalview.datamodel.AlignmentI;
19 import jalview.datamodel.AnnotatedCollectionI;
20 import jalview.datamodel.Annotation;
21 import jalview.datamodel.ContiguousI;
22 import jalview.datamodel.Mapping;
23 import jalview.datamodel.SequenceI;
24 import jalview.datamodel.features.FeatureMatcherSetI;
25 import jalview.schemes.FeatureSettingsAdapter;
26 import jalview.util.ArrayUtils;
27 import jalview.util.MapList;
28 import jalview.util.MathUtils;
29 import jalview.util.Pair;
30 import jalview.workers.AlignCalcWorker;
31 import jalview.ws.params.ArgumentI;
32 import jalview.ws2.actions.AbstractPollableTask;
33 import jalview.ws2.actions.BaseJob;
34 import jalview.ws2.actions.ServiceInputInvalidException;
35 import jalview.ws2.actions.api.JobI;
36 import jalview.ws2.actions.api.TaskEventListener;
37 import jalview.ws2.actions.api.TaskI;
38 import jalview.ws2.api.Credentials;
39 import jalview.ws2.api.JobStatus;
40 import jalview.ws2.api.WebServiceJobHandle;
41 import jalview.ws2.client.api.AnnotationWebServiceClientI;
42 import jalview.ws2.helpers.DelegateJobEventListener;
43 import jalview.ws2.helpers.TaskEventSupport;
45 import static java.util.Objects.requireNonNullElse;
47 public class AnnotationTask implements TaskI<AnnotationResult>
49 private final long uid = MathUtils.getUID();
51 private AnnotationWebServiceClientI client;
53 private final AnnotationAction action;
55 private final List<ArgumentI> args;
57 private final Credentials credentials;
59 private final AlignViewportI viewport;
61 private final TaskEventSupport<AnnotationResult> eventHandler;
63 private JobStatus taskStatus = null;
65 private AlignCalcWorkerAdapter worker = null;
67 private List<AnnotationJob> jobs = Collections.emptyList();
69 private AnnotationResult result = null;
71 private DelegateJobEventListener<AnnotationResult> jobEventHandler;
73 private class AlignCalcWorkerAdapter extends AlignCalcWorker
74 implements PollableAlignCalcWorkerI
76 private boolean restarting = false;
78 AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan)
80 super(viewport, null);
81 this.calcMan = calcMan;
84 String getServiceName()
86 return action.getWebService().getName();
90 public void startUp() throws Throwable
92 if (alignViewport.isClosed())
95 throw new IllegalStateException("Starting annotation for closed viewport");
98 eventHandler.fireTaskRestarted();
101 jobs = Collections.emptyList();
105 } catch (ServiceInputInvalidException e)
107 setStatus(JobStatus.INVALID);
108 eventHandler.fireTaskException(e);
111 setStatus(JobStatus.READY);
112 eventHandler.fireTaskStarted(jobs);
115 job.addPropertyChagneListener(jobEventHandler);
120 } catch (IOException e)
122 eventHandler.fireTaskException(e);
124 setStatus(JobStatus.SERVER_ERROR);
127 setStatus(JobStatus.SUBMITTED);
131 public boolean poll() throws Throwable
133 boolean done = AnnotationTask.this.poll();
134 updateGlobalStatus();
137 retrieveAndProcessResult();
138 eventHandler.fireTaskCompleted(result);
143 private void retrieveAndProcessResult() throws IOException
145 result = retrieveResult();
146 updateOurAnnots(result.annotations);
147 if (result.transferFeatures)
149 final var featureColours = result.featureColours;
150 final var featureFilters = result.featureFilters;
151 viewport.applyFeaturesStyle(new FeatureSettingsAdapter()
154 public FeatureColourI getFeatureColour(String type)
156 return featureColours.get(type);
160 public FeatureMatcherSetI getFeatureFilters(String type)
162 return featureFilters.get(type);
166 public boolean isFeatureDisplayed(String type)
168 return featureColours.containsKey(type);
175 public void updateAnnotation()
177 var job = jobs.size() > 0 ? jobs.get(0) : null;
178 if (!calcMan.isWorking(this) && job != null)
180 var ret = updateResultAnnotation(job, job.returnedAnnotations);
181 updateOurAnnots(ret.get0());
185 private void updateOurAnnots(List<AlignmentAnnotation> newAnnots)
187 List<AlignmentAnnotation> oldAnnots = requireNonNullElse(ourAnnots, Collections.emptyList());
188 ourAnnots = newAnnots;
189 AlignmentI alignment = viewport.getAlignment();
190 for (AlignmentAnnotation an : oldAnnots)
192 if (!newAnnots.contains(an))
194 alignment.deleteAnnotation(an);
198 for (AlignmentAnnotation an : ourAnnots)
200 viewport.getAlignment().validateAnnotation(an);
212 super.abortAndDestroy();
220 if (job.isInputValid() && !job.isCompleted())
222 /* if done was called but job is not completed then it
223 * must have been stopped by an exception */
224 job.setStatus(JobStatus.SERVER_ERROR);
227 updateGlobalStatus();
228 // dispose of unfinished jobs just in case
233 public AnnotationTask(AnnotationWebServiceClientI client,
234 AnnotationAction action, List<ArgumentI> args, Credentials credentials,
235 AlignViewportI viewport,
236 TaskEventListener<AnnotationResult> eventListener)
238 this.client = client;
239 this.action = action;
241 this.credentials = credentials;
242 this.viewport = viewport;
243 this.eventHandler = new TaskEventSupport<>(this, eventListener);
244 this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
253 public void start(AlignCalcManagerI2 calcManager)
255 if (this.worker != null)
256 throw new IllegalStateException("task already started");
257 this.worker = new AlignCalcWorkerAdapter(calcManager);
258 if (taskStatus != JobStatus.CANCELLED)
260 List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
261 AlignCalcWorkerAdapter.class);
262 for (var worker : oldWorkers)
264 if (action.getWebService().getName().equalsIgnoreCase(
265 ((AlignCalcWorkerAdapter) worker).getServiceName()))
267 // remove interactive workers for the same service.
268 calcManager.removeWorker(worker);
269 calcManager.cancelWorker(worker);
272 if (action.getWebService().isInteractive())
273 calcManager.registerWorker(worker);
275 calcManager.startWorker(worker);
280 * The following methods are mostly copied from the {@link AbstractPollableTask}
281 * TODO: move common functionality to a base class
284 public JobStatus getStatus()
289 private void setStatus(JobStatus status)
291 if (this.taskStatus != status)
293 Cache.log.debug(String.format("%s status change to %s", this, status.name()));
294 this.taskStatus = status;
295 eventHandler.fireTaskStatusChanged(status);
299 private void updateGlobalStatus()
302 for (BaseJob job : jobs)
304 JobStatus status = job.getStatus();
305 int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
306 if (precedence < jobPrecedence)
307 precedence = jobPrecedence;
311 setStatus(JobStatus.statusPrecedence[precedence]);
316 public List<? extends JobI> getSubJobs()
322 * Create and return a list of annotation jobs from the current state of the
323 * viewport. Returned job are not started by this method and should be stored
324 * in a field and started separately.
326 * @return list of annotation jobs
327 * @throws ServiceInputInvalidException
328 * input data is not valid
330 private List<AnnotationJob> prepare() throws ServiceInputInvalidException
332 AlignmentI alignment = viewport.getAlignment();
333 if (alignment == null || alignment.getWidth() <= 0 ||
334 alignment.getSequences() == null)
335 throw new ServiceInputInvalidException("Alignment does not contain sequences");
336 if (alignment.isNucleotide() && !action.doAllowNucleotide())
337 throw new ServiceInputInvalidException(
338 action.getFullName() + " does not allow nucleotide sequences");
339 if (!alignment.isNucleotide() && !action.doAllowProtein())
340 throw new ServiceInputInvalidException(
341 action.getFullName() + " does not allow protein sequences");
342 boolean bySequence = !action.isAlignmentAnalysis();
343 AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
344 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
345 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
346 inputSeqs = alignment;
347 boolean submitGaps = action.isAlignmentAnalysis();
348 boolean requireAligned = action.getRequireAlignedSequences();
349 boolean filterSymbols = action.getFilterSymbols();
350 int minSize = action.getMinSequences();
351 AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
352 submitGaps, requireAligned, filterSymbols, minSize);
353 if (!job.isInputValid())
355 job.setStatus(JobStatus.INVALID);
356 throw new ServiceInputInvalidException("Annotation job has invalid input");
358 job.setStatus(JobStatus.READY);
362 private void startJobs() throws IOException
364 for (BaseJob job : jobs)
366 if (job.isInputValid() && job.getStatus() == JobStatus.READY)
368 var serverJob = client.submit(job.getInputSequences(),
370 job.setServerJob(serverJob);
371 job.setStatus(JobStatus.SUBMITTED);
376 private boolean poll() throws IOException
378 boolean allDone = true;
379 for (BaseJob job : jobs)
381 if (job.isInputValid() && !job.getStatus().isDone())
383 WebServiceJobHandle serverJob = job.getServerJob();
384 job.setStatus(client.getStatus(serverJob));
385 job.setLog(client.getLog(serverJob));
386 job.setErrorLog(client.getErrorLog(serverJob));
388 allDone &= job.isCompleted();
393 private AnnotationResult retrieveResult() throws IOException
395 final Map<String, FeatureColourI> featureColours = new HashMap<>();
396 final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
397 var job = jobs.get(0);
398 List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
399 job.getServerJob(), job.getInputSequences(), featureColours,
402 * copy over each annotation row returned and also defined on each
403 * sequence, excluding regions not annotated due to gapMap/column
406 // update calcId if it is not already set on returned annotation
407 for (AlignmentAnnotation annot : returnedAnnot)
409 if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
411 annot.setCalcId(action.getFullName());
413 annot.autoCalculated = action.isAlignmentAnalysis() &&
414 action.getWebService().isInteractive();
416 job.returnedAnnotations = returnedAnnot;
417 job.featureColours = featureColours;
418 job.featureFilters = featureFilters;
419 var ret = updateResultAnnotation(job, returnedAnnot);
420 var annotations = ret.get0();
421 var transferFeatures = ret.get1();
422 return new AnnotationResult(annotations, transferFeatures, featureColours,
426 private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
427 AnnotationJob job, List<AlignmentAnnotation> annotations)
429 List<AlignmentAnnotation> newAnnots = new ArrayList<>();
430 // update graphGroup for all annotation
431 /* find a graphGroup greater than any existing one, could be moved
432 * to Alignment#getNewGraphGroup() - returns next unused graph group */
434 if (viewport.getAlignment().getAlignmentAnnotation() != null)
436 for (var ala : viewport.getAlignment().getAlignmentAnnotation())
438 graphGroup = Math.max(graphGroup, ala.graphGroup);
441 // update graphGroup in the annotation rows returned form service'
442 /* TODO: look at sequence annotation rows and update graph groups in the
443 * case of reference annotation */
444 for (AlignmentAnnotation ala : annotations)
446 if (ala.graphGroup > 0)
447 ala.graphGroup += graphGroup;
448 SequenceI aseq = null;
449 // transfer sequence refs and adjust gapMap
450 if (ala.sequenceRef != null)
452 aseq = job.seqNames.get(ala.sequenceRef.getName());
454 ala.sequenceRef = aseq;
456 Annotation[] resAnnot = ala.annotations;
457 boolean[] gapMap = job.gapMap;
458 Annotation[] gappedAnnot = new Annotation[Math.max(
459 viewport.getAlignment().getWidth(), gapMap.length)];
460 for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
462 if (gapMap.length > ap && !gapMap[ap])
463 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
464 else if (p < resAnnot.length)
465 gappedAnnot[ap] = resAnnot[p++];
466 // is this loop exhaustive of resAnnot?
468 ala.annotations = gappedAnnot;
470 AlignmentAnnotation newAnnot = viewport.getAlignment()
471 .updateFromOrCopyAnnotation(ala);
474 aseq.addAlignmentAnnotation(newAnnot);
475 newAnnot.adjustForAlignment();
476 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
477 newAnnot, newAnnot.label, newAnnot.getCalcId());
479 newAnnots.add(newAnnot);
482 boolean transferFeatures = false;
483 for (SequenceI sq : job.getInputSequences())
485 if (!sq.getFeatures().hasFeatures() &&
486 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
488 transferFeatures = true;
489 SequenceI seq = job.seqNames.get(sq.getName());
491 int start = job.start, end = job.end;
492 boolean[] gapMap = job.gapMap;
493 ContiguousI seqRange = seq.findPositions(start, end);
494 while ((dseq = seq).getDatasetSequence() != null)
496 seq = seq.getDatasetSequence();
498 List<ContiguousI> sourceRange = new ArrayList<>();
499 if (gapMap.length >= end)
501 int lastcol = start, col = start;
504 if (col == end || !gapMap[col])
506 if (lastcol <= (col - 1))
508 seqRange = seq.findPositions(lastcol, col);
509 sourceRange.add(seqRange);
513 } while (col++ < end);
517 sourceRange.add(seq.findPositions(start, end));
521 int sourceStartEnd[] = new int[sourceRange.size() * 2];
522 for (ContiguousI range : sourceRange)
524 sourceStartEnd[i++] = range.getBegin();
525 sourceStartEnd[i++] = range.getEnd();
527 Mapping mp = new Mapping(new MapList(
528 sourceStartEnd, new int[]
529 { seq.getStart(), seq.getEnd() }, 1, 1));
530 dseq.transferAnnotation(sq, mp);
533 return new Pair<>(newAnnots, transferFeatures);
537 public AnnotationResult getResult()
545 setStatus(JobStatus.CANCELLED);
553 public void cancelJobs()
555 for (BaseJob job : jobs)
557 if (!job.isCompleted())
561 if (job.getServerJob() != null)
563 client.cancel(job.getServerJob());
565 job.setStatus(JobStatus.CANCELLED);
566 } catch (IOException e)
568 Cache.log.error(String.format(
569 "failed to cancel job %s", job.getServerJob()), e);
576 public String toString()
578 var status = taskStatus != null ? taskStatus.name() : "UNSET";
579 return String.format("AnnotationTask(%d, %s)", uid, status);