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 this.taskStatus = status;
294 eventHandler.fireTaskStatusChanged(status);
298 private void updateGlobalStatus()
301 for (BaseJob job : jobs)
303 JobStatus status = job.getStatus();
304 int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status);
305 if (precedence < jobPrecedence)
306 precedence = jobPrecedence;
310 setStatus(JobStatus.statusPrecedence[precedence]);
315 public List<? extends JobI> getSubJobs()
321 * Create and return a list of annotation jobs from the current state of the
322 * viewport. Returned job are not started by this method and should be stored
323 * in a field and started separately.
325 * @return list of annotation jobs
326 * @throws ServiceInputInvalidException
327 * input data is not valid
329 private List<AnnotationJob> prepare() throws ServiceInputInvalidException
331 AlignmentI alignment = viewport.getAlignment();
332 if (alignment == null || alignment.getWidth() <= 0 ||
333 alignment.getSequences() == null)
334 throw new ServiceInputInvalidException("Alignment does not contain sequences");
335 if (alignment.isNucleotide() && !action.doAllowNucleotide())
336 throw new ServiceInputInvalidException(
337 action.getFullName() + " does not allow nucleotide sequences");
338 if (!alignment.isNucleotide() && !action.doAllowProtein())
339 throw new ServiceInputInvalidException(
340 action.getFullName() + " does not allow protein sequences");
341 boolean bySequence = !action.isAlignmentAnalysis();
342 AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null;
343 if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
344 inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
345 inputSeqs = alignment;
346 boolean submitGaps = action.isAlignmentAnalysis();
347 boolean requireAligned = action.getRequireAlignedSequences();
348 boolean filterSymbols = action.getFilterSymbols();
349 int minSize = action.getMinSequences();
350 AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
351 submitGaps, requireAligned, filterSymbols, minSize);
352 if (!job.isInputValid())
354 job.setStatus(JobStatus.INVALID);
355 throw new ServiceInputInvalidException("Annotation job has invalid input");
357 job.setStatus(JobStatus.READY);
361 private void startJobs() throws IOException
363 for (BaseJob job : jobs)
365 if (job.isInputValid() && job.getStatus() == JobStatus.READY)
367 var serverJob = client.submit(job.getInputSequences(),
369 job.setServerJob(serverJob);
370 job.setStatus(JobStatus.SUBMITTED);
375 private boolean poll() throws IOException
377 boolean allDone = true;
378 for (BaseJob job : jobs)
380 if (job.isInputValid() && !job.getStatus().isDone())
382 WebServiceJobHandle serverJob = job.getServerJob();
383 job.setStatus(client.getStatus(serverJob));
384 job.setLog(client.getLog(serverJob));
385 job.setErrorLog(client.getErrorLog(serverJob));
387 allDone &= job.isCompleted();
392 private AnnotationResult retrieveResult() throws IOException
394 final Map<String, FeatureColourI> featureColours = new HashMap<>();
395 final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
396 var job = jobs.get(0);
397 List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
398 job.getServerJob(), job.getInputSequences(), featureColours,
401 * copy over each annotation row returned and also defined on each
402 * sequence, excluding regions not annotated due to gapMap/column
405 // update calcId if it is not already set on returned annotation
406 for (AlignmentAnnotation annot : returnedAnnot)
408 if (annot.getCalcId() == null || annot.getCalcId().isEmpty())
410 annot.setCalcId(action.getFullName());
412 annot.autoCalculated = action.isAlignmentAnalysis() &&
413 action.getWebService().isInteractive();
415 job.returnedAnnotations = returnedAnnot;
416 job.featureColours = featureColours;
417 job.featureFilters = featureFilters;
418 var ret = updateResultAnnotation(job, returnedAnnot);
419 var annotations = ret.get0();
420 var transferFeatures = ret.get1();
421 return new AnnotationResult(annotations, transferFeatures, featureColours,
425 private Pair<List<AlignmentAnnotation>, Boolean> updateResultAnnotation(
426 AnnotationJob job, List<AlignmentAnnotation> annotations)
428 List<AlignmentAnnotation> newAnnots = new ArrayList<>();
429 // update graphGroup for all annotation
430 /* find a graphGroup greater than any existing one, could be moved
431 * to Alignment#getNewGraphGroup() - returns next unused graph group */
433 if (viewport.getAlignment().getAlignmentAnnotation() != null)
435 for (var ala : viewport.getAlignment().getAlignmentAnnotation())
437 graphGroup = Math.max(graphGroup, ala.graphGroup);
440 // update graphGroup in the annotation rows returned form service'
441 /* TODO: look at sequence annotation rows and update graph groups in the
442 * case of reference annotation */
443 for (AlignmentAnnotation ala : annotations)
445 if (ala.graphGroup > 0)
446 ala.graphGroup += graphGroup;
447 SequenceI aseq = null;
448 // transfer sequence refs and adjust gapMap
449 if (ala.sequenceRef != null)
451 aseq = job.seqNames.get(ala.sequenceRef.getName());
453 ala.sequenceRef = aseq;
455 Annotation[] resAnnot = ala.annotations;
456 boolean[] gapMap = job.gapMap;
457 Annotation[] gappedAnnot = new Annotation[Math.max(
458 viewport.getAlignment().getWidth(), gapMap.length)];
459 for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++)
461 if (gapMap.length > ap && !gapMap[ap])
462 gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
463 else if (p < resAnnot.length)
464 gappedAnnot[ap] = resAnnot[p++];
465 // is this loop exhaustive of resAnnot?
467 ala.annotations = gappedAnnot;
469 AlignmentAnnotation newAnnot = viewport.getAlignment()
470 .updateFromOrCopyAnnotation(ala);
473 aseq.addAlignmentAnnotation(newAnnot);
474 newAnnot.adjustForAlignment();
475 AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
476 newAnnot, newAnnot.label, newAnnot.getCalcId());
478 newAnnots.add(newAnnot);
481 boolean transferFeatures = false;
482 for (SequenceI sq : job.getInputSequences())
484 if (!sq.getFeatures().hasFeatures() &&
485 (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
487 transferFeatures = true;
488 SequenceI seq = job.seqNames.get(sq.getName());
490 int start = job.start, end = job.end;
491 boolean[] gapMap = job.gapMap;
492 ContiguousI seqRange = seq.findPositions(start, end);
493 while ((dseq = seq).getDatasetSequence() != null)
495 seq = seq.getDatasetSequence();
497 List<ContiguousI> sourceRange = new ArrayList<>();
498 if (gapMap.length >= end)
500 int lastcol = start, col = start;
503 if (col == end || !gapMap[col])
505 if (lastcol <= (col - 1))
507 seqRange = seq.findPositions(lastcol, col);
508 sourceRange.add(seqRange);
512 } while (col++ < end);
516 sourceRange.add(seq.findPositions(start, end));
520 int sourceStartEnd[] = new int[sourceRange.size() * 2];
521 for (ContiguousI range : sourceRange)
523 sourceStartEnd[i++] = range.getBegin();
524 sourceStartEnd[i++] = range.getEnd();
526 Mapping mp = new Mapping(new MapList(
527 sourceStartEnd, new int[]
528 { seq.getStart(), seq.getEnd() }, 1, 1));
529 dseq.transferAnnotation(sq, mp);
532 return new Pair<>(newAnnots, transferFeatures);
536 public AnnotationResult getResult()
544 setStatus(JobStatus.CANCELLED);
552 public void cancelJobs()
554 for (BaseJob job : jobs)
556 if (!job.isCompleted())
560 if (job.getServerJob() != null)
562 client.cancel(job.getServerJob());
564 job.setStatus(JobStatus.CANCELLED);
565 } catch (IOException e)
567 Cache.log.error(String.format(
568 "failed to cancel job %s", job.getServerJob()), e);