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 import static java.util.Objects.requireNonNullElse;
48 public class AnnotationTask implements TaskI<AnnotationResult>
50 private final long uid = MathUtils.getUID();
52 private AnnotationWebServiceClientI client;
54 private final AnnotationAction action;
56 private final List<ArgumentI> args;
58 private final Credentials credentials;
60 private final AlignViewportI viewport;
62 private final TaskEventSupport<AnnotationResult> eventHandler;
64 private JobStatus taskStatus = null;
66 private AlignCalcWorkerAdapter worker = null;
68 private List<AnnotationJob> jobs = Collections.emptyList();
70 private AnnotationResult result = null;
72 private DelegateJobEventListener<AnnotationResult> jobEventHandler;
74 private class AlignCalcWorkerAdapter extends AlignCalcWorker
75 implements PollableAlignCalcWorkerI
77 private boolean restarting = false;
79 AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan)
81 super(viewport, null);
82 this.calcMan = calcMan;
85 String getServiceName()
87 return action.getWebService().getName();
91 public void startUp() throws Throwable
93 if (alignViewport.isClosed())
96 throw new IllegalStateException("Starting annotation for closed viewport");
99 eventHandler.fireTaskRestarted();
102 jobs = Collections.emptyList();
106 } catch (ServiceInputInvalidException e)
108 setStatus(JobStatus.INVALID);
109 eventHandler.fireTaskException(e);
112 setStatus(JobStatus.READY);
113 eventHandler.fireTaskStarted(jobs);
116 job.addPropertyChagneListener(jobEventHandler);
121 } catch (IOException e)
123 eventHandler.fireTaskException(e);
125 setStatus(JobStatus.SERVER_ERROR);
128 setStatus(JobStatus.SUBMITTED);
132 public boolean poll() throws Throwable
134 boolean done = AnnotationTask.this.poll();
135 updateGlobalStatus();
138 retrieveAndProcessResult();
139 eventHandler.fireTaskCompleted(result);
144 private void retrieveAndProcessResult() throws IOException
146 result = retrieveResult();
147 updateOurAnnots(result.annotations);
148 if (result.transferFeatures)
150 final var featureColours = result.featureColours;
151 final var featureFilters = result.featureFilters;
152 viewport.applyFeaturesStyle(new FeatureSettingsAdapter()
155 public FeatureColourI getFeatureColour(String type)
157 return featureColours.get(type);
161 public FeatureMatcherSetI getFeatureFilters(String type)
163 return featureFilters.get(type);
167 public boolean isFeatureDisplayed(String type)
169 return featureColours.containsKey(type);
176 public void updateAnnotation()
178 var job = jobs.size() > 0 ? jobs.get(0) : null;
179 if (!calcMan.isWorking(this) && job != null)
181 var ret = updateResultAnnotation(job, job.returnedAnnotations);
182 updateOurAnnots(ret.get0());
186 private void updateOurAnnots(List<AlignmentAnnotation> newAnnots)
188 List<AlignmentAnnotation> oldAnnots = requireNonNullElse(ourAnnots, Collections.emptyList());
189 ourAnnots = newAnnots;
190 AlignmentI alignment = viewport.getAlignment();
191 for (AlignmentAnnotation an : oldAnnots)
193 if (!newAnnots.contains(an))
195 alignment.deleteAnnotation(an);
199 for (AlignmentAnnotation an : ourAnnots)
201 viewport.getAlignment().validateAnnotation(an);
213 calcMan.disableWorker(this);
214 super.abortAndDestroy();
222 if (job.isInputValid() && !job.isCompleted())
224 /* if done was called but job is not completed then it
225 * must have been stopped by an exception */
226 job.setStatus(JobStatus.SERVER_ERROR);
229 updateGlobalStatus();
230 // dispose of unfinished jobs just in case
235 public String toString()
237 return AnnotationTask.this.toString() + "$AlignCalcWorker@"
238 + Integer.toHexString(hashCode());
242 public AnnotationTask(AnnotationWebServiceClientI client,
243 AnnotationAction action, List<ArgumentI> args, Credentials credentials,
244 AlignViewportI viewport,
245 TaskEventListener<AnnotationResult> eventListener)
247 this.client = client;
248 this.action = action;
250 this.credentials = credentials;
251 this.viewport = viewport;
252 this.eventHandler = new TaskEventSupport<>(this, eventListener);
253 this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler);
262 public void start(AlignCalcManagerI2 calcManager)
264 if (this.worker != null)
265 throw new IllegalStateException("task already started");
266 this.worker = new AlignCalcWorkerAdapter(calcManager);
267 if (taskStatus != JobStatus.CANCELLED)
269 List<AlignCalcWorkerI> oldWorkers = calcManager.getWorkersOfClass(
270 AlignCalcWorkerAdapter.class);
271 for (var worker : oldWorkers)
273 if (action.getWebService().getName().equalsIgnoreCase(
274 ((AlignCalcWorkerAdapter) worker).getServiceName()))
276 // remove interactive workers for the same service.
277 calcManager.removeWorker(worker);
278 calcManager.cancelWorker(worker);
281 if (action.getWebService().isInteractive())
282 calcManager.registerWorker(worker);
284 calcManager.startWorker(worker);
289 * The following methods are mostly copied from the {@link AbstractPollableTask}
290 * TODO: move common functionality to a base class
293 public JobStatus getStatus()
298 private void setStatus(JobStatus status)
300 if (this.taskStatus != status)
302 Console.debug(String.format("%s status change to %s", this, status.name()));
303 this.taskStatus = status;
304 eventHandler.fireTaskStatusChanged(status);
308 private void updateGlobalStatus()
311 for (BaseJob job : jobs)
313 JobStatus status = job.getStatus();
314 int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
315 if (precedence < jobPrecedence)
316 precedence = jobPrecedence;
320 setStatus(JobStatus.statusPrecedence[precedence]);
325 public List<? extends JobI> getSubJobs()
331 * Create and return a list of annotation jobs from the current state of the
332 * viewport. Returned job are not started by this method and should be stored
333 * in a field and started separately.
335 * @return list of annotation jobs
336 * @throws ServiceInputInvalidException
337 * input data is not valid
339 private List<AnnotationJob> prepare() throws ServiceInputInvalidException
341 AlignmentI alignment = viewport.getAlignment();
342 if (alignment == null || alignment.getWidth() <= 0 ||
343 alignment.getSequences() == null)
344 throw new ServiceInputInvalidException("Alignment does not contain sequences");
345 if (alignment.isNucleotide() && !action.doAllowNucleotide())
346 throw new ServiceInputInvalidException(
347 action.getFullName() + " does not allow nucleotide sequences");
348 if (!alignment.isNucleotide() && !action.doAllowProtein())
349 throw new ServiceInputInvalidException(
350 action.getFullName() + " does not allow protein sequences");
351 boolean bySequence = !action.isAlignmentAnalysis();
352 AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
353 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
354 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
355 inputSeqs = alignment;
356 boolean submitGaps = action.isAlignmentAnalysis();
357 boolean requireAligned = action.getRequireAlignedSequences();
358 boolean filterSymbols = action.getFilterSymbols();
359 int minSize = action.getMinSequences();
360 AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
361 submitGaps, requireAligned, filterSymbols, minSize);
362 if (!job.isInputValid())
364 job.setStatus(JobStatus.INVALID);
365 throw new ServiceInputInvalidException("Annotation job has invalid input");
367 job.setStatus(JobStatus.READY);
371 private void startJobs() throws IOException
373 for (BaseJob job : jobs)
375 if (job.isInputValid() && job.getStatus() == JobStatus.READY)
377 var serverJob = client.submit(job.getInputSequences(),
379 job.setServerJob(serverJob);
380 job.setStatus(JobStatus.SUBMITTED);
385 private boolean poll() throws IOException
387 boolean allDone = true;
388 for (BaseJob job : jobs)
390 if (job.isInputValid() && !job.getStatus().isDone())
392 WebServiceJobHandle serverJob = job.getServerJob();
393 job.setStatus(client.getStatus(serverJob));
394 job.setLog(client.getLog(serverJob));
395 job.setErrorLog(client.getErrorLog(serverJob));
397 allDone &= job.isCompleted();
402 private AnnotationResult retrieveResult() throws IOException
404 final Map<String, FeatureColourI> featureColours = new HashMap<>();
405 final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
406 var job = jobs.get(0);
407 List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
408 job.getServerJob(), job.getInputSequences(), featureColours,
411 * copy over each annotation row returned and also defined on each
412 * sequence, excluding regions not annotated due to gapMap/column
415 // update calcId if it is not already set on returned annotation
416 for (AlignmentAnnotation annot : returnedAnnot)
418 if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
420 annot.setCalcId(action.getFullName());
422 annot.autoCalculated = action.isAlignmentAnalysis() &&
423 action.getWebService().isInteractive();
425 job.returnedAnnotations = returnedAnnot;
426 job.featureColours = featureColours;
427 job.featureFilters = featureFilters;
428 var ret = updateResultAnnotation(job, returnedAnnot);
429 var annotations = ret.get0();
430 var transferFeatures = ret.get1();
431 return new AnnotationResult(annotations, transferFeatures, featureColours,
435 private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
436 AnnotationJob job, List<AlignmentAnnotation> annotations)
438 List<AlignmentAnnotation> newAnnots = new ArrayList<>();
439 // update graphGroup for all annotation
440 /* find a graphGroup greater than any existing one, could be moved
441 * to Alignment#getNewGraphGroup() - returns next unused graph group */
443 if (viewport.getAlignment().getAlignmentAnnotation() != null)
445 for (var ala : viewport.getAlignment().getAlignmentAnnotation())
447 graphGroup = Math.max(graphGroup, ala.graphGroup);
450 // update graphGroup in the annotation rows returned form service'
451 /* TODO: look at sequence annotation rows and update graph groups in the
452 * case of reference annotation */
453 for (AlignmentAnnotation ala : annotations)
455 if (ala.graphGroup > 0)
456 ala.graphGroup += graphGroup;
457 SequenceI aseq = null;
458 // transfer sequence refs and adjust gapMap
459 if (ala.sequenceRef != null)
461 aseq = job.seqNames.get(ala.sequenceRef.getName());
463 ala.sequenceRef = aseq;
465 Annotation[] resAnnot = ala.annotations;
466 boolean[] gapMap = job.gapMap;
467 Annotation[] gappedAnnot = new Annotation[Math.max(
468 viewport.getAlignment().getWidth(), gapMap.length)];
469 for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
471 if (gapMap.length > ap && !gapMap[ap])
472 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
473 else if (p < resAnnot.length)
474 gappedAnnot[ap] = resAnnot[p++];
475 // is this loop exhaustive of resAnnot?
477 ala.annotations = gappedAnnot;
479 AlignmentAnnotation newAnnot = viewport.getAlignment()
480 .updateFromOrCopyAnnotation(ala);
483 aseq.addAlignmentAnnotation(newAnnot);
484 newAnnot.adjustForAlignment();
485 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
486 newAnnot, newAnnot.label, newAnnot.getCalcId());
488 newAnnots.add(newAnnot);
491 boolean transferFeatures = false;
492 for (SequenceI sq : job.getInputSequences())
494 if (!sq.getFeatures().hasFeatures() &&
495 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
497 transferFeatures = true;
498 SequenceI seq = job.seqNames.get(sq.getName());
500 int start = job.start, end = job.end;
501 boolean[] gapMap = job.gapMap;
502 ContiguousI seqRange = seq.findPositions(start, end);
503 while ((dseq = seq).getDatasetSequence() != null)
505 seq = seq.getDatasetSequence();
507 List<ContiguousI> sourceRange = new ArrayList<>();
508 if (gapMap.length >= end)
510 int lastcol = start, col = start;
513 if (col == end || !gapMap[col])
515 if (lastcol <= (col - 1))
517 seqRange = seq.findPositions(lastcol, col);
518 sourceRange.add(seqRange);
522 } while (col++ < end);
526 sourceRange.add(seq.findPositions(start, end));
530 int sourceStartEnd[] = new int[sourceRange.size() * 2];
531 for (ContiguousI range : sourceRange)
533 sourceStartEnd[i++] = range.getBegin();
534 sourceStartEnd[i++] = range.getEnd();
536 Mapping mp = new Mapping(new MapList(
537 sourceStartEnd, new int[]
538 { seq.getStart(), seq.getEnd() }, 1, 1));
539 dseq.transferAnnotation(sq, mp);
542 return new Pair<>(newAnnots, transferFeatures);
546 public AnnotationResult getResult()
554 setStatus(JobStatus.CANCELLED);
562 public void cancelJobs()
564 for (BaseJob job : jobs)
566 if (!job.isCompleted())
570 if (job.getServerJob() != null)
572 client.cancel(job.getServerJob());
574 job.setStatus(JobStatus.CANCELLED);
575 } catch (IOException e)
577 Console.error(String.format(
578 "failed to cancel job %s", job.getServerJob()), e);
585 public String toString()
587 var status = taskStatus != null ? taskStatus.name() : "UNSET";
588 return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, status);