JAL-3878 Implement AlignmentOperation WIP
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 20 Sep 2021 16:39:57 +0000 (18:39 +0200)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 20 Sep 2021 16:44:44 +0000 (18:44 +0200)
src/jalview/ws2/operations/AlignmentOperation.java [new file with mode: 0644]

diff --git a/src/jalview/ws2/operations/AlignmentOperation.java b/src/jalview/ws2/operations/AlignmentOperation.java
new file mode 100644 (file)
index 0000000..e3560bf
--- /dev/null
@@ -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<AlignmentI> supplier;
+
+  public AlignmentOperation(WebServiceI service, ResultSupplier<AlignmentI> 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(
+            "<strong>%s</strong><br/>%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<List<ArgumentI>> openEditParamsDialog(WebServiceI service,
+      WsParamSetI preset, List<ArgumentI> 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<ArgumentI> args = Collections.emptyList();
+    private String alnTitle = "";
+    private boolean submitGaps = false;
+    private boolean preserveOrder = false;
+    private WSJobList jobs = new WSJobList();
+    private WebserviceInfo wsInfo;
+    private Map<Long, Integer> exceptionCount = new HashMap<>();
+    private final int MAX_RETRY = 5;
+
+    AlignmentWorker(AlignmentView msa, List<ArgumentI> 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<WSJob> 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<Long, AlignmentI> 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<Long, AlignmentI> alignments)
+    {
+      List<AlignmentOrder> 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;
+    }
+
+  }
+
+}