JAL-3706 handle virtual feature extending to unmapped stop codon
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 10 Aug 2020 15:04:14 +0000 (16:04 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 10 Aug 2020 15:04:14 +0000 (16:04 +0100)
src/jalview/datamodel/MappedFeatures.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/gui/PopupMenu.java
src/jalview/io/SequenceAnnotationReport.java
test/jalview/datamodel/MappedFeaturesTest.java
test/jalview/io/SequenceAnnotationReportTest.java

index d652a97..1f672be 100644 (file)
@@ -276,7 +276,8 @@ public class MappedFeatures
 
   /**
    * Answers the mapped ranges (as one or more [start, end] positions) which
-   * correspond to the given [begin, end] range of the linked sequence.
+   * correspond to the given [begin, end] range of (some feature on) the linked
+   * sequence.
    * 
    * <pre>
    * Example: MappedFeatures with CDS features mapped to peptide 
@@ -293,9 +294,27 @@ public class MappedFeatures
    */
   public int[] getMappedPositions(int begin, int end)
   {
+    int[] result = null;
     MapList map = mapping.getMap();
-    return mapping.to == featureSequence ? map.locateInFrom(begin, end)
-            : map.locateInTo(begin, end);
+    if (mapping.to == featureSequence)
+    {
+      result = map.locateInFrom(begin, end);
+      if (result == null)
+      {
+        // fudge for feature (e.g. CDS) extending to a mapped stop codon
+        result = map.locateInFrom(begin, end-3);
+      }
+    }
+    else
+    {
+      result = map.locateInTo(begin, end);
+      if (result == null)
+      {
+        // fudge for feature (e.g. CDS) extending to a mapped stop codon
+        result = map.locateInTo(begin, end-3);
+      }
+    }
+    return result;
   }
 
   /**
index 6eeba2f..bbf1b45 100755 (executable)
@@ -612,10 +612,9 @@ public class SequenceFeature implements FeatureLocationI
     String consequence = "";
     if (mf != null)
     {
-      int[] beginRange = mf.getMappedPositions(begin, begin);
-      int[] endRange = mf.getMappedPositions(end, end);
-      int from = beginRange[0];
-      int to = endRange[endRange.length - 1];
+      int[] localRange = mf.getMappedPositions(begin, end);
+      int from = localRange[0];
+      int to = localRange[localRange.length - 1];
       String s = mf.isFromCds() ? "Peptide Location" : "Coding location";
       sb.append(String.format(ROW_DATA, s, seqName, from == to ? from
               : from + (isContactFeature() ? ":" : "-") + to));
index 568f7f1..eaff5ef 100644 (file)
@@ -831,10 +831,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       /*
        * show local rather than linked feature coordinates
        */
-      int[] beginRange = mf.getMappedPositions(start, start);
-      start = beginRange[0];
-      int[] endRange = mf.getMappedPositions(end, end);
-      end = endRange[endRange.length - 1];
+      int[] localRange = mf.getMappedPositions(start, end);
+      start = localRange[0];
+      end = localRange[localRange.length - 1];
     }
     StringBuilder desc = new StringBuilder();
     desc.append(sf.getType()).append(" ").append(String.valueOf(start));
index 27c1652..8b1f7ff 100644 (file)
@@ -120,21 +120,11 @@ public class SequenceAnnotationReport
     }
   };
 
-  private boolean forTooltip;
-
   /**
-   * Constructor given a flag which affects behaviour
-   * <ul>
-   * <li>if true, generates feature details suitable to show in a tooltip</li>
-   * <li>if false, generates feature details in a form suitable for the sequence
-   * details report</li>
-   * </ul>
-   * 
-   * @param isForTooltip
+   * Constructor
    */
   public SequenceAnnotationReport(boolean isForTooltip)
   {
-    this.forTooltip = isForTooltip;
     if (linkImageURL == null)
     {
       linkImageURL = getClass().getResource("/images/link.gif").toString();
@@ -210,41 +200,32 @@ public class SequenceAnnotationReport
      * if this is a virtual features, convert begin/end to the
      * coordinates of the sequence it is mapped to
      */
-    int[] beginRange = null;
-    int[] endRange = null;
+    int[] localRange = null;
     if (mf != null)
     {
-      beginRange = mf.getMappedPositions(begin, begin);
-      endRange = mf.getMappedPositions(end, end);
-      if (beginRange == null || endRange == null)
+      localRange = mf.getMappedPositions(begin, end);
+      if (localRange == null)
       {
         // something went wrong
         return false;
       }
-      begin = beginRange[0];
-      end = endRange[endRange.length - 1];
+      begin = localRange[0];
+      end = localRange[localRange.length - 1];
     }
 
     StringBuilder sb = new StringBuilder();
     if (feature.isContactFeature())
     {
       /*
-       * include if rpos is at start or end position of [mapped] feature
+       * contact features are rendered slightly differently; note the check for
+       * 'start or end position only' was applied earlier when finding features
        */
-      boolean showContact = (mf == null) && (rpos == begin || rpos == end);
-      boolean showMappedContact = (mf != null) && ((rpos >= beginRange[0]
-              && rpos <= beginRange[beginRange.length - 1])
-              || (rpos >= endRange[0]
-                      && rpos <= endRange[endRange.length - 1]));
-      if (showContact || showMappedContact)
+      if (sb0.length() > 6)
       {
-        if (sb0.length() > 6)
-        {
-          sb.append("<br/>");
-        }
-        sb.append(feature.getType()).append(" ").append(begin).append(":")
-                .append(end);
+        sb.append("<br/>");
       }
+      sb.append(feature.getType()).append(" ").append(begin).append(":")
+              .append(end);
       return appendText(sb0, sb, maxlength);
     }
 
index de4ce6c..5d00089 100644 (file)
@@ -2,8 +2,6 @@ package jalview.datamodel;
 
 import static org.testng.Assert.assertEquals;
 
-import jalview.util.MapList;
-
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -11,6 +9,8 @@ import java.util.Map;
 
 import org.testng.annotations.Test;
 
+import jalview.util.MapList;
+
 public class MappedFeaturesTest
 {
   @Test(groups = "Functional")
@@ -21,7 +21,7 @@ public class MappedFeaturesTest
      * dna/10-20 aCGTaGctGAa (codons CGT=R, GGA = G)
      * mapping: 3:1 from [11-13,15,18-19] to peptide/1-2 RG 
      */
-    SequenceI from = new Sequence("dna/10-20", "acgTAGCTGAA");
+    SequenceI from = new Sequence("dna/10-20", "aCGTaGctGAa");
     SequenceI to = new Sequence("peptide", "RG");
     MapList map = new MapList(new int[] { 11, 13, 15, 15, 18, 19 },
             new int[]
@@ -117,4 +117,45 @@ public class MappedFeaturesTest
     variant = mf.findProteinVariants(sf9);
     assertEquals(variant, "");
   }
+
+  @Test(groups = "Functional")
+  public void testGetMappedPositions()
+  {
+    // CDS including stop codon taa
+    SequenceI cds = new Sequence("cds/10-21", "ATGcgtGGAtaa");
+    SequenceI peptide = new Sequence("peptide", "MRG"); // ATG, CGT, GGA
+    
+    /*
+     * emulate 'map from' range based on CDS _including_ stop codon
+     */
+    MapList map = new MapList(new int[] { 10, 21 }, new int[] { 1, 3 }, 3,
+            1);
+    Mapping mapping = new Mapping(peptide, map);
+
+    MappedFeatures mf = new MappedFeatures(mapping, cds, 2, 'M', null);
+    
+    /*
+     * scenario: sequence_variant feature on CDS at position 14;
+     * find the corresponding position on peptide
+     */
+    int[] pepPos = mf.getMappedPositions(14,  14);
+    assertEquals(pepPos[0], 2);
+    assertEquals(pepPos[1], 2);
+    
+    /*
+     * scenario: exon feature on CDS including stop codon;
+     */
+    pepPos = mf.getMappedPositions(10, 21);
+    assertEquals(pepPos[0], 1);
+    assertEquals(pepPos[1], 3);
+    
+    /*
+     * now with the mapping from protein to CDS
+     */
+    mapping = new Mapping(cds, map.getInverse());
+    mf = new MappedFeatures(mapping, peptide, 15, 't', null);
+    int[] cdsPos = mf.getMappedPositions(2,  2);
+    assertEquals(cdsPos[0], 13);
+    assertEquals(cdsPos[1], 15);
+  }
 }
index 772ed2b..57821c9 100644 (file)
@@ -61,23 +61,20 @@ public class SequenceAnnotationReportTest
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
-    sb.append("123456");
+    sb.append("foo ");
     SequenceFeature sf = new SequenceFeature("disulfide bond", "desc", 1,
             3, 1.2f, "group");
 
-    // residuePos == 2 does not match start or end of feature, nothing done:
-    sar.appendFeature(sb, 2, null, sf, null, 0);
-    assertEquals("123456", sb.toString());
-
     // residuePos == 1 matches start of feature, text appended (but no <br/>)
     // feature score is not included
     sar.appendFeature(sb, 1, null, sf, null, 0);
-    assertEquals("123456disulfide bond 1:3", sb.toString());
+    assertEquals("foo disulfide bond 1:3", sb.toString());
 
-    // residuePos == 3 matches end of feature, text appended
+    // residuePos == 2 doesnt' match end of feature, text appended anyway
+    // (the test for this is handled by FeatureStore.findContactFeatures())
     // <br/> is prefixed once sb.length() > 6
-    sar.appendFeature(sb, 3, null, sf, null, 0);
-    assertEquals("123456disulfide bond 1:3<br/>disulfide bond 1:3",
+    sar.appendFeature(sb, 2, null, sf, null, 0);
+    assertEquals("foo disulfide bond 1:3<br/>disulfide bond 1:3",
             sb.toString());
   }