From: Mateusz Warowny Date: Tue, 23 Nov 2021 15:35:45 +0000 (+0100) Subject: JAL-3878 Add annotation operation and worker to the services. X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=fe057c5e0d0cabaa0a41a4ff76ea2920882599db;p=jalview.git JAL-3878 Add annotation operation and worker to the services. --- diff --git a/src/jalview/ws2/gui/AnnotationMenuBuilder.java b/src/jalview/ws2/gui/AnnotationMenuBuilder.java new file mode 100644 index 0000000..5ddaa19 --- /dev/null +++ b/src/jalview/ws2/gui/AnnotationMenuBuilder.java @@ -0,0 +1,181 @@ +package jalview.ws2.gui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +import jalview.gui.AlignFrame; +import jalview.gui.WsJobParameters; +import jalview.util.MessageManager; +import jalview.ws.params.ArgumentI; +import jalview.ws.params.ParamDatastoreI; +import jalview.ws.params.WsParamSetI; +import jalview.ws2.operations.AnnotationOperation; +import jalview.ws2.operations.AnnotationWorker; + +public class AnnotationMenuBuilder implements MenuEntryProviderI +{ + final AnnotationOperation operation; + + public AnnotationMenuBuilder(AnnotationOperation operation) + { + this.operation = operation; + } + + @Override + public void buildMenu(JMenu parent, AlignFrame frame) + { + if (operation.isInteractive()) + buildInteractiveMenu(parent, frame); + else + buildClassicMenu(parent, frame); + } + + protected void buildClassicMenu(JMenu parent, AlignFrame frame) + { + final var calcName = operation.getName(); + final var calcManager = frame.getViewport().getCalcManager(); + { + var item = new JMenuItem(MessageManager.formatMessage( + "label.calcname_with_default_settings", calcName)); + item.addActionListener((event) -> { + var worker = createWorker(Collections.emptyList(), frame); + calcManager.startWorker(worker); + }); + parent.add(item); + } + if (operation.hasParameters()) + { + var item = new JMenuItem( + MessageManager.getString("label.edit_settings_and_run")); + item.setToolTipText(MessageManager.getString( + "label.view_and_change_parameters_before_running_calculation")); + item.addActionListener((event) -> { + openEditParamsDialog(operation.getParamStore(), null, null) + .thenAcceptAsync((arguments) -> { + if (arguments != null) + { + var worker = createWorker(arguments, frame); + calcManager.startWorker(worker); + } + }); + }); + parent.add(item); + } + } + + protected void buildInteractiveMenu(JMenu parent, AlignFrame frame) + { + final var calcName = operation.getName(); + final var calcManager = frame.getViewport().getCalcManager(); + final var arguments = new ArrayList(); + final JCheckBoxMenuItem runItem; + { + // TODO use MessageManager and set tool tip text + runItem = new JCheckBoxMenuItem( + String.format("%s calculations", calcName)); + runItem.addActionListener((event) -> { + calcManager.removeWorkersForName(calcName); + var worker = createWorker(arguments, frame); + calcManager.registerWorker(worker); + }); + parent.add(runItem); + } + JMenuItem _editItem = null; + if (operation.hasParameters()) + { + // TODO use MessageManager and set tool tip text + _editItem = new JMenuItem( + String.format("Edit %s settings", calcName)); + _editItem.addActionListener((event) -> { + openEditParamsDialog(operation.getParamStore(), null, null) + .thenAcceptAsync((args) -> { + if (arguments != null) + { + arguments.clear(); + arguments.addAll(args); + calcManager.removeWorkersForName(calcName); + var worker = createWorker(arguments, frame); + calcManager.registerWorker(worker); + } + }); + }); + parent.add(_editItem); + } + final var editItem = _editItem; + + parent.addMenuListener(new MenuListener() + { + @Override + public void menuSelected(MenuEvent e) + { + var isNuc = frame.getViewport().getAlignment().isNucleotide(); + var menuEnabled = (isNuc && operation.isNucleotideOperation()) || + (!isNuc && operation.isProteinOperation()); + runItem.setEnabled(menuEnabled); + if (editItem != null) + editItem.setEnabled(menuEnabled); + boolean currentlyRunning = calcManager.getWorkersForName(calcName).size() > 0; + runItem.setSelected(currentlyRunning); + } + + @Override + public void menuDeselected(MenuEvent e) + { + } + + @Override + public void menuCanceled(MenuEvent e) + { + } + }); + } + + private CompletionStage> openEditParamsDialog( + ParamDatastoreI paramStore, WsParamSetI preset, + List arguments) + { + WsJobParameters jobParams; + if (preset == null && arguments != null && arguments.size() > 0) + jobParams = new WsJobParameters(paramStore, preset, arguments); + else + jobParams = new WsJobParameters(paramStore, preset, null); + if (preset != null) + { + jobParams.setName(MessageManager.getString( + "label.adjusting_parameters_for_calculation")); + } + var stage = jobParams.showRunDialog(); + return stage.thenApply((startJob) -> { + if (startJob) + { + if (jobParams.getPreset() == null) + { + return jobParams.getJobParams(); + } + else + { + return jobParams.getPreset().getArguments(); + } + } + else + { + return null; + } + }); + } + + private AnnotationWorker createWorker(List arguments, AlignFrame frame) + { + /* What is the purpose of AlignViewport and AlignmentViewPanel? */ + return new AnnotationWorker(operation, arguments, frame, frame); + } + +} diff --git a/src/jalview/ws2/gui/ProgressBarUpdater.java b/src/jalview/ws2/gui/ProgressBarUpdater.java new file mode 100644 index 0000000..89deb06 --- /dev/null +++ b/src/jalview/ws2/gui/ProgressBarUpdater.java @@ -0,0 +1,50 @@ +package jalview.ws2.gui; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import jalview.gui.IProgressIndicator; +import jalview.ws2.WSJob; +import jalview.ws2.WSJobStatus; + +/** + * Monitors annotation jobs' status and updates progress indicators accordingly. + * + * @author mmwarowny + * + */ +public class ProgressBarUpdater implements PropertyChangeListener +{ + private IProgressIndicator progressIndicator; + + public ProgressBarUpdater(IProgressIndicator progressIndicator) + { + this.progressIndicator = progressIndicator; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) + { + switch (evt.getPropertyName()) + { + case "status": + statusChanged(evt); + break; + } + } + + private void statusChanged(PropertyChangeEvent evt) + { + var job = (WSJob) evt.getSource(); + var oldStatus = (WSJobStatus) evt.getOldValue(); + var newStatus = (WSJobStatus) evt.getNewValue(); + if (!oldStatus.isSubmitted() && newStatus.isSubmitted()) + { + progressIndicator.setProgressBar(job.getServiceName(), job.getUid()); + } + if (newStatus.isDone() || newStatus.isCancelled()) + { + progressIndicator.removeProgressBar(job.getUid()); + } + } +} diff --git a/src/jalview/ws2/operations/AnnotationOperation.java b/src/jalview/ws2/operations/AnnotationOperation.java new file mode 100644 index 0000000..14e21f3 --- /dev/null +++ b/src/jalview/ws2/operations/AnnotationOperation.java @@ -0,0 +1,41 @@ +package jalview.ws2.operations; + +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.WSJob; +import jalview.ws2.WebServiceI; +import jalview.ws2.gui.AnnotationMenuBuilder; +import jalview.ws2.gui.MenuEntryProviderI; + +public class AnnotationOperation extends AbstractOperation +{ + @FunctionalInterface + public static interface AnnotationResultSupplier + { + List attachAnnotations(WSJob job, + List seqs, Map featureColours, + Map featureFilters) throws IOException; + } + + AnnotationResultSupplier annotationSupplier; + + public AnnotationOperation(WebServiceI service, String typeName, + AnnotationResultSupplier annotationSupplier) + { + super(service, typeName); + this.annotationSupplier = annotationSupplier; + } + + @Override + public MenuEntryProviderI getMenuBuilder() + { + return new AnnotationMenuBuilder(this); + } + +} diff --git a/src/jalview/ws2/operations/AnnotationWorker.java b/src/jalview/ws2/operations/AnnotationWorker.java new file mode 100644 index 0000000..22aebf4 --- /dev/null +++ b/src/jalview/ws2/operations/AnnotationWorker.java @@ -0,0 +1,602 @@ +package jalview.ws2.operations; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import jalview.analysis.AlignSeq; +import jalview.analysis.AlignmentAnnotationUtils; +import jalview.analysis.SeqsetUtils; +import jalview.api.AlignCalcManagerI2; +import jalview.api.AlignmentViewPanel; +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.Sequence; +import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.gui.AlignFrame; +import jalview.gui.AlignViewport; +import jalview.gui.IProgressIndicator; +import jalview.gui.IProgressIndicatorHandler; +import jalview.schemes.FeatureSettingsAdapter; +import jalview.schemes.ResidueProperties; +import jalview.util.MapList; +import jalview.ws.params.ArgumentI; +import jalview.ws2.WSJob; +import jalview.ws2.WSJobStatus; +import jalview.ws2.gui.ProgressBarUpdater; + +import static java.lang.String.format; + +public class AnnotationWorker extends AbstractWorker + implements PollableAlignCalcWorkerI +{ + AnnotationOperation operation; + + private WSJobList jobs = new WSJobList<>(); + + AnnotationJob job; + + private List args = Collections.emptyList(); + + private AlignViewport viewport; + + private AlignmentViewPanel alignPanel; + + private IProgressIndicator progressIndicator; + + private AlignFrame frame; + + private AlignCalcManagerI2 calcMan; + + protected List ourAnnots; + + // TODO: convert to bitset + private boolean[] gapMap = new boolean[0]; + + private class JobInput + { + List sequences; + + Map seqNames; + + int start, end; + } + + public class AnnotationJob extends WSJob + { + private List sequences; + + private int start, end; + + private Map seqNames; + + private boolean transferSequenceFeatures = false; + + private AnnotationJob(String serviceProvider, String serviceName, + String hostName) + { + super(serviceProvider, serviceName, hostName); + } + + private void setInput(JobInput input) + { + this.sequences = input.sequences; + this.start = input.start; + this.end = input.end; + this.seqNames = input.seqNames; + } + } + + public AnnotationWorker(AnnotationOperation operation, + List args, AlignFrame frame, + IProgressIndicator progressIndicator) + { + this.operation = operation; + this.args = args; + this.viewport = frame.getCurrentView(); + this.alignPanel = frame.alignPanel; + this.progressIndicator = progressIndicator; + this.frame = frame; + this.calcMan = viewport.getCalcManager(); + } + + @Override + public String getCalcName() + { + return operation.getName(); + } + + @Override + public Operation getOperation() + { + return operation; + } + + @Override + public WSJobList getJobs() + { + return jobs; + } + + @Override + public boolean involves(AlignmentAnnotation annot) + { + return ourAnnots != null && ourAnnots.contains(annot); + } + + @Override + public void updateAnnotation() + { + updateResultAnnotation(ourAnnots); + } + + @Override + public void removeAnnotation() + { + if (ourAnnots != null && viewport != null) + { + AlignmentI alignment = viewport.getAlignment(); + synchronized (ourAnnots) + { + for (AlignmentAnnotation aa : ourAnnots) + { + alignment.deleteAnnotation(aa, true); + } + } + ourAnnots.clear(); + } + } + + @Override + public boolean isDeletable() + { + return true; + } + + @Override + public void startUp() throws IOException + { + if (viewport.isClosed()) + { + return; + } + + /* What "bySequence" means in this context and + * what is the SelectionGroup and why is it only relevant when + * not dealing with alignment analysis? */ + boolean bySequence = !operation.isAlignmentAnalysis(); + var input = prepareInput(viewport.getAlignment(), + bySequence ? viewport.getSelectionGroup() : null); + if (input.sequences == null || !checkInputSequencesValid(input.sequences)) + { + Cache.log.info("Sequences for analysis service were null"); + return; + } + Cache.log.debug(format("submitting %d sequences to %s", + input.sequences.size(), operation.getName())); + job = new AnnotationJob(operation.getWebService().getProviderName(), + operation.getWebService().getName(), operation.getWebService().getHostName()); + jobs.add(job); + listeners.fireJobCreated(job); + job.setInput(input); + // Should this part be moved out of this class to one of the gui + // classes? + if (progressIndicator != null) + { + job.addPropertyChangeListener("status", new ProgressBarUpdater(progressIndicator)); + progressIndicator.registerHandler(job.getUid(), new IProgressIndicatorHandler() + { + @Override + public boolean cancelActivity(long id) + { + calcMan.cancelWorker(AnnotationWorker.this); + return true; + } + + @Override + public boolean canCancel() + { + return isDeletable(); + } + }); + } + String jobId = operation.getWebService().submit(input.sequences, args); + job.setJobId(jobId); + Cache.log.debug(format("Service %s: submitted job id %s", + operation.getHostName(), jobId)); + listeners.fireWorkerStarted(); + } + + private JobInput prepareInput(AlignmentI alignment, + AnnotatedCollectionI inputSeqs) + { + if (alignment == null || alignment.getWidth() <= 0 || + alignment.getSequences() == null) + return null; + if (alignment.isNucleotide() && !operation.isNucleotideOperation()) + return null; + if (!alignment.isNucleotide() && !operation.isProteinOperation()) + return null; + if (inputSeqs == null || inputSeqs.getWidth() <= 0 || + inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1) + inputSeqs = alignment; + + List seqs = new ArrayList<>(); + final boolean submitGaps = operation.isAlignmentAnalysis(); + final int minlen = 10; + int ln = -1; // I think this variable is redundant + Map seqNames = null; + if (!operation.isAlignmentAnalysis()) + seqNames = new HashMap<>(); + 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; + // is it trying to find the length of a sequence excluding gaps? + if (!operation.isAlignmentAnalysis()) + // why starting at positions to the right from the end/start? + 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()); + if (seqNames != null) + { + seqNames.put(newName, sq); + } + SequenceI seq; + if (submitGaps) + { + seq = new Sequence(newName, sq.getSequenceAsString()); + seqs.add(seq); + if (gapMap == null || gapMap.length < seq.getLength()) + { + boolean[] tg = gapMap; + gapMap = new boolean[seq.getLength()]; + System.arraycopy(tg, 0, gapMap, 0, tg.length); + for (int p = tg.length; p < gapMap.length; p++) + { + gapMap[p] = false; // init as a gap + } + } + for (int apos : sq.gapMap()) + { + char sqc = sq.getCharAt(apos); + boolean isStandard = sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20 + : ResidueProperties.nucleotideIndex[sqc] < 5; + if (!operation.getFilterNonStandardSymbols() || isStandard) + { + gapMap[apos] = true; + } + } + } + else + { + // TODO: add ability to exclude hidden regions + String sqstring = sq.getSequenceAsString(start, end + 1); + seq = new Sequence(newName, + AlignSeq.extractGaps(jalview.util.Comparison.GapChars, sqstring)); + seqs.add(seq); + // for annotation need to also record map to sequence start/end + // position in range + // then transfer back to original sequence on return. + } + ln = Integer.max(seq.getLength(), ln); + } + } + if (operation.getNeedsAlignedSequences() && submitGaps) + { + int realw = 0; + for (int i = 0; i < gapMap.length; i++) + { + if (gapMap[i]) + { + realw++; + } + } + // try real hard to return something submittable + // TODO: some of AAcon measures need a minimum of two or three amino + // acids at each position, and AAcon doesn't gracefully degrade. + for (int p = 0; p < seqs.size(); p++) + { + SequenceI sq = seqs.get(p); + // strip gapped columns + char[] padded = new char[realw]; + char[] orig = sq.getSequence(); + for (int i = 0, pp = 0; i < realw; pp++) + { + if (gapMap[pp]) + { + if (orig.length > pp) + { + padded[i++] = orig[pp]; + } + else + { + padded[i++] = '-'; + } + } + } + seqs.set(p, new Sequence(sq.getName(), new String(padded))); + } + } + var inp = new JobInput(); + inp.sequences = seqs; + inp.seqNames = seqNames; + inp.start = start; + inp.end = end; + return inp; + } + + private boolean checkInputSequencesValid(List sequences) + { + int nvalid = 0; + boolean allowProtein = operation.isProteinOperation(), + allowNucleotides = operation.isNucleotideOperation(); + for (SequenceI sq : sequences) + { + if (sq.getStart() <= sq.getEnd() && + (sq.isProtein() ? allowProtein : allowNucleotides)) + { + nvalid++; + } + } + return nvalid >= operation.getMinSequences(); + } + + @Override + public void cancel() + { + try + { + operation.getWebService().cancel(job); + } catch (IOException e) + { + Cache.log.error(format("Failed to cancel job %s.", job), e); + } + } + + @Override + public void done() + { + Cache.log.debug(format("Polling loop exited, job %s is %s", job, job.getStatus())); + if (!job.getStatus().isCompleted()) + { + return; + } + var featureRenderer = alignPanel.cloneFeatureRenderer(); + Map featureColours = new HashMap<>(); + Map featureFilters = new HashMap<>(); + List returnedAnnot = null; + try + { + returnedAnnot = operation.annotationSupplier.attachAnnotations( + job, job.sequences, featureColours, featureFilters); + } catch (Exception e) + { + if (!operation.getWebService().handleCollectionError(job, e)) + { + Cache.log.error("Couldn't get annotations for job.", e); + job.setStatus(WSJobStatus.SERVER_ERROR); + listeners.firePollException(job, e); + } + return; + } + Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows" + : ("" + returnedAnnot.size()))); + Cache.log.debug( + String.format("There were %s feature colours and %s filters defined", + featureColours.size(), featureFilters.size())); + if (returnedAnnot != null) + { + for (AlignmentAnnotation aa : returnedAnnot) + { + // assume that any CalcIds already set + if (aa.getCalcId() == null || aa.getCalcId().equals("")) + { + aa.setCalcId(operation.getName()); + } + // autocalculated annotation are created by interactive alignment + // analysis services + aa.autoCalculated = operation.isAlignmentAnalysis() + && operation.isInteractive(); + } + } + updateResultAnnotation(returnedAnnot); + if (job.transferSequenceFeatures) + { + Cache.log.debug(format("Updating feature display settings and transferring" + + "features from job %s at %s", job, operation.getHostName())); + 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); + } + }); + if (frame.alignPanel == alignPanel) + { + viewport.setShowSequenceFeatures(true); + frame.setMenusForViewport(); + } + } + Cache.log.debug("Annotation service task finished."); + } + + // What is the purpose of this method? + // When is it called (apart from the above)? + private void updateResultAnnotation(List annotations) + { + var currentAnnotations = Objects.requireNonNullElse( + viewport.getAlignment().getAlignmentAnnotation(), + new AlignmentAnnotation[0]); + List newAnnots = new ArrayList<>(); + // what is the graph group and why starting from 1? + int graphGroup = 1; + for (AlignmentAnnotation alna : currentAnnotations) + { + graphGroup = Integer.max(graphGroup, alna.graphGroup); + } + for (AlignmentAnnotation ala : annotations) + { + if (ala.graphGroup > 0) + { + ala.graphGroup += graphGroup; + } + + // stores original sequence, in what case it ends up as null? + SequenceI aseq = null; + if (ala.sequenceRef != null) + { + SequenceI seq = job.seqNames.get(ala.sequenceRef.getName()); + aseq = seq; + while (seq.getDatasetSequence() != null) + { + seq = seq.getDatasetSequence(); + } + } + Annotation[] resAnnot = ala.annotations; + Annotation[] gappedAnnot = new Annotation[Math + .max(viewport.getAlignment().getWidth(), gapMap.length)]; + // is it adding gaps which were previously removed to the annotation? + for (int p = 0, ap = job.start; ap < gappedAnnot.length; ap++) + { + if (gapMap != null && gapMap.length > ap && !gapMap[ap]) + { + gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN); + } + else if (p < resAnnot.length) + { + gappedAnnot[ap] = resAnnot[p++]; + } + } + // replacing sequence with the original one? + ala.sequenceRef = aseq; + 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); + } + + for (SequenceI sq : job.sequences) + { + // what are DBRefs? why are they relevant here? + if (!sq.getFeatures().hasFeatures() && + (sq.getDBRefs() == null || sq.getDBRefs().size() == 0)) + { + continue; + } + job.transferSequenceFeatures = true; + SequenceI seq = job.seqNames.get(sq.getName()); + SequenceI dseq; + ContiguousI seqRange = seq.findPositions(job.start, job.end); + + while ((dseq = seq).getDatasetSequence() != null) + { + seq = seq.getDatasetSequence(); + } + List sourceRange = new ArrayList<>(); + if (gapMap != null && gapMap.length > job.end) + { + int lastcol = job.start, col = job.start; + do + { + if (col == job.end || !gapMap[col]) + { + if (lastcol <= col - 1) + { + seqRange = seq.findPositions(lastcol, col); + sourceRange.add(seqRange); + } + lastcol = col + 1; + } + } while (++col < job.end); + } + else + { + sourceRange.add(seq.findPositions(job.start, job.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); + } + updateOurAnnots(newAnnots); + } + + protected void updateOurAnnots(List annots) + { + List our = ourAnnots; + ourAnnots = Collections.synchronizedList(annots); + AlignmentI alignment = viewport.getAlignment(); + if (our != null) + { + if (our.size() > 0) + { + for (AlignmentAnnotation an : our) + { + if (!ourAnnots.contains(an)) + { + // remove the old annotation + alignment.deleteAnnotation(an); + } + } + } + our.clear(); + } + // validate rows and update Alignment state + synchronized (ourAnnots) + { + for (AlignmentAnnotation an : ourAnnots) + { + viewport.getAlignment().validateAnnotation(an); + } + } + // TODO: may need a menu refresh after this + // af.setMenusForViewport(); + alignPanel.adjustAnnotationHeight(); + } +} diff --git a/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java b/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java index b6004fc..cda6702 100644 --- a/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java +++ b/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java @@ -50,10 +50,10 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI } catch (MalformedURLException e) { Cache.log.warn("Problem whilst trying to make a URL from '" - + Objects.toString(url, "") + "'. " - + "This was probably due to malformed comma-separated-list " - + "in the " + SLIVKA_HOST_URLS - + " entry of ${HOME}/.jalview_properties"); + + Objects.toString(url, "") + "'. " + + "This was probably due to malformed comma-separated-list " + + "in the " + SLIVKA_HOST_URLS + + " entry of ${HOME}/.jalview_properties"); Cache.log.debug("Exception occurred while reading url list", e); } } @@ -89,7 +89,7 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI } catch (IOException e) { Cache.log.error("Slivka could not retrieve services list from " + url, - e); + e); return STATUS_INVALID; } } @@ -129,10 +129,10 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI public CompletableFuture startDiscoverer() { CompletableFuture task = CompletableFuture - .supplyAsync(() -> { - reloadServices(); - return SlivkaWSDiscoverer.this; - }); + .supplyAsync(() -> { + reloadServices(); + return SlivkaWSDiscoverer.this; + }); task.thenRun(() -> fireOperationsChanged(getOperations())); discoveryTasks.add(task); return task; @@ -142,7 +142,7 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI { Cache.log.info("Reloading Slivka services"); fireOperationsChanged(Collections.emptyList()); - ArrayList allOperations= new ArrayList<>(); + ArrayList allOperations = new ArrayList<>(); for (String url : getUrls()) { SlivkaClient client = new SlivkaClient(url); @@ -163,23 +163,26 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI { String[] path = classifier.split("\\s*::\\s*"); if (path.length >= 3 && path[0].toLowerCase().equals("operation") - && path[1].toLowerCase().equals("analysis")) + && path[1].toLowerCase().equals("analysis")) { switch (path[path.length - 1].toLowerCase()) { case "rna secondary structure prediction": - op = new OperationStub(webService, "Secondary Structure Prediction"); + op = new AnnotationOperation(webService, + "Secondary Structure Prediction", webService::attachAnnotations); op.setInteractive(true); op.setAlignmentAnalysis(true); op.setProteinOperation(false); break; case "sequence alignment analysis (conservation)": - op = new OperationStub(webService, "Conservation"); + op = new AnnotationOperation(webService, "Conservation", + webService::attachAnnotations); op.setAlignmentAnalysis(true); op.setInteractive(true); break; case "protein sequence analysis": - op = new OperationStub(webService, "Protein Disorder"); + op = new AnnotationOperation(webService, "Protein Disorder", + webService::attachAnnotations); break; case "multiple sequence alignment": op = new AlignmentOperation(webService, webService::getAlignment); @@ -191,7 +194,8 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI } } } - if (op != null) { + if (op != null) + { allOperations.add(op); } } diff --git a/src/jalview/ws2/slivka/SlivkaWebService.java b/src/jalview/ws2/slivka/SlivkaWebService.java index bf61ca6..9078013 100644 --- a/src/jalview/ws2/slivka/SlivkaWebService.java +++ b/src/jalview/ws2/slivka/SlivkaWebService.java @@ -1,5 +1,6 @@ package jalview.ws2.slivka; +import static java.lang.String.format; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -10,14 +11,17 @@ import java.util.Collection; import java.util.EnumMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import jalview.api.AlignViewportI; +import jalview.api.FeatureColourI; import jalview.bin.Cache; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.io.AnnotationFile; import jalview.io.DataSourceType; import jalview.io.FeaturesFile; @@ -239,60 +243,49 @@ public class SlivkaWebService implements WebServiceI return null; } - public FeaturesFile getFeaturesFile(WSJob job, - List dataset, AlignViewportI viewport) throws IOException + public List attachAnnotations(WSJob job, + List dataset, Map featureColours, + Map featureFilters) throws IOException { + RemoteFile annotFile = null; + RemoteFile featFile = null; + var slivkaJob = client.getJob(job.getJobId()); Collection files = slivkaJob.getResults(); for (RemoteFile f : files) { - if (f.getMediaType().equals("application/jalview-features")) - { - return new FeaturesFile(f.getContentUrl().toString(), DataSourceType.URL); - } + if (f.getMediaType().equals("application/jalview-annotations")) + annotFile = f; + else if (f.getMediaType().equals("application/jalview-features")) + featFile = f; } - return null; - } + Alignment aln = new Alignment(dataset.toArray(new SequenceI[0])); - public List getAnnotations(WSJob job, - List dataset, AlignViewportI viewport) throws IOException - { - var slivkaJob = client.getJob(job.getJobId()); - Collection files = slivkaJob.getResults(); - for (RemoteFile f : files) + boolean annotPresent = annotFile != null; + if (annotFile != null) { - if (f.getMediaType().equals("application/jalview-annotations")) - { - Alignment aln = new Alignment(dataset.toArray(new SequenceI[0])); - AnnotationFile af = new AnnotationFile(); - boolean valid = af.readAnnotationFileWithCalcId(aln, service.getId(), - f.getContentUrl().toString(), DataSourceType.URL); - if (valid) - { - return Arrays.asList(aln.getAlignmentAnnotation()); - } - else - { - throw new IOException("Unable to read annotations from file " + - f.getContentUrl().toString()); - } - } + AnnotationFile af = new AnnotationFile(); + annotPresent = af.readAnnotationFileWithCalcId( + aln, service.getId(), annotFile.getContentUrl().toString(), + DataSourceType.URL); } - return null; - } + if (annotPresent) + Cache.log.debug(format("Annotation file loaded %s", annotFile)); + else + Cache.log.debug(format("No annotations loaded from %s", annotFile)); - public JPredFile getPrediction(WSJob job, List dataset, - AlignViewportI viewport) throws IOException - { - Collection files = client.getJob(job.getJobId()).getResults(); - for (RemoteFile f : files) + boolean featPresent = featFile != null; + if (featFile != null) { - if (f.getLabel().equals("concise")) - { - return new JPredFile(f.getContentUrl(), DataSourceType.URL); - } + FeaturesFile ff = new FeaturesFile(featFile.getContentUrl().toString(), + DataSourceType.URL); + featPresent = ff.parse(aln, featureColours, true); } - return null; + if (featPresent) + Cache.log.debug(format("Features file loaded %s", featFile)); + else + Cache.log.debug(format("No features loaded from %s", annotFile)); + return Arrays.asList(aln.getAlignmentAnnotation()); } @Override