Merge branch 'merge/develop_JAL-3725' into develop
authorJim Procter <j.procter@dundee.ac.uk>
Fri, 28 Jan 2022 17:08:35 +0000 (17:08 +0000)
committerJim Procter <j.procter@dundee.ac.uk>
Fri, 28 Jan 2022 17:08:35 +0000 (17:08 +0000)
 Conflicts:
src/jalview/gui/PopupMenu.java

1  2 
src/jalview/gui/PopupMenu.java
src/jalview/util/MappingUtils.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java

@@@ -839,15 -839,13 +839,14 @@@ public class PopupMenu extends JPopupMe
        /*
         * show local rather than linked feature coordinates
         */
-       int[] beginRange = mf.getMappedPositions(start, start);
-       int[] endRange = mf.getMappedPositions(end, end);
-       if (beginRange == null || endRange == null)
+       int[] localRange = mf.getMappedPositions(start, end);
+       if (localRange == null)
        {
 +        // e.g. variant extending to stop codon so not mappable
          return;
        }
-       start = beginRange[0];
-       end = endRange[endRange.length - 1];
+       start = localRange[0];
+       end = localRange[localRange.length - 1];
      }
      StringBuilder desc = new StringBuilder();
      desc.append(sf.getType()).append(" ").append(String.valueOf(start));
@@@ -36,7 -36,6 +36,7 @@@ import jalview.commands.EditCommand.Act
  import jalview.commands.EditCommand.Edit;
  import jalview.commands.OrderCommand;
  import jalview.datamodel.AlignedCodonFrame;
 +import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentOrder;
  import jalview.datamodel.ColumnSelection;
@@@ -366,45 -365,52 +366,45 @@@ public final class MappingUtil
  
        for (AlignedCodonFrame acf : codonFrames)
        {
 -        SequenceI mappedSequence = targetIsNucleotide
 -                ? acf.getDnaForAaSeq(selected)
 -                : acf.getAaForDnaSeq(selected);
 -        if (mappedSequence != null)
 +        for (SequenceI seq : mapTo.getAlignment().getSequences())
          {
 -          for (SequenceI seq : mapTo.getAlignment().getSequences())
 +          SequenceI peptide = targetIsNucleotide ? selected : seq;
 +          SequenceI cds = targetIsNucleotide ? seq : selected;
 +          SequenceToSequenceMapping s2s = acf.getCoveringMapping(cds,
 +                  peptide);
 +          if (s2s == null)
            {
 -            int mappedStartResidue = 0;
 -            int mappedEndResidue = 0;
 -            if (seq.getDatasetSequence() == mappedSequence)
 -            {
 -              /*
 -               * Found a sequence mapping. Locate the start/end mapped residues.
 -               */
 -              List<AlignedCodonFrame> mapping = Arrays
 -                      .asList(new AlignedCodonFrame[]
 -                      { acf });
 -              SearchResultsI sr = buildSearchResults(selected,
 -                      startResiduePos, mapping);
 -              for (SearchResultMatchI m : sr.getResults())
 -              {
 -                mappedStartResidue = m.getStart();
 -                mappedEndResidue = m.getEnd();
 -              }
 -              sr = buildSearchResults(selected, endResiduePos, mapping);
 -              for (SearchResultMatchI m : sr.getResults())
 -              {
 -                mappedStartResidue = Math.min(mappedStartResidue,
 -                        m.getStart());
 -                mappedEndResidue = Math.max(mappedEndResidue, m.getEnd());
 -              }
 -
 -              /*
 -               * Find the mapped aligned columns, save the range. Note findIndex
 -               * returns a base 1 position, SequenceGroup uses base 0
 -               */
 -              int mappedStartCol = seq.findIndex(mappedStartResidue) - 1;
 -              minStartCol = minStartCol == -1 ? mappedStartCol
 -                      : Math.min(minStartCol, mappedStartCol);
 -              int mappedEndCol = seq.findIndex(mappedEndResidue) - 1;
 -              maxEndCol = maxEndCol == -1 ? mappedEndCol
 -                      : Math.max(maxEndCol, mappedEndCol);
 -              mappedGroup.addSequence(seq, false);
 -              break;
 -            }
 +            continue;
            }
 +          int mappedStartResidue = 0;
 +          int mappedEndResidue = 0;
 +          List<AlignedCodonFrame> mapping = Arrays.asList(acf);
 +          SearchResultsI sr = buildSearchResults(selected, startResiduePos,
 +                  mapping);
 +          for (SearchResultMatchI m : sr.getResults())
 +          {
 +            mappedStartResidue = m.getStart();
 +            mappedEndResidue = m.getEnd();
 +          }
 +          sr = buildSearchResults(selected, endResiduePos, mapping);
 +          for (SearchResultMatchI m : sr.getResults())
 +          {
 +            mappedStartResidue = Math.min(mappedStartResidue, m.getStart());
 +            mappedEndResidue = Math.max(mappedEndResidue, m.getEnd());
 +          }
 +
 +          /*
 +           * Find the mapped aligned columns, save the range. Note findIndex
 +           * returns a base 1 position, SequenceGroup uses base 0
 +           */
 +          int mappedStartCol = seq.findIndex(mappedStartResidue) - 1;
 +          minStartCol = minStartCol == -1 ? mappedStartCol
 +                  : Math.min(minStartCol, mappedStartCol);
 +          int mappedEndCol = seq.findIndex(mappedEndResidue) - 1;
 +          maxEndCol = maxEndCol == -1 ? mappedEndCol
 +                  : Math.max(maxEndCol, mappedEndCol);
 +          mappedGroup.addSequence(seq, false);
 +          break;
          }
        }
      }
      {
        for (AlignedCodonFrame acf : mappings)
        {
 -        SequenceI mappedSeq = mappingToNucleotide ? acf.getDnaForAaSeq(seq)
 -                : acf.getAaForDnaSeq(seq);
 -        if (mappedSeq != null)
 -        {
            for (SequenceI seq2 : mapTo.getSequences())
            {
 -            if (seq2.getDatasetSequence() == mappedSeq)
 +            /*
 +             * the corresponding peptide / CDS is the one for which there is
 +             * a complete ('covering') mapping to 'seq'
 +             */
 +            SequenceI peptide = mappingToNucleotide ? seq2 : seq;
 +            SequenceI cds = mappingToNucleotide ? seq : seq2;
 +            SequenceToSequenceMapping s2s = acf.getCoveringMapping(cds,
 +                    peptide);
 +            if (s2s != null)
              {
                mappedOrder.add(seq2);
                j++;
                break;
              }
            }
 -        }
        }
      }
  
  
      if (colsel == null)
      {
 -      return; // mappedColumns;
 +      return; 
      }
  
      char fromGapChar = mapFrom.getAlignment().getGapCharacter();
        mapHiddenColumns(regions.next(), codonFrames, newHidden,
                fromSequences, toSequences, fromGapChar);
      }
 -    return; // mappedColumns;
 +    return; 
    }
  
    /**
           */
          for (SequenceI toSeq : toSequences)
          {
 -          if (toSeq.getDatasetSequence() == mappedSeq)
 +          if (toSeq.getDatasetSequence() == mappedSeq
 +                  && mappedStartResidue >= toSeq.getStart()
 +                  && mappedEndResidue <= toSeq.getEnd())
            {
              int mappedStartCol = toSeq.findIndex(mappedStartResidue);
              int mappedEndCol = toSeq.findIndex(mappedEndResidue);
      {
        if (range.length % 2 != 0)
        {
-         System.err.println(
+         Cache.log.error(
                  "Error unbalance start/end ranges: " + ranges.toString());
          return 0;
        }
          /*
           * not coded for [start1, end1, start2, end2, ...]
           */
-         System.err.println(
+         Cache.log.error(
                  "MappingUtils.removeEndPositions doesn't handle multiple  ranges");
          return;
        }
          /*
           * not coded for a reverse strand range (end < start)
           */
-         System.err.println(
+         Cache.log.error(
                  "MappingUtils.removeEndPositions doesn't handle reverse strand");
          return;
        }
      }
      return result;
    }
+   /*
+    * Returns the maximal start-end positions in the given (ordered) list of
+    * ranges which is overlapped by the given begin-end range, or null if there
+    * is no overlap.
+    * 
+    * <pre>
+    * Examples:
+    *   if ranges is {[4, 8], [10, 12], [16, 19]}
+    * then
+    *   findOverlap(ranges, 1, 20) == [4, 19]
+    *   findOverlap(ranges, 6, 11) == [6, 11]
+    *   findOverlap(ranges, 9, 15) == [10, 12]
+    *   findOverlap(ranges, 13, 15) == null
+    * </pre>
+    * 
+    * @param ranges
+    * @param begin
+    * @param end
+    * @return
+    */
+   protected static int[] findOverlap(List<int[]> ranges, final int begin,
+           final int end)
+   {
+     boolean foundStart = false;
+     int from = 0;
+     int to = 0;
+     /*
+      * traverse the ranges to find the first position (if any) >= begin,
+      * and the last position (if any) <= end
+      */
+     for (int[] range : ranges)
+     {
+       if (!foundStart)
+       {
+         if (range[0] >= begin)
+         {
+           /*
+            * first range that starts with, or follows, begin
+            */
+           foundStart = true;
+           from = Math.max(range[0], begin);
+         }
+         else if (range[1] >= begin)
+         {
+           /*
+            * first range that contains begin
+            */
+           foundStart = true;
+           from = begin;
+         }
+       }
+       if (range[0] <= end)
+       {
+         to = Math.min(end, range[1]);
+       }
+     }
+     return foundStart && to >= from ? new int[] { from, to } : null;
+   }
  }
@@@ -46,7 -46,7 +46,7 @@@ public class MapListTes
    {
      Cache.initLogger();
    }
 -
 +  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
    {
       * no overlap
       */
      assertNull(ml.locateInFrom(0, 0));
-     
    }
  
    /**
      assertEquals("[10, 10, 12, 12, 14, 14]",
              Arrays.toString(ml.locateInFrom(3, 3)));
      assertEquals("[16, 18]", Arrays.toString(ml.locateInFrom(4, 4)));
-     
      /*
       * codons at 11-16, 21-26, 31-36 mapped to peptide positions 1, 3-4, 6-8
       */
    }
  
    /**
+    * Tests for method that locates the overlap of the ranges in the 'from' map
+    * for given range in the 'to' map
+    */
+   @Test(groups = { "Functional" })
+   public void testGetOverlapsInFrom_withIntrons()
+   {
+     /*
+      * Exons at positions [2, 3, 5] [6, 7, 9] [10, 12, 14] [16, 17, 18] i.e.
+      * 2-3, 5-7, 9-10, 12-12, 14-14, 16-18
+      */
+     int[] codons = { 2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 16, 18 };
+     int[] protein = { 11, 14 };
+     MapList ml = new MapList(codons, protein, 3, 1);
+     assertEquals("[2, 3, 5, 5]",
+             Arrays.toString(ml.getOverlapsInFrom(11, 11)));
+     assertEquals("[2, 3, 5, 7, 9, 9]",
+             Arrays.toString(ml.getOverlapsInFrom(11, 12)));
+     // out of range 5' :
+     assertEquals("[2, 3, 5, 7, 9, 9]",
+             Arrays.toString(ml.getOverlapsInFrom(8, 12)));
+     // out of range 3' :
+     assertEquals("[10, 10, 12, 12, 14, 14, 16, 18]",
+             Arrays.toString(ml.getOverlapsInFrom(13, 16)));
+     // out of range both :
+     assertEquals("[2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 16, 18]",
+             Arrays.toString(ml.getOverlapsInFrom(1, 16)));
+     // no overlap:
+     assertNull(ml.getOverlapsInFrom(20, 25));
+   }
+   /**
+    * Tests for method that locates the overlap of the ranges in the 'to' map for
+    * given range in the 'from' map
+    */
+   @Test(groups = { "Functional" })
+   public void testGetOverlapsInTo_withIntrons()
+   {
+     /*
+      * Exons at positions [2, 3, 5] [6, 7, 9] [10, 12, 14] [17, 18, 19] i.e.
+      * 2-3, 5-7, 9-10, 12-12, 14-14, 17-19
+      */
+     int[] codons = { 2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 17, 19 };
+     /*
+      * Mapped proteins at positions 1, 3, 4, 6 in the sequence
+      */
+     int[] protein = { 1, 1, 3, 4, 6, 6 };
+     MapList ml = new MapList(codons, protein, 3, 1);
+     /*
+      * Can't map from an unmapped position
+      */
+     assertNull(ml.getOverlapsInTo(1, 1));
+     assertNull(ml.getOverlapsInTo(4, 4));
+     assertNull(ml.getOverlapsInTo(15, 16));
+     /*
+      * nor from a range that includes no mapped position (exon)
+      */
+     assertNull(ml.getOverlapsInTo(15, 16));
+     // end of codon 1 maps to first peptide
+     assertEquals("[1, 1]", Arrays.toString(ml.getOverlapsInTo(2, 2)));
+     // end of codon 1 and start of codon 2 maps to first 2 peptides
+     assertEquals("[1, 1, 3, 3]", Arrays.toString(ml.getOverlapsInTo(3, 7)));
+     // range overlaps 5' end of dna:
+     assertEquals("[1, 1, 3, 3]", Arrays.toString(ml.getOverlapsInTo(1, 6)));
+     assertEquals("[1, 1, 3, 3]", Arrays.toString(ml.getOverlapsInTo(1, 8)));
+     // range overlaps 3' end of dna:
+     assertEquals("[6, 6]", Arrays.toString(ml.getOverlapsInTo(17, 24)));
+     assertEquals("[6, 6]", Arrays.toString(ml.getOverlapsInTo(16, 24)));
+     // dna positions 8, 11 are intron but include end of exon 2 and start of
+     // exon 3
+     assertEquals("[3, 4]", Arrays.toString(ml.getOverlapsInTo(8, 11)));
+   }
+   /**
     * Tests for method that locates ranges in the 'to' map for given range in the
     * 'from' map.
     */
       */
      assertEquals("[1, 4]", Arrays.toString(ml.locateInTo(1, 13)));
      assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(-1, 2)));
-     
      /*
       * no 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
       */
      assertArrayEquals(new int[] { 5, 6 }, merged.get(1));
      assertArrayEquals(new int[] { 12, 8 }, merged.get(2));
      assertArrayEquals(new int[] { 8, 7 }, merged.get(3));
 -
 +    
      // 'subsumed' ranges are preserved
      ranges.clear();
      ranges.add(new int[] { 10, 30 });
 -    ranges.add(new int[] { 15, 25 });
 +    ranges.add(new int[] { 15, 25 }); 
 +
      merged = MapList.coalesceRanges(ranges);
      assertEquals(2, merged.size());
      assertArrayEquals(new int[] { 10, 30 }, merged.get(0));
      toRanges = compound.getToRanges();
      assertEquals(2, toRanges.size());
      assertArrayEquals(new int[] { 931, 901 }, toRanges.get(0));
-     assertArrayEquals(new int[] { 600, 582}, toRanges.get(1));
+     assertArrayEquals(new int[] { 600, 582 }, toRanges.get(1));
  
      /*
       * 1:1 plus 1:3 should result in 1:3
       * no overlap
       */
      assertNull(ml.locateInTo(0, 0));
-     
      /*
       * partial overlap
       */
      ml = new MapList(gene, cds, 1, 1);
      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
    public void testAddOffsetPositions()
    {
      List<int[]> mapped = new ArrayList<>();
-     int[] range = new int[] {10, 20};
+     int[] range = new int[] { 10, 20 };
      BitSet offsets = new BitSet();
  
      MapList.addOffsetPositions(mapped, 0, range, offsets);
      assertArrayEquals(new int[] { 14, 13 }, mapped.get(1));
      assertArrayEquals(new int[] { 10, 10 }, mapped.get(2));
    }
-   
    @Test(groups = { "Functional" })
    public void testGetPositionsForOffsets()
    {
      BitSet offsets = new BitSet();
      List<int[]> mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertTrue(mapped.isEmpty()); // no ranges and no offsets!
-     
      offsets.set(5, 1000);
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertTrue(mapped.isEmpty()); // no ranges
-     
      /*
       * one range with overlap of offsets
       */
-     ranges.add(new int[] {15, 25});
+     ranges.add(new int[] { 15, 25 });
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertEquals(1, mapped.size());
-     assertArrayEquals(new int[] {20,  25}, mapped.get(0));
-     
+     assertArrayEquals(new int[] { 20, 25 }, mapped.get(0));
      /*
       * two ranges
       */
-     ranges.add(new int[] {300, 320});
+     ranges.add(new int[] { 300, 320 });
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertEquals(2, mapped.size());
-     assertArrayEquals(new int[] {20,  25}, mapped.get(0));
-     assertArrayEquals(new int[] {300, 320}, mapped.get(1));
-     
+     assertArrayEquals(new int[] { 20, 25 }, mapped.get(0));
+     assertArrayEquals(new int[] { 300, 320 }, mapped.get(1));
      /*
       * boundary case - right end of first range overlaps
       */
      offsets.set(10);
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertEquals(1, mapped.size());
-     assertArrayEquals(new int[] {25,  25}, mapped.get(0));
-     
+     assertArrayEquals(new int[] { 25, 25 }, mapped.get(0));
      /*
       * boundary case - left end of second range overlaps
       */
      offsets.set(11);
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertEquals(2, mapped.size());
-     assertArrayEquals(new int[] {25,  25}, mapped.get(0));
-     assertArrayEquals(new int[] {300, 300}, mapped.get(1));
-     
+     assertArrayEquals(new int[] { 25, 25 }, mapped.get(0));
+     assertArrayEquals(new int[] { 300, 300 }, mapped.get(1));
      /*
       * offsets into a circular range are reported in
       * the order in which they are traversed
       */
      ranges.clear();
-     ranges.add(new int[] {100, 150});
-     ranges.add(new int[] {60, 80});
+     ranges.add(new int[] { 100, 150 });
+     ranges.add(new int[] { 60, 80 });
      offsets.clear();
      offsets.set(45, 55); // sets bits 45 to 54
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertEquals(2, mapped.size());
-     assertArrayEquals(new int[] {145, 150}, mapped.get(0)); // offsets 45-50
-     assertArrayEquals(new int[] {60, 63}, mapped.get(1)); // offsets 51-54
+     assertArrayEquals(new int[] { 145, 150 }, mapped.get(0)); // offsets 45-50
+     assertArrayEquals(new int[] { 60, 63 }, mapped.get(1)); // offsets 51-54
  
      /*
       * reverse range overlap is reported with start < end
       */
      ranges.clear();
-     ranges.add(new int[] {4321, 4000});
+     ranges.add(new int[] { 4321, 4000 });
      offsets.clear();
      offsets.set(20, 22); // sets bits 20 and 21
      offsets.set(30);
      mapped = MapList.getPositionsForOffsets(ranges, offsets);
      assertEquals(2, mapped.size());
-     assertArrayEquals(new int[] {4301, 4300}, mapped.get(0));
-     assertArrayEquals(new int[] {4291, 4291}, mapped.get(1));
+     assertArrayEquals(new int[] { 4301, 4300 }, mapped.get(0));
+     assertArrayEquals(new int[] { 4291, 4291 }, mapped.get(1));
    }
-   
    @Test(groups = { "Functional" })
    public void testGetMappedOffsetsForPositions()
    {
       * start by verifying the examples in the method's Javadoc!
       */
      List<int[]> ranges = new ArrayList<>();
-     ranges.add(new int[] {10, 20});
-     ranges.add(new int[] {31, 40});
-     BitSet overlaps = MapList.getMappedOffsetsForPositions(1, 9, ranges, 1, 1);
+     ranges.add(new int[] { 10, 20 });
+     ranges.add(new int[] { 31, 40 });
+     BitSet overlaps = MapList.getMappedOffsetsForPositions(1, 9, ranges, 1,
+             1);
      assertTrue(overlaps.isEmpty());
      overlaps = MapList.getMappedOffsetsForPositions(1, 11, ranges, 1, 1);
      assertEquals(2, overlaps.cardinality());
      assertTrue(overlaps.get(1));
      overlaps = MapList.getMappedOffsetsForPositions(15, 35, ranges, 1, 1);
      assertEquals(11, overlaps.cardinality());
-     for (int i = 5 ; i <= 11 ; i++)
+     for (int i = 5; i <= 11; i++)
      {
        assertTrue(overlaps.get(i));
      }
-     
      ranges.clear();
-     ranges.add(new int[] {1, 200});
+     ranges.add(new int[] { 1, 200 });
      overlaps = MapList.getMappedOffsetsForPositions(9, 9, ranges, 1, 3);
      assertEquals(3, overlaps.cardinality());
      assertTrue(overlaps.get(24));
      assertTrue(overlaps.get(25));
      assertTrue(overlaps.get(26));
-     
      ranges.clear();
-     ranges.add(new int[] {101, 150});
-     ranges.add(new int[] {171, 180});
+     ranges.add(new int[] { 101, 150 });
+     ranges.add(new int[] { 171, 180 });
      overlaps = MapList.getMappedOffsetsForPositions(101, 102, ranges, 3, 1);
      assertEquals(1, overlaps.cardinality());
      assertTrue(overlaps.get(0));
      overlaps = MapList.getMappedOffsetsForPositions(150, 171, ranges, 3, 1);
      assertEquals(1, overlaps.cardinality());
      assertTrue(overlaps.get(16));
-     
      ranges.clear();
-     ranges.add(new int[] {101, 150});
-     ranges.add(new int[] {21, 30});
+     ranges.add(new int[] { 101, 150 });
+     ranges.add(new int[] { 21, 30 });
      overlaps = MapList.getMappedOffsetsForPositions(24, 40, ranges, 3, 1);
      assertEquals(3, overlaps.cardinality());
      assertTrue(overlaps.get(17));
      assertTrue(overlaps.get(18));
      assertTrue(overlaps.get(19));
-     
      /*
       * reverse range 1:1 (e.g. reverse strand gene to transcript)
       */
      ranges.clear();
-     ranges.add(new int[] {20, 10});
+     ranges.add(new int[] { 20, 10 });
      overlaps = MapList.getMappedOffsetsForPositions(12, 13, ranges, 1, 1);
      assertEquals(2, overlaps.cardinality());
      assertTrue(overlaps.get(7));
      assertTrue(overlaps.get(8));
-     
      /*
       * reverse range 3:1 (e.g. reverse strand gene to peptide)
       * from EMBL:J03321 to P0CE20
       */
      ranges.clear();
-     ranges.add(new int[] {1480, 488});
-     overlaps = MapList.getMappedOffsetsForPositions(1460, 1460, ranges, 3, 1);
+     ranges.add(new int[] { 1480, 488 });
+     overlaps = MapList.getMappedOffsetsForPositions(1460, 1460, ranges, 3,
+             1);
      // 1460 is the end of the 7th codon
      assertEquals(1, overlaps.cardinality());
      assertTrue(overlaps.get(6));
      // add one base (part codon)
-     overlaps = MapList.getMappedOffsetsForPositions(1459, 1460, ranges, 3, 1);
+     overlaps = MapList.getMappedOffsetsForPositions(1459, 1460, ranges, 3,
+             1);
      assertEquals(2, overlaps.cardinality());
      assertTrue(overlaps.get(6));
      assertTrue(overlaps.get(7));
      // add second base (part codon)
-     overlaps = MapList.getMappedOffsetsForPositions(1458, 1460, ranges, 3, 1);
+     overlaps = MapList.getMappedOffsetsForPositions(1458, 1460, ranges, 3,
+             1);
      assertEquals(2, overlaps.cardinality());
      assertTrue(overlaps.get(6));
      assertTrue(overlaps.get(7));
      // add third base (whole codon)
-     overlaps = MapList.getMappedOffsetsForPositions(1457, 1460, ranges, 3, 1);
+     overlaps = MapList.getMappedOffsetsForPositions(1457, 1460, ranges, 3,
+             1);
      assertEquals(2, overlaps.cardinality());
      assertTrue(overlaps.get(6));
      assertTrue(overlaps.get(7));
      // add one more base (part codon)
-     overlaps = MapList.getMappedOffsetsForPositions(1456, 1460, ranges, 3, 1);
+     overlaps = MapList.getMappedOffsetsForPositions(1456, 1460, ranges, 3,
+             1);
      assertEquals(3, overlaps.cardinality());
      assertTrue(overlaps.get(6));
      assertTrue(overlaps.get(7));
@@@ -22,9 -22,10 +22,10 @@@ package jalview.util
  
  import static org.testng.AssertJUnit.assertEquals;
  import static org.testng.AssertJUnit.assertFalse;
+ import static org.testng.AssertJUnit.assertNull;
  import static org.testng.AssertJUnit.assertSame;
  import static org.testng.AssertJUnit.assertTrue;
- import static org.testng.AssertJUnit.fail;
+ import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
  
  import java.awt.Color;
  import java.io.IOException;
@@@ -36,16 -37,6 +37,16 @@@ import java.util.List
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
 +import java.awt.Color;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Iterator;
 +import java.util.List;
 +
 +import org.testng.annotations.BeforeClass;
 +import org.testng.annotations.Test;
 +
  import jalview.api.AlignViewportI;
  import jalview.bin.Cache;
  import jalview.commands.EditCommand;
@@@ -253,7 -244,7 +254,7 @@@ public class MappingUtilsTes
      protein.setCodonFrames(acfList);
  
      /*
 -     * Select Seq1 and Seq3 in the protein (startRes=endRes=0)
 +     * Select Seq1 and Seq3 in the protein
       */
      SequenceGroup sg = new SequenceGroup();
      sg.setColourText(true);
      sg.setOutlineColour(Color.LIGHT_GRAY);
      sg.addSequence(protein.getSequenceAt(0), false);
      sg.addSequence(protein.getSequenceAt(2), false);
 +    sg.setEndRes(protein.getWidth() - 1);
  
      /*
       * Verify the mapped sequence group in dna
      assertSame(cdna.getSequenceAt(0), mappedGroup.getSequences().get(0));
      assertSame(cdna.getSequenceAt(2), mappedGroup.getSequences().get(1));
      assertEquals(0, mappedGroup.getStartRes());
 -    assertEquals(2, mappedGroup.getEndRes());
 +    assertEquals(2, mappedGroup.getEndRes()); // 3 columns (1 codon)
  
      /*
       * Verify mapping sequence group from dna to protein
    }
  
    @Test(groups = "Functional")
-   public void testListToArray()
+   public void testFindOverlap()
    {
      List<int[]> ranges = new ArrayList<>();
-     int[] result = MappingUtils.rangeListToArray(ranges);
-     assertEquals(result.length, 0);
-     ranges.add(new int[] { 24, 12 });
-     result = MappingUtils.rangeListToArray(ranges);
-     assertEquals(result.length, 2);
-     assertEquals(result[0], 24);
-     assertEquals(result[1], 12);
-     ranges.add(new int[] { -7, 30 });
-     result = MappingUtils.rangeListToArray(ranges);
-     assertEquals(result.length, 4);
-     assertEquals(result[0], 24);
-     assertEquals(result[1], 12);
-     assertEquals(result[2], -7);
-     assertEquals(result[3], 30);
-     try
-     {
-       MappingUtils.rangeListToArray(null);
-       fail("Expected exception");
-     } catch (NullPointerException e)
-     {
-       // expected
-     }
+     ranges.add(new int[] { 4, 8 });
+     ranges.add(new int[] { 10, 12 });
+     ranges.add(new int[] { 16, 19 });
+     int[] overlap = MappingUtils.findOverlap(ranges, 5, 13);
+     assertArrayEquals(overlap, new int[] { 5, 12 });
+     overlap = MappingUtils.findOverlap(ranges, -100, 100);
+     assertArrayEquals(overlap, new int[] { 4, 19 });
+     overlap = MappingUtils.findOverlap(ranges, 7, 17);
+     assertArrayEquals(overlap, new int[] { 7, 17 });
+     overlap = MappingUtils.findOverlap(ranges, 13, 15);
+     assertNull(overlap);
    }
 +  
 +  /**
 +   * Test mapping a sequence group where sequences in and outside the group
 +   * share a dataset sequence (e.g. alternative CDS for the same gene)
 +   * <p>
 +   * This scenario doesn't arise after JAL-3763 changes, but test left as still valid
 +   * @throws IOException
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testMapSequenceGroup_sharedDataset() throws IOException
 +  {
 +    /*
 +     * Set up dna and protein Seq1/2/3 with mappings (held on the protein
 +     * viewport). CDS sequences share the same 'gene' dataset sequence.
 +     */
 +    SequenceI dna = new Sequence("dna", "aaatttgggcccaaatttgggccc");
 +    SequenceI cds1 = new Sequence("cds1/1-6", "aaattt");
 +    SequenceI cds2 = new Sequence("cds1/4-9", "tttggg");
 +    SequenceI cds3 = new Sequence("cds1/19-24", "gggccc");
 +
 +    cds1.setDatasetSequence(dna);
 +    cds2.setDatasetSequence(dna);
 +    cds3.setDatasetSequence(dna);
 +
 +    SequenceI pep1 = new Sequence("pep1", "KF");
 +    SequenceI pep2 = new Sequence("pep2", "FG");
 +    SequenceI pep3 = new Sequence("pep3", "GP");
 +    pep1.createDatasetSequence();
 +    pep2.createDatasetSequence();
 +    pep3.createDatasetSequence();
 +
 +    /*
 +     * add mappings from coding positions of dna to respective peptides
 +     */
 +    AlignedCodonFrame acf = new AlignedCodonFrame();
 +    acf.addMap(dna, pep1,
 +            new MapList(new int[]
 +            { 1, 6 }, new int[] { 1, 2 }, 3, 1));
 +    acf.addMap(dna, pep2,
 +            new MapList(new int[]
 +            { 4, 9 }, new int[] { 1, 2 }, 3, 1));
 +    acf.addMap(dna, pep3,
 +            new MapList(new int[]
 +            { 19, 24 }, new int[] { 1, 2 }, 3, 1));
 +
 +    List<AlignedCodonFrame> acfList = Arrays
 +            .asList(new AlignedCodonFrame[]
 +            { acf });
 +
 +    AlignmentI cdna = new Alignment(new SequenceI[] { cds1, cds2, cds3 });
 +    AlignmentI protein = new Alignment(
 +            new SequenceI[]
 +            { pep1, pep2, pep3 });
 +    AlignViewportI cdnaView = new AlignViewport(cdna);
 +    AlignViewportI peptideView = new AlignViewport(protein);
 +    protein.setCodonFrames(acfList);
 +
 +    /*
 +     * Select pep1 and pep3 in the protein alignment
 +     */
 +    SequenceGroup sg = new SequenceGroup();
 +    sg.setColourText(true);
 +    sg.setIdColour(Color.GREEN);
 +    sg.setOutlineColour(Color.LIGHT_GRAY);
 +    sg.addSequence(pep1, false);
 +    sg.addSequence(pep3, false);
 +    sg.setEndRes(protein.getWidth() - 1);
 +
 +    /*
 +     * Verify the mapped sequence group in dna is cds1 and cds3
 +     */
 +    SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg,
 +            peptideView, cdnaView);
 +    assertTrue(mappedGroup.getColourText());
 +    assertSame(sg.getIdColour(), mappedGroup.getIdColour());
 +    assertSame(sg.getOutlineColour(), mappedGroup.getOutlineColour());
 +    assertEquals(2, mappedGroup.getSequences().size());
 +    assertSame(cds1, mappedGroup.getSequences().get(0));
 +    assertSame(cds3, mappedGroup.getSequences().get(1));
 +    // columns 1-6 selected (0-5 base zero)
 +    assertEquals(0, mappedGroup.getStartRes());
 +    assertEquals(5, mappedGroup.getEndRes());
 +
 +    /*
 +     * Select mapping sequence group from dna to protein
 +     */
 +    sg.clear();
 +    sg.addSequence(cds2, false);
 +    sg.addSequence(cds1, false);
 +    sg.setStartRes(0);
 +    sg.setEndRes(cdna.getWidth() - 1);
 +    mappedGroup = MappingUtils.mapSequenceGroup(sg, cdnaView, peptideView);
 +    assertTrue(mappedGroup.getColourText());
 +    assertSame(sg.getIdColour(), mappedGroup.getIdColour());
 +    assertSame(sg.getOutlineColour(), mappedGroup.getOutlineColour());
 +    assertEquals(2, mappedGroup.getSequences().size());
 +    assertSame(protein.getSequenceAt(1), mappedGroup.getSequences().get(0));
 +    assertSame(protein.getSequenceAt(0), mappedGroup.getSequences().get(1));
 +    assertEquals(0, mappedGroup.getStartRes());
 +    assertEquals(1, mappedGroup.getEndRes()); // two columns
 +  }
  }