JAL-4241 Fix annotation and feature alignment with selection
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Fri, 28 Jul 2023 14:13:28 +0000 (16:13 +0200)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 31 Jul 2023 16:00:05 +0000 (18:00 +0200)
src/jalview/ws2/actions/annotation/AlignCalcWorkerAdapter.java
src/jalview/ws2/actions/annotation/AnnotationJob.java
src/jalview/ws2/actions/annotation/AnnotationResult.java
src/jalview/ws2/actions/annotation/AnnotationTask.java
src/jalview/ws2/gui/AnnotationServiceGuiHandler.java

index 0905ea2..60d7f15 100644 (file)
@@ -3,8 +3,6 @@ package jalview.ws2.actions.annotation;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.function.Consumer;
 
 import jalview.analysis.AlignmentAnnotationUtils;
 import jalview.api.AlignViewportI;
@@ -45,9 +43,9 @@ public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableA
           ala.graphGroup += graphGroup;
         var newAnnot = alignViewport.getAlignment()
             .updateFromOrCopyAnnotation(ala);
-        if (ala.sequenceRef != null)
+        if (newAnnot.sequenceRef != null)
         {
-          ala.sequenceRef.addAlignmentAnnotation(newAnnot);
+          newAnnot.sequenceRef.addAlignmentAnnotation(newAnnot);
           newAnnot.adjustForAlignment();
           AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
               newAnnot, newAnnot.label, newAnnot.getCalcId());
@@ -59,7 +57,7 @@ public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableA
           AlignCalcWorkerAdapter.this,
           new AnnotationResult(
               annotations,
-              result.transferFeatures,
+              result.hasFeatures,
               result.featureColours,
               result.featureFilters));
     }
@@ -179,7 +177,7 @@ public class AlignCalcWorkerAdapter extends AlignCalcWorker implements PollableA
   }
 
   private WorkerListener listener = WorkerListener.NULL_LISTENER;
-  
+
   public void setWorkerListener(WorkerListener listener)
   {
     if (listener == null) listener = WorkerListener.NULL_LISTENER;
index 23e462b..467dafd 100644 (file)
@@ -2,20 +2,15 @@ package jalview.ws2.actions.annotation;
 
 import java.util.ArrayList;
 import java.util.BitSet;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import jalview.analysis.AlignSeq;
 import jalview.analysis.SeqsetUtils;
-import jalview.api.FeatureColourI;
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
 import jalview.ws2.actions.BaseJob;
@@ -26,25 +21,18 @@ public class AnnotationJob extends BaseJob
 
   final Map<String, SequenceI> seqNames;
 
-  final int start, end;
-  
-  final int minSize;
+  final int regionStart, regionEnd;
 
-  List<AlignmentAnnotation> returnedAnnotations = Collections.emptyList();
-  
-  Map<String, FeatureColourI> featureColours = Collections.emptyMap();
-  
-  Map<String, FeatureMatcherSetI> featureFilters = Collections.emptyMap();
-  
+  final int minSize;
 
   public AnnotationJob(List<SequenceI> inputSeqs, boolean[] gapMap,
-      Map<String, SequenceI> seqNames, int start, int end, int minSize)
+          Map<String, SequenceI> seqNames, int start, int end, int minSize)
   {
     super(inputSeqs);
     this.gapMap = gapMap;
     this.seqNames = seqNames;
-    this.start = start;
-    this.end = end;
+    this.regionStart = start;
+    this.regionEnd = end;
     this.minSize = minSize;
   }
 
@@ -58,16 +46,15 @@ public class AnnotationJob extends BaseJob
     return nvalid >= minSize;
   }
 
-  public static AnnotationJob create(AnnotatedCollectionI inputSeqs, 
-      boolean bySequence, boolean submitGaps, boolean requireAligned, 
-      boolean filterNonStandardResidues, int minSize)
+  public static AnnotationJob create(SequenceCollectionI inputSeqs,
+          boolean bySequence, boolean submitGaps, boolean requireAligned,
+          boolean filterNonStandardResidues, int minSize)
   {
-    List<SequenceI> seqs = new ArrayList<>();
+    List<SequenceI> seqences = new ArrayList<>();
     int minlen = 10;
-    int ln = -1;
-    Map<String, SequenceI> seqNames = bySequence ? new HashMap<>() : null;
-    BitSet gapMap = new BitSet();
-    int gapMapSize = 0;
+    int width = 0;
+    Map<String, SequenceI> namesMap = bySequence ? new HashMap<>() : null;
+    BitSet residueMap = new BitSet();
     int start = inputSeqs.getStartRes();
     int end = inputSeqs.getEndRes();
     // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
@@ -76,69 +63,88 @@ public class AnnotationJob extends BaseJob
     // persisted/restored
     for (SequenceI sq : inputSeqs.getSequences())
     {
-      int sqlen;
-      if (bySequence)
-        sqlen = sq.findPosition(end + 1) - sq.findPosition(start + 1);
+      int sqLen = (bySequence)
+              ? sq.findPosition(end + 1) - sq.findPosition(start + 1)
+              : sq.getEnd() - sq.getStart();
+      if (sqLen < minlen)
+        continue;
+      String newName = SeqsetUtils.unique_name(seqences.size() + 1);
+      if (namesMap != null)
+        namesMap.put(newName, sq);
+      Sequence seq;
+      if (submitGaps)
+      {
+        seq = new Sequence(newName, sq.getSequenceAsString());
+        updateResidueMap(residueMap, seq, filterNonStandardResidues);
+      }
       else
-        sqlen = sq.getEnd() - sq.getStart();
-      if (sqlen >= minlen)
       {
-        String newName = SeqsetUtils.unique_name(seqs.size() + 1);
-        if (seqNames != null)
-          seqNames.put(newName, sq);
-        Sequence seq;
-        if (submitGaps)
-        {
-          seq = new Sequence(newName, sq.getSequenceAsString());
-          gapMapSize = Math.max(gapMapSize, seq.getLength());
-          for (int pos : sq.gapMap())
-          {
-            char sqchr = sq.getCharAt(pos);
-            boolean include = !filterNonStandardResidues;
-            include |= sq.isProtein() ? ResidueProperties.aaIndex[sqchr] < 20
-                : ResidueProperties.nucleotideIndex[sqchr] < 5;
-            if (include)
-              gapMap.set(pos);
-          }
-        }
-        else
-        {
-          // TODO: add ability to exclude hidden regions
-          seq = new Sequence(newName, AlignSeq.extractGaps(Comparison.GapChars,
-              sq.getSequenceAsString(start, end + 1)));
-          // for annotation need to also record map to sequence start/end
-          // position in range
-          // then transfer back to original sequence on return.
-        }
-        seqs.add(seq);
-        ln = Math.max(ln, seq.getLength());
+        // TODO: add ability to exclude hidden regions
+        seq = new Sequence(newName,
+                AlignSeq.extractGaps(Comparison.GapChars,
+                        sq.getSequenceAsString(start, end + 1)));
+        // for annotation need to also record map to sequence start/end
+        // position in range
+        // then transfer back to original sequence on return.
       }
+      seqences.add(seq);
+      width = Math.max(width, seq.getLength());
     }
 
     if (requireAligned && submitGaps)
     {
-      int realWidth = gapMap.cardinality();
-      for (int i = 0; i < seqs.size(); i++)
+      for (int i = 0; i < seqences.size(); i++)
+      {
+        SequenceI sq = seqences.get(i);
+        char[] padded = fitSequenceToResidueMap(sq.getSequence(),
+                residueMap);
+        seqences.set(i, new Sequence(sq.getName(), padded));
+      }
+    }
+    boolean[] gapMapArray = null;
+    if (submitGaps)
+    {
+      gapMapArray = new boolean[width];
+      for (int i = 0; i < width; i++)
+        gapMapArray[i] = residueMap.get(i);
+    }
+    return new AnnotationJob(seqences, gapMapArray, namesMap, start, end,
+            minSize);
+  }
+
+  private static void updateResidueMap(BitSet residueMap, SequenceI seq,
+          boolean filterNonStandardResidues)
+  {
+    for (int pos : seq.gapMap())
+    {
+      char sqchr = seq.getCharAt(pos);
+      boolean include = !filterNonStandardResidues;
+      include |= seq.isProtein() ? ResidueProperties.aaIndex[sqchr] < 20
+              : ResidueProperties.nucleotideIndex[sqchr] < 5;
+      if (include)
+        residueMap.set(pos);
+    }
+  }
+
+  /**
+   * Fits the sequence to the residue map removing empty columns where residue
+   * map is unset and padding the sequence with gaps at the end if needed.
+   */
+  private static char[] fitSequenceToResidueMap(char[] sequence,
+          BitSet residueMap)
+  {
+    int width = residueMap.cardinality();
+    char[] padded = new char[width];
+    for (int op = 0, pp = 0; pp < width; op++)
+    {
+      if (residueMap.get(op))
       {
-        SequenceI sq = seqs.get(i);
-        char[] padded = new char[realWidth];
-        char[] original = sq.getSequence();
-        for (int op = 0, pp = 0; pp < realWidth; op++)
-        {
-          if (gapMap.get(op))
-          {
-            if (original.length > op)
-              padded[pp++] = original[op];
-            else
-              padded[pp++] = '-';
-          }
-        }
-        seqs.set(i, new Sequence(sq.getName(), padded));
+        if (sequence.length > op)
+          padded[pp++] = sequence[op];
+        else
+          padded[pp++] = '-';
       }
     }
-    boolean[] gapMapArray = new boolean[gapMapSize];
-    for (int i = 0; i < gapMapSize; i++)
-      gapMapArray[i] = gapMap.get(i);
-    return new AnnotationJob(seqs, gapMapArray, seqNames, start, end, minSize);
+    return padded;
   }
 }
index 373ecbb..86614a1 100644 (file)
@@ -19,17 +19,17 @@ public class AnnotationResult
 {
   final List<AlignmentAnnotation> annotations;
 
-  final boolean transferFeatures;
+  final boolean hasFeatures;
 
   final Map<String, FeatureColourI> featureColours;
 
   final Map<String, FeatureMatcherSetI> featureFilters;
 
-  public AnnotationResult(List<AlignmentAnnotation> annotations, boolean transferFeatures,
+  public AnnotationResult(List<AlignmentAnnotation> annotations, boolean hasFeatures,
       Map<String, FeatureColourI> featureColours, Map<String, FeatureMatcherSetI> featureFilters)
   {
     this.annotations = annotations;
-    this.transferFeatures = transferFeatures;
+    this.hasFeatures = hasFeatures;
     this.featureColours = featureColours;
     this.featureFilters = featureFilters;
   }
@@ -39,9 +39,9 @@ public class AnnotationResult
     return annotations;
   }
   
-  public boolean getTransferFeatures()
+  public boolean getHasFeatures()
   {
-    return transferFeatures;
+    return hasFeatures;
   }
 
   public Map<String, FeatureColourI> getFeatureColours()
index 866f862..6cb5aa6 100644 (file)
@@ -6,7 +6,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import jalview.analysis.AlignmentAnnotationUtils;
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.AlignmentAnnotation;
@@ -25,7 +24,8 @@ import jalview.ws2.api.Credentials;
 import jalview.ws2.api.JobStatus;
 import jalview.ws2.client.api.AnnotationWebServiceClientI;
 
-public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
+public class AnnotationTask
+        extends BaseTask<AnnotationJob, AnnotationResult>
 {
   private AnnotationWebServiceClientI client;
 
@@ -36,8 +36,8 @@ public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
   private final AnnotatedCollectionI selectionGroup;
 
   public AnnotationTask(AnnotationWebServiceClientI client,
-      AnnotationAction action, List<ArgumentI> args, Credentials credentials,
-      AlignViewportI viewport)
+          AnnotationAction action, List<ArgumentI> args,
+          Credentials credentials, AlignViewportI viewport)
   {
     super(client, args, credentials);
     this.client = client;
@@ -50,52 +50,57 @@ public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
    * Create and return a list of annotation jobs from the current state of the
    * viewport. Returned job are not started by this method and should be stored
    * in a field and started separately.
-   * 
+   *
    * @return list of annotation jobs
    * @throws ServiceInputInvalidException
    *           input data is not valid
    */
   @Override
-  public List<AnnotationJob> prepareJobs() throws ServiceInputInvalidException
+  public List<AnnotationJob> prepareJobs()
+          throws ServiceInputInvalidException
   {
-    if (alignment == null || alignment.getWidth() <= 0 ||
-        alignment.getSequences() == null)
-      throw new ServiceInputInvalidException("Alignment does not contain sequences");
-    if (alignment.isNucleotide() && !action.doAllowNucleotide())
+    if (alignment == null || alignment.getWidth() <= 0
+            || alignment.getSequences() == null)
       throw new ServiceInputInvalidException(
-          action.getFullName() + " does not allow nucleotide sequences");
+              "Alignment does not contain sequences");
+    if (alignment.isNucleotide() && !action.doAllowNucleotide())
+      throw new ServiceInputInvalidException(action.getFullName()
+              + " does not allow nucleotide sequences");
     if (!alignment.isNucleotide() && !action.doAllowProtein())
       throw new ServiceInputInvalidException(
-          action.getFullName() + " does not allow protein sequences");
+              action.getFullName() + " does not allow protein sequences");
     boolean bySequence = !action.isAlignmentAnalysis();
     AnnotatedCollectionI inputSeqs = bySequence ? selectionGroup : null;
-    if (inputSeqs == null || inputSeqs.getWidth() <= 0 ||
-        inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1)
+    if (inputSeqs == null || inputSeqs.getWidth() <= 0
+            || inputSeqs.getSequences() == null
+            || inputSeqs.getSequences().size() < 1)
       inputSeqs = alignment;
     boolean submitGaps = action.isAlignmentAnalysis();
     boolean requireAligned = action.getRequireAlignedSequences();
     boolean filterSymbols = action.getFilterSymbols();
     int minSize = action.getMinSequences();
     AnnotationJob job = AnnotationJob.create(inputSeqs, bySequence,
-        submitGaps, requireAligned, filterSymbols, minSize);
+            submitGaps, requireAligned, filterSymbols, minSize);
     if (!job.isInputValid())
     {
       job.setStatus(JobStatus.INVALID);
-      throw new ServiceInputInvalidException("Annotation job has invalid input");
+      throw new ServiceInputInvalidException(
+              "Annotation job has invalid input");
     }
     job.setStatus(JobStatus.READY);
     return List.of(job);
   }
 
   @Override
-  protected AnnotationResult collectResult(List<AnnotationJob> jobs) throws IOException
+  protected AnnotationResult collectResult(List<AnnotationJob> jobs)
+          throws IOException
   {
     final Map<String, FeatureColourI> featureColours = new HashMap<>();
     final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
     var job = jobs.get(0);
     List<AlignmentAnnotation> returnedAnnot = client.attachAnnotations(
-        job.getServerJob(), job.getInputSequences(), featureColours,
-        featureFilters);
+            job.getServerJob(), job.getInputSequences(), featureColours,
+            featureFilters);
     /* TODO
      * copy over each annotation row returned and also defined on each
      * sequence, excluding regions not annotated due to gapMap/column
@@ -104,34 +109,40 @@ public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
     udpateCalcId(returnedAnnot);
     for (AlignmentAnnotation ala : returnedAnnot)
     {
-      SequenceI aseq = null;
-      if (ala.sequenceRef != null)
+      SequenceI seq = (ala.sequenceRef == null) ? null
+              : job.seqNames.get(ala.sequenceRef.getName());
+      if (job.gapMap != null && job.gapMap.length > 0)
+        ala.annotations = createGappedAnnotations(ala.annotations,
+                job.gapMap);
+      if (seq != null)
       {
-        SequenceI seq = job.seqNames.get(ala.sequenceRef.getName());
-        aseq = seq.getRootDatasetSequence();
+        int startRes = seq.findPosition(job.regionStart);
+        ala.createSequenceMapping(seq, startRes, false);
       }
-      ala.sequenceRef = aseq;
-      Annotation[] gappedAnnots = createGappedAnnotations(ala.annotations, job.start, job.gapMap);
-      ala.annotations = gappedAnnots;
     }
 
     boolean hasFeatures = false;
     for (SequenceI sq : job.getInputSequences())
     {
-      if (!sq.getFeatures().hasFeatures() && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
+      if (!sq.getFeatures().hasFeatures()
+              && (sq.getDBRefs() == null || sq.getDBRefs().isEmpty()))
         continue;
       hasFeatures = true;
       SequenceI seq = job.seqNames.get(sq.getName());
       SequenceI datasetSeq = seq.getRootDatasetSequence();
-      List<ContiguousI> sourceRange = findContiguousRanges(datasetSeq, job.gapMap, job.start, job.end);
+      List<ContiguousI> sourceRange = findContiguousRanges(seq,
+              job.gapMap, job.regionStart, job.regionEnd);
       int[] sourceStartEnd = ContiguousI.toStartEndArray(sourceRange);
-      Mapping mp = new Mapping(new MapList(
-          sourceStartEnd, new int[]
-          { datasetSeq.getStart(), datasetSeq.getEnd() }, 1, 1));
+      Mapping mp = new Mapping(
+          new MapList(
+              sourceStartEnd,
+              new int[] { datasetSeq.getStart(), datasetSeq.getEnd() },
+              1, 1));
       datasetSeq.transferAnnotation(sq, mp);
     }
 
-    return new AnnotationResult(returnedAnnot, hasFeatures, featureColours, featureFilters);
+    return new AnnotationResult(returnedAnnot, hasFeatures, featureColours,
+            featureFilters);
   }
 
   /**
@@ -141,22 +152,24 @@ public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
   {
     for (var annotation : annotations)
     {
-      if (annotation.getCalcId() == null || annotation.getCalcId().isEmpty())
+      if (annotation.getCalcId() == null
+              || annotation.getCalcId().isEmpty())
       {
         annotation.setCalcId(action.getFullName());
       }
-      annotation.autoCalculated = action.isAlignmentAnalysis() &&
-          action.getWebService().isInteractive();
+      annotation.autoCalculated = action.isAlignmentAnalysis()
+              && action.getWebService().isInteractive();
     }
   }
 
-  private Annotation[] createGappedAnnotations(Annotation[] annotations, int start, boolean[] gapMap)
+  private Annotation[] createGappedAnnotations(Annotation[] annotations,
+          boolean[] gapMap)
   {
-    var size = Math.max(alignment.getWidth(), gapMap.length);
+    var size = Math.max(annotations.length, gapMap.length);
     Annotation[] gappedAnnotations = new Annotation[size];
-    for (int p = 0, ap = start; ap < size; ap++)
+    for (int p = 0, ap = 0; ap < size; ap++)
     {
-      if (gapMap != null && gapMap.length > ap && !gapMap[ap])
+      if (ap < gapMap.length && !gapMap[ap])
       {
         gappedAnnotations[ap] = new Annotation("", "", ' ', Float.NaN);
       }
@@ -168,10 +181,12 @@ public class AnnotationTask extends BaseTask<AnnotationJob, AnnotationResult>
     return gappedAnnotations;
   }
 
-  private List<ContiguousI> findContiguousRanges(SequenceI seq, boolean[] gapMap, int start, int end)
+  // TODO: review ant test!!!
+  private List<ContiguousI> findContiguousRanges(SequenceI seq,
+          boolean[] gapMap, int start, int end)
   {
     if (gapMap == null || gapMap.length < end)
-      return List.of(seq.findPositions(start, end));
+      return List.of(seq.findPositions(start + 1, end + 1));
     List<ContiguousI> ranges = new ArrayList<>();
     int lastcol = start, col = start;
     do
index a7202e9..8fc07a5 100644 (file)
@@ -66,7 +66,7 @@ public class AnnotationServiceGuiHandler
   {
     if (result == null)
       return;
-    if (result.getTransferFeatures())
+    if (result.getHasFeatures())
     {
       alignFrame.getViewport().applyFeaturesStyle(new FeatureSettingsAdapter()
       {