JAL-3761 debugged and working (but not tidied) locateInFrom2/To2
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 20 Nov 2020 11:58:42 +0000 (11:58 +0000)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 16 Sep 2021 09:18:07 +0000 (10:18 +0100)
src/jalview/util/MapList.java
test/jalview/util/MapListTest.java

index 2e673e0..0d71bb4 100644 (file)
@@ -703,12 +703,13 @@ public class MapList
    */
   public int[] locateInFrom(int start, int end)
   {
+    return locateInFrom2(start, end);
+    
     // inefficient implementation
-    int fromStart[] = shiftTo(start);
+    // int fromStart[] = shiftTo(start);
     // needs to be inclusive of end of symbol position
-    int fromEnd[] = shiftTo(end);
-    System.out.println("locateInFrom");
-    return getIntervals(fromShifts, fromStart, fromEnd, fromRatio);
+    // int fromEnd[] = shiftTo(end);
+    // return getIntervals(fromShifts, fromStart, fromEnd, fromRatio);
   }
 
   /**
@@ -722,9 +723,11 @@ public class MapList
    */
   public int[] locateInTo(int start, int end)
   {
-    int toStart[] = shiftFrom(start);
-    int toEnd[] = shiftFrom(end);
-    return getIntervals(toShifts, toStart, toEnd, toRatio);
+    return locateInTo2(start, end);
+    
+    // int toStart[] = shiftFrom(start);
+    // int toEnd[] = shiftFrom(end);
+    // return getIntervals(toShifts, toStart, toEnd, toRatio);
   }
 
   /**
@@ -1212,6 +1215,7 @@ public class MapList
     List<int[]> toRanges = new ArrayList<>();
     for (int[] range : getToRanges())
     {
+      int fromLength = Math.abs(range[1] - range[0]) + 1;
       int[] transferred = map.locateInTo(range[0], range[1]);
       if (transferred == null || transferred.length % 2 != 0)
       {
@@ -1222,11 +1226,21 @@ public class MapList
        *  convert [start1, end1, start2, end2, ...] 
        *  to [[start1, end1], [start2, end2], ...]
        */
+      int toLength = 0;
       for (int i = 0; i < transferred.length;)
       {
         toRanges.add(new int[] { transferred[i], transferred[i + 1] });
+        toLength += Math.abs(transferred[i + 1] - transferred[i]) + 1;
         i += 2;
       }
+      
+      /*
+       * check we mapped the full range - if not, abort
+       */
+      if (fromLength * map.getToRatio() != toLength * map.getFromRatio())
+      {
+        return null;
+      }
     }
 
     return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
@@ -1245,8 +1259,8 @@ public class MapList
 
   /**
    * Returns the [start, end, start, end, ...] ranges in the 'from' range that
-   * map to the given start-end in the 'to' range. Returns null if either
-   * {@code start} or {@code end} is not a mapped 'to' range position.
+   * map to positions between {@code start} and {@code end} in the 'to' range.
+   * Returns null if no mapped positions are found in start-end.
    * 
    * @param start
    * @param end
@@ -1295,6 +1309,13 @@ public class MapList
           List<int[]> ranges1, List<int[]> ranges2, int wordLength1,
           int wordLength2)
   {
+    if (end < start)
+    {
+      int tmp = end;
+      end = start;
+      start = tmp;
+    }
+
     /*
      * first traverse ranges1 and record count of mapped positions 
      * to any that overlap start-end
@@ -1306,7 +1327,7 @@ public class MapList
     }
 
     /*
-     * convert positions to equivalent 'word' positions in ranges
+     * convert positions to equivalent 'word' positions in ranges2
      */
     mapWords(overlaps, wordLength1, wordLength2);
 
@@ -1317,24 +1338,52 @@ public class MapList
     List<int[]> mapped = new ArrayList<>();
     final int s1 = overlaps.size();
     final int s2 = ranges2.size();
-    int rangeIndex = 0;
-    int rangeOffset = 0;
-    int mappedCount = 0;
+    int ranges2Index = 0;
     
-    for (int i = 0 ; i < s1 ; i++)
+    /*
+     * count of mapped positions preceding ranges2[ranges2Index] 
+     */
+    int traversed = 0;
+
+    /*
+     * for each [from-to] range in overlaps:
+     * - walk (what remains of) ranges2
+     * - record the values at offsets [from-to]
+     * - stop when past 'to' positions (or at end of ranges2)
+     */
+    for (int i = 0; i < s1; i++)
     {
-      /*
-       * for each range in overlaps, walk ranges2 and record the values 
-       * at the offsets, advancing rangeIndex / Offset
-       */
-      int [] mappedRange = ranges2.get(rangeIndex);
-      int [] overlap = overlaps.get(s1);
-      while (mappedCount < overlap[1])
+      int[] overlap = overlaps.get(i);
+      final int toAdd = overlap[1] - overlap[0] + 1;
+      int added = 0; // how much of overlap has been 'found'
+      for (; added < toAdd && ranges2Index < s2; ranges2Index++)
       {
-        
+        int[] range2 = ranges2.get(ranges2Index);
+        int rangeStart = range2[0];
+        int rangeEnd = range2[1];
+        boolean reverseStrand = range2[1] < range2[0];
+        int rangeLength = Math.abs(rangeEnd - rangeStart) + 1;
+        if (traversed + rangeLength <= overlap[0])
+        {
+          /*
+           * precedes overlap - keep looking
+           */
+          traversed += rangeLength;
+          continue;
+        }
+        int overlapStart = overlap[0] - traversed;
+        int overlapEnd = Math.min(overlapStart + toAdd - added - 1,
+                rangeLength - 1);
+        int mappedFrom = range2[0] + (reverseStrand ? - overlapStart : overlapStart);
+        int mappedTo = range2[0] + (reverseStrand ? - overlapEnd : overlapEnd);
+        mapped.add(new int[] { mappedFrom, mappedTo });
+        int found = overlapEnd - overlapStart + 1;
+        added += found;
+        overlap[0] += found;
+        traversed += rangeLength;
       }
     }
-    
+
     return mapped;
   }
 
@@ -1389,10 +1438,10 @@ public class MapList
 
   /**
    * Helper method that returns a (possibly empty) list of offsets in
-   * {@code ranges} to subranges that overlap {@code start-end}. The list
-   * returned holds counts of the number of positions traversed (inclusive) to
-   * reach the overlapping positions, not the overlapping values. Returns null
-   * if there are no overlaps.
+   * {@code ranges} to subranges that overlap {@code start-end} (where start <=
+   * end}. The list returned holds counts of the number of positions traversed
+   * (exclusive) to reach the overlapping positions, not the overlapping values.
+   * Returns null if there are no overlaps.
    * 
    * @param ranges
    * @param start
@@ -1416,8 +1465,8 @@ public class MapList
 
   /**
    * A helper method that checks whether {@code range} overlaps
-   * {@code start-end}, and if so adds the positional offset of the overlap to
-   * {@code positions}.
+   * {@code start-end}, and if so adds the offset of the overlap in
+   * {@code range}, plus {@code pos}, to {@code positions}.
    * 
    * @param positions
    *          a list of map offsets to add to
@@ -1445,10 +1494,25 @@ public class MapList
          * overlap
          */
         int overlapStart = Math.max(start, range[0]);
+        int overlapStartOffset = pos + overlapStart - range[0];
         int overlapEnd = Math.min(end, range[1]);
-        positions
-                .add(new int[]
-                { 1 + overlapStart - range[0], 1 + overlapEnd - range[0] });
+        int overlapEndOffset = pos + overlapEnd - range[0];
+        int[] lastOverlap = positions.isEmpty() ? null
+                : positions.get(positions.size() - 1);
+        if (lastOverlap != null && overlapStartOffset == lastOverlap[1] + 1)
+        {
+          /*
+           * just extending the last overlap range
+           */
+          lastOverlap[1] = overlapEndOffset;
+        }
+        else
+        {
+          /*
+           * add a new (discontiguous) overlap range
+           */
+          positions.add(new int[] { overlapStartOffset, overlapEndOffset });
+        }
       }
     }
     else
@@ -1465,7 +1529,8 @@ public class MapList
         int overlapEnd = Math.min(end, range[0]);
         positions
                 .add(new int[]
-                { 1 + range[0] - overlapStart, 1 + range[0] - overlapEnd });
+                { pos + range[0] - overlapEnd,
+                    pos + range[0] - overlapStart });
       }
     }
   }
index 07da78f..fd34902 100644 (file)
@@ -53,7 +53,7 @@ public class MapListTest
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Functional" }, enabled = false)
   public void testSomething()
   {
     MapList ml = new MapList(new int[] { 1, 5, 10, 15, 25, 20 },
@@ -271,17 +271,24 @@ public class MapListTest
     assertEquals("[10, 12]", Arrays.toString(ml.locateInFrom(4, 4)));
     assertEquals("[1, 6]", Arrays.toString(ml.locateInFrom(1, 2)));
     assertEquals("[1, 9]", Arrays.toString(ml.locateInFrom(1, 3)));
+    // reversed range treated as if forwards:
+    assertEquals("[1, 9]", Arrays.toString(ml.locateInFrom(3, 1)));
     assertEquals("[1, 12]", Arrays.toString(ml.locateInFrom(1, 4)));
     assertEquals("[4, 9]", Arrays.toString(ml.locateInFrom(2, 3)));
     assertEquals("[4, 12]", Arrays.toString(ml.locateInFrom(2, 4)));
     assertEquals("[7, 12]", Arrays.toString(ml.locateInFrom(3, 4)));
     assertEquals("[10, 12]", Arrays.toString(ml.locateInFrom(4, 4)));
-    // reversing the range reverses the result:
-    assertEquals("[12, 7]", Arrays.toString(ml.locateInFrom(4, 3))); // fails with [10, 9] !
 
+    /*
+     * partial overlap
+     */
+    assertEquals("[1, 12]", Arrays.toString(ml.locateInFrom(1, 5)));
+    assertEquals("[1, 3]", Arrays.toString(ml.locateInFrom(-1, 1)));
+
+    /*
+     * no overlap
+     */
     assertNull(ml.locateInFrom(0, 0));
-    assertNull(ml.locateInFrom(1, 5));
-    assertNull(ml.locateInFrom(-1, 1));
   }
 
   /**
@@ -314,8 +321,8 @@ public class MapListTest
     assertEquals("[12, 10]", Arrays.toString(ml.locateInFrom(1, 1)));
     assertEquals("[9, 4]", Arrays.toString(ml.locateInFrom(2, 3)));
   }
-  
-   /**
+
+  /**
    * Tests for method that locates ranges in the 'to' map for given range in the
    * 'from' map.
    */
@@ -337,8 +344,8 @@ public class MapListTest
     assertEquals("[1, 4]", Arrays.toString(ml.locateInTo(1, 12)));
     assertEquals("[2, 2]", Arrays.toString(ml.locateInTo(4, 6)));
     assertEquals("[2, 4]", Arrays.toString(ml.locateInTo(4, 12)));
-    // reversing the 'from' range reverses the result
-    assertEquals("[4, 2]", Arrays.toString(ml.locateInTo(12, 4)));
+    // reverse range treated as if forwards:
+    assertEquals("[2, 4]", Arrays.toString(ml.locateInTo(12, 4)));
 
     /*
      * A part codon is treated as if a whole one.
@@ -350,9 +357,16 @@ public class MapListTest
     assertEquals("[1, 4]", Arrays.toString(ml.locateInTo(3, 11)));
     assertEquals("[2, 4]", Arrays.toString(ml.locateInTo(5, 11)));
 
+    /*
+     * partial overlap
+     */
+    assertEquals("[1, 4]", Arrays.toString(ml.locateInTo(1, 13)));
+    assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(-1, 2)));
+    
+    /*
+     * no overlap
+     */
     assertNull(ml.locateInTo(0, 0));
-    assertNull(ml.locateInTo(1, 13));
-    assertNull(ml.locateInTo(-1, 1));
   }
 
   /**
@@ -374,14 +388,6 @@ public class MapListTest
     MapList ml = new MapList(codons, protein, 3, 1);
 
     /*
-     * Can't map from an unmapped position
-     */
-    assertNull(ml.locateInTo(1, 2));
-    assertNull(ml.locateInTo(1, 4));
-    assertNull(ml.locateInTo(2, 4));
-    assertNull(ml.locateInTo(4, 4));
-
-    /*
      * Valid range or subrange of codon1 maps to protein1
      */
     assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(2, 2)));
@@ -395,6 +401,18 @@ public class MapListTest
 
     // codon positions 7 to 17 (part) cover proteins 2/3/4 at positions 3/4/6
     assertEquals("[3, 4, 6, 6]", Arrays.toString(ml.locateInTo(7, 17)));
+
+    /*
+     * partial overlap
+     */
+    assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(1, 2)));
+    assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(1, 4)));
+    assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(2, 4)));
+    
+    /*
+     * no overlap
+     */
+    assertNull(ml.locateInTo(4, 4));
   }
 
   /**
@@ -941,10 +959,10 @@ public class MapListTest
     assertArrayEquals(new int[] { 71, 126 }, toRanges.get(1));
 
     /*
-     * method returns null if not all regions are mapped through
+     * if not all regions are mapped through, returns what is
      */
     ml1 = new MapList(new int[] { 1, 50 }, new int[] { 101, 150 }, 1, 1);
-    ml2 = new MapList(new int[] { 131, 180 }, new int[] { 201, 250 }, 1, 3);
+    ml2 = new MapList(new int[] { 131, 180 }, new int[] { 201, 250 }, 1, 1);
     compound = ml1.traverse(ml2);
     assertNull(compound);
   }
@@ -1000,19 +1018,29 @@ public class MapListTest
     assertEquals("[4, 9]", Arrays.toString(ml.locateInFrom(2, 3)));
     assertEquals("[7, 12, 12, 17]", Arrays.toString(ml.locateInFrom(3, 6)));
 
+    /*
+     * partial overlap of range
+     */
+    assertEquals("[4, 12, 12, 17]", Arrays.toString(ml.locateInFrom(2, 7)));
+    assertEquals("[1, 3]", Arrays.toString(ml.locateInFrom(-1, 1)));
+
+    /*
+     * no overlap in range
+     */
     assertNull(ml.locateInFrom(0, 0));
-    assertNull(ml.locateInFrom(1, 7));
-    assertNull(ml.locateInFrom(-1, 1));
 
     /*
      * gene to CDS...from EMBL:MN908947
      */
-    int [] gene = new int[] { 266, 13468, 13468, 21555 };
-    int [] cds = new int[] { 1, 21291 };
+    int[] gene = new int[] { 266, 13468, 13468, 21555 };
+    int[] cds = new int[] { 1, 21291 };
     ml = new MapList(gene, cds, 1, 1);
-    assertEquals("[13468, 13468]", Arrays.toString(ml.locateInFrom(13203, 13203)));
-    assertEquals("[13468, 13468]", Arrays.toString(ml.locateInFrom(13204, 13204)));
-    assertEquals("[13468, 13468]", Arrays.toString(ml.locateInFrom(13203, 13204)));
+    assertEquals("[13468, 13468]",
+            Arrays.toString(ml.locateInFrom(13203, 13203)));
+    assertEquals("[13468, 13468]",
+            Arrays.toString(ml.locateInFrom(13204, 13204)));
+    assertEquals("[13468, 13468, 13468, 13468]",
+            Arrays.toString(ml.locateInFrom(13203, 13204)));
   }
 
   /**
@@ -1038,17 +1066,36 @@ public class MapListTest
     assertEquals("[4, 6]", Arrays.toString(ml.locateInTo(11, 15)));
     assertEquals("[6, 6]", Arrays.toString(ml.locateInTo(15, 17)));
 
+    /*
+     * no overlap
+     */
     assertNull(ml.locateInTo(0, 0));
-    assertNull(ml.locateInTo(1, 18));
-    assertNull(ml.locateInTo(-1, 1));
+    
+    /*
+     * partial overlap
+     */
+    assertEquals("[1, 6]", Arrays.toString(ml.locateInTo(1, 18)));
+    assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(-1, 1)));
 
     /*
      * gene to CDS...from EMBL:MN908947
+     * the base at 13468 is used twice in transcription
      */
-    int [] gene = new int[] { 266, 13468, 13468, 21555 };
-    int [] cds = new int[] { 1, 21291 };
+    int[] gene = new int[] { 266, 13468, 13468, 21555 };
+    int[] cds = new int[] { 1, 21291 };
     ml = new MapList(gene, cds, 1, 1);
-    assertEquals("[13203, 13204]", Arrays.toString(ml.locateInTo(13468, 13468)));
+    assertEquals("[13203, 13204]",
+            Arrays.toString(ml.locateInTo(13468, 13468)));
+    
+    /*
+     * gene to protein
+     * the base at 13468 is in the codon for 4401N and also 4402R
+     */
+    gene = new int[] { 266, 13468, 13468, 21552 }; // stop codon excluded
+    protein = new int[] { 1, 7096 };
+    ml = new MapList(gene, protein, 3, 1);
+    assertEquals("[4401, 4402]",
+            Arrays.toString(ml.locateInTo(13468, 13468)));
   }
 
   @Test(groups = { "Functional" })
@@ -1074,121 +1121,253 @@ public class MapListTest
     }
 
     List<int[]> intervals = new ArrayList<>();
-    assertNull(MapList.countPositions(intervals,  1));
-    
+    assertNull(MapList.countPositions(intervals, 1));
+
     /*
      * forward strand
      */
-    intervals.add(new int[] {10, 20});
-    assertNull(MapList.countPositions(intervals,  9));
-    assertNull(MapList.countPositions(intervals,  21));
-    assertArrayEquals(new int[] {1, 1}, MapList.countPositions(intervals,  10));
-    assertArrayEquals(new int[] {6, 1}, MapList.countPositions(intervals,  15));
-    assertArrayEquals(new int[] {11, 1}, MapList.countPositions(intervals,  20));
-
-    intervals.add(new int[] {25, 25});
-    assertArrayEquals(new int[] {12, 1}, MapList.countPositions(intervals,  25));
-
-    // next interval repeats position 25 - which should be counted twice if traversed
-    intervals.add(new int[] {25, 26});
-    assertArrayEquals(new int[] {12, 1}, MapList.countPositions(intervals,  25));
-    assertArrayEquals(new int[] {14, 1}, MapList.countPositions(intervals,  26));
-    
+    intervals.add(new int[] { 10, 20 });
+    assertNull(MapList.countPositions(intervals, 9));
+    assertNull(MapList.countPositions(intervals, 21));
+    assertArrayEquals(new int[] { 1, 1 },
+            MapList.countPositions(intervals, 10));
+    assertArrayEquals(new int[] { 6, 1 },
+            MapList.countPositions(intervals, 15));
+    assertArrayEquals(new int[] { 11, 1 },
+            MapList.countPositions(intervals, 20));
+
+    intervals.add(new int[] { 25, 25 });
+    assertArrayEquals(new int[] { 12, 1 },
+            MapList.countPositions(intervals, 25));
+
+    // next interval repeats position 25 - which should be counted twice if
+    // traversed
+    intervals.add(new int[] { 25, 26 });
+    assertArrayEquals(new int[] { 12, 1 },
+            MapList.countPositions(intervals, 25));
+    assertArrayEquals(new int[] { 14, 1 },
+            MapList.countPositions(intervals, 26));
+
     /*
      * reverse strand
      */
     intervals.clear();
-    intervals.add(new int[] {5, -5});
-    assertNull(MapList.countPositions(intervals,  6));
-    assertNull(MapList.countPositions(intervals,  -6));
-    assertArrayEquals(new int[] {1, -1}, MapList.countPositions(intervals,  5));
-    assertArrayEquals(new int[] {7, -1}, MapList.countPositions(intervals,  -1));
-    assertArrayEquals(new int[] {11, -1}, MapList.countPositions(intervals,  -5));
-    
+    intervals.add(new int[] { 5, -5 });
+    assertNull(MapList.countPositions(intervals, 6));
+    assertNull(MapList.countPositions(intervals, -6));
+    assertArrayEquals(new int[] { 1, -1 },
+            MapList.countPositions(intervals, 5));
+    assertArrayEquals(new int[] { 7, -1 },
+            MapList.countPositions(intervals, -1));
+    assertArrayEquals(new int[] { 11, -1 },
+            MapList.countPositions(intervals, -5));
+
     /*
      * reverse then forward
      */
-    intervals.add(new int[] {5, 10});
-    assertArrayEquals(new int[] {13, 1}, MapList.countPositions(intervals,  6));
-    
+    intervals.add(new int[] { 5, 10 });
+    assertArrayEquals(new int[] { 13, 1 },
+            MapList.countPositions(intervals, 6));
+
     /*
      * reverse then forward then reverse
      */
-    intervals.add(new int[] {-10, -20});
-    assertArrayEquals(new int[] {20, -1}, MapList.countPositions(intervals,  -12));
-    
+    intervals.add(new int[] { -10, -20 });
+    assertArrayEquals(new int[] { 20, -1 },
+            MapList.countPositions(intervals, -12));
+
     /*
      * an interval [x, x] is treated as forward
      */
-    intervals.add(new int[] {30, 30});
-    assertArrayEquals(new int[] {29, 1}, MapList.countPositions(intervals,  30));
-    
+    intervals.add(new int[] { 30, 30 });
+    assertArrayEquals(new int[] { 29, 1 },
+            MapList.countPositions(intervals, 30));
+
     /*
      * it is the first matched occurrence that is returned
      */
     intervals.clear();
-    intervals.add(new int[] {1, 2});
-    intervals.add(new int[] {2, 3});
-    assertArrayEquals(new int[] {2, 1}, MapList.countPositions(intervals,  2));
-    intervals.add(new int[] {-1, -2});
-    intervals.add(new int[] {-2, -3});
-    assertArrayEquals(new int[] {6, -1}, MapList.countPositions(intervals,  -2));
+    intervals.add(new int[] { 1, 2 });
+    intervals.add(new int[] { 2, 3 });
+    assertArrayEquals(new int[] { 2, 1 },
+            MapList.countPositions(intervals, 2));
+    intervals.add(new int[] { -1, -2 });
+    intervals.add(new int[] { -2, -3 });
+    assertArrayEquals(new int[] { 6, -1 },
+            MapList.countPositions(intervals, -2));
+  }
+
+  /**
+   * Tests for helper method that adds any overlap (plus offset) to a list of
+   * overlaps
+   */
+  @Test(groups = { "Functional" })
+  public void testAddOverlap()
+  {
+    List<int[]> overlaps = new ArrayList<>();
+    int[] candidate = new int[] { 10, 19 };
+    MapList.addOverlap(overlaps, 5, candidate, 20, 30); // doesn't overlap
+    assertTrue(overlaps.isEmpty());
+    MapList.addOverlap(overlaps, 5, candidate, 31, 40); // doesn't overlap
+    assertTrue(overlaps.isEmpty());
+
+    /*
+     * 10-19 overlaps 15-25 at 15-19, which is offset 5-9 in 10-19
+     * + 5 initial offset
+     */
+    MapList.addOverlap(overlaps, 5, candidate, 15, 25);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 10, 14 }, overlaps.get(0));
+
+    /*
+     * reverse range overlap:
+     * 300-20 overlaps 15-25 at 25-20, which is offset 275-280 in 300-20
+     * + 8 initial offset
+     */
+    overlaps.clear();
+    candidate = new int[] { 300, 20 };
+    MapList.addOverlap(overlaps, 8, candidate, 15, 25);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 283, 288 }, overlaps.get(0));
   }
 
   @Test(groups = { "Functional" })
   public void testFindOverlapPositions()
   {
     List<int[]> ranges = new ArrayList<>();
-    List<int[]> overlaps = MapList.findOverlapPositions(ranges,  20,  30);
+    List<int[]> overlaps = MapList.findOverlapPositions(ranges, 20, 30);
+    assertTrue(overlaps.isEmpty()); // nothing to overlap
+
+    ranges.add(new int[] { 15, 25 });
+    overlaps = MapList.findOverlapPositions(ranges, 5, 10);
+    assertTrue(overlaps.isEmpty()); // no overlap
+
+    overlaps = MapList.findOverlapPositions(ranges, 20, 20);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 5, 5 }, overlaps.get(0));
+
+    overlaps = MapList.findOverlapPositions(ranges, 5, 19);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 0, 4 }, overlaps.get(0));
+
+    ranges.add(new int[] { 35, 45 });
+    overlaps = MapList.findOverlapPositions(ranges, 26, 34);
     assertTrue(overlaps.isEmpty());
-    ranges.add(new int[] {15, 25});
-    overlaps = MapList.findOverlapPositions(ranges,  20,  30);
+
+    /*
+     * 24-37 overlaps the end of 15-25 and the start of 35-45
+     * - offset positions are contiguous in the map so merged
+     */
+    overlaps = MapList.findOverlapPositions(ranges, 24, 37);
     assertEquals(1, overlaps.size());
-    assertArrayEquals(new int[] {6, 11}, overlaps.get(0));
+    assertArrayEquals(new int[] { 9, 13 }, overlaps.get(0));
+
+    /*
+     * EMBL:MN908947  https://www.ebi.ac.uk/ena/browser/api/embl/MN908947 
+     * (Covid-SARS-2) CDS mapping with 'slippage'
+     * (base 13468 is used twice in transcription)
+     */
+    ranges.clear();
+    ranges.add(new int[] { 266, 13468 });
+    ranges.add(new int[] { 13468, 21555 });
+
+    // 13468 occupies two offsets in the range list
+    overlaps = MapList.findOverlapPositions(ranges, 13468, 13468);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 13202, 13203 }, overlaps.get(0));
+    overlaps = MapList.findOverlapPositions(ranges, 13469, 13470);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 13204, 13205 }, overlaps.get(0));
+
+    /*
+     * EMBL:J03321 https://www.ebi.ac.uk/ena/browser/api/embl/J03321
+     * circular dna: CDS at [7022-7502, 1-437] 
+     * = 481 + 437 = 918 bases = 305 aa's + stop codon
+     */
+    ranges.clear();
+    ranges.add(new int[] { 7022, 7502 });
+    ranges.add(new int[] { 1, 437 });
+
+    overlaps = MapList.findOverlapPositions(ranges, 438, 7021);
+    assertTrue(overlaps.isEmpty());
+
+    // overlap first exon only:
+    overlaps = MapList.findOverlapPositions(ranges, 7000, 7100);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 0, 78 }, overlaps.get(0));
+
+    // overlap second exon only: offset to mapping includes first exon
+    overlaps = MapList.findOverlapPositions(ranges, 400, 500);
+    assertEquals(1, overlaps.size());
+    assertArrayEquals(new int[] { 880, 917 }, overlaps.get(0));
+
+    // overlap both exons: first exon overlap precedes second exon overlap
+    // offsets of overlaps are not contiguous
+    overlaps = MapList.findOverlapPositions(ranges, 200, 7500);
+    assertEquals(2, overlaps.size());
+    // first overlap is offsets of 7022-7500 in exon 1 (7022-7502):
+    assertArrayEquals(new int[] { 0, 478 }, overlaps.get(0));
+    // second overlap offsets is (first exon length = 481) + (200-437)
+    assertArrayEquals(new int[] { 680, 917 }, overlaps.get(1));
   }
 
   @Test(groups = { "Functional" })
   public void testMapWords()
   {
     List<int[]> ranges = new ArrayList<>();
-    
+
     /*
      * 1:1 (trivial) case
      */
-    ranges.add(new int[] {2, 4});
-    ranges.add(new int[] {6, 9});
-    MapList.mapWords(ranges,  1, 1);
+    ranges.add(new int[] { 2, 4 });
+    ranges.add(new int[] { 6, 9 });
+    MapList.mapWords(ranges, 1, 1);
     assertEquals(ranges.size(), 2);
-    assertArrayEquals(new int[] {2, 4}, ranges.get(0));
-    assertArrayEquals(new int[] {6, 9}, ranges.get(1));
-    
+    assertArrayEquals(new int[] { 2, 4 }, ranges.get(0));
+    assertArrayEquals(new int[] { 6, 9 }, ranges.get(1));
+
     /*
      * 1:3 case (peptide to codon ranges)
      */
-    MapList.mapWords(ranges,  1, 3);
+    MapList.mapWords(ranges, 1, 3);
     assertEquals(ranges.size(), 2);
-    assertArrayEquals(new int[] {6, 14}, ranges.get(0));
-    assertArrayEquals(new int[] {18, 29}, ranges.get(1));
-    
+    assertArrayEquals(new int[] { 6, 14 }, ranges.get(0));
+    assertArrayEquals(new int[] { 18, 29 }, ranges.get(1));
+
     /*
      * 3:1 case (codon or part codon to peptide)
      */
     ranges.clear();
-    ranges.add(new int[] {0, 5}); // 2 whole codons
-    ranges.add(new int[] {7, 11}); // part + whole codon
-    ranges.add(new int[] {15, 19}); // whole + part codon
-    ranges.add(new int[] {23, 27}); // part + part codon
-    ranges.add(new int[] {30, 30}); // first base of codon
-    ranges.add(new int[] {31, 31}); // second base of codon
-    ranges.add(new int[] {32, 32}); // third base of codon
-    MapList.mapWords(ranges,  3, 1);
+    ranges.add(new int[] { 0, 5 }); // 2 whole codons
+    ranges.add(new int[] { 7, 11 }); // part + whole codon
+    ranges.add(new int[] { 15, 19 }); // whole + part codon
+    ranges.add(new int[] { 23, 27 }); // part + part codon
+    ranges.add(new int[] { 30, 30 }); // first base of codon
+    ranges.add(new int[] { 31, 31 }); // second base of codon
+    ranges.add(new int[] { 32, 32 }); // third base of codon
+    MapList.mapWords(ranges, 3, 1);
     assertEquals(ranges.size(), 7);
-    assertArrayEquals(new int[] {0, 1}, ranges.get(0));
-    assertArrayEquals(new int[] {2, 3}, ranges.get(1));
-    assertArrayEquals(new int[] {5, 6}, ranges.get(2));
-    assertArrayEquals(new int[] {7, 9}, ranges.get(3));
-    assertArrayEquals(new int[] {10, 10}, ranges.get(4));
-    assertArrayEquals(new int[] {10, 10}, ranges.get(5));
-    assertArrayEquals(new int[] {10, 10}, ranges.get(6));
+    assertArrayEquals(new int[] { 0, 1 }, ranges.get(0));
+    assertArrayEquals(new int[] { 2, 3 }, ranges.get(1));
+    assertArrayEquals(new int[] { 5, 6 }, ranges.get(2));
+    assertArrayEquals(new int[] { 7, 9 }, ranges.get(3));
+    assertArrayEquals(new int[] { 10, 10 }, ranges.get(4));
+    assertArrayEquals(new int[] { 10, 10 }, ranges.get(5));
+    assertArrayEquals(new int[] { 10, 10 }, ranges.get(6));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testLocateInFrom2()
+  {
+    /*
+     * codons at 11-16, 21-26, 31-36 mapped to peptide positions 1, 3-4, 6-8
+     */
+    MapList ml = new MapList(new int[] { 11, 16, 21, 26, 31, 36 },
+            new int[]
+            { 1, 1, 3, 4, 6, 8 }, 3, 1);
+    assertArrayEquals(new int[] { 11, 13 }, ml.locateInFrom2(1, 1));
+    assertArrayEquals(new int[] { 11, 16 }, ml.locateInFrom2(1, 3));
+    assertArrayEquals(new int[] { 11, 16, 21, 23 }, ml.locateInFrom2(1, 4));
+    assertArrayEquals(new int[] { 14, 16, 21, 23 }, ml.locateInFrom2(3, 4));
   }
 }