From 8514428bfdbabfcb9f3f7e3bd46b10a32333439e Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Tue, 15 Mar 2022 15:48:30 +0100 Subject: [PATCH] JAL-3878 Connect alignment services with gui --- .../ws2/gui/AlignmentServiceGuiHandler.java | 298 ++++++++++++++++++++ src/jalview/ws2/helpers/WSClientTaskWrapper.java | 52 ++++ 2 files changed, 350 insertions(+) create mode 100644 src/jalview/ws2/gui/AlignmentServiceGuiHandler.java create mode 100644 src/jalview/ws2/helpers/WSClientTaskWrapper.java diff --git a/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java b/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java new file mode 100644 index 0000000..2540dd1 --- /dev/null +++ b/src/jalview/ws2/gui/AlignmentServiceGuiHandler.java @@ -0,0 +1,298 @@ +package jalview.ws2.gui; + +import static java.util.Objects.requireNonNullElse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.JInternalFrame; + +import jalview.bin.Cache; +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.SplitFrame; +import jalview.gui.WebserviceInfo; +import jalview.util.ArrayUtils; +import jalview.util.MessageManager; +import jalview.util.Pair; +import jalview.ws2.actions.alignment.AlignmentAction; +import jalview.ws2.actions.alignment.AlignmentResult; +import jalview.ws2.actions.api.JobI; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.JobStatus; +import jalview.ws2.api.WebService; +import jalview.ws2.helpers.WSClientTaskWrapper; + +class AlignmentServiceGuiHandler + implements TaskEventListener +{ + private final WebService service; + + private final AlignFrame frame; + + private WebserviceInfo infoPanel; + + private String alnTitle; // title of the alignment used in new window + + private JobI[] jobs = new JobI[0]; + + private int[] tabs = new int[0]; + + private int[] logOffset = new int[0]; + + private int[] errLogOffset = new int[0]; + + public AlignmentServiceGuiHandler(AlignmentAction action, AlignFrame frame) + { + this.service = action.getWebService(); + this.frame = frame; + String panelInfo = String.format("%s using service hosted at %s%n%s", + service.getName(), service.getUrl(), service.getDescription()); + infoPanel = new WebserviceInfo(service.getName(), panelInfo, false); + String actionName = requireNonNullElse(action.getName(), "Alignment"); + alnTitle = String.format("%s %s of %s", service.getName(), actionName, + frame.getTitle()); + } + + @Override + public void taskStatusChanged(TaskI source, JobStatus status) + { + switch (status) + { + case INVALID: + infoPanel.setVisible(false); + JvOptionPane.showMessageDialog(frame, + MessageManager.getString("info.invalid_msa_input_mininfo"), + MessageManager.getString("info.invalid_msa_notenough"), + JvOptionPane.INFORMATION_MESSAGE); + break; + case READY: + infoPanel.setthisService(new WSClientTaskWrapper(source)); + infoPanel.setVisible(true); + // intentional no break + case SUBMITTED: + case QUEUED: + infoPanel.setStatus(WebserviceInfo.STATE_QUEUING); + break; + case RUNNING: + case UNKNOWN: // unsure what to do with unknown + infoPanel.setStatus(WebserviceInfo.STATE_RUNNING); + break; + case COMPLETED: + infoPanel.setProgressBar( + MessageManager.getString("status.collecting_job_results"), + jobs[0].getInternalId()); + infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_OK); + break; + case FAILED: + infoPanel.removeProgressBar(jobs[0].getInternalId()); + infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_ERROR); + break; + case CANCELLED: + infoPanel.setStatus(WebserviceInfo.STATE_CANCELLED_OK); + break; + case SERVER_ERROR: + infoPanel.removeProgressBar(jobs[0].getInternalId()); + infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR); + break; + } + } + + @Override + public void taskStarted(TaskI source, List subJobs) + { + jobs = subJobs.toArray(new JobI[0]); + tabs = new int[subJobs.size()]; + logOffset = new int[subJobs.size()]; + errLogOffset = new int[subJobs.size()]; + for (int i = 0; i < subJobs.size(); i++) + { + JobI job = jobs[i]; + int tabIndex = infoPanel.addJobPane(); + tabs[i] = tabIndex; + infoPanel.setProgressName(String.format("region %d", i), tabIndex); + infoPanel.setProgressText(tabIndex, alnTitle + "\nJob details\n"); + // jobs should not have states other than invalid or ready at this point + if (job.getStatus() == JobStatus.INVALID) + infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_STOPPED_OK); + else if (job.getStatus() == JobStatus.READY) + infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_QUEUING); + } + } + + @Override + public void taskCompleted(TaskI source, AlignmentResult result) + { + if (result == null) + { + infoPanel.setFinishedNoResults(); + return; + } + infoPanel.showResultsNewFrame.addActionListener(evt -> { + var aln = result.getAlignment(); + // copy alignment for each frame to have its own isntance + var alnCpy = new Alignment(aln); + alnCpy.setGapCharacter(aln.getGapCharacter()); + alnCpy.setDataset(aln.getDataset()); + displayResultsNewFrame(alnCpy, result.getAlignmentOrders(), + result.getHiddenColumns()); + }); + infoPanel.setResultsReady(); + } + + private void displayResultsNewFrame(Alignment aln, + List alorders, HiddenColumns hidden) + { + AlignFrame newFrame = new AlignFrame(aln, hidden, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + newFrame.getFeatureRenderer().transferSettings( + frame.getFeatureRenderer().getSettings()); + if (alorders.size() > 0) + { + addSortByMenuItems(newFrame, alorders); + } + + var requestingFrame = frame; + var splitContainer = requestingFrame.getSplitViewContainer(); + if (splitContainer != null && splitContainer.getComplement(requestingFrame) != null) + { + AlignmentI complement = splitContainer.getComplement(requestingFrame); + String complementTitle = splitContainer.getComplementTitle(requestingFrame); + Alignment copyComplement = new Alignment(complement); + copyComplement.setGapCharacter(complement.getGapCharacter()); + copyComplement.setDataset(complement.getDataset()); + copyComplement.alignAs(aln); + if (copyComplement.getHeight() > 0) + { + newFrame.setTitle(alnTitle); + AlignFrame newFrame2 = new AlignFrame(copyComplement, + AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); + newFrame2.setTitle(complementTitle); + String linkedTitle = MessageManager.getString("label.linked_view_title"); + JInternalFrame splitFrame = new SplitFrame( + aln.isNucleotide() ? newFrame : newFrame2, + aln.isNucleotide() ? newFrame2 : newFrame); + Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1); + return; + } + } + // no split frame or failed to create complementary alignment + Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + } + + private void addSortByMenuItems(AlignFrame frame, List alorders) + { + if (alorders.size() == 1) + { + frame.addSortByOrderMenuItem(service.getName() + " Ordering", + alorders.get(0)); + return; + } + BitSet collected = new BitSet(alorders.size()); + for (int i = 0, N = alorders.size(); i < N; i++) + { + if (collected.get(i)) + continue; + var regions = new ArrayList(); + var order = alorders.get(i); + for (int j = i; j < N; j++) + { + if (!collected.get(j) && alorders.get(j).equals(order)) + { + regions.add(Integer.toString(j + 1)); + collected.set(j); + } + } + var orderName = String.format("%s Region %s Ordering", + service.getName(), String.join(",", regions)); + frame.addSortByOrderMenuItem(orderName, order); + } + } + + @Override + public void taskException(TaskI source, Exception e) + { + Cache.log.error(String.format("Service %s raised an exception.", service.getName()), e); + infoPanel.appendProgressText(e.getMessage()); + } + + @Override + public void taskRestarted(TaskI source) + { + // alignment services are not restartable + } + + @Override + public void subJobStatusChanged(TaskI source, JobI job, JobStatus status) + { + int i = ArrayUtils.indexOf(jobs, job); + assert i >= 0 : "job does not exist"; + if (i < 0) + // safeguard that should not happen irl + return; + int wsStatus; + switch (status) + { + case INVALID: + case COMPLETED: + wsStatus = WebserviceInfo.STATE_STOPPED_OK; + break; + case READY: + case SUBMITTED: + case QUEUED: + wsStatus = WebserviceInfo.STATE_QUEUING; + break; + case RUNNING: + case UNKNOWN: + wsStatus = WebserviceInfo.STATE_RUNNING; + break; + case FAILED: + wsStatus = WebserviceInfo.STATE_STOPPED_ERROR; + break; + case CANCELLED: + wsStatus = WebserviceInfo.STATE_CANCELLED_OK; + break; + case SERVER_ERROR: + wsStatus = WebserviceInfo.STATE_STOPPED_SERVERERROR; + break; + default: + throw new AssertionError("Non-exhaustive switch statement"); + } + infoPanel.setStatus(tabs[i], wsStatus); + } + + @Override + public void subJobLogChanged(TaskI source, JobI job, String log) + { + int i = ArrayUtils.indexOf(jobs, job); + assert i >= 0 : "job does not exist"; + if (i < 0) + // safeguard that should never happen + return; + infoPanel.appendProgressText(tabs[i], log.substring(logOffset[i])); + } + + @Override + public void subJobErrorLogChanged(TaskI source, JobI job, String log) + { + int i = ArrayUtils.indexOf(jobs, job); + assert i >= 0 : "job does not exist"; + if (i < 0) + // safeguard that should never happen + return; + infoPanel.appendProgressText(tabs[i], log.substring(errLogOffset[i])); + } + +} diff --git a/src/jalview/ws2/helpers/WSClientTaskWrapper.java b/src/jalview/ws2/helpers/WSClientTaskWrapper.java new file mode 100644 index 0000000..c9032f6 --- /dev/null +++ b/src/jalview/ws2/helpers/WSClientTaskWrapper.java @@ -0,0 +1,52 @@ +package jalview.ws2.helpers; + +import jalview.gui.WebserviceInfo; +import jalview.ws.WSClientI; +import jalview.ws2.actions.api.TaskI; + +/** + * A simple wrapper around the {@link TaskI} implementing {@link WSClientI}. Its + * main purpose is to delegate the call to {@link #cancelJob} to the underlying + * task. + * + * @author mmwarowny + */ +public class WSClientTaskWrapper implements WSClientI +{ + private TaskI delegate; + + private boolean cancellable; + + private boolean canMerge; + + public WSClientTaskWrapper(TaskI task, boolean cancellable, boolean canMerge) + { + this.delegate = task; + this.cancellable = cancellable; + this.canMerge = canMerge; + } + + public WSClientTaskWrapper(TaskI task) + { + this(task, true, false); + } + + @Override + public boolean isCancellable() + { + return cancellable; + } + + @Override + public boolean canMergeResults() + { + return canMerge; + } + + @Override + public void cancelJob() + { + delegate.cancel(); + } + +} -- 1.7.10.2