From 1f1b6f8994fb96cfac294f218e1777bf7f68804a Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Thu, 7 Apr 2022 14:30:04 +0200 Subject: [PATCH] JAL-3878 Add action and task for annotation services --- src/jalview/util/Pair.java | 88 +++ .../ws2/actions/annotation/AnnotationAction.java | 128 +++++ .../ws2/actions/annotation/AnnotationJob.java | 144 +++++ .../actions/annotation/AnnotationProviderI.java | 48 ++ .../ws2/actions/annotation/AnnotationResult.java | 56 ++ .../ws2/actions/annotation/AnnotationTask.java | 573 ++++++++++++++++++++ .../client/api/AnnotationWebServiceClientI.java | 16 + 7 files changed, 1053 insertions(+) create mode 100644 src/jalview/util/Pair.java create mode 100644 src/jalview/ws2/actions/annotation/AnnotationAction.java create mode 100644 src/jalview/ws2/actions/annotation/AnnotationJob.java create mode 100644 src/jalview/ws2/actions/annotation/AnnotationProviderI.java create mode 100644 src/jalview/ws2/actions/annotation/AnnotationResult.java create mode 100644 src/jalview/ws2/actions/annotation/AnnotationTask.java create mode 100644 src/jalview/ws2/client/api/AnnotationWebServiceClientI.java diff --git a/src/jalview/util/Pair.java b/src/jalview/util/Pair.java new file mode 100644 index 0000000..63cf7e9 --- /dev/null +++ b/src/jalview/util/Pair.java @@ -0,0 +1,88 @@ +package jalview.util; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * A generic immutable pair of values. + * + * @author mmwarowny + * + * @param + * first value type + * @param + * second value type + */ +public class Pair implements Iterable +{ + final T val0; + + final U val1; + + public Pair(T val0, U val1) + { + this.val0 = val0; + this.val1 = val1; + } + + /** + * Return value at the specified index cast to type R + * @param return type + * @param index item index + * @return value at given index + * @throws ClassCastException value cannot be cast to R + * @throws IndexOutOfBoundsException index outside tuple size + */ + @SuppressWarnings("unchecked") + public R get(int index) throws ClassCastException, IndexOutOfBoundsException + { + if (index == 0) + return (R) val0; + if (index == 1) + return (R) val1; + throw new IndexOutOfBoundsException(index); + } + + /** + * @return 0th value of the pair + */ + public T get0() + { + return val0; + } + + /** + * @return 1st value of the pair + */ + public U get1() + { + return val1; + } + + /** + * @return tuple size + */ + public int size() + { + return 2; + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof Pair) + { + Pair other = (Pair) obj; + return Objects.equals(val0, other.val0) && + Objects.equals(val1, other.val1); + } + return false; + } + + @Override + public Iterator iterator() + { + return List.of(val0, val1).iterator(); + } +} diff --git a/src/jalview/ws2/actions/annotation/AnnotationAction.java b/src/jalview/ws2/actions/annotation/AnnotationAction.java new file mode 100644 index 0000000..02829fd --- /dev/null +++ b/src/jalview/ws2/actions/annotation/AnnotationAction.java @@ -0,0 +1,128 @@ +package jalview.ws2.actions.annotation; + +import java.util.List; +import java.util.Objects; + +import jalview.viewmodel.AlignmentViewport; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.BaseAction; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.Credentials; +import jalview.ws2.client.api.AnnotationWebServiceClientI; + +public class AnnotationAction extends BaseAction +{ + /** + * A builder of {@link AnnotationAction} instances. + */ + public static class Builder extends BaseAction.Builder + { + protected AnnotationWebServiceClientI client; + + protected boolean alignmentAnalysis = false; + + protected boolean requireAlignedSequences = false; + + protected boolean filterSymbols = true; + + public Builder(AnnotationWebServiceClientI client) + { + super(); + Objects.requireNonNull(client); + this.client = client; + } + + /** + * Set if action is an alignment analysis action. + */ + public void alignmentAnalysis(boolean val) + { + alignmentAnalysis = val; + } + + /** + * Set if action require aligned sequences. + */ + public void requireAlignedSequences(boolean val) + { + requireAlignedSequences = val; + } + + /** + * Set if action requires non-standard residues to be filtered out + */ + public void filterSymbols(boolean val) + { + filterSymbols = val; + } + + public AnnotationAction build() + { + return new AnnotationAction(this); + } + } + + public static Builder newBuilder(AnnotationWebServiceClientI client) + { + return new Builder(client); + } + + protected final AnnotationWebServiceClientI client; + + protected final boolean alignmentAnalysis; + + protected final boolean requireAlignedSequences; + + protected final boolean filterSymbols; + + protected AnnotationAction(Builder builder) + { + super(builder); + client = builder.client; + alignmentAnalysis = builder.alignmentAnalysis; + requireAlignedSequences = builder.requireAlignedSequences; + filterSymbols = builder.filterSymbols; + } + + @Override + public TaskI perform(AlignmentViewport viewport, + List args, Credentials credentials, + TaskEventListener handler) + { + var task = new AnnotationTask(client, this, args, credentials, viewport, + handler); + task.start(viewport.getCalcManager()); + return task; + } + + /** + * Return if this action is an alignment analysis service. + */ + public boolean isAlignmentAnalysis() + { + return alignmentAnalysis; + } + + /** + * Return if this action require sequences to be aligned. + */ + public boolean getRequireAlignedSequences() + { + return requireAlignedSequences; + } + + /** + * Return if this action require non-standard symbols to be filtered out. + */ + public boolean getFilterSymbols() + { + return filterSymbols; + } + + @Override + public boolean isActive(AlignmentViewport viewport) + { + return false; + } +} diff --git a/src/jalview/ws2/actions/annotation/AnnotationJob.java b/src/jalview/ws2/actions/annotation/AnnotationJob.java new file mode 100644 index 0000000..23e462b --- /dev/null +++ b/src/jalview/ws2/actions/annotation/AnnotationJob.java @@ -0,0 +1,144 @@ +package jalview.ws2.actions.annotation; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jalview.analysis.AlignSeq; +import jalview.analysis.SeqsetUtils; +import jalview.api.FeatureColourI; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AnnotatedCollectionI; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.schemes.ResidueProperties; +import jalview.util.Comparison; +import jalview.ws2.actions.BaseJob; + +public class AnnotationJob extends BaseJob +{ + final boolean[] gapMap; + + final Map seqNames; + + final int start, end; + + final int minSize; + + List returnedAnnotations = Collections.emptyList(); + + Map featureColours = Collections.emptyMap(); + + Map featureFilters = Collections.emptyMap(); + + + public AnnotationJob(List inputSeqs, boolean[] gapMap, + Map seqNames, int start, int end, int minSize) + { + super(inputSeqs); + this.gapMap = gapMap; + this.seqNames = seqNames; + this.start = start; + this.end = end; + this.minSize = minSize; + } + + @Override + public boolean isInputValid() + { + int nvalid = 0; + for (SequenceI sq : getInputSequences()) + if (sq.getStart() <= sq.getEnd()) + nvalid++; + return nvalid >= minSize; + } + + public static AnnotationJob create(AnnotatedCollectionI inputSeqs, + boolean bySequence, boolean submitGaps, boolean requireAligned, + boolean filterNonStandardResidues, int minSize) + { + List seqs = new ArrayList<>(); + int minlen = 10; + int ln = -1; + Map seqNames = bySequence ? new HashMap<>() : null; + BitSet gapMap = new BitSet(); + int gapMapSize = 0; + int start = inputSeqs.getStartRes(); + int end = inputSeqs.getEndRes(); + // TODO: URGENT! unify with JPred / MSA code to handle hidden regions + // correctly + // TODO: push attributes into WsJob instance (so they can be safely + // persisted/restored + for (SequenceI sq : inputSeqs.getSequences()) + { + int sqlen; + if (bySequence) + sqlen = sq.findPosition(end + 1) - sq.findPosition(start + 1); + else + sqlen = sq.getEnd() - sq.getStart(); + if (sqlen >= minlen) + { + String newName = SeqsetUtils.unique_name(seqs.size() + 1); + if (seqNames != null) + seqNames.put(newName, sq); + Sequence seq; + if (submitGaps) + { + seq = new Sequence(newName, sq.getSequenceAsString()); + gapMapSize = Math.max(gapMapSize, seq.getLength()); + for (int pos : sq.gapMap()) + { + char sqchr = sq.getCharAt(pos); + boolean include = !filterNonStandardResidues; + include |= sq.isProtein() ? ResidueProperties.aaIndex[sqchr] < 20 + : ResidueProperties.nucleotideIndex[sqchr] < 5; + if (include) + gapMap.set(pos); + } + } + else + { + // TODO: add ability to exclude hidden regions + seq = new Sequence(newName, AlignSeq.extractGaps(Comparison.GapChars, + sq.getSequenceAsString(start, end + 1))); + // for annotation need to also record map to sequence start/end + // position in range + // then transfer back to original sequence on return. + } + seqs.add(seq); + ln = Math.max(ln, seq.getLength()); + } + } + + if (requireAligned && submitGaps) + { + int realWidth = gapMap.cardinality(); + for (int i = 0; i < seqs.size(); i++) + { + SequenceI sq = seqs.get(i); + char[] padded = new char[realWidth]; + char[] original = sq.getSequence(); + for (int op = 0, pp = 0; pp < realWidth; op++) + { + if (gapMap.get(op)) + { + if (original.length > op) + padded[pp++] = original[op]; + else + padded[pp++] = '-'; + } + } + seqs.set(i, new Sequence(sq.getName(), padded)); + } + } + boolean[] gapMapArray = new boolean[gapMapSize]; + for (int i = 0; i < gapMapSize; i++) + gapMapArray[i] = gapMap.get(i); + return new AnnotationJob(seqs, gapMapArray, seqNames, start, end, minSize); + } +} diff --git a/src/jalview/ws2/actions/annotation/AnnotationProviderI.java b/src/jalview/ws2/actions/annotation/AnnotationProviderI.java new file mode 100644 index 0000000..3a836a0 --- /dev/null +++ b/src/jalview/ws2/actions/annotation/AnnotationProviderI.java @@ -0,0 +1,48 @@ +package jalview.ws2.actions.annotation; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import jalview.api.FeatureColourI; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.ws2.api.WebServiceJobHandle; +import jalview.ws2.client.api.AnnotationWebServiceClientI; +import jalview.ws2.client.api.WebServiceClientI; + +/** + * An interface for providing annotation results to the annotation services. It + * declares a method to attach annotations fetched from the server to sequences. + * Web service clients wanting to support annotation acitons must implement this + * interface in addition to {@link WebServiceClientI} + * + * @author mmwarowny + * + * @see AnnotationWebServiceClientI + */ +public interface AnnotationProviderI +{ + /** + * Retrieves annotations from the job result on the server and attaches them + * to provided sequences. Additionally, adds feature colours and filters to + * provided containers. + * + * @param job + * web service job + * @param sequences + * sequences the annotations will be added to + * @param colours + * container for feature colours + * @param filters + * container for feature filters + * @return sequence and alignment annotation rows that should be made + * visible/updated on alignment + * @throws IOException + * annotation retrieval failed + */ + public List attachAnnotations(WebServiceJobHandle job, + List sequences, Map colours, + Map filters) throws IOException; +} diff --git a/src/jalview/ws2/actions/annotation/AnnotationResult.java b/src/jalview/ws2/actions/annotation/AnnotationResult.java new file mode 100644 index 0000000..373ecbb --- /dev/null +++ b/src/jalview/ws2/actions/annotation/AnnotationResult.java @@ -0,0 +1,56 @@ +package jalview.ws2.actions.annotation; + +import java.util.List; +import java.util.Map; + +import jalview.api.FeatureColourI; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.features.FeatureMatcherSetI; + +/** + * A simple data container storing the output of annotation tasks. The object is + * constructed on {@link AnnotationTask} completion and passed to an appropriate + * handler. + * + * @author mmwarowny + * + */ +public class AnnotationResult +{ + final List annotations; + + final boolean transferFeatures; + + final Map featureColours; + + final Map featureFilters; + + public AnnotationResult(List annotations, boolean transferFeatures, + Map featureColours, Map featureFilters) + { + this.annotations = annotations; + this.transferFeatures = transferFeatures; + this.featureColours = featureColours; + this.featureFilters = featureFilters; + } + + public List getAnnotations() + { + return annotations; + } + + public boolean getTransferFeatures() + { + return transferFeatures; + } + + public Map getFeatureColours() + { + return featureColours; + } + + public Map getFeatureFilters() + { + return featureFilters; + } +} diff --git a/src/jalview/ws2/actions/annotation/AnnotationTask.java b/src/jalview/ws2/actions/annotation/AnnotationTask.java new file mode 100644 index 0000000..86657c4 --- /dev/null +++ b/src/jalview/ws2/actions/annotation/AnnotationTask.java @@ -0,0 +1,573 @@ +package jalview.ws2.actions.annotation; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jalview.analysis.AlignmentAnnotationUtils; +import jalview.api.AlignCalcManagerI2; +import jalview.api.AlignCalcWorkerI; +import jalview.api.AlignViewportI; +import jalview.api.FeatureColourI; +import jalview.api.PollableAlignCalcWorkerI; +import jalview.bin.Cache; +import jalview.datamodel.AlignmentAnnotation; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AnnotatedCollectionI; +import jalview.datamodel.Annotation; +import jalview.datamodel.ContiguousI; +import jalview.datamodel.Mapping; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.schemes.FeatureSettingsAdapter; +import jalview.util.ArrayUtils; +import jalview.util.MapList; +import jalview.util.MathUtils; +import jalview.util.Pair; +import jalview.workers.AlignCalcWorker; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.AbstractPollableTask; +import jalview.ws2.actions.BaseJob; +import jalview.ws2.actions.ServiceInputInvalidException; +import jalview.ws2.actions.api.JobI; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.Credentials; +import jalview.ws2.api.JobStatus; +import jalview.ws2.api.WebServiceJobHandle; +import jalview.ws2.client.api.AnnotationWebServiceClientI; +import jalview.ws2.helpers.DelegateJobEventListener; +import jalview.ws2.helpers.TaskEventSupport; + +import static java.util.Objects.requireNonNullElse; + +public class AnnotationTask implements TaskI +{ + private final long uid = MathUtils.getUID(); + + private AnnotationWebServiceClientI client; + + private final AnnotationAction action; + + private final List args; + + private final Credentials credentials; + + private final AlignViewportI viewport; + + private final TaskEventSupport eventHandler; + + private JobStatus taskStatus = null; + + private AlignCalcWorkerAdapter worker = null; + + private List jobs = Collections.emptyList(); + + private AnnotationResult result = null; + + private DelegateJobEventListener jobEventHandler; + + private class AlignCalcWorkerAdapter extends AlignCalcWorker + implements PollableAlignCalcWorkerI + { + private boolean restarting = false; + + AlignCalcWorkerAdapter(AlignCalcManagerI2 calcMan) + { + super(viewport, null); + this.calcMan = calcMan; + } + + String getServiceName() + { + return action.getWebService().getName(); + } + + @Override + public void startUp() throws Throwable + { + if (alignViewport.isClosed()) + { + stop(); + throw new IllegalStateException("Starting annotation for closed viewport"); + } + if (restarting) + eventHandler.fireTaskRestarted(); + else + restarting = true; + jobs = Collections.emptyList(); + try + { + jobs = prepare(); + } catch (ServiceInputInvalidException e) + { + setStatus(JobStatus.INVALID); + eventHandler.fireTaskException(e); + throw e; + } + setStatus(JobStatus.READY); + eventHandler.fireTaskStarted(jobs); + for (var job : jobs) + { + job.addPropertyChagneListener(jobEventHandler); + } + try + { + startJobs(); + } catch (IOException e) + { + eventHandler.fireTaskException(e); + cancelJobs(); + setStatus(JobStatus.SERVER_ERROR); + throw e; + } + setStatus(JobStatus.SUBMITTED); + } + + @Override + public boolean poll() throws Throwable + { + boolean done = AnnotationTask.this.poll(); + updateGlobalStatus(); + if (done) + { + retrieveAndProcessResult(); + eventHandler.fireTaskCompleted(result); + } + return done; + } + + private void retrieveAndProcessResult() throws IOException + { + result = retrieveResult(); + updateOurAnnots(result.annotations); + if (result.transferFeatures) + { + final var featureColours = result.featureColours; + final var featureFilters = result.featureFilters; + viewport.applyFeaturesStyle(new FeatureSettingsAdapter() + { + @Override + public FeatureColourI getFeatureColour(String type) + { + return featureColours.get(type); + } + + @Override + public FeatureMatcherSetI getFeatureFilters(String type) + { + return featureFilters.get(type); + } + + @Override + public boolean isFeatureDisplayed(String type) + { + return featureColours.containsKey(type); + } + }); + } + } + + @Override + public void updateAnnotation() + { + var job = jobs.size() > 0 ? jobs.get(0) : null; + if (!calcMan.isWorking(this) && job != null) + { + var ret = updateResultAnnotation(job, job.returnedAnnotations); + updateOurAnnots(ret.get0()); + } + } + + private void updateOurAnnots(List newAnnots) + { + List oldAnnots = requireNonNullElse(ourAnnots, Collections.emptyList()); + ourAnnots = newAnnots; + AlignmentI alignment = viewport.getAlignment(); + for (AlignmentAnnotation an : oldAnnots) + { + if (!newAnnots.contains(an)) + { + alignment.deleteAnnotation(an); + } + } + oldAnnots.clear(); + for (AlignmentAnnotation an : ourAnnots) + { + viewport.getAlignment().validateAnnotation(an); + } + } + + @Override + public void cancel() + { + cancelJobs(); + } + + void stop() + { + super.abortAndDestroy(); + } + + @Override + public void done() + { + for (var job : jobs) + { + if (job.isInputValid() && !job.isCompleted()) + { + /* if done was called but job is not completed then it + * must have been stopped by an exception */ + job.setStatus(JobStatus.SERVER_ERROR); + } + } + updateGlobalStatus(); + // dispose of unfinished jobs just in case + cancelJobs(); + } + } + + public AnnotationTask(AnnotationWebServiceClientI client, + AnnotationAction action, List args, Credentials credentials, + AlignViewportI viewport, + TaskEventListener eventListener) + { + this.client = client; + this.action = action; + this.args = args; + this.credentials = credentials; + this.viewport = viewport; + this.eventHandler = new TaskEventSupport<>(this, eventListener); + this.jobEventHandler = new DelegateJobEventListener<>(this.eventHandler); + } + + @Override + public long getUid() + { + return uid; + } + + public void start(AlignCalcManagerI2 calcManager) + { + if (this.worker != null) + throw new IllegalStateException("task already started"); + this.worker = new AlignCalcWorkerAdapter(calcManager); + if (taskStatus != JobStatus.CANCELLED) + { + List oldWorkers = calcManager.getWorkersOfClass( + AlignCalcWorkerAdapter.class); + for (var worker : oldWorkers) + { + if (action.getWebService().getName().equalsIgnoreCase( + ((AlignCalcWorkerAdapter) worker).getServiceName())) + { + // remove interactive workers for the same service. + calcManager.removeWorker(worker); + calcManager.cancelWorker(worker); + } + } + if (action.getWebService().isInteractive()) + calcManager.registerWorker(worker); + else + calcManager.startWorker(worker); + } + } + + /* + * The following methods are mostly copied from the {@link AbstractPollableTask} + * TODO: move common functionality to a base class + */ + @Override + public JobStatus getStatus() + { + return taskStatus; + } + + private void setStatus(JobStatus status) + { + if (this.taskStatus != status) + { + this.taskStatus = status; + eventHandler.fireTaskStatusChanged(status); + } + } + + private void updateGlobalStatus() + { + int precedence = -1; + for (BaseJob job : jobs) + { + JobStatus status = job.getStatus(); + int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status); + if (precedence < jobPrecedence) + precedence = jobPrecedence; + } + if (precedence >= 0) + { + setStatus(JobStatus.statusPrecedence[precedence]); + } + } + + @Override + public List getSubJobs() + { + return jobs; + } + + /** + * Create and return a list of annotation jobs from the current state of the + * viewport. Returned job are not started by this method and should be stored + * in a field and started separately. + * + * @return list of annotation jobs + * @throws ServiceInputInvalidException + * input data is not valid + */ + private List prepare() throws ServiceInputInvalidException + { + AlignmentI alignment = viewport.getAlignment(); + if (alignment == null || alignment.getWidth() <= 0 || + alignment.getSequences() == null) + throw new ServiceInputInvalidException("Alignment does not contain sequences"); + if (alignment.isNucleotide() && !action.doAllowNucleotide()) + throw new ServiceInputInvalidException( + action.getFullName() + " does not allow nucleotide sequences"); + if (!alignment.isNucleotide() && !action.doAllowProtein()) + throw new ServiceInputInvalidException( + action.getFullName() + " does not allow protein sequences"); + boolean bySequence = !action.isAlignmentAnalysis(); + AnnotatedCollectionI inputSeqs = bySequence ? viewport.getSelectionGroup() : null; + if (inputSeqs == null || inputSeqs.getWidth() <= 0 || + inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1) + inputSeqs = alignment; + boolean submitGaps = action.isAlignmentAnalysis(); + boolean requireAligned = action.getRequireAlignedSequences(); + boolean filterSymbols = action.getFilterSymbols(); + int minSize = action.getMinSequences(); + AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence, + submitGaps, requireAligned, filterSymbols, minSize); + if (!job.isInputValid()) + { + job.setStatus(JobStatus.INVALID); + throw new ServiceInputInvalidException("Annotation job has invalid input"); + } + job.setStatus(JobStatus.READY); + return List.of(job); + } + + private void startJobs() throws IOException + { + for (BaseJob job : jobs) + { + if (job.isInputValid() && job.getStatus() == JobStatus.READY) + { + var serverJob = client.submit(job.getInputSequences(), + args, credentials); + job.setServerJob(serverJob); + job.setStatus(JobStatus.SUBMITTED); + } + } + } + + private boolean poll() throws IOException + { + boolean allDone = true; + for (BaseJob job : jobs) + { + if (job.isInputValid() && !job.getStatus().isDone()) + { + WebServiceJobHandle serverJob = job.getServerJob(); + job.setStatus(client.getStatus(serverJob)); + job.setLog(client.getLog(serverJob)); + job.setErrorLog(client.getErrorLog(serverJob)); + } + allDone &= job.isCompleted(); + } + return allDone; + } + + private AnnotationResult retrieveResult() throws IOException + { + final Map featureColours = new HashMap<>(); + final Map featureFilters = new HashMap<>(); + var job = jobs.get(0); + List returnedAnnot = client.attachAnnotations( + job.getServerJob(), job.getInputSequences(), featureColours, + featureFilters); + /* TODO + * copy over each annotation row returned and also defined on each + * sequence, excluding regions not annotated due to gapMap/column + * visibility */ + + // update calcId if it is not already set on returned annotation + for (AlignmentAnnotation annot : returnedAnnot) + { + if (annot.getCalcId() == null || annot.getCalcId().isEmpty()) + { + annot.setCalcId(action.getFullName()); + } + annot.autoCalculated = action.isAlignmentAnalysis() && + action.getWebService().isInteractive(); + } + job.returnedAnnotations = returnedAnnot; + job.featureColours = featureColours; + job.featureFilters = featureFilters; + var ret = updateResultAnnotation(job, returnedAnnot); + var annotations = ret.get0(); + var transferFeatures = ret.get1(); + return new AnnotationResult(annotations, transferFeatures, featureColours, + featureFilters); + } + + private Pair, Boolean> updateResultAnnotation( + AnnotationJob job, List annotations) + { + List newAnnots = new ArrayList<>(); + // update graphGroup for all annotation + /* find a graphGroup greater than any existing one, could be moved + * to Alignment#getNewGraphGroup() - returns next unused graph group */ + int graphGroup = 1; + if (viewport.getAlignment().getAlignmentAnnotation() != null) + { + for (var ala : viewport.getAlignment().getAlignmentAnnotation()) + { + graphGroup = Math.max(graphGroup, ala.graphGroup); + } + } + // update graphGroup in the annotation rows returned form service' + /* TODO: look at sequence annotation rows and update graph groups in the + * case of reference annotation */ + for (AlignmentAnnotation ala : annotations) + { + if (ala.graphGroup > 0) + ala.graphGroup += graphGroup; + SequenceI aseq = null; + // transfer sequence refs and adjust gapMap + if (ala.sequenceRef != null) + { + aseq = job.seqNames.get(ala.sequenceRef.getName()); + } + ala.sequenceRef = aseq; + + Annotation[] resAnnot = ala.annotations; + boolean[] gapMap = job.gapMap; + Annotation[] gappedAnnot = new Annotation[Math.max( + viewport.getAlignment().getWidth(), gapMap.length)]; + for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++) + { + if (gapMap.length > ap && !gapMap[ap]) + gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN); + else if (p < resAnnot.length) + gappedAnnot[ap] = resAnnot[p++]; + // is this loop exhaustive of resAnnot? + } + ala.annotations = gappedAnnot; + + AlignmentAnnotation newAnnot = viewport.getAlignment() + .updateFromOrCopyAnnotation(ala); + if (aseq != null) + { + aseq.addAlignmentAnnotation(newAnnot); + newAnnot.adjustForAlignment(); + AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith( + newAnnot, newAnnot.label, newAnnot.getCalcId()); + } + newAnnots.add(newAnnot); + } + + boolean transferFeatures = false; + for (SequenceI sq : job.getInputSequences()) + { + if (!sq.getFeatures().hasFeatures() && + (sq.getDBRefs() == null || sq.getDBRefs().size() == 0)) + continue; + transferFeatures = true; + SequenceI seq = job.seqNames.get(sq.getName()); + SequenceI dseq; + int start = job.start, end = job.end; + boolean[] gapMap = job.gapMap; + ContiguousI seqRange = seq.findPositions(start, end); + while ((dseq = seq).getDatasetSequence() != null) + { + seq = seq.getDatasetSequence(); + } + List sourceRange = new ArrayList<>(); + if (gapMap.length >= end) + { + int lastcol = start, col = start; + do + { + if (col == end || !gapMap[col]) + { + if (lastcol <= (col - 1)) + { + seqRange = seq.findPositions(lastcol, col); + sourceRange.add(seqRange); + } + lastcol = col + 1; + } + } while (col++ < end); + } + else + { + sourceRange.add(seq.findPositions(start, end)); + } + + int i = 0; + int sourceStartEnd[] = new int[sourceRange.size() * 2]; + for (ContiguousI range : sourceRange) + { + sourceStartEnd[i++] = range.getBegin(); + sourceStartEnd[i++] = range.getEnd(); + } + Mapping mp = new Mapping(new MapList( + sourceStartEnd, new int[] + { seq.getStart(), seq.getEnd() }, 1, 1)); + dseq.transferAnnotation(sq, mp); + } + + return new Pair<>(newAnnots, transferFeatures); + } + + @Override + public AnnotationResult getResult() + { + return result; + } + + @Override + public void cancel() + { + setStatus(JobStatus.CANCELLED); + if (worker != null) + { + worker.stop(); + } + cancelJobs(); + } + + public void cancelJobs() + { + for (BaseJob job : jobs) + { + if (!job.isCompleted()) + { + try + { + if (job.getServerJob() != null) + { + client.cancel(job.getServerJob()); + } + job.setStatus(JobStatus.CANCELLED); + } catch (IOException e) + { + Cache.log.error(String.format( + "failed to cancel job %s", job.getServerJob()), e); + } + } + } + } +} diff --git a/src/jalview/ws2/client/api/AnnotationWebServiceClientI.java b/src/jalview/ws2/client/api/AnnotationWebServiceClientI.java new file mode 100644 index 0000000..a6370ea --- /dev/null +++ b/src/jalview/ws2/client/api/AnnotationWebServiceClientI.java @@ -0,0 +1,16 @@ +package jalview.ws2.client.api; + +import jalview.ws2.actions.annotation.AnnotationProviderI; + +/** + * A mixin interface used by annotation services combining + * {@link WebServiceClientI} and {@link AnnotationProviderI} functionality into + * one interface. Annotation services use this interface to issue queries to the + * server. + * + * @author mmwarowny + */ +public interface AnnotationWebServiceClientI extends WebServiceClientI, AnnotationProviderI +{ + +} -- 1.7.10.2