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.bin.Console;
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.SequenceI;
25 import jalview.datamodel.features.FeatureMatcherSetI;
26 import jalview.schemes.FeatureSettingsAdapter;
27 import jalview.util.ArrayUtils;
28 import jalview.util.MapList;
29 import jalview.util.MathUtils;
30 import jalview.util.Pair;
31 import jalview.workers.AlignCalcWorker;
32 import jalview.ws.params.ArgumentI;
33 import jalview.ws2.actions.AbstractPollableTask;
34 import jalview.ws2.actions.BaseJob;
35 import jalview.ws2.actions.ServiceInputInvalidException;
36 import jalview.ws2.actions.api.JobI;
37 import jalview.ws2.actions.api.TaskEventListener;
38 import jalview.ws2.actions.api.TaskI;
39 import jalview.ws2.api.Credentials;
40 import jalview.ws2.api.JobStatus;
41 import jalview.ws2.api.WebServiceJobHandle;
42 import jalview.ws2.client.api.AnnotationWebServiceClientI;
43 import jalview.ws2.helpers.DelegateJobEventListener;
44 import jalview.ws2.helpers.TaskEventSupport;
46 public class AnnotationTask implements TaskI<AnnotationResult>
48 private final long uid = MathUtils.getUID();
50 private AnnotationWebServiceClientI client;
52 private final AnnotationAction action;
54 private final List<ArgumentI> args;
56 private final Credentials credentials;
58 private final AlignViewportI viewport;
60 private final TaskEventSupport<AnnotationResult> eventHandler;
62 private JobStatus taskStatus = null;
64 private AlignCalcWorkerAdapter worker = null;
66 private List<AnnotationJob> jobs = Collections.emptyList();
68 private AnnotationResult result = null;
70 private DelegateJobEventListener<AnnotationResult> jobEventHandler;
72 private class AlignCalcWorkerAdapter extends AlignCalcWorker
73 implements PollableAlignCalcWorkerI
75 private boolean restarting = false;
77 AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan)
79 super(viewport, null);
80 this.calcMan = calcMan;
83 String getServiceName()
85 return action.getWebService().getName();
89 public void startUp() throws Throwable
91 if (alignViewport.isClosed())
94 throw new IllegalStateException("Starting annotation for closed viewport");
97 eventHandler.fireTaskRestarted();
100 jobs = Collections.emptyList();
104 } catch (ServiceInputInvalidException e)
106 setStatus(JobStatus.INVALID);
107 eventHandler.fireTaskException(e);
110 setStatus(JobStatus.READY);
111 eventHandler.fireTaskStarted(jobs);
114 job.addPropertyChagneListener(jobEventHandler);
119 } catch (IOException e)
121 eventHandler.fireTaskException(e);
123 setStatus(JobStatus.SERVER_ERROR);
126 setStatus(JobStatus.SUBMITTED);
130 public boolean poll() throws Throwable
132 boolean done = AnnotationTask.this.poll();
133 updateGlobalStatus();
136 retrieveAndProcessResult();
137 eventHandler.fireTaskCompleted(result);
142 private void retrieveAndProcessResult() throws IOException
144 result = retrieveResult();
145 updateOurAnnots(result.annotations);
146 if (result.transferFeatures)
148 final var featureColours = result.featureColours;
149 final var featureFilters = result.featureFilters;
150 viewport.applyFeaturesStyle(new FeatureSettingsAdapter()
153 public FeatureColourI getFeatureColour(String type)
155 return featureColours.get(type);
159 public FeatureMatcherSetI getFeatureFilters(String type)
161 return featureFilters.get(type);
165 public boolean isFeatureDisplayed(String type)
167 return featureColours.containsKey(type);
174 public void updateAnnotation()
176 var job = jobs.size() > 0 ? jobs.get(0) : null;
177 if (!calcMan.isWorking(this) && job != null)
179 var ret = updateResultAnnotation(job, job.returnedAnnotations);
180 updateOurAnnots(ret.get0());
184 private void updateOurAnnots(List<AlignmentAnnotation> newAnnots)
186 List<AlignmentAnnotation> oldAnnots = ourAnnots != null ? ourAnnots : Collections.emptyList();
187 ourAnnots = newAnnots;
188 AlignmentI alignment = viewport.getAlignment();
189 for (AlignmentAnnotation an : oldAnnots)
191 if (!newAnnots.contains(an))
193 alignment.deleteAnnotation(an);
197 for (AlignmentAnnotation an : ourAnnots)
199 viewport.getAlignment().validateAnnotation(an);
211 calcMan.disableWorker(this);
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 String toString()
235 return AnnotationTask.this.toString() + "$AlignCalcWorker@"
236 + Integer.toHexString(hashCode());
240 public AnnotationTask(AnnotationWebServiceClientI client,
241 AnnotationAction action, List<ArgumentI> args, Credentials credentials,
242 AlignViewportI viewport,
243 TaskEventListener<AnnotationResult> eventListener)
245 this.client = client;
246 this.action = action;
248 this.credentials = credentials;
249 this.viewport = viewport;
250 this.eventHandler = new TaskEventSupport<>(this, eventListener);
251 this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
260 public void start(AlignCalcManagerI2 calcManager)
262 if (this.worker != null)
263 throw new IllegalStateException("task already started");
264 this.worker = new AlignCalcWorkerAdapter(calcManager);
265 if (taskStatus != JobStatus.CANCELLED)
267 List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
268 AlignCalcWorkerAdapter.class);
269 for (var worker : oldWorkers)
271 if (action.getWebService().getName().equalsIgnoreCase(
272 ((AlignCalcWorkerAdapter) worker).getServiceName()))
274 // remove interactive workers for the same service.
275 calcManager.removeWorker(worker);
276 calcManager.cancelWorker(worker);
279 if (action.getWebService().isInteractive())
280 calcManager.registerWorker(worker);
282 calcManager.startWorker(worker);
287 * The following methods are mostly copied from the {@link AbstractPollableTask}
288 * TODO: move common functionality to a base class
291 public JobStatus getStatus()
296 private void setStatus(JobStatus status)
298 if (this.taskStatus != status)
300 Console.debug(String.format("%s status change to %s", this, status.name()));
301 this.taskStatus = status;
302 eventHandler.fireTaskStatusChanged(status);
306 private void updateGlobalStatus()
309 for (BaseJob job : jobs)
311 JobStatus status = job.getStatus();
312 int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
313 if (precedence < jobPrecedence)
314 precedence = jobPrecedence;
318 setStatus(JobStatus.statusPrecedence[precedence]);
323 public List<? extends JobI> getSubJobs()
329 * Create and return a list of annotation jobs from the current state of the
330 * viewport. Returned job are not started by this method and should be stored
331 * in a field and started separately.
333 * @return list of annotation jobs
334 * @throws ServiceInputInvalidException
335 * input data is not valid
337 private List<AnnotationJob> prepare() throws ServiceInputInvalidException
339 AlignmentI alignment = viewport.getAlignment();
340 if (alignment == null || alignment.getWidth() <= 0 ||
341 alignment.getSequences() == null)
342 throw new ServiceInputInvalidException("Alignment does not contain sequences");
343 if (alignment.isNucleotide() && !action.doAllowNucleotide())
344 throw new ServiceInputInvalidException(
345 action.getFullName() + " does not allow nucleotide sequences");
346 if (!alignment.isNucleotide() && !action.doAllowProtein())
347 throw new ServiceInputInvalidException(
348 action.getFullName() + " does not allow protein sequences");
349 boolean bySequence = !action.isAlignmentAnalysis();
350 AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
351 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
352 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
353 inputSeqs = alignment;
354 boolean submitGaps = action.isAlignmentAnalysis();
355 boolean requireAligned = action.getRequireAlignedSequences();
356 boolean filterSymbols = action.getFilterSymbols();
357 int minSize = action.getMinSequences();
358 AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
359 submitGaps, requireAligned, filterSymbols, minSize);
360 if (!job.isInputValid())
362 job.setStatus(JobStatus.INVALID);
363 throw new ServiceInputInvalidException("Annotation job has invalid input");
365 job.setStatus(JobStatus.READY);
369 private void startJobs() throws IOException
371 for (BaseJob job : jobs)
373 if (job.isInputValid() && job.getStatus() == JobStatus.READY)
375 var serverJob = client.submit(job.getInputSequences(),
377 job.setServerJob(serverJob);
378 job.setStatus(JobStatus.SUBMITTED);
383 private boolean poll() throws IOException
385 boolean allDone = true;
386 for (BaseJob job : jobs)
388 if (job.isInputValid() && !job.getStatus().isDone())
390 WebServiceJobHandle serverJob = job.getServerJob();
391 job.setStatus(client.getStatus(serverJob));
392 job.setLog(client.getLog(serverJob));
393 job.setErrorLog(client.getErrorLog(serverJob));
395 allDone &= job.isCompleted();
400 private AnnotationResult retrieveResult() throws IOException
402 final Map<String, FeatureColourI> featureColours = new HashMap<>();
403 final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
404 var job = jobs.get(0);
405 List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
406 job.getServerJob(), job.getInputSequences(), featureColours,
409 * copy over each annotation row returned and also defined on each
410 * sequence, excluding regions not annotated due to gapMap/column
413 // update calcId if it is not already set on returned annotation
414 for (AlignmentAnnotation annot : returnedAnnot)
416 if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
418 annot.setCalcId(action.getFullName());
420 annot.autoCalculated = action.isAlignmentAnalysis() &&
421 action.getWebService().isInteractive();
423 job.returnedAnnotations = returnedAnnot;
424 job.featureColours = featureColours;
425 job.featureFilters = featureFilters;
426 var ret = updateResultAnnotation(job, returnedAnnot);
427 var annotations = ret.get0();
428 var transferFeatures = ret.get1();
429 return new AnnotationResult(annotations, transferFeatures, featureColours,
433 private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
434 AnnotationJob job, List<AlignmentAnnotation> annotations)
436 List<AlignmentAnnotation> newAnnots = new ArrayList<>();
437 // update graphGroup for all annotation
438 /* find a graphGroup greater than any existing one, could be moved
439 * to Alignment#getNewGraphGroup() - returns next unused graph group */
441 if (viewport.getAlignment().getAlignmentAnnotation() != null)
443 for (var ala : viewport.getAlignment().getAlignmentAnnotation())
445 graphGroup = Math.max(graphGroup, ala.graphGroup);
448 // update graphGroup in the annotation rows returned form service'
449 /* TODO: look at sequence annotation rows and update graph groups in the
450 * case of reference annotation */
451 for (AlignmentAnnotation ala : annotations)
453 if (ala.graphGroup > 0)
454 ala.graphGroup += graphGroup;
455 SequenceI aseq = null;
456 // transfer sequence refs and adjust gapMap
457 if (ala.sequenceRef != null)
459 aseq = job.seqNames.get(ala.sequenceRef.getName());
461 ala.sequenceRef = aseq;
463 Annotation[] resAnnot = ala.annotations;
464 boolean[] gapMap = job.gapMap;
465 Annotation[] gappedAnnot = new Annotation[Math.max(
466 viewport.getAlignment().getWidth(), gapMap.length)];
467 for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
469 if (gapMap.length > ap && !gapMap[ap])
470 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
471 else if (p < resAnnot.length)
472 gappedAnnot[ap] = resAnnot[p++];
473 // is this loop exhaustive of resAnnot?
475 ala.annotations = gappedAnnot;
477 AlignmentAnnotation newAnnot = viewport.getAlignment()
478 .updateFromOrCopyAnnotation(ala);
481 aseq.addAlignmentAnnotation(newAnnot);
482 newAnnot.adjustForAlignment();
483 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
484 newAnnot, newAnnot.label, newAnnot.getCalcId());
486 newAnnots.add(newAnnot);
489 boolean transferFeatures = false;
490 for (SequenceI sq : job.getInputSequences())
492 if (!sq.getFeatures().hasFeatures() &&
493 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
495 transferFeatures = true;
496 SequenceI seq = job.seqNames.get(sq.getName());
498 int start = job.start, end = job.end;
499 boolean[] gapMap = job.gapMap;
500 ContiguousI seqRange = seq.findPositions(start, end);
501 while ((dseq = seq).getDatasetSequence() != null)
503 seq = seq.getDatasetSequence();
505 List<ContiguousI> sourceRange = new ArrayList<>();
506 if (gapMap.length >= end)
508 int lastcol = start, col = start;
511 if (col == end || !gapMap[col])
513 if (lastcol <= (col - 1))
515 seqRange = seq.findPositions(lastcol, col);
516 sourceRange.add(seqRange);
520 } while (col++ < end);
524 sourceRange.add(seq.findPositions(start, end));
528 int sourceStartEnd[] = new int[sourceRange.size() * 2];
529 for (ContiguousI range : sourceRange)
531 sourceStartEnd[i++] = range.getBegin();
532 sourceStartEnd[i++] = range.getEnd();
534 Mapping mp = new Mapping(new MapList(
535 sourceStartEnd, new int[]
536 { seq.getStart(), seq.getEnd() }, 1, 1));
537 dseq.transferAnnotation(sq, mp);
540 return new Pair<>(newAnnots, transferFeatures);
544 public AnnotationResult getResult()
552 setStatus(JobStatus.CANCELLED);
560 public void cancelJobs()
562 for (BaseJob job : jobs)
564 if (!job.isCompleted())
568 if (job.getServerJob() != null)
570 client.cancel(job.getServerJob());
572 job.setStatus(JobStatus.CANCELLED);
573 } catch (IOException e)
575 Console.error(String.format(
576 "failed to cancel job %s", job.getServerJob()), e);
583 public String toString()
585 var status = taskStatus != null ? taskStatus.name() : "UNSET";
586 return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);