From 5b991d012d631b363fa112e26c14790e7e090bb2 Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Mon, 20 Sep 2021 18:39:57 +0200 Subject: [PATCH] JAL-3878 Implement AlignmentOperation WIP --- src/jalview/ws2/operations/AlignmentOperation.java | 380 ++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 src/jalview/ws2/operations/AlignmentOperation.java diff --git a/src/jalview/ws2/operations/AlignmentOperation.java b/src/jalview/ws2/operations/AlignmentOperation.java new file mode 100644 index 0000000..e3560bf --- /dev/null +++ b/src/jalview/ws2/operations/AlignmentOperation.java @@ -0,0 +1,380 @@ +package jalview.ws2.operations; + +import static java.lang.String.format; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +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.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.ToolTipManager; + +import org.codehaus.groovy.ast.GenericsType.GenericsTypeName; + +import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.AlignmentOrder; +import jalview.datamodel.AlignmentView; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; +import jalview.gui.JvSwingUtils; +import jalview.gui.WebserviceInfo; +import jalview.gui.WsJobParameters; +import jalview.util.MathUtils; +import jalview.util.MessageManager; +import jalview.ws.params.ArgumentI; +import jalview.ws.params.WsParamSetI; +import jalview.ws2.MenuEntryProviderI; +import jalview.ws2.ResultSupplier; +import jalview.ws2.WSJob; +import jalview.ws2.WSJobStatus; +import jalview.ws2.WebServiceExecutor; +import jalview.ws2.WebServiceI; +import jalview.ws2.WebServiceInfoUpdater; +import jalview.ws2.WebServiceWorkerI; +import jalview.ws2.utils.WSJobList; + +/** + * + * @author mmwarowny + * + */ +public class AlignmentOperation implements Operation +{ + final WebServiceI service; + final ResultSupplier supplier; + + public AlignmentOperation(WebServiceI service, ResultSupplier supplier) { + this.service = service; + this.supplier = supplier; + } + + @Override public int getMinSequences() { return 2; } + @Override public int getMaxSequences() { return Integer.MAX_VALUE; } + @Override public boolean isProteinOperation() { return true; } + @Override public boolean isNucleotideOperation() { return true; } + @Override public boolean canSubmitGaps() { + // hack copied from original jabaws code, don't blame me + return service.getName().contains("lustal"); + } + @Override public MenuEntryProviderI getMenuBuilder() { return this::buildMenu; } + + protected void buildMenu(JMenu parent, AlignFrame frame) { + if (canSubmitGaps()) { + var alignSubmenu = new JMenu(service.getName()); + buildMenu(alignSubmenu, frame, false); + parent.add(alignSubmenu); + var realignSubmenu = new JMenu(MessageManager.formatMessage( + "label.realign_with_params", service.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 = service.getName(); + + final AlignmentView msa = frame.gatherSequencesForAlignment(); + final AlignmentI dataset = frame.getViewport().getAlignment().getDataset(); + String title = frame.getTitle(); + WebServiceExecutor executor = frame.getViewport().getWSExecutor(); + { + 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) -> { + if (msa != null) + { + WebServiceWorkerI worker = new AlignmentWorker( + msa, Collections.emptyList(), title, submitGaps, true, dataset); + executor.submit(worker); + } + }); + parent.add(item); + } + + if (service.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) -> { + if (msa != null) + { + openEditParamsDialog(service, null, null).thenAcceptAsync((arguments) ->{ + if (arguments != null) + { + WebServiceWorkerI worker = new AlignmentWorker( + msa, arguments, title, submitGaps, true, dataset); + executor.submit(worker); + } + }); + } + }); + parent.add(item); + } + + var presets = service.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) -> { + if (msa != null) + { + WebServiceWorkerI worker = new AlignmentWorker( + msa, preset.getArguments(), title, submitGaps, true, dataset); + executor.submit(worker); + } + }); + presetList.add(item); + } + parent.add(presetList); + } + } + + + private CompletionStage> openEditParamsDialog(WebServiceI service, + WsParamSetI preset, List arguments) + { + WsJobParameters jobParams; + if (preset == null && arguments != null && arguments.size() > 0) + jobParams = new WsJobParameters(service.getParamStore(), preset, arguments); + else + jobParams = new WsJobParameters(service.getParamStore(), preset, null); + 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; + } + }); + } + + /** + * Implementation of the web service worker performing multiple sequence + * alignment. + * + * @author mmwarowny + * + */ + private class AlignmentWorker implements WebServiceWorkerI { + + private long uid = MathUtils.getUID(); + private final AlignmentView msa; + private final AlignmentI seqdata; + private List args = Collections.emptyList(); + private String alnTitle = ""; + private boolean submitGaps = false; + private boolean preserveOrder = false; + private WSJobList jobs = new WSJobList(); + private WebserviceInfo wsInfo; + private Map exceptionCount = new HashMap<>(); + private final int MAX_RETRY = 5; + + AlignmentWorker(AlignmentView msa, List args, String alnTitle, + boolean submitGaps, boolean preserveOrder, AlignmentI seqdata) + { + this.msa = msa; + this.seqdata = seqdata; + this.args = args; + this.alnTitle = alnTitle; + this.submitGaps = submitGaps; + this.preserveOrder = preserveOrder; + + String panelInfo = String.format("%s using service hosted at %s%n%s", + service.getName(), service.getHostName(), + Objects.requireNonNullElse(service.getDescription(), "")); + wsInfo = new WebserviceInfo(service.getName(), panelInfo, false); + } + + + @Override + public long getUID() { return uid; } + + @Override + public List getJobs() + { + return Collections.unmodifiableList(jobs); + } + + @Override + public void startJobs() throws IOException + { + String outputHeader = String.format("%s of %s%nJob details%n", + submitGaps ? "Re-alignment" : "Alignment", alnTitle); + SequenceI[][] conmsa = msa.getVisibleContigs('-'); + if (conmsa == null) { + return; + } + WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo); + updater.setOutputHeader(outputHeader); + for (int i = 0; i < conmsa.length; i++) { + WSJob job = service.submit(List.of(conmsa[i]), args); + job.setJobNum(wsInfo.addJobPane()); + job.addPropertyChangeListener(updater); + jobs.add(job); + if (conmsa.length > 0) { + wsInfo.setProgressName(String.format("region %d", i), job.getJobNum()); + } + wsInfo.setProgressText(job.getJobNum(), outputHeader); + } + } + + @Override + public boolean pollJobs() + { + boolean done = true; + for (WSJob job : getJobs()) { + if (!job.getStatus().isDone() ) { + try { + service.updateProgress(job); + exceptionCount.remove(job.getUid()); + } + catch (IOException e) { + int count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY); + if (--count <= 0) { + job.setStatus(WSJobStatus.SERVER_ERROR); + } + exceptionCount.put(job.getUid(), count); + } + } + done &= job.getStatus().isDone(); + } + updateWSInfoGlobalStatus(); + return done; + } + + private void updateWSInfoGlobalStatus() { + 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); + } + } + } + + @Override + public void done() { + long progbarId = MathUtils.getUID(); + wsInfo.setProgressBar( + MessageManager.getString("status.collecting_job_results"), + progbarId); + Map results = new LinkedHashMap<>(); + for (WSJob job : getJobs()) { + try { + AlignmentI alignment = supplier.getResult(job); + if (alignment != null) { + results.put(job.getUid(), alignment); + } + } + catch (Exception e) { + if (!service.handleCollectionError(job, e)) { + Cache.log.error("Couldn't get alignment for job.", e); + // TODO: Increment exception count and retry. + job.setStatus(WSJobStatus.SERVER_ERROR); + } + } + } + updateWSInfoGlobalStatus(); + if (results.size() > 0) { + wsInfo.showResultsNewFrame.addActionListener( + evt -> displayResults(results)); + wsInfo.setResultsReady(); + } + else { + wsInfo.setFinishedNoResults(); + } + wsInfo.removeProgressBar(progbarId); + } + + private void displayResults(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++) { + WSJob job = jobs.get(i); + AlignmentI aln = alignments.get(job.getUid()); + if (aln != null) { + /* + * Get the alignment including any empty sequences in the original + * order with original ids. + */ + char gapChar = aln.getGapCharacter(); + int alSeqLen = aln.getSequences().size(); + SequenceI[] alSeqs = new SequenceI[alSeqLen]; + alSeqs = aln.getSequences().toArray(alSeqs); + } + else { + results[i] = null; + } + } + } + + @Override + public WebServiceI getWebService() + { + return service; + } + + } + +} -- 1.7.10.2