Merge branch 'task/JAL-3763_newDatasetForCds' into merge/develop_task/JAL-3763_newDat...
authorJim Procter <j.procter@dundee.ac.uk>
Wed, 26 Jan 2022 19:36:23 +0000 (19:36 +0000)
committerJim Procter <j.procter@dundee.ac.uk>
Wed, 26 Jan 2022 19:36:23 +0000 (19:36 +0000)
 Conflicts:
src/jalview/analysis/AlignmentUtils.java
src/jalview/util/MapList.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java

1  2 
src/jalview/analysis/AlignmentUtils.java
src/jalview/datamodel/SearchResults.java
src/jalview/gui/PopupMenu.java
src/jalview/util/MappingUtils.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java

   */
  package jalview.analysis;
  
 +import java.util.Locale;
 +
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.HashSet;
+ import java.util.Iterator;
+ import java.util.LinkedHashMap;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Map.Entry;
+ import java.util.NoSuchElementException;
+ import java.util.Set;
+ import java.util.SortedMap;
+ import java.util.TreeMap;
+ import jalview.bin.Cache;
  import jalview.commands.RemoveGapColCommand;
  import jalview.datamodel.AlignedCodon;
  import jalview.datamodel.AlignedCodonFrame;
@@@ -46,22 -61,6 +63,6 @@@ import jalview.util.IntRangeComparator
  import jalview.util.MapList;
  import jalview.util.MappingUtils;
  
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.Iterator;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
- import java.util.NoSuchElementException;
- import java.util.Set;
- import java.util.SortedMap;
- import java.util.TreeMap;
  /**
   * grab bag of useful alignment manipulation operations Expect these to be
   * refactored elsewhere at some point.
@@@ -183,9 -182,9 +184,9 @@@ public class AlignmentUtil
        // TODO use Character.toLowerCase to avoid creating String objects?
        char[] upstream = new String(ds
                .getSequence(s.getStart() - 1 - ustream_ds, s.getStart() - 1))
 -                      .toLowerCase().toCharArray();
 +                      .toLowerCase(Locale.ROOT).toCharArray();
        char[] downstream = new String(
 -              ds.getSequence(s_end - 1, s_end + dstream_ds)).toLowerCase()
 +              ds.getSequence(s_end - 1, s_end + dstream_ds)).toLowerCase(Locale.ROOT)
                        .toCharArray();
        char[] coreseq = s.getSequence();
        char[] nseq = new char[offset + upstream.length + downstream.length
      if (cdnaLength != mappedLength && cdnaLength > 2)
      {
        String lastCodon = String.valueOf(cdnaSeqChars,
 -              cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase();
 +              cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase(Locale.ROOT);
        for (String stop : ResidueProperties.STOP_CODONS)
        {
          if (lastCodon.equals(stop))
       */
      int startOffset = 0;
      if (cdnaLength != mappedLength && cdnaLength > 2
 -            && String.valueOf(cdnaSeqChars, 0, CODON_LENGTH).toUpperCase()
 +            && String.valueOf(cdnaSeqChars, 0, CODON_LENGTH).toUpperCase(Locale.ROOT)
                      .equals(ResidueProperties.START))
      {
        startOffset += CODON_LENGTH;
  
      SequenceI newSeq = null;
  
-     final MapList maplist = mapping.getMap();
-     if (maplist.isContiguous() && maplist.isFromForwardStrand())
-     {
-       /*
-        * just a subsequence, keep same dataset sequence
-        */
-       int start = maplist.getFromLowest();
-       int end = maplist.getFromHighest();
-       newSeq = seq.getSubSequence(start - 1, end);
-       newSeq.setName(seqId);
-     }
-     else
-     {
-       /*
-        * construct by splicing mapped from ranges
-        */
-       char[] seqChars = seq.getSequence();
-       List<int[]> fromRanges = maplist.getFromRanges();
-       int cdsWidth = MappingUtils.getLength(fromRanges);
-       char[] newSeqChars = new char[cdsWidth];
+     /*
+      * construct CDS sequence by splicing mapped from ranges
+      */
+     char[] seqChars = seq.getSequence();
+     List<int[]> fromRanges = mapping.getMap().getFromRanges();
+     int cdsWidth = MappingUtils.getLength(fromRanges);
+     char[] newSeqChars = new char[cdsWidth];
  
-       int newPos = 0;
-       for (int[] range : fromRanges)
+     int newPos = 0;
+     for (int[] range : fromRanges)
+     {
+       if (range[0] <= range[1])
        {
-         if (range[0] <= range[1])
-         {
-           // forward strand mapping - just copy the range
-           int length = range[1] - range[0] + 1;
-           System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
-                   length);
-           newPos += length;
-         }
-         else
+         // forward strand mapping - just copy the range
+         int length = range[1] - range[0] + 1;
+         System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
+                 length);
+         newPos += length;
+       }
+       else
+       {
+         // reverse strand mapping - copy and complement one by one
+         for (int i = range[0]; i >= range[1]; i--)
          {
-           // reverse strand mapping - copy and complement one by one
-           for (int i = range[0]; i >= range[1]; i--)
-           {
-             newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
-           }
+           newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
          }
        }
  
            }
            else
            {
-             System.err.println(
-                     "JAL-2154 regression: warning - found (and ignnored a duplicate CDS sequence):"
-                             + mtch.toString());
+             Cache.log.error(
+                     "JAL-2154 regression: warning - found (and ignored) a duplicate CDS sequence:" + mtch.toString());
            }
          }
        }
@@@ -33,7 -33,6 +33,7 @@@ import java.util.List
   */
  public class SearchResults implements SearchResultsI
  {
 +  private int count;
  
    private List<SearchResultMatchI> matches = new ArrayList<>();
  
      if (!matches.contains(m))
      {
        matches.add(m);
 +      count++;
      }
      return m;
    }
  
    @Override
 +  public void addResult(SequenceI seq, int[] positions)
 +  {
 +    /*
 +     * we only increment the match count by 1 - or not at all,
 +     * if the matches are all duplicates of existing
 +     */
 +    int beforeCount = count;
 +    for (int i = 0; i < positions.length - 1; i += 2)
 +    {
 +      addResult(seq, positions[i], positions[i + 1]);
 +    }
 +    if (count > beforeCount)
 +    {
 +      count = beforeCount + 1;
 +    }
 +  }
 +
 +  @Override
    public boolean involvesSequence(SequenceI sequence)
    {
      final int start = sequence.getStart();
    }
  
    @Override
 -  public int getSize()
 +  public int getCount()
    {
 -    return matches.size();
 +    return count;
    }
  
    @Override
    }
  
    /**
-    * Two SearchResults are considered equal if they contain the same matches in
-    * the same order.
+    * Two SearchResults are considered equal if they contain the same matches
+    * (Sequence, start position, end position) in the same order
+    * 
+    * @see Match#equals(Object)
     */
    @Override
    public boolean equals(Object obj)
@@@ -20,8 -20,6 +20,8 @@@
   */
  package jalview.gui;
  
 +import java.util.Locale;
 +
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.event.ActionEvent;
@@@ -840,8 -838,13 +840,13 @@@ public class PopupMenu extends JPopupMe
         * show local rather than linked feature coordinates
         */
        int[] beginRange = mf.getMappedPositions(start, start);
-       start = beginRange[0];
        int[] endRange = mf.getMappedPositions(end, end);
+       if (beginRange == null || endRange == null)
+       {
+         // e.g. variant extending to stop codon so not mappable
+         return;
+       }
+       start = beginRange[0];
        end = endRange[endRange.length - 1];
      }
      StringBuilder desc = new StringBuilder();
          for (int d = 0; d < nd; d++)
          {
            DBRefEntry e = dbr.get(d);
 -          String src = e.getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase();
 +          String src = e.getSource(); // jalview.util.DBRefUtils.getCanonicalName(dbr[d].getSource()).toUpperCase(Locale.ROOT);
            Object[] sarray = commonDbrefs.get(src);
            if (sarray == null)
            {
        boolean usingNames = false;
        // Now see which parts of the group apply for this URL
        String ltarget = urlLink.getTarget(); // jalview.util.DBRefUtils.getCanonicalName(urlLink.getTarget());
 -      Object[] idset = commonDbrefs.get(ltarget.toUpperCase());
 +      Object[] idset = commonDbrefs.get(ltarget.toUpperCase(Locale.ROOT));
        String[] seqstr, ids; // input to makeUrl
        if (idset != null)
        {
   */
  package jalview.util;
  
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.HashMap;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Map;
 +
  import jalview.analysis.AlignmentSorter;
  import jalview.api.AlignViewportI;
+ import jalview.bin.Cache;
  import jalview.commands.CommandI;
  import jalview.commands.EditCommand;
  import jalview.commands.EditCommand.Action;
  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;
@@@ -46,6 -41,13 +48,6 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  
 -import java.util.ArrayList;
 -import java.util.Arrays;
 -import java.util.HashMap;
 -import java.util.Iterator;
 -import java.util.List;
 -import java.util.Map;
 -
  /**
   * Helper methods for manipulations involving sequence mappings.
   * 
@@@ -78,7 -80,7 +80,7 @@@ public final class MappingUtil
        action = action.getUndoAction();
      }
      // TODO write this
-     System.err.println("MappingUtils.mapCutOrPaste not yet implemented");
+     Cache.log.error("MappingUtils.mapCutOrPaste not yet implemented");
    }
  
    /**
  
        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);
        }
      }
    }
 +
 +  /**
 +   * Converts a list of {@code start-end} ranges to a single array of
 +   * {@code start1, end1, start2, ... } ranges
 +   * 
 +   * @param ranges
 +   * @return
 +   */
 +  public static int[] rangeListToArray(List<int[]> ranges)
 +  {
 +    int rangeCount = ranges.size();
 +    int[] result = new int[rangeCount * 2];
 +    int j = 0;
 +    for (int i = 0; i < rangeCount; i++)
 +    {
 +      int[] range = ranges.get(i);
 +      result[j++] = range[0];
 +      result[j++] = range[1];
 +    }
 +    return result;
 +  }
  }
@@@ -39,9 -39,9 +39,9 @@@ import jalview.api.AlignViewportI
  import jalview.api.FeatureColourI;
  import jalview.api.FeaturesDisplayedI;
  import jalview.datamodel.AlignedCodonFrame;
+ import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.MappedFeatures;
- import jalview.datamodel.Mapping;
  import jalview.datamodel.SearchResultMatchI;
  import jalview.datamodel.SearchResults;
  import jalview.datamodel.SearchResultsI;
@@@ -104,11 -104,11 +104,11 @@@ public abstract class FeatureRendererMo
  
    Map<String, Float> featureOrder = null;
  
 -  protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
 -          this);
 -
    protected AlignViewportI av;
  
 +  private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
 +          this);
 +
    @Override
    public AlignViewportI getViewport()
    {
        {
          firing = Boolean.TRUE;
          findAllFeatures(true); // add all new features as visible
 -        changeSupport.firePropertyChange("changeSupport", null, null);
 +        notifyFeaturesChanged();
          firing = Boolean.FALSE;
        }
      }
    }
  
    @Override
 +  public void notifyFeaturesChanged()
 +  {
 +    changeSupport.firePropertyChange("changeSupport", null, null);
 +  }
 +
 +  @Override
    public List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
    {
      /*
       * todo: direct lookup of CDS for peptide and vice-versa; for now,
       * have to search through an unordered list of mappings for a candidate
       */
-     Mapping mapping = null;
+     SequenceToSequenceMapping mapping = null;
      SequenceI mapFrom = null;
  
      for (AlignedCodonFrame acf : mappings)
      {
-       mapping = acf.getMappingForSequence(sequence);
-       if (mapping == null || !mapping.getMap().isTripletMap())
+       mapping = acf.getCoveringCodonMapping(ds);
+       if (mapping == null)
        {
-         continue; // we are only looking for 3:1 or 1:3 mappings
+         continue;
        }
        SearchResultsI sr = new SearchResults();
-       acf.markMappedRegion(ds, pos, sr);
+       mapping.markMappedRegion(ds, pos, sr);
        for (SearchResultMatchI match : sr.getResults())
        {
          int fromRes = match.getStart();
        }
      }
      
-     return new MappedFeatures(mapping, mapFrom, pos, residue, result);
+     return new MappedFeatures(mapping.getMapping(), mapFrom, pos, residue, result);
    }
  
    @Override
@@@ -25,12 -25,10 +25,12 @@@ import static org.testng.AssertJUnit.as
  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.util.ArrayList;
  import java.util.Arrays;
 +import java.util.BitSet;
  import java.util.List;
  
  import org.testng.annotations.BeforeClass;
@@@ -46,7 -44,7 +46,7 @@@ public class MapListTes
    {
      Cache.initLogger();
    }
+   
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
    {
@@@ -54,7 -52,7 +54,7 @@@
      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 },
      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)));
  
 +    /*
 +     * 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));
 +    
    }
  
    /**
      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
 +     */
 +    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.locateInFrom(1, 1));
 +    assertArrayEquals(new int[] { 11, 16 }, ml.locateInFrom(1, 3));
 +    assertArrayEquals(new int[] { 11, 16, 21, 23 }, ml.locateInFrom(1, 4));
 +    assertArrayEquals(new int[] { 14, 16, 21, 23 }, ml.locateInFrom(3, 4));
 +
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testLocateInFrom_reverseStrand()
 +  {
 +    int[] codons = new int[] { 12, 1 };
 +    int[] protein = new int[] { 1, 4 };
 +    MapList ml = new MapList(codons, protein, 3, 1);
 +    assertEquals("[12, 10]", Arrays.toString(ml.locateInFrom(1, 1)));
 +    assertEquals("[9, 4]", Arrays.toString(ml.locateInFrom(2, 3)));
    }
  
    /**
      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)));
 +    // 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.
      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));
    }
  
    /**
      MapList ml = new MapList(codons, protein, 3, 1);
  
      /*
 -     * Can't map from an unmapped position
 -     */
 -    assertNull(ml.locateInTo(1, 2));
 -    assertNull(ml.locateInTo(2, 4));
 -    assertNull(ml.locateInTo(4, 4));
 -
 -    /*
 -     * Valid range or subrange of codon1 maps to protein1.
 +     * Valid range or subrange of codon1 maps to protein1
       */
      assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(2, 2)));
      assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(3, 3)));
      // 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));
    }
  
    /**
    public void testAddMapList_sameMap()
    {
      MapList ml = new MapList(new int[] { 11, 15, 20, 25, 35, 30 },
 -            new int[] { 72, 22 }, 1, 3);
 +            new int[]
 +            { 72, 22 }, 1, 3);
      String before = ml.toString();
      ml.addMapList(ml);
      assertEquals(before, ml.toString());
      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
      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);
    }
              1);
      assertTrue(ml.isToForwardStrand());
    }
 +
 +  /**
 +   * Test for mapping with overlapping ranges
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testLocateInFrom_withOverlap()
 +  {
 +    /*
 +     * gene to protein...
 +     */
 +    int[] codons = new int[] { 1, 12, 12, 17 };
 +    int[] protein = new int[] { 1, 6 };
 +    MapList ml = new MapList(codons, protein, 3, 1);
 +    assertEquals("[1, 3]", Arrays.toString(ml.locateInFrom(1, 1)));
 +    assertEquals("[4, 6]", Arrays.toString(ml.locateInFrom(2, 2)));
 +    assertEquals("[7, 9]", Arrays.toString(ml.locateInFrom(3, 3)));
 +    assertEquals("[10, 12]", Arrays.toString(ml.locateInFrom(4, 4)));
 +    assertEquals("[12, 14]", Arrays.toString(ml.locateInFrom(5, 5)));
 +    assertEquals("[15, 17]", Arrays.toString(ml.locateInFrom(6, 6)));
 +    assertEquals("[1, 6]", Arrays.toString(ml.locateInFrom(1, 2)));
 +    assertEquals("[1, 9]", Arrays.toString(ml.locateInFrom(1, 3)));
 +    assertEquals("[1, 12]", Arrays.toString(ml.locateInFrom(1, 4)));
 +    assertEquals("[1, 12, 12, 14]", Arrays.toString(ml.locateInFrom(1, 5)));
 +    assertEquals("[1, 12, 12, 17]", Arrays.toString(ml.locateInFrom(1, 6)));
 +    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));
 +
 +    /*
 +     * gene to CDS...from EMBL:MN908947
 +     */
 +    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, 13468, 13468]",
 +            Arrays.toString(ml.locateInFrom(13203, 13204)));
 +  }
 +
 +  /**
 +   * Test for mapping with overlapping ranges
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testLocateInTo_withOverlap()
 +  {
 +    /*
 +     * gene to protein...
 +     */
 +    int[] codons = new int[] { 1, 12, 12, 17 };
 +    int[] protein = new int[] { 1, 6 };
 +    MapList ml = new MapList(codons, protein, 3, 1);
 +    assertEquals("[1, 1]", Arrays.toString(ml.locateInTo(1, 1)));
 +    assertEquals("[1, 3]", Arrays.toString(ml.locateInTo(3, 8)));
 +    assertEquals("[1, 4]", Arrays.toString(ml.locateInTo(2, 11)));
 +    assertEquals("[1, 4]", Arrays.toString(ml.locateInTo(3, 11)));
 +
 +    // we want base 12 to map to both of the amino acids it codes for
 +    assertEquals("[4, 5]", Arrays.toString(ml.locateInTo(12, 12)));
 +    assertEquals("[4, 5]", Arrays.toString(ml.locateInTo(11, 12)));
 +    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));
 +    
 +    /*
 +     * 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 };
 +    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
 +     */
 +    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" })
 +  public void testTraverseToPosition()
 +  {
 +    List<int[]> ranges = new ArrayList<>();
 +    assertNull(MapList.traverseToPosition(ranges, 0));
 +
 +    ranges.add(new int[] { 3, 6 });
 +    assertNull(MapList.traverseToPosition(ranges, 0));
 +  }
 +
 +  @Test(groups = { "Functional" })
 +  public void testCountPositions()
 +  {
 +    try
 +    {
 +      MapList.countPositions(null, 1);
 +      fail("expected exception");
 +    } catch (NullPointerException e)
 +    {
 +      // expected
 +    }
 +
 +    List<int[]> intervals = new ArrayList<>();
 +    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));
 +
 +    /*
 +     * 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));
 +
 +    /*
 +     * reverse then forward
 +     */
 +    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));
 +
 +    /*
 +     * an interval [x, x] is treated as forward
 +     */
 +    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));
 +  }
 +
 +  /**
 +   * Tests for helper method that adds any overlap (plus offset) to a set of
 +   * overlaps
 +   */
 +  @Test(groups = { "Functional" })
 +  public void testAddOffsetPositions()
 +  {
 +    List<int[]> mapped = new ArrayList<>();
 +    int[] range = new int[] {10, 20};
 +    BitSet offsets = new BitSet();
 +
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertTrue(mapped.isEmpty()); // nothing marked for overlap
 +
 +    offsets.set(11);
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertTrue(mapped.isEmpty()); // no offset 11 in range
 +
 +    offsets.set(4, 6); // this sets bits 4 and 5
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertEquals(1, mapped.size());
 +    assertArrayEquals(new int[] { 14, 15 }, mapped.get(0));
 +
 +    mapped.clear();
 +    offsets.set(10);
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertEquals(2, mapped.size());
 +    assertArrayEquals(new int[] { 14, 15 }, mapped.get(0));
 +    assertArrayEquals(new int[] { 20, 20 }, mapped.get(1));
 +
 +    /*
 +     * reverse range
 +     */
 +    range = new int[] { 20, 10 };
 +    mapped.clear();
 +    offsets.clear();
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertTrue(mapped.isEmpty()); // nothing marked for overlap
 +    offsets.set(11);
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertTrue(mapped.isEmpty()); // no offset 11 in range
 +    offsets.set(0);
 +    offsets.set(10);
 +    offsets.set(6, 8); // sets bits 6 and 7
 +    MapList.addOffsetPositions(mapped, 0, range, offsets);
 +    assertEquals(3, mapped.size());
 +    assertArrayEquals(new int[] { 20, 20 }, mapped.get(0));
 +    assertArrayEquals(new int[] { 14, 13 }, mapped.get(1));
 +    assertArrayEquals(new int[] { 10, 10 }, mapped.get(2));
 +  }
 +  
 +  @Test(groups = { "Functional" })
 +  public void testGetPositionsForOffsets()
 +  {
 +    List<int[]> ranges = new ArrayList<>();
 +    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});
 +    mapped = MapList.getPositionsForOffsets(ranges, offsets);
 +    assertEquals(1, mapped.size());
 +    assertArrayEquals(new int[] {20,  25}, mapped.get(0));
 +    
 +    /*
 +     * two ranges
 +     */
 +    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));
 +    
 +    /*
 +     * boundary case - right end of first range overlaps
 +     */
 +    offsets.clear();
 +    offsets.set(10);
 +    mapped = MapList.getPositionsForOffsets(ranges, offsets);
 +    assertEquals(1, mapped.size());
 +    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));
 +    
 +    /*
 +     * 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});
 +    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
 +
 +    /*
 +     * reverse range overlap is reported with start < end
 +     */
 +    ranges.clear();
 +    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));
 +  }
 +  
 +  @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);
 +    assertTrue(overlaps.isEmpty());
 +    overlaps = MapList.getMappedOffsetsForPositions(1, 11, ranges, 1, 1);
 +    assertEquals(2, overlaps.cardinality());
 +    assertTrue(overlaps.get(0));
 +    assertTrue(overlaps.get(1));
 +    overlaps = MapList.getMappedOffsetsForPositions(15, 35, ranges, 1, 1);
 +    assertEquals(11, overlaps.cardinality());
 +    for (int i = 5 ; i <= 11 ; i++)
 +    {
 +      assertTrue(overlaps.get(i));
 +    }
 +    
 +    ranges.clear();
 +    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});
 +    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});
 +    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});
 +    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);
 +    // 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);
 +    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);
 +    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);
 +    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);
 +    assertEquals(3, overlaps.cardinality());
 +    assertTrue(overlaps.get(6));
 +    assertTrue(overlaps.get(7));
 +    assertTrue(overlaps.get(8));
 +  }
  }
@@@ -24,18 -24,17 +24,28 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertFalse;
  import static org.testng.AssertJUnit.assertSame;
  import static org.testng.AssertJUnit.assertTrue;
 +import static org.testng.AssertJUnit.fail;
 +
 +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 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;
@@@ -65,6 -64,7 +75,6 @@@ public class MappingUtilsTes
    {
      Cache.initLogger();
    }
 -  
  
    @BeforeClass(alwaysRun = true)
    public void setUpJvOptionPane()
      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
      assertEquals(9, ranges.get(0)[1]);
    }
  
 +  @Test(groups = "Functional")
 +  public void testListToArray()
 +  {
 +    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
 +    }
 +  }
++  
+   /**
+    * 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
+   }
  }