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 calcMan.disableWorker(this);
213 super.abortAndDestroy();
221 if (job.isInputValid() && !job.isCompleted())
223 /* if done was called but job is not completed then it
224 * must have been stopped by an exception */
225 job.setStatus(JobStatus.SERVER_ERROR);
228 updateGlobalStatus();
229 // dispose of unfinished jobs just in case
234 public String toString()
236 return AnnotationTask.this.toString() + "$AlignCalcWorker@"
237 + Integer.toHexString(hashCode());
241 public AnnotationTask(AnnotationWebServiceClientI client,
242 AnnotationAction action, List<ArgumentI> args, Credentials credentials,
243 AlignViewportI viewport,
244 TaskEventListener<AnnotationResult> eventListener)
246 this.client = client;
247 this.action = action;
249 this.credentials = credentials;
250 this.viewport = viewport;
251 this.eventHandler = new TaskEventSupport<>(this, eventListener);
252 this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
261 public void start(AlignCalcManagerI2 calcManager)
263 if (this.worker != null)
264 throw new IllegalStateException("task already started");
265 this.worker = new AlignCalcWorkerAdapter(calcManager);
266 if (taskStatus != JobStatus.CANCELLED)
268 List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
269 AlignCalcWorkerAdapter.class);
270 for (var worker : oldWorkers)
272 if (action.getWebService().getName().equalsIgnoreCase(
273 ((AlignCalcWorkerAdapter) worker).getServiceName()))
275 // remove interactive workers for the same service.
276 calcManager.removeWorker(worker);
277 calcManager.cancelWorker(worker);
280 if (action.getWebService().isInteractive())
281 calcManager.registerWorker(worker);
283 calcManager.startWorker(worker);
288 * The following methods are mostly copied from the {@link AbstractPollableTask}
289 * TODO: move common functionality to a base class
292 public JobStatus getStatus()
297 private void setStatus(JobStatus status)
299 if (this.taskStatus != status)
301 Cache.log.debug(String.format("%s status change to %s", this, status.name()));
302 this.taskStatus = status;
303 eventHandler.fireTaskStatusChanged(status);
307 private void updateGlobalStatus()
310 for (BaseJob job : jobs)
312 JobStatus status = job.getStatus();
313 int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
314 if (precedence < jobPrecedence)
315 precedence = jobPrecedence;
319 setStatus(JobStatus.statusPrecedence[precedence]);
324 public List<? extends JobI> getSubJobs()
330 * Create and return a list of annotation jobs from the current state of the
331 * viewport. Returned job are not started by this method and should be stored
332 * in a field and started separately.
334 * @return list of annotation jobs
335 * @throws ServiceInputInvalidException
336 * input data is not valid
338 private List<AnnotationJob> prepare() throws ServiceInputInvalidException
340 AlignmentI alignment = viewport.getAlignment();
341 if (alignment == null || alignment.getWidth() <= 0 ||
342 alignment.getSequences() == null)
343 throw new ServiceInputInvalidException("Alignment does not contain sequences");
344 if (alignment.isNucleotide() && !action.doAllowNucleotide())
345 throw new ServiceInputInvalidException(
346 action.getFullName() + " does not allow nucleotide sequences");
347 if (!alignment.isNucleotide() && !action.doAllowProtein())
348 throw new ServiceInputInvalidException(
349 action.getFullName() + " does not allow protein sequences");
350 boolean bySequence = !action.isAlignmentAnalysis();
351 AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
352 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
353 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
354 inputSeqs = alignment;
355 boolean submitGaps = action.isAlignmentAnalysis();
356 boolean requireAligned = action.getRequireAlignedSequences();
357 boolean filterSymbols = action.getFilterSymbols();
358 int minSize = action.getMinSequences();
359 AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
360 submitGaps, requireAligned, filterSymbols, minSize);
361 if (!job.isInputValid())
363 job.setStatus(JobStatus.INVALID);
364 throw new ServiceInputInvalidException("Annotation job has invalid input");
366 job.setStatus(JobStatus.READY);
370 private void startJobs() throws IOException
372 for (BaseJob job : jobs)
374 if (job.isInputValid() && job.getStatus() == JobStatus.READY)
376 var serverJob = client.submit(job.getInputSequences(),
378 job.setServerJob(serverJob);
379 job.setStatus(JobStatus.SUBMITTED);
384 private boolean poll() throws IOException
386 boolean allDone = true;
387 for (BaseJob job : jobs)
389 if (job.isInputValid() && !job.getStatus().isDone())
391 WebServiceJobHandle serverJob = job.getServerJob();
392 job.setStatus(client.getStatus(serverJob));
393 job.setLog(client.getLog(serverJob));
394 job.setErrorLog(client.getErrorLog(serverJob));
396 allDone &= job.isCompleted();
401 private AnnotationResult retrieveResult() throws IOException
403 final Map<String, FeatureColourI> featureColours = new HashMap<>();
404 final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
405 var job = jobs.get(0);
406 List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
407 job.getServerJob(), job.getInputSequences(), featureColours,
410 * copy over each annotation row returned and also defined on each
411 * sequence, excluding regions not annotated due to gapMap/column
414 // update calcId if it is not already set on returned annotation
415 for (AlignmentAnnotation annot : returnedAnnot)
417 if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
419 annot.setCalcId(action.getFullName());
421 annot.autoCalculated = action.isAlignmentAnalysis() &&
422 action.getWebService().isInteractive();
424 job.returnedAnnotations = returnedAnnot;
425 job.featureColours = featureColours;
426 job.featureFilters = featureFilters;
427 var ret = updateResultAnnotation(job, returnedAnnot);
428 var annotations = ret.get0();
429 var transferFeatures = ret.get1();
430 return new AnnotationResult(annotations, transferFeatures, featureColours,
434 private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
435 AnnotationJob job, List<AlignmentAnnotation> annotations)
437 List<AlignmentAnnotation> newAnnots = new ArrayList<>();
438 // update graphGroup for all annotation
439 /* find a graphGroup greater than any existing one, could be moved
440 * to Alignment#getNewGraphGroup() - returns next unused graph group */
442 if (viewport.getAlignment().getAlignmentAnnotation() != null)
444 for (var ala : viewport.getAlignment().getAlignmentAnnotation())
446 graphGroup = Math.max(graphGroup, ala.graphGroup);
449 // update graphGroup in the annotation rows returned form service'
450 /* TODO: look at sequence annotation rows and update graph groups in the
451 * case of reference annotation */
452 for (AlignmentAnnotation ala : annotations)
454 if (ala.graphGroup > 0)
455 ala.graphGroup += graphGroup;
456 SequenceI aseq = null;
457 // transfer sequence refs and adjust gapMap
458 if (ala.sequenceRef != null)
460 aseq = job.seqNames.get(ala.sequenceRef.getName());
462 ala.sequenceRef = aseq;
464 Annotation[] resAnnot = ala.annotations;
465 boolean[] gapMap = job.gapMap;
466 Annotation[] gappedAnnot = new Annotation[Math.max(
467 viewport.getAlignment().getWidth(), gapMap.length)];
468 for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
470 if (gapMap.length > ap && !gapMap[ap])
471 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
472 else if (p < resAnnot.length)
473 gappedAnnot[ap] = resAnnot[p++];
474 // is this loop exhaustive of resAnnot?
476 ala.annotations = gappedAnnot;
478 AlignmentAnnotation newAnnot = viewport.getAlignment()
479 .updateFromOrCopyAnnotation(ala);
482 aseq.addAlignmentAnnotation(newAnnot);
483 newAnnot.adjustForAlignment();
484 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
485 newAnnot, newAnnot.label, newAnnot.getCalcId());
487 newAnnots.add(newAnnot);
490 boolean transferFeatures = false;
491 for (SequenceI sq : job.getInputSequences())
493 if (!sq.getFeatures().hasFeatures() &&
494 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
496 transferFeatures = true;
497 SequenceI seq = job.seqNames.get(sq.getName());
499 int start = job.start, end = job.end;
500 boolean[] gapMap = job.gapMap;
501 ContiguousI seqRange = seq.findPositions(start, end);
502 while ((dseq = seq).getDatasetSequence() != null)
504 seq = seq.getDatasetSequence();
506 List<ContiguousI> sourceRange = new ArrayList<>();
507 if (gapMap.length >= end)
509 int lastcol = start, col = start;
512 if (col == end || !gapMap[col])
514 if (lastcol <= (col - 1))
516 seqRange = seq.findPositions(lastcol, col);
517 sourceRange.add(seqRange);
521 } while (col++ < end);
525 sourceRange.add(seq.findPositions(start, end));
529 int sourceStartEnd[] = new int[sourceRange.size() * 2];
530 for (ContiguousI range : sourceRange)
532 sourceStartEnd[i++] = range.getBegin();
533 sourceStartEnd[i++] = range.getEnd();
535 Mapping mp = new Mapping(new MapList(
536 sourceStartEnd, new int[]
537 { seq.getStart(), seq.getEnd() }, 1, 1));
538 dseq.transferAnnotation(sq, mp);
541 return new Pair<>(newAnnots, transferFeatures);
545 public AnnotationResult getResult()
553 setStatus(JobStatus.CANCELLED);
561 public void cancelJobs()
563 for (BaseJob job : jobs)
565 if (!job.isCompleted())
569 if (job.getServerJob() != null)
571 client.cancel(job.getServerJob());
573 job.setStatus(JobStatus.CANCELLED);
574 } catch (IOException e)
576 Cache.log.error(String.format(
577 "failed to cancel job %s", job.getServerJob()), e);
584 public String toString()
586 var status = taskStatus != null ? taskStatus.name() : "UNSET";
587 return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);