From: Mateusz Warowny Date: Mon, 22 Nov 2021 13:58:34 +0000 (+0100) Subject: JAL-3878 Add multiple sequence operation. X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=f53c2d7d680de63a6721371814145ffc1b14e034;p=jalview.git JAL-3878 Add multiple sequence operation. --- diff --git a/src/jalview/ws2/gui/AlignmentMenuBuilder.java b/src/jalview/ws2/gui/AlignmentMenuBuilder.java new file mode 100644 index 0000000..b29bde5 --- /dev/null +++ b/src/jalview/ws2/gui/AlignmentMenuBuilder.java @@ -0,0 +1,206 @@ +package jalview.ws2.gui; + +import static java.lang.String.format; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.ToolTipManager; + +import jalview.datamodel.AlignmentView; +import jalview.gui.AlignFrame; +import jalview.gui.AlignViewport; +import jalview.gui.JvSwingUtils; +import jalview.gui.WebserviceInfo; +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.PollingTaskExecutor; +import jalview.ws2.operations.AlignmentOperation; +import jalview.ws2.operations.AlignmentWorker; + +public class AlignmentMenuBuilder implements MenuEntryProviderI +{ + AlignmentOperation operation; + + public AlignmentMenuBuilder(AlignmentOperation operation) + { + this.operation = operation; + } + + public void buildMenu(JMenu parent, AlignFrame frame) + { + if (operation.canSubmitGaps()) + { + var alignSubmenu = new JMenu(operation.getName()); + buildMenu(alignSubmenu, frame, false); + parent.add(alignSubmenu); + var realignSubmenu = new JMenu(MessageManager.formatMessage( + "label.realign_with_params", operation.getName())); + realignSubmenu.setToolTipText(MessageManager + .getString("label.align_sequences_to_existing_alignment")); + buildMenu(realignSubmenu, frame, true); + parent.add(realignSubmenu); + } + else + { + buildMenu(parent, frame, false); + } + } + + protected void buildMenu(JMenu parent, AlignFrame frame, + boolean submitGaps) + { + final String action = submitGaps ? "Align" : "Realign"; + final var calcName = operation.getName(); + + { + var item = new JMenuItem(MessageManager.formatMessage( + "label.calcname_with_default_settings", calcName)); + item.setToolTipText(MessageManager + .formatMessage("label.action_with_default_settings", action)); + item.addActionListener((event) -> { + final AlignmentView msa = frame.gatherSequencesForAlignment(); + if (msa != null) + { + startWorker(frame, msa, Collections.emptyList(), submitGaps); + } + }); + 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_alignment")); + item.addActionListener((event) -> { + AlignmentView msa = frame.gatherSequencesForAlignment(); + if (msa != null) + { + openEditParamsDialog(operation.getParamStore(), null, null) + .thenAcceptAsync((arguments) -> { + if (arguments != null) + { + startWorker(frame, msa, arguments, submitGaps); + } + }); + } + }); + parent.add(item); + } + + var presets = operation.getParamStore().getPresets(); + if (presets != null && presets.size() > 0) + { + final var presetList = new JMenu(MessageManager + .formatMessage("label.run_with_preset_params", calcName)); + final var showToolTipFor = ToolTipManager.sharedInstance() + .getDismissDelay(); + for (final var preset : presets) + { + var item = new JMenuItem(preset.getName()); + final int QUICK_TOOLTIP = 1500; + item.addMouseListener(new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent e) + { + ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP); + } + + @Override + public void mouseExited(MouseEvent e) + { + ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor); + } + }); + String tooltip = JvSwingUtils.wrapTooltip(true, + format("%s
%s", + MessageManager.getString( + preset.isModifiable() ? "label.user_preset" + : "label.service_preset"), + preset.getDescription())); + item.setToolTipText(tooltip); + item.addActionListener((event) -> { + AlignmentView msa = frame.gatherSequencesForAlignment(); + startWorker(frame, msa, preset.getArguments(), submitGaps); + }); + presetList.add(item); + } + parent.add(presetList); + } + } + + 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 void startWorker(AlignFrame frame, AlignmentView msa, + List arguments, boolean submitGaps) + { + AlignViewport viewport = frame.getViewport(); + PollingTaskExecutor executor = frame.getViewport().getWSExecutor(); + if (msa != null) + { + + String panelInfo = String.format("%s using service hosted at %s%n%s", + operation.getName(), operation.getHostName(), + Objects.requireNonNullElse(operation.getDescription(), "")); + var wsInfo = new WebserviceInfo(operation.getName(), panelInfo, false); + + final String alnTitle = frame.getTitle(); + AlignmentWorker worker = new AlignmentWorker(operation, msa, + arguments, submitGaps, true, viewport); + String outputHeader = String.format("%s of %s%nJob details%n", + submitGaps ? "Re-alignment" : "Alignment", alnTitle); + + var awl = new AlignmentProgressUpdater(worker, wsInfo, frame, + outputHeader); + worker.setResultConsumer(awl); + worker.addListener(awl); + + executor.submit(worker); + } + + } + +} diff --git a/src/jalview/ws2/gui/AlignmentProgressUpdater.java b/src/jalview/ws2/gui/AlignmentProgressUpdater.java new file mode 100644 index 0000000..9704432 --- /dev/null +++ b/src/jalview/ws2/gui/AlignmentProgressUpdater.java @@ -0,0 +1,170 @@ +package jalview.ws2.gui; + +import static java.lang.String.format; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import jalview.ws2.operations.WebServiceWorkerListener; +import jalview.datamodel.Alignment; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AlignmentOrder; +import jalview.datamodel.HiddenColumns; +import jalview.gui.AlignFrame; +import jalview.gui.Desktop; +import jalview.gui.JvOptionPane; +import jalview.gui.WebserviceInfo; +import jalview.util.MessageManager; +import jalview.ws2.operations.AlignmentWorker.AlignmentResult; +import jalview.ws2.WSJob; +import jalview.ws2.operations.WebServiceWorkerI; + +public class AlignmentProgressUpdater + implements WebServiceWorkerListener, Consumer +{ + WebServiceWorkerI worker; + + WebserviceInfo wsInfo; + + AlignFrame frame; + + private final WebServiceInfoUpdater wsInfoUpdater; + + String outputHeader; + + AlignmentProgressUpdater(WebServiceWorkerI worker, + WebserviceInfo wsInfo, AlignFrame frame, String header) + { + this.worker = worker; + this.wsInfo = wsInfo; + this.outputHeader = header; + this.wsInfoUpdater = new WebServiceInfoUpdater(worker, wsInfo); + wsInfoUpdater.setOutputHeader(header); + } + + @Override + public void workerStarted(WebServiceWorkerI source) + { + wsInfo.setVisible(true); + } + + @Override + public void workerNotStarted(WebServiceWorkerI source) + { + wsInfo.setVisible(false); + // TODO show notification dialog. + JvOptionPane.showMessageDialog(frame, + MessageManager.getString("info.invalid_msa_input_mininfo"), + MessageManager.getString("info.invalid_msa_notenough"), + JvOptionPane.INFORMATION_MESSAGE); + } + + @Override + public void jobCreated(WebServiceWorkerI source, WSJob job) + { + int tabIndex = wsInfo.addJobPane(); + wsInfo.setProgressName(format("region %d", job.getJobNum()), tabIndex); + wsInfo.setProgressText(tabIndex, outputHeader); + job.addPropertyChangeListener(wsInfoUpdater); + } + + @Override + public void pollException(WebServiceWorkerI source, WSJob job, Exception e) + { + wsInfo.appendProgressText(job.getJobNum(), + MessageManager.formatMessage("info.server_exception", + source.getOperation().getName(), e.getMessage())); + } + + @Override + public void workerCompleting(WebServiceWorkerI source) + { + wsInfo.setProgressBar( + MessageManager.getString("status.collecting_job_results"), + worker.getUID()); + } + + @Override + public void workerCompleted(WebServiceWorkerI source) + { + wsInfo.removeProgressBar(worker.getUID()); + } + + @Override + public void accept(AlignmentResult out) + { + if (out != null) + { + wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame( + new Alignment(out.getAlignment()), out.getAlignmentOrders(), + out.getHiddenColumns())); + wsInfo.setResultsReady(); + } + else + { + wsInfo.setFinishedNoResults(); + } + } + + private void displayNewFrame(AlignmentI aln, + List alorders, HiddenColumns hidden) + { + AlignFrame frame = new AlignFrame(aln, hidden, + AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); + // TODO store feature renderer settings in worker object + // frame.getFeatureRenderer().transferSettings(featureSettings); + var regions = sortOrders(alorders); + if (alorders.size() == 1) + { + frame.addSortByOrderMenuItem(format("%s Ordering", + worker.getOperation().getName()), alorders.get(0)); + } + else + { + for (int i = 0; i < alorders.size(); i++) + { + final int j = i; + Iterable iter = () -> regions.get(j).stream() + .map(it -> Integer.toString(it)).iterator(); + var orderName = format("%s Region %s Ordering", + worker.getOperation().getName(), String.join(",", iter)); + frame.addSortByOrderMenuItem(orderName, alorders.get(i)); + } + } + + /* TODO + * If alignment was requested from one half of a SplitFrame, show in a + * SplitFrame with the other pane similarly aligned. + */ + + Desktop.addInternalFrame(frame, frame.getTitle(), AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + } + + private List> sortOrders(List alorders) + { + List> regions = new ArrayList<>(); + for (int i = 0; i < alorders.size(); i++) + { + List regs = new ArrayList<>(); + regs.add(i); + int j = i + 1; + while (j < alorders.size()) + { + if (alorders.get(i).equals(alorders.get(j))) + { + alorders.remove(j); + regs.add(j); + } + else + { + j++; + } + } + regions.add(regs); + } + return regions; + } + +} diff --git a/src/jalview/ws2/gui/WebServiceInfoUpdater.java b/src/jalview/ws2/gui/WebServiceInfoUpdater.java new file mode 100644 index 0000000..f674a9c --- /dev/null +++ b/src/jalview/ws2/gui/WebServiceInfoUpdater.java @@ -0,0 +1,145 @@ +package jalview.ws2.gui; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; + +import jalview.gui.WebserviceInfo; +import jalview.ws2.WSJob; +import jalview.ws2.WSJobStatus; +import jalview.ws2.operations.WebServiceWorkerI; + +/** + * A helper class that can be attached as a listener to the {@link WSJob} + * object. It updates the job status in the {@link jalview.gui.WebServiceInfo} + * window according to the state changes of the job object. + * + * The {@link WebServiceInfoUpdater} object allows to decouple GUI updates + * from the web service worker logic. + * + * @author mmwarowny + * + */ +public class WebServiceInfoUpdater implements PropertyChangeListener +{ + private final WebServiceWorkerI worker; + private final WebserviceInfo wsInfo; + + private String outputHeader = ""; + + public WebServiceInfoUpdater(WebServiceWorkerI worker, WebserviceInfo wsInfo) + { + this.worker = worker; + this.wsInfo = wsInfo; + } + + public String getOutputHeader() + { + return outputHeader; + } + + public void setOutputHeader(String header) + { + this.outputHeader = header; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) + { + switch (evt.getPropertyName()) + { + case "status": + statusChanged(evt); + break; + case "log": + logChanged(evt); + break; + case "errorLog": + errorLogChanged(evt); + break; + } + } + + private void statusChanged(PropertyChangeEvent evt) + { + WSJob job = (WSJob) evt.getSource(); + WSJobStatus status = (WSJobStatus) evt.getNewValue(); + int wsInfoStatus = 0; + switch (status) + { + case READY: + case SUBMITTED: + case QUEUED: + wsInfoStatus = WebserviceInfo.STATE_QUEUING; + break; + case RUNNING: + wsInfoStatus = WebserviceInfo.STATE_RUNNING; + break; + case FINISHED: + wsInfoStatus = WebserviceInfo.STATE_STOPPED_OK; + break; + case CANCELLED: + wsInfoStatus = WebserviceInfo.STATE_CANCELLED_OK; + break; + case INVALID: + case BROKEN: + case FAILED: + case UNKNOWN: + wsInfoStatus = WebserviceInfo.STATE_STOPPED_ERROR; + break; + case SERVER_ERROR: + wsInfoStatus = WebserviceInfo.STATE_STOPPED_SERVERERROR; + break; + } + wsInfo.setStatus(job.getJobNum(), wsInfoStatus); + updateWSInfoGlobalStatus(); + } + + private void logChanged(PropertyChangeEvent evt) + { + WSJob job = (WSJob) evt.getSource(); + String oldLog = (String) evt.getOldValue(); + String newLog = (String) evt.getNewValue(); + wsInfo.appendProgressText(job.getJobNum(), + newLog.substring(oldLog.length())); + } + + private void errorLogChanged(PropertyChangeEvent evt) + { + WSJob job = (WSJob) evt.getSource(); + String oldLog = (String) evt.getOldValue(); + String newLog = (String) evt.getNewValue(); + wsInfo.appendProgressText(job.getJobNum(), + newLog.substring(oldLog.length())); + } + + + private void updateWSInfoGlobalStatus() + { + var jobs = worker.getJobs(); + if (jobs.countRunning() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_RUNNING); + } + else if (jobs.countQueuing() > 0 + || jobs.countSubmitted() < jobs.size()) + { + wsInfo.setStatus(WebserviceInfo.STATE_QUEUING); + } + else + { + if (jobs.countSuccessful() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK); + } + else if (jobs.countCancelled() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK); + } + else if (jobs.countFailed() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR); + } + } + } +} diff --git a/src/jalview/ws2/operations/AlignmentOperation.java b/src/jalview/ws2/operations/AlignmentOperation.java new file mode 100644 index 0000000..2b510e9 --- /dev/null +++ b/src/jalview/ws2/operations/AlignmentOperation.java @@ -0,0 +1,35 @@ +package jalview.ws2.operations; + +import java.io.IOException; + +import jalview.datamodel.AlignmentI; +import jalview.ws2.WSJob; +import jalview.ws2.WebServiceI; +import jalview.ws2.gui.AlignmentMenuBuilder; +import jalview.ws2.gui.MenuEntryProviderI; + +public class AlignmentOperation extends AbstractOperation +{ + public static interface AlignmentResultSupplier { + public AlignmentI getAlignment(WSJob job) throws IOException; + } + + private AlignmentResultSupplier alignmentSupplier; + + public AlignmentOperation(WebServiceI service, AlignmentResultSupplier alignmentSupplier) + { + super(service, "Alignment"); + this.alignmentSupplier = alignmentSupplier; + } + + @Override + public MenuEntryProviderI getMenuBuilder() + { + return new AlignmentMenuBuilder(this); + } + + public AlignmentResultSupplier getAlignmentSupplier() + { + return alignmentSupplier; + } +} diff --git a/src/jalview/ws2/operations/AlignmentWorker.java b/src/jalview/ws2/operations/AlignmentWorker.java new file mode 100644 index 0000000..79ef527 --- /dev/null +++ b/src/jalview/ws2/operations/AlignmentWorker.java @@ -0,0 +1,415 @@ +package jalview.ws2.operations; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import jalview.analysis.AlignSeq; +import jalview.analysis.AlignmentSorter; +import jalview.analysis.SeqsetUtils; +import jalview.analysis.SeqsetUtils.SequenceInfo; +import jalview.bin.Cache; +import jalview.datamodel.AlignedCodonFrame; +import jalview.datamodel.Alignment; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AlignmentOrder; +import jalview.datamodel.AlignmentView; +import jalview.datamodel.HiddenColumns; +import jalview.datamodel.Sequence; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignViewport; +import jalview.util.MessageManager; +import jalview.ws.params.ArgumentI; +import jalview.ws2.WSJob; +import jalview.ws2.WSJobStatus; + +public class AlignmentWorker extends AbstractPollableWorker +{ + private AlignmentOperation operation; + + private Consumer resultConsumer; + + private AlignmentView msa; + + private AlignmentI dataset; + + private AlignViewport viewport; + + private List codonFrame = new ArrayList<>(); + + private List args = Collections.emptyList(); + + private boolean submitGaps = false; + + private boolean preserveOrder = false; + + private char gapCharacter; + + private WSJobList jobs = new WSJobList<>(); + + private Map exceptionCount = new HashMap<>(); + + private static final int MAX_RETRY = 5; + + private static class JobInput + { + final List inputSequences; + + final List emptySequences; + + final Map sequenceNames; + + private JobInput(List inputSequences, + List emptySequences, + Map names) + { + this.inputSequences = Collections.unmodifiableList(inputSequences); + this.emptySequences = Collections.unmodifiableList(emptySequences); + this.sequenceNames = names; + } + + boolean isInputValid() + { + return inputSequences.size() >= 2; + } + } + + public class AlignmentJob extends WSJob + { + private List inputSequences; + private List emptySequences; + private Map sequenceNames; + + private AlignmentJob(String serviceProvider, String serviceName, + String hostName) + { + super(serviceProvider, serviceName, hostName); + } + + private void setInput(JobInput input) { + inputSequences = input.inputSequences; + emptySequences = input.emptySequences; + sequenceNames = input.sequenceNames; + } + } + + public class AlignmentResult + { + AlignmentI aln; + + List alorders; + + HiddenColumns hidden; + + AlignmentResult(AlignmentI aln, List alorders, + HiddenColumns hidden) + { + this.aln = aln; + this.alorders = alorders; + this.hidden = hidden; + } + + public AlignmentI getAlignment() + { + return aln; + } + + public List getAlignmentOrders() + { + return alorders; + } + + public HiddenColumns getHiddenColumns() + { + return hidden; + } + } + + public AlignmentWorker(AlignmentOperation operation, + AlignmentView msa, List args, + boolean submitGaps, boolean preserveOrder, AlignViewport viewport) + { + this.operation = operation; + this.msa = msa; + this.dataset = viewport.getAlignment().getDataset(); + List cf = Objects.requireNonNullElse( + viewport.getAlignment().getCodonFrames(), Collections.emptyList()); + this.codonFrame.addAll(cf); + this.args = args; + this.submitGaps = submitGaps; + this.preserveOrder = preserveOrder; + this.viewport = viewport; + this.gapCharacter = viewport.getGapCharacter(); + } + + @Override + public Operation getOperation() + { + return operation; + } + + @Override + public WSJobList getJobs() + { + return jobs; + } + + public void setResultConsumer(Consumer consumer) + { + this.resultConsumer = consumer; + } + + @Override + public void start() throws IOException + { + Cache.log.info(String.format("Starting new %s job.", operation.getName())); + SequenceI[][] conmsa = msa.getVisibleContigs('-'); + if (conmsa == null) + { + return; + } + int numValid = 0; + for (int i = 0; i < conmsa.length; i++) + { + JobInput input = prepareInputData(conmsa[i], 2, submitGaps); + AlignmentJob job = new AlignmentJob(operation.service.getProviderName(), + operation.getName(), operation.getHostName()); + job.setJobNum(i); + job.setInput(input); + jobs.add(job); + listeners.fireJobCreated(job); + if (input.isInputValid()) + { + int count; + String jobId = null; + do + { + count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY); + try + { + jobId = operation.service.submit(input.inputSequences, args); + Cache.log.debug((String.format("Job %s submitted", job))); + exceptionCount.remove(job.getUid()); + } catch (IOException e) + { + exceptionCount.put(job.getUid(), --count); + } + } while (jobId == null && count > 0); + if (jobId != null) + { + job.setJobId(jobId); + job.setStatus(WSJobStatus.SUBMITTED); + numValid++; + } + else + { + job.setStatus(WSJobStatus.SERVER_ERROR); + } + } + else + { + job.setStatus(WSJobStatus.INVALID); + job.setErrorLog( + MessageManager.getString("label.empty_alignment_job")); + } + } + if (numValid > 0) + { + listeners.fireWorkerStarted(); + } + else + { + listeners.fireWorkerNotStarted(); + } + } + + + private static JobInput prepareInputData(SequenceI[] sequences, + int minLength, boolean submitGaps) + { + assert minLength >= 0 : MessageManager.getString( + "error.implementation_error_minlen_must_be_greater_zero"); + int numSeq = 0; + for (SequenceI seq : sequences) + { + if (seq.getEnd() - seq.getStart() >= minLength) + { + numSeq++; + } + } + + List inputSequences = new ArrayList<>(); + List emptySequences = new ArrayList<>(); + Map names = new LinkedHashMap<>(); + for (int i = 0; i < sequences.length; i++) + { + SequenceI seq = sequences[i]; + String newName = SeqsetUtils.unique_name(i); + var hash = SeqsetUtils.SeqCharacterHash(seq); + names.put(newName, hash); + if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength) + { + String seqString = seq.getSequenceAsString(); + if (!submitGaps) + { + seqString = AlignSeq.extractGaps( + jalview.util.Comparison.GapChars, seqString); + } + inputSequences.add(new Sequence(newName, seqString)); + } + else + { + String seqString = ""; + if (seq.getEnd() >= seq.getStart()) // true if gaps only + { + seqString = seq.getSequenceAsString(); + if (!submitGaps) + { + seqString = AlignSeq.extractGaps( + jalview.util.Comparison.GapChars, seqString); + } + } + emptySequences.add(new Sequence(newName, seqString)); + } + } + + return new JobInput(inputSequences, emptySequences, names); + } + + @Override + public void done() + { + listeners.fireWorkerCompleting(); + Map results = new LinkedHashMap<>(); + for (WSJob job : getJobs()) + { + if (job.getStatus().isFailed()) + continue; + try + { + AlignmentI alignment = operation.getAlignmentSupplier().getAlignment(job); + if (alignment != null) + { + results.put(job.getUid(), alignment); + } + } catch (Exception e) + { + if (!operation.getWebService().handleCollectionError(job, e)) + { + Cache.log.error("Couldn't get alignment for job.", e); + // TODO: Increment exception count and retry. + job.setStatus(WSJobStatus.SERVER_ERROR); + } + } + } + if (results.size() > 0) + { + AlignmentResult out = prepareResult(results); + resultConsumer.accept(out); + } + else + { + resultConsumer.accept(null); + } + listeners.fireWorkerCompleted(); + } + + private AlignmentResult prepareResult(Map alignments) + { + List alorders = new ArrayList<>(); + SequenceI[][] results = new SequenceI[jobs.size()][]; + AlignmentOrder[] orders = new AlignmentOrder[jobs.size()]; + for (int i = 0; i < jobs.size(); i++) + { + AlignmentJob job = jobs.get(i); + AlignmentI aln = alignments.get(job.getUid()); + if (aln != null) // equivalent of job.hasResults() + { + /* Get the alignment including any empty sequences in the original + * order with original ids. */ + char gapChar = aln.getGapCharacter(); + List emptySeqs = job.emptySequences; + List alnSeqs = aln.getSequences(); + // find the width of the longest sequence + int width = 0; + for (var seq : alnSeqs) + width = Integer.max(width, seq.getLength()); + for (var emptySeq : emptySeqs) + width = Integer.max(width, emptySeq.getLength()); + // pad shorter sequences with gaps + String gapSeq = String.join("", + Collections.nCopies(width, Character.toString(gapChar))); + List seqs = new ArrayList<>( + alnSeqs.size() + emptySeqs.size()); + seqs.addAll(alnSeqs); + seqs.addAll(emptySeqs); + for (var seq : seqs) + { + if (seq.getLength() < width) + seq.setSequence(seq.getSequenceAsString() + + gapSeq.substring(seq.getLength())); + } + SequenceI[] result = seqs.toArray(new SequenceI[0]); + AlignmentOrder msaOrder = new AlignmentOrder(result); + AlignmentSorter.recoverOrder(result); + Map names = new HashMap<>(job.sequenceNames); + // FIXME first call to deuniquify alters original alignment + SeqsetUtils.deuniquify(names, result); + alorders.add(msaOrder); + results[i] = result; + orders[i] = msaOrder; + } + else + { + results[i] = null; + } + } + + Object[] newView = msa.getUpdatedView(results, orders, gapCharacter); + // free references to original data + for (int i = 0; i < jobs.size(); i++) + { + results[i] = null; + orders[i] = null; + } + SequenceI[] alignment = (SequenceI[]) newView[0]; + HiddenColumns hidden = (HiddenColumns) newView[1]; + Alignment aln = new Alignment(alignment); + aln.setProperty("Alignment Program", operation.getName()); + if (dataset != null) + aln.setDataset(dataset); + + propagateDatasetMappings(aln); + return new AlignmentResult(aln, alorders, hidden); + // displayNewFrame(aln, alorders, hidden); + } + + /* + * conserves dataset references to sequence objects returned from web + * services. propagate codon frame data to alignment. + */ + private void propagateDatasetMappings(Alignment aln) + { + if (codonFrame != null) + { + SequenceI[] alignment = aln.getSequencesArray(); + for (SequenceI seq : alignment) + { + for (AlignedCodonFrame acf : codonFrame) + { + if (acf != null && acf.involvesSequence(seq)) + { + aln.addCodonFrame(acf); + break; + } + } + } + } + } +} diff --git a/src/jalview/ws2/operations/WebServiceWorkerI.java b/src/jalview/ws2/operations/WebServiceWorkerI.java index 5ec7e97..e81dfcb 100644 --- a/src/jalview/ws2/operations/WebServiceWorkerI.java +++ b/src/jalview/ws2/operations/WebServiceWorkerI.java @@ -4,6 +4,8 @@ import jalview.ws2.WSJob; public interface WebServiceWorkerI { + long getUID(); + Operation getOperation(); WSJobList getJobs(); diff --git a/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java b/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java index 802254b..b6004fc 100644 --- a/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java +++ b/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java @@ -182,7 +182,7 @@ public class SlivkaWSDiscoverer implements WebServiceDiscovererI op = new OperationStub(webService, "Protein Disorder"); break; case "multiple sequence alignment": - op = new OperationStub(webService, "Alignment"); + op = new AlignmentOperation(webService, webService::getAlignment); break; } if (op != null) diff --git a/src/jalview/ws2/slivka/SlivkaWebService.java b/src/jalview/ws2/slivka/SlivkaWebService.java index a2d7f20..bf61ca6 100644 --- a/src/jalview/ws2/slivka/SlivkaWebService.java +++ b/src/jalview/ws2/slivka/SlivkaWebService.java @@ -218,8 +218,7 @@ public class SlivkaWebService implements WebServiceI return false; } - public AlignmentI getAlignment(WSJob job, List dataset, - AlignViewportI viewport) throws IOException + public AlignmentI getAlignment(WSJob job) throws IOException { Collection files; var slivkaJob = client.getJob(job.getJobId());