JAL-3878 Add annotation operation and worker to the services.
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Tue, 23 Nov 2021 15:35:45 +0000 (16:35 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Tue, 23 Nov 2021 15:35:45 +0000 (16:35 +0100)
src/jalview/ws2/gui/AnnotationMenuBuilder.java [new file with mode: 0644]
src/jalview/ws2/gui/ProgressBarUpdater.java [new file with mode: 0644]
src/jalview/ws2/operations/AnnotationOperation.java [new file with mode: 0644]
src/jalview/ws2/operations/AnnotationWorker.java [new file with mode: 0644]
src/jalview/ws2/slivka/SlivkaWSDiscoverer.java
src/jalview/ws2/slivka/SlivkaWebService.java

diff --git a/src/jalview/ws2/gui/AnnotationMenuBuilder.java b/src/jalview/ws2/gui/AnnotationMenuBuilder.java
new file mode 100644 (file)
index 0000000..5ddaa19
--- /dev/null
@@ -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<ArgumentI>();
+    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<List<ArgumentI>> openEditParamsDialog(
+      ParamDatastoreI paramStore, WsParamSetI preset,
+      List<ArgumentI> 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<ArgumentI> 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 (file)
index 0000000..89deb06
--- /dev/null
@@ -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 (file)
index 0000000..14e21f3
--- /dev/null
@@ -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<AlignmentAnnotation> attachAnnotations(WSJob job,
+        List<SequenceI> seqs, Map<String, FeatureColourI> featureColours,
+        Map<String, FeatureMatcherSetI> 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 (file)
index 0000000..22aebf4
--- /dev/null
@@ -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<AnnotationJob> jobs = new WSJobList<>();
+
+  AnnotationJob job;
+
+  private List<ArgumentI> args = Collections.emptyList();
+
+  private AlignViewport viewport;
+
+  private AlignmentViewPanel alignPanel;
+
+  private IProgressIndicator progressIndicator;
+
+  private AlignFrame frame;
+
+  private AlignCalcManagerI2 calcMan;
+
+  protected List<AlignmentAnnotation> ourAnnots;
+
+  // TODO: convert to bitset
+  private boolean[] gapMap = new boolean[0];
+
+  private class JobInput
+  {
+    List<SequenceI> sequences;
+
+    Map<String, SequenceI> seqNames;
+
+    int start, end;
+  }
+
+  public class AnnotationJob extends WSJob
+  {
+    private List<SequenceI> sequences;
+
+    private int start, end;
+
+    private Map<String, SequenceI> 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<ArgumentI> 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<? extends WSJob> 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<SequenceI> seqs = new ArrayList<>();
+    final boolean submitGaps = operation.isAlignmentAnalysis();
+    final int minlen = 10;
+    int ln = -1; // I think this variable is redundant
+    Map<String, SequenceI> 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<SequenceI> 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<String, FeatureColourI> featureColours = new HashMap<>();
+    Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+    List<AlignmentAnnotation> 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<AlignmentAnnotation> annotations)
+  {
+    var currentAnnotations = Objects.requireNonNullElse(
+        viewport.getAlignment().getAlignmentAnnotation(),
+        new AlignmentAnnotation[0]);
+    List<AlignmentAnnotation> 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<ContiguousI> 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<AlignmentAnnotation> annots)
+  {
+    List<AlignmentAnnotation> 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();
+  }
+}
index b6004fc..cda6702 100644 (file)
@@ -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, "<null>") + "'. "
-                + "This was probably due to malformed comma-separated-list "
-                + "in the " + SLIVKA_HOST_URLS
-                + " entry of ${HOME}/.jalview_properties");
+            + Objects.toString(url, "<null>") + "'. "
+            + "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<WebServiceDiscovererI> startDiscoverer()
   {
     CompletableFuture<WebServiceDiscovererI> 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<Operation> allOperations= new ArrayList<>();
+    ArrayList<Operation> 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);
         }
       }
index bf61ca6..9078013 100644 (file)
@@ -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<SequenceI> dataset, AlignViewportI viewport) throws IOException
+  public List<AlignmentAnnotation> attachAnnotations(WSJob job,
+      List<SequenceI> dataset, Map<String, FeatureColourI> featureColours,
+      Map<String, FeatureMatcherSetI> featureFilters) throws IOException
   {
+    RemoteFile annotFile = null;
+    RemoteFile featFile = null;
+
     var slivkaJob = client.getJob(job.getJobId());
     Collection<RemoteFile> 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<AlignmentAnnotation> getAnnotations(WSJob job,
-      List<SequenceI> dataset, AlignViewportI viewport) throws IOException
-  {
-    var slivkaJob = client.getJob(job.getJobId());
-    Collection<RemoteFile> 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<SequenceI> dataset,
-      AlignViewportI viewport) throws IOException
-  {
-    Collection<RemoteFile> 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