JAL-845 implement alignment of protein to match cDNA alignment
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 3 Mar 2015 10:35:19 +0000 (10:35 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 3 Mar 2015 10:35:19 +0000 (10:35 +0000)
22 files changed:
src/jalview/analysis/AlignmentUtils.java
src/jalview/api/SplitContainerI.java
src/jalview/datamodel/AlignedCodon.java
src/jalview/datamodel/AlignedCodonFrame.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/IncompleteCodonException.java [new file with mode: 0644]
src/jalview/datamodel/Mapping.java
src/jalview/datamodel/SearchResults.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/SplitFrame.java
src/jalview/io/VamsasAppDatastore.java
src/jalview/io/vamsas/Rangetype.java
src/jalview/jbgui/GSplitFrame.java
src/jalview/util/MapList.java
src/jalview/util/MappingUtils.java
src/jalview/ws/AWSThread.java
src/jalview/ws/jws2/MsaWSThread.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/datamodel/AlignedCodonIteratorTest.java [new file with mode: 0644]
test/jalview/datamodel/SearchResultsTest.java
test/jalview/util/MapListTest.java

index 0ae782e..74406dc 100644 (file)
  */
 package jalview.analysis;
 
+import jalview.datamodel.AlignedCodon;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Mapping;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
@@ -30,10 +32,15 @@ import jalview.schemes.ResidueProperties;
 import jalview.util.MapList;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
+import java.util.TreeMap;
 
 /**
  * grab bag of useful alignment manipulation operations Expect these to be
@@ -207,10 +214,11 @@ public class AlignmentUtils
 
   /**
    * Build mapping of protein to cDNA alignment. Mappings are made between
-   * sequences which have the same name and compatible lengths. Has a 3-valued
-   * result: either Mapped (at least one sequence mapping was created),
-   * AlreadyMapped (all possible sequence mappings already exist), or NotMapped
-   * (no possible sequence mappings exist).
+   * sequences which have the same name and compatible lengths. Any new mappings
+   * are added to the protein alignment. Has a 3-valued result: either Mapped
+   * (at least one sequence mapping was created), AlreadyMapped (all possible
+   * sequence mappings already exist), or NotMapped (no possible sequence
+   * mappings exist).
    * 
    * @param proteinAlignment
    * @param cdnaAlignment
@@ -780,4 +788,118 @@ public class AlignmentUtils
     alignedSeq.setSequence(newseq.toString());
     return alignedSeq;
   }
+
+  /**
+   * Realigns the given protein to match the alignment of the dna, using codon
+   * mappings to translate aligned codon positions to protein residues.
+   * 
+   * @param protein
+   *          the alignment whose sequences are realigned by this method
+   * @param dna
+   *          the dna alignment whose alignment we are 'copying'
+   * @return the number of sequences that were realigned
+   */
+  public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
+  {
+    Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
+
+    /*
+     * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
+     * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
+     * comparator keeps the codon positions ordered.
+     */
+    Map<AlignedCodon, Map<SequenceI, String>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, String>>(
+            new CodonComparator());
+    for (SequenceI dnaSeq : dna.getSequences())
+    {
+      for (AlignedCodonFrame mapping : mappings)
+      {
+        Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
+        SequenceI prot = mapping.findAlignedSequence(
+                dnaSeq.getDatasetSequence(), protein);
+        if (prot != null)
+        {
+          addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
+                  seqMap, alignedCodons);
+        }
+      }
+    }
+    return alignProteinAs(protein, alignedCodons);
+  }
+
+  /**
+   * Update the aligned protein sequences to match the codon alignments given in
+   * the map.
+   * 
+   * @param protein
+   * @param alignedCodons
+   *          an ordered map of codon positions (columns), with sequence/peptide
+   *          values present in each column
+   * @return
+   */
+  protected static int alignProteinAs(AlignmentI protein,
+          Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
+  {
+    /*
+     * Prefill aligned sequences with gaps before inserting aligned protein
+     * residues.
+     */
+    int alignedWidth = alignedCodons.size();
+    char[] gaps = new char[alignedWidth];
+    Arrays.fill(gaps, protein.getGapCharacter());
+    String allGaps = String.valueOf(gaps);
+    for (SequenceI seq : protein.getSequences())
+    {
+      seq.setSequence(allGaps);
+    }
+
+    int column = 0;
+    for (AlignedCodon codon : alignedCodons.keySet())
+    {
+      final Map<SequenceI, String> columnResidues = alignedCodons.get(codon);
+      for (Entry<SequenceI, String> entry : columnResidues
+              .entrySet())
+      {
+        // place translated codon at its column position in sequence
+        entry.getKey().getSequence()[column] = entry.getValue().charAt(0);
+      }
+      column++;
+    }
+    return 0;
+  }
+
+  /**
+   * Populate the map of aligned codons by traversing the given sequence
+   * mapping, locating the aligned positions of mapped codons, and adding those
+   * positions and their translation products to the map.
+   * 
+   * @param dna
+   *          the aligned sequence we are mapping from
+   * @param protein
+   *          the sequence to be aligned to the codons
+   * @param gapChar
+   *          the gap character in the dna sequence
+   * @param seqMap
+   *          a mapping to a sequence translation
+   * @param alignedCodons
+   *          the map we are building up
+   */
+  static void addCodonPositions(SequenceI dna, SequenceI protein,
+          char gapChar,
+          Mapping seqMap,
+          Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
+  {
+    Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
+    while (codons.hasNext())
+    {
+      AlignedCodon codon = codons.next();
+      Map<SequenceI, String> seqProduct = alignedCodons.get(codon);
+      if (seqProduct == null)
+      {
+        seqProduct = new HashMap<SequenceI, String>();
+        alignedCodons.put(codon, seqProduct);
+      }
+      seqProduct.put(protein, codon.product);
+    }
+  }
 }
index 22f5201..2abf4d8 100644 (file)
@@ -1,5 +1,9 @@
 package jalview.api;
 
+import jalview.datamodel.AlignmentI;
+
+import java.awt.Component;
+
 /**
  * Describes a visual container that can show two alignments.
  * 
@@ -16,6 +20,21 @@ public interface SplitContainerI
    * @param show
    */
   // TODO need an interface for AlignFrame?
-  void setComplementVisible(Object alignFrame, boolean show);
+  void setComplementVisible(Component alignFrame, boolean show);
+
+  /**
+   * Returns the alignment that is complementary to the one in the given
+   * AlignFrame, or null.
+   */
+  AlignmentI getComplement(Component af);
+
+  /**
+   * Returns the frame title for the alignment that is complementary to the one
+   * in the given AlignFrame, or null.
+   * 
+   * @param af
+   * @return
+   */
+  String getComplementTitle(Component af);
 
 }
index d0e62a1..0daa3fb 100644 (file)
@@ -2,7 +2,8 @@ package jalview.datamodel;
 
 /**
  * Holds the aligned column positions (base 0) for one codon in a nucleotide
- * sequence. The object is immutable once created.
+ * sequence, and (optionally) its peptide translation. The object is immutable
+ * once created.
  * 
  * Example: in "G-AT-C-GA" the aligned codons are (0, 2, 3) and (5, 7, 8).
  * 
@@ -17,11 +18,19 @@ public final class AlignedCodon
 
   public final int pos3;
 
+  public final String product;
+
   public AlignedCodon(int i, int j, int k)
   {
+    this(i, j, k, null);
+  }
+
+  public AlignedCodon(int i, int j, int k, String prod)
+  {
     pos1 = i;
     pos2 = j;
     pos3 = k;
+    product = prod;
   }
 
   /**
@@ -42,7 +51,10 @@ public final class AlignedCodon
   }
 
   /**
-   * Two aligned codons are equal if all their base positions are the same.
+   * Two aligned codons are equal if all their base positions are the same. We
+   * don't care about the protein product. This test is required for correct
+   * alignment of translated gapped dna alignments (the same codon positions in
+   * different sequences occupy the same column in the translated alignment).
    */
   @Override
   public boolean equals(Object o)
@@ -66,6 +78,9 @@ public final class AlignedCodon
   @Override
   public String toString()
   {
-    return "[" + pos1 + ", " + pos2 + ", " + pos3 + "]";
+    StringBuilder sb = new StringBuilder();
+    sb.append("[").append(pos1).append(", ").append(pos2).append(", ")
+            .append(pos3).append("]");
+    return sb.toString();
   }
 }
index 9c17306..cbddf1c 100644 (file)
@@ -123,6 +123,32 @@ public class AlignedCodonFrame
   }
 
   /**
+   * Returns the first mapping found which is to or from the given sequence, or
+   * null.
+   * 
+   * @param seq
+   * @return
+   */
+  public Mapping getMappingForSequence(SequenceI seq)
+  {
+    if (dnaSeqs == null)
+    {
+      return null;
+    }
+    SequenceI seqDs = seq.getDatasetSequence();
+    seqDs = seqDs != null ? seqDs : seq;
+
+    for (int ds = 0; ds < dnaSeqs.length; ds++)
+    {
+      if (dnaSeqs[ds] == seqDs || dnaToProt[ds].to == seqDs)
+      {
+        return dnaToProt[ds];
+      }
+    }
+    return null;
+  }
+
+  /**
    * Return the corresponding aligned or dataset aa sequence for given dna
    * sequence, null if not found.
    * 
index 56545e1..4558d8d 100755 (executable)
@@ -1688,10 +1688,7 @@ public class Alignment implements AlignmentI
     boolean thatIsProtein = !al.isNucleotide();
     if (!thatIsProtein && !thisIsNucleotide)
     {
-      System.err
-              .println("Alignment of protein from cDNA not yet implemented");
-      return 0;
-      // todo: build it - a variant of Dna.CdnaTranslate()
+      return AlignmentUtils.alignProteinAsDna(this, al);
     }
 
     char thisGapChar = this.getGapCharacter();
diff --git a/src/jalview/datamodel/IncompleteCodonException.java b/src/jalview/datamodel/IncompleteCodonException.java
new file mode 100644 (file)
index 0000000..f716a53
--- /dev/null
@@ -0,0 +1,13 @@
+package jalview.datamodel;
+
+/**
+ * An exception to indicate that less than 3 nucleotide bases are available when
+ * trying to form a codon.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class IncompleteCodonException extends RuntimeException
+{
+
+}
index f2c16d0..be8fd58 100644 (file)
@@ -22,11 +22,245 @@ package jalview.datamodel;
 
 import jalview.util.MapList;
 
+import java.util.Iterator;
+import java.util.NoSuchElementException;
 import java.util.Vector;
 
 public class Mapping
 {
   /**
+   * An iterator that serves the aligned codon positions (with their protein
+   * products).
+   * 
+   * @author gmcarstairs
+   *
+   */
+  public class AlignedCodonIterator implements Iterator<AlignedCodon>
+  {
+    /*
+     * The gap character used in the aligned sequence
+     */
+    private final char gap;
+
+    /*
+     * The characters of the aligned sequence e.g. "-cGT-ACgTG-"
+     */
+    private final char[] alignedSeq;
+
+    /*
+     * Next position (base 0) in the aligned sequence
+     */
+    private int alignedColumn = 0;
+
+    /*
+     * Count of bases up to and including alignedColumn position
+     */
+    private int alignedBases = 0;
+
+    /*
+     * [start, end] from ranges (base 1)
+     */
+    private Iterator<int[]> fromRanges;
+
+    /*
+     * [start, end] to ranges (base 1)
+     */
+    private Iterator<int[]> toRanges;
+
+    /*
+     * The current [start, end] (base 1) from range
+     */
+    private int[] currentFromRange = null;
+
+    /*
+     * The current [start, end] (base 1) to range
+     */
+    private int[] currentToRange = null;
+
+    /*
+     * The next 'from' position (base 1) to process
+     */
+    private int fromPosition = 0;
+
+    /*
+     * The next 'to' position (base 1) to process
+     */
+    private int toPosition = 0;
+
+    /**
+     * Constructor
+     * 
+     * @param cs
+     *          the aligned sequence characters
+     * @param gapChar
+     */
+    public AlignedCodonIterator(char[] cs, char gapChar)
+    {
+      this.alignedSeq = cs;
+      this.gap = gapChar;
+      fromRanges = map.getFromRanges().iterator();
+      toRanges = map.getToRanges().iterator();
+      if (fromRanges.hasNext())
+      {
+        currentFromRange = fromRanges.next();
+        fromPosition = currentFromRange[0];
+      }
+      if (toRanges.hasNext())
+      {
+        currentToRange = toRanges.next();
+        toPosition = currentToRange[0];
+      }
+    }
+
+    /**
+     * Returns true unless we have already traversed the whole mapping.
+     */
+    @Override
+    public boolean hasNext()
+    {
+      if (fromRanges.hasNext())
+      {
+        return true;
+      }
+      if (currentFromRange == null || fromPosition >= currentFromRange[1])
+      {
+        return false;
+      }
+      return true;
+    }
+
+    /**
+     * Returns the next codon's aligned positions, and translated value.
+     * 
+     * @throws NoSuchElementException
+     *           if hasNext() would have returned false
+     * @throws IncompleteCodonException
+     *           if not enough mapped bases are left to make up a codon
+     */
+    @Override
+    public AlignedCodon next() throws IncompleteCodonException
+    {
+      if (!hasNext())
+      {
+        throw new NoSuchElementException();
+      }
+
+      int[] codon = getNextCodon();
+      int[] alignedCodon = getAlignedCodon(codon);
+
+      String peptide = getPeptide();
+      return new AlignedCodon(alignedCodon[0], alignedCodon[1],
+              alignedCodon[2], peptide);
+    }
+
+    /**
+     * Retrieve the translation as the 'mapped to' position in the mapped to
+     * sequence.
+     * 
+     * @return
+     */
+    private String getPeptide()
+    {
+      // TODO should ideally handle toRatio other than 1 as well...
+      // i.e. code like getNextCodon()
+      if (toPosition <= currentToRange[1]) {
+        char pep = Mapping.this.to.getSequence()[toPosition - 1];
+        toPosition++;
+        return String.valueOf(pep);
+      }
+      if (!toRanges.hasNext())
+      {
+        throw new NoSuchElementException("Ran out of peptide at position "
+                + toPosition);
+      }
+      currentToRange = toRanges.next();
+      toPosition = currentToRange[0];
+      return getPeptide();
+    }
+
+    /**
+     * Get the (base 1) dataset positions for the next codon in the mapping.
+     * 
+     * @throws IncompleteCodonException
+     *           if less than 3 remaining bases are mapped
+     */
+    private int[] getNextCodon()
+    {
+      int[] codon = new int[3];
+      int codonbase = 0;
+
+      while (codonbase < 3)
+      {
+        if (fromPosition <= currentFromRange[1])
+        {
+          /*
+           * Add next position from the current start-end range
+           */
+          codon[codonbase++] = fromPosition++;
+        }
+        else
+        {
+          /*
+           * Move to the next range - if there is one
+           */
+          if (!fromRanges.hasNext())
+          {
+            throw new IncompleteCodonException();
+          }
+          currentFromRange = fromRanges.next();
+          fromPosition = currentFromRange[0];
+        }
+      }
+      return codon;
+    }
+
+    /**
+     * Get the aligned column positions (base 0) for the given sequence
+     * positions (base 1), by counting ungapped characters in the aligned
+     * sequence.
+     * 
+     * @param codon
+     * @return
+     */
+    private int[] getAlignedCodon(int[] codon)
+    {
+      int[] aligned = new int[codon.length];
+      for (int i = 0; i < codon.length; i++)
+      {
+        aligned[i] = getAlignedColumn(codon[i]);
+      }
+      return aligned;
+    }
+
+    /**
+     * Get the aligned column position (base 0) for the given sequence position
+     * (base 1).
+     * 
+     * @param sequencePos
+     * @return
+     */
+    private int getAlignedColumn(int sequencePos)
+    {
+      while (alignedBases < sequencePos
+              && alignedColumn < alignedSeq.length)
+      {
+        if (alignedSeq[alignedColumn++] != gap)
+        {
+          alignedBases++;
+        }
+      }
+      return alignedColumn - 1;
+    }
+
+    @Override
+    public void remove()
+    {
+      // ignore
+    }
+
+  }
+
+  /**
    * Contains the start-end pairs mapping from the associated sequence to the
    * sequence in the database coordinate system. It also takes care of step
    * difference between coordinate systems.
@@ -477,4 +711,9 @@ public class Mapping
     super.finalize();
   }
 
+  public Iterator<AlignedCodon> getCodonIterator(SequenceI seq, char gapChar)
+  {
+    return new AlignedCodonIterator(seq.getSequence(), gapChar);
+  }
+
 }
index a53d103..fb420b9 100755 (executable)
@@ -40,16 +40,26 @@ public class SearchResults
   {
     SequenceI sequence;
 
-    /*
+    /**
      * Start position of match in sequence (base 1)
      */
     int start;
 
-    /*
+    /**
      * End position (inclusive) (base 1)
      */
     int end;
 
+    /**
+     * Constructor
+     * 
+     * @param seq
+     *          a sequence
+     * @param start
+     *          start position of matched range (base 1)
+     * @param end
+     *          end of matched range (inclusive, base 1)
+     */
     public Match(SequenceI seq, int start, int end)
     {
       sequence = seq;
@@ -79,7 +89,10 @@ public class SearchResults
     public String toString()
     {
       char[] chars = sequence.getSequence();
-      return String.valueOf(Arrays.copyOfRange(chars, start - 1, end));
+      // convert start/end to base 0 (with bounds check)
+      final int from = Math.max(start - 1, 0);
+      final int to = Math.min(end, chars.length + 1);
+      return String.valueOf(Arrays.copyOfRange(chars, from, to));
     }
   }
 
index 2b92142..9604458 100644 (file)
@@ -4907,7 +4907,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       final SequenceI[] seqs = viewport.getSelectionGroup() == null ? viewport
               .getAlignment().getSequencesArray() : viewport
               .getSelectionAsNewSequence();
-      viewport.openSplitFrame(af, seqs);
+      viewport.openSplitFrame(af, seqs, al.getCodonFrames());
       // Desktop.addInternalFrame(af, newTitle, DEFAULT_WIDTH, DEFAULT_HEIGHT);
     }
   }
index 8342151..f01e775 100644 (file)
@@ -1787,20 +1787,20 @@ public class Jalview2XML
       mp = new Mapping();
 
       jalview.util.MapList mlst = jmp.getMap();
-      int r[] = mlst.getFromRanges();
-      for (int s = 0; s < r.length; s += 2)
+      List<int[]> r = mlst.getFromRanges();
+      for (int[] range : r)
       {
         MapListFrom mfrom = new MapListFrom();
-        mfrom.setStart(r[s]);
-        mfrom.setEnd(r[s + 1]);
+        mfrom.setStart(range[0]);
+        mfrom.setEnd(range[1]);
         mp.addMapListFrom(mfrom);
       }
       r = mlst.getToRanges();
-      for (int s = 0; s < r.length; s += 2)
+      for (int[] range : r)
       {
         MapListTo mto = new MapListTo();
-        mto.setStart(r[s]);
-        mto.setEnd(r[s + 1]);
+        mto.setStart(range[0]);
+        mto.setEnd(range[1]);
         mp.addMapListTo(mto);
       }
       mp.setMapFromUnit(mlst.getFromRatio());
index 81a6ddc..3382f30 100644 (file)
@@ -1,5 +1,6 @@
 package jalview.gui;
 
+import jalview.api.SplitContainerI;
 import jalview.api.ViewStyleI;
 import jalview.datamodel.AlignmentI;
 import jalview.jbgui.GAlignFrame;
@@ -37,7 +38,7 @@ import javax.swing.event.InternalFrameEvent;
  * @author gmcarstairs
  *
  */
-public class SplitFrame extends GSplitFrame
+public class SplitFrame extends GSplitFrame implements SplitContainerI
 {
   private static final long serialVersionUID = 1L;
 
@@ -540,4 +541,39 @@ public class SplitFrame extends GSplitFrame
   {
     Desktop.instance.gatherViews(this);
   }
+
+  /**
+   * Returns the alignment in the complementary frame to the one given.
+   */
+  @Override
+  public AlignmentI getComplement(Component alignFrame)
+  {
+    if (alignFrame == this.getTopFrame())
+    {
+      return ((AlignFrame) getBottomFrame()).viewport.getAlignment();
+    }
+    else if (alignFrame == this.getBottomFrame())
+    {
+      return ((AlignFrame) getTopFrame()).viewport.getAlignment();
+    }
+    return null;
+  }
+
+  /**
+   * Returns the title of the complementary frame to the one given.
+   */
+  @Override
+  public String getComplementTitle(Component alignFrame)
+  {
+    if (alignFrame == this.getTopFrame())
+    {
+      return ((AlignFrame) getBottomFrame()).getTitle();
+    }
+    else if (alignFrame == this.getBottomFrame())
+    {
+      return ((AlignFrame) getTopFrame()).getTitle();
+    }
+    return null;
+  }
 }
+
index 1d36865..d00f0b1 100644 (file)
@@ -2570,15 +2570,15 @@ public class VamsasAppDatastore
    * initialise a range type object from a set of start/end inclusive intervals
    * 
    * @param mrt
-   * @param range
+   * @param ranges
    */
-  private void initRangeType(RangeType mrt, int[] range)
+  private void initRangeType(RangeType mrt, List<int[]> ranges)
   {
-    for (int i = 0; i < range.length; i += 2)
+    for (int[] range : ranges)
     {
       Seg vSeg = new Seg();
-      vSeg.setStart(range[i]);
-      vSeg.setEnd(range[i + 1]);
+      vSeg.setStart(range[0]);
+      vSeg.setEnd(range[1]);
       mrt.addSeg(vSeg);
     }
   }
index 3cc977f..49227fd 100644 (file)
  */
 package jalview.io.vamsas;
 
+import jalview.io.VamsasAppDatastore;
+import jalview.util.MessageManager;
+
+import java.util.List;
 import java.util.Vector;
 
 import uk.ac.vamsas.client.Vobject;
@@ -28,8 +32,6 @@ import uk.ac.vamsas.objects.core.MapType;
 import uk.ac.vamsas.objects.core.Mapped;
 import uk.ac.vamsas.objects.core.RangeType;
 import uk.ac.vamsas.objects.core.Seg;
-import jalview.io.VamsasAppDatastore;
-import jalview.util.MessageManager;
 
 /**
  * Enhances DatastoreItem objects with additional functions to do with RangeType
@@ -221,15 +223,15 @@ public abstract class Rangetype extends DatastoreItem
    * initialise a range type object from a set of start/end inclusive intervals
    * 
    * @param mrt
-   * @param range
+   * @param ranges
    */
-  protected void initRangeType(RangeType mrt, int[] range)
+  protected void initRangeType(RangeType mrt, List<int[]> ranges)
   {
-    for (int i = 0; i < range.length; i += 2)
+    for (int[] range : ranges)
     {
       Seg vSeg = new Seg();
-      vSeg.setStart(range[i]);
-      vSeg.setEnd(range[i + 1]);
+      vSeg.setStart(range[0]);
+      vSeg.setEnd(range[1]);
       vSeg.setInclusive(true);
       mrt.addSeg(vSeg);
     }
index 062fe9f..27b3324 100644 (file)
@@ -1,6 +1,5 @@
 package jalview.jbgui;
 
-import jalview.api.SplitContainerI;
 import jalview.util.Platform;
 
 import java.awt.Component;
@@ -12,7 +11,7 @@ import javax.swing.JInternalFrame;
 import javax.swing.JSplitPane;
 import javax.swing.plaf.basic.BasicInternalFrameUI;
 
-public class GSplitFrame extends JInternalFrame implements SplitContainerI
+public class GSplitFrame extends JInternalFrame
 {
   private static final long serialVersionUID = 1L;
 
@@ -120,8 +119,7 @@ public class GSplitFrame extends JInternalFrame implements SplitContainerI
    * Make the complement of the specified split component visible or hidden,
    * adjusting the position of the split divide.
    */
-  @Override
-  public void setComplementVisible(Object alignFrame, boolean show)
+  public void setComplementVisible(Component alignFrame, boolean show)
   {
     if (alignFrame == this.topFrame)
     {
@@ -138,5 +136,4 @@ public class GSplitFrame extends JInternalFrame implements SplitContainerI
     }
     validate();
   }
-
 }
index 32f0256..48aebbf 100644 (file)
@@ -101,23 +101,23 @@ public class MapList
   }
 
   /**
-   * Returns the flattened 'from' ranges as [start1, end1, start2, end2, ...]
+   * Returns the 'from' ranges as {[start1, end1], [start2, end2], ...}
    * 
    * @return
    */
-  public int[] getFromRanges()
+  public List<int[]> getFromRanges()
   {
-    return getRanges(fromShifts);
+    return fromShifts;
   }
 
   /**
-   * Returns the flattened 'to' ranges as [start1, end1, start2, end2, ...]
+   * Returns the 'to' ranges as {[start1, end1], [start2, end2], ...}
    * 
    * @return
    */
-  public int[] getToRanges()
+  public List<int[]> getToRanges()
   {
-    return getRanges(toShifts);
+    return toShifts;
   }
 
   /**
@@ -191,6 +191,8 @@ public class MapList
    */
   public MapList(int from[], int to[], int fromRatio, int toRatio)
   {
+    this.fromRatio = fromRatio;
+    this.toRatio = toRatio;
     fromLowest = from[0];
     fromHighest = from[1];
     for (int i = 0; i < from.length; i += 2)
@@ -211,8 +213,6 @@ public class MapList
       toShifts.add(new int[]
       { to[i], to[i + 1] });
     }
-    this.fromRatio = fromRatio;
-    this.toRatio = toRatio;
   }
 
   /**
@@ -249,6 +249,38 @@ public class MapList
   }
 
   /**
+   * Constructor given ranges as lists of [start, end] positions
+   * 
+   * @param fromRange
+   * @param toRange
+   * @param fromRatio
+   * @param toRatio
+   */
+  public MapList(List<int[]> fromRange, List<int[]> toRange,
+          int fromRatio, int toRatio)
+  {
+    this.fromShifts = fromRange;
+    this.toShifts = toRange;
+    this.fromRatio = fromRatio;
+    this.toRatio = toRatio;
+
+    fromLowest = Integer.MAX_VALUE;
+    fromHighest = 0;
+    for (int[] range : fromRange) {
+      fromLowest = Math.min(fromLowest, range[0]);
+      fromHighest = Math.max(fromHighest, range[1]);
+    }
+
+    toLowest = Integer.MAX_VALUE;
+    toHighest = 0;
+    for (int[] range : toRange)
+    {
+      toLowest = Math.min(toLowest, range[0]);
+      toHighest = Math.max(toHighest, range[1]);
+    }
+  }
+
+  /**
    * get all mapped positions from 'from' to 'to'
    * 
    * @return int[][] { int[] { fromStart, fromFinish, toStart, toFinish }, int
index 1f2e8db..6ddaa99 100644 (file)
@@ -284,9 +284,11 @@ public final class MappingUtils
             .getCodonFrames();
 
     /*
-     * Copy group name, colours, but not sequences
+     * Copy group name, name colours, but not sequences or sequence colour
+     * scheme
      */
     SequenceGroup mappedGroup = new SequenceGroup(sg);
+    sg.cs = mapTo.getGlobalColourScheme();
     mappedGroup.clear();
     // TODO set width of mapped group
 
index 1c9931d..3359546 100644 (file)
@@ -90,7 +90,10 @@ public abstract class AWSThread extends Thread
    */
   protected String WsUrl = null;
 
-  private boolean fromSplitFrame;
+  /*
+   * The AlignFrame from which the service was requested.
+   */
+  private AlignFrame alignFrame;
 
   /**
    * generic web service job/subjob poll loop
@@ -365,8 +368,7 @@ public abstract class AWSThread extends Thread
           AlignmentView alview, String wsurl2)
   {
     super();
-    // this.alignFrame = alframe;
-    this.fromSplitFrame = alframe.getSplitViewContainer() != null;
+    this.alignFrame = alframe;
     currentView = alframe.getCurrentView().getAlignment();
     featureSettings = alframe.getFeatureRenderer().getSettings();
     defGapChar = alframe.getViewport().getGapCharacter();
@@ -385,8 +387,8 @@ public abstract class AWSThread extends Thread
     }
   }
 
-  protected boolean isFromSplitFrame()
+  protected AlignFrame getRequestingAlignFrame()
   {
-    return this.fromSplitFrame;
+    return this.alignFrame;
   }
 }
index 60694e3..0178130 100644 (file)
@@ -451,7 +451,7 @@ class MsaWSThread extends AWS2Thread implements WSClientI
    * @param presorder
    *          boolean
    */
-  MsaWSThread(MsaWS server, String wsUrl, WebserviceInfo wsinfo,
+  private MsaWSThread(MsaWS server, String wsUrl, WebserviceInfo wsinfo,
           jalview.gui.AlignFrame alFrame, AlignmentView alview,
           String wsname, boolean subgaps, boolean presorder)
   {
@@ -991,24 +991,35 @@ class MsaWSThread extends AWS2Thread implements WSClientI
      * If alignment was requested from one half of a SplitFrame, show in a
      * SplitFrame with the other pane similarly aligned.
      */
-    if (this.isFromSplitFrame())
+    AlignFrame requestedBy = getRequestingAlignFrame();
+    if (requestedBy != null && requestedBy.getSplitViewContainer() != null)
     {
-      // TODO will only work for protein, as it holds the codon frame mappings
-      // may need this thread to hold a reference to the requesting AlignFrame
-      AlignmentI complement = al.getAlignedComplement(al.getCodonFrames());
-      AlignFrame af2 = new AlignFrame(complement, AlignFrame.DEFAULT_WIDTH,
-              AlignFrame.DEFAULT_HEIGHT);
-      String linkedTitle = MessageManager
-              .getString("label.linked_view_title");
-      JInternalFrame splitFrame = new SplitFrame(al.isNucleotide() ? af
-              : af2, al.isNucleotide() ? af2 : af);
-      Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
-    }
-    else
-    {
-      Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
-              AlignFrame.DEFAULT_HEIGHT);
+      AlignmentI complement = requestedBy.getSplitViewContainer()
+              .getComplement(requestedBy);
+      String complementTitle = requestedBy.getSplitViewContainer()
+              .getComplementTitle(requestedBy);
+      AlignmentI copyComplement = new Alignment(complement);
+      copyComplement.alignAs(al);
+      if (copyComplement.getHeight() > 0)
+      {
+        af.setTitle(alTitle);
+        AlignFrame af2 = new AlignFrame(copyComplement,
+                AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+        af2.setTitle(complementTitle);
+        String linkedTitle = MessageManager
+                .getString("label.linked_view_title");
+        JInternalFrame splitFrame = new SplitFrame(al.isNucleotide() ? af
+                : af2, al.isNucleotide() ? af2 : af);
+        Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
+        return;
+      }
     }
+
+    /*
+     * Not from SplitFrame, or failed to created a complementary alignment
+     */
+    Desktop.addInternalFrame(af, alTitle, AlignFrame.DEFAULT_WIDTH,
+            AlignFrame.DEFAULT_HEIGHT);
   }
 
   /**
index c29658b..2711a36 100644 (file)
@@ -36,6 +36,7 @@ import jalview.util.MapList;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -186,9 +187,11 @@ public class AlignmentUtilsTests
     assertEquals(3, mapList.getFromRatio());
     assertEquals(1, mapList.getToRatio());
     assertTrue(Arrays.equals(new int[]
-    { 1, 9 }, mapList.getFromRanges()));
+    { 1, 9 }, mapList.getFromRanges().get(0)));
+    assertEquals(1, mapList.getFromRanges().size());
     assertTrue(Arrays.equals(new int[]
-    { 1, 3 }, mapList.getToRanges()));
+    { 1, 3 }, mapList.getToRanges().get(0)));
+    assertEquals(1, mapList.getToRanges().size());
 
     /*
      * Inspect mappings for Mouse protein
@@ -208,9 +211,11 @@ public class AlignmentUtilsTests
       assertEquals(3, mapList.getFromRatio());
       assertEquals(1, mapList.getToRatio());
       assertTrue(Arrays.equals(new int[]
-      { 1, 9 }, mapList.getFromRanges()));
+      { 1, 9 }, mapList.getFromRanges().get(0)));
+      assertEquals(1, mapList.getFromRanges().size());
       assertTrue(Arrays.equals(new int[]
-      { 1, 3 }, mapList.getToRanges()));
+      { 1, 3 }, mapList.getToRanges().get(0)));
+      assertEquals(1, mapList.getToRanges().size());
     }
   }
 
@@ -259,20 +264,26 @@ public class AlignmentUtilsTests
     Mapping[] protMappings = humanMapping.getProtMappings();
     // two mappings, both to cDNA with stop codon
     assertEquals(2, protMappings.length);
+
     MapList mapList = protMappings[0].getMap();
     assertEquals(3, mapList.getFromRatio());
     assertEquals(1, mapList.getToRatio());
     assertTrue(Arrays.equals(new int[]
-    { 1, 9 }, mapList.getFromRanges()));
+    { 1, 9 }, mapList.getFromRanges().get(0)));
+    assertEquals(1, mapList.getFromRanges().size());
     assertTrue(Arrays.equals(new int[]
-    { 1, 3 }, mapList.getToRanges()));
+    { 1, 3 }, mapList.getToRanges().get(0)));
+    assertEquals(1, mapList.getToRanges().size());
+
     mapList = protMappings[1].getMap();
     assertEquals(3, mapList.getFromRatio());
     assertEquals(1, mapList.getToRatio());
     assertTrue(Arrays.equals(new int[]
-    { 1, 9 }, mapList.getFromRanges()));
+    { 1, 9 }, mapList.getFromRanges().get(0)));
+    assertEquals(1, mapList.getFromRanges().size());
     assertTrue(Arrays.equals(new int[]
-    { 1, 3 }, mapList.getToRanges()));
+    { 1, 3 }, mapList.getToRanges().get(0)));
+    assertEquals(1, mapList.getToRanges().size());
 
     /*
      * Inspect mapping for Mouse protein - should map to 1st/3rd/5th cDNA seqs
@@ -296,27 +307,33 @@ public class AlignmentUtilsTests
     assertEquals(3, mapList.getFromRatio());
     assertEquals(1, mapList.getToRatio());
     assertTrue(Arrays.equals(new int[]
-    { 4, 12 }, mapList.getFromRanges()));
+    { 4, 12 }, mapList.getFromRanges().get(0)));
+    assertEquals(1, mapList.getFromRanges().size());
     assertTrue(Arrays.equals(new int[]
-    { 1, 3 }, mapList.getToRanges()));
+    { 1, 3 }, mapList.getToRanges().get(0)));
+    assertEquals(1, mapList.getToRanges().size());
 
     // second mapping to cDNA with stop codon
     mapList = protMappings[1].getMap();
     assertEquals(3, mapList.getFromRatio());
     assertEquals(1, mapList.getToRatio());
     assertTrue(Arrays.equals(new int[]
-    { 1, 9 }, mapList.getFromRanges()));
+    { 1, 9 }, mapList.getFromRanges().get(0)));
+    assertEquals(1, mapList.getFromRanges().size());
     assertTrue(Arrays.equals(new int[]
-    { 1, 3 }, mapList.getToRanges()));
+    { 1, 3 }, mapList.getToRanges().get(0)));
+    assertEquals(1, mapList.getToRanges().size());
 
     // third mapping to cDNA with start and stop codon
     mapList = protMappings[2].getMap();
     assertEquals(3, mapList.getFromRatio());
     assertEquals(1, mapList.getToRatio());
     assertTrue(Arrays.equals(new int[]
-    { 4, 12 }, mapList.getFromRanges()));
+    { 4, 12 }, mapList.getFromRanges().get(0)));
+    assertEquals(1, mapList.getFromRanges().size());
     assertTrue(Arrays.equals(new int[]
-    { 1, 3 }, mapList.getToRanges()));
+    { 1, 3 }, mapList.getToRanges().get(0)));
+    assertEquals(1, mapList.getToRanges().size());
   }
 
   /**
@@ -507,4 +524,48 @@ public class AlignmentUtilsTests
     assertEquals("---TGCCAT---TAC------CAG---", aligned.getSequenceAsString());
     assertSame(aligned.getDatasetSequence(), dna.getDatasetSequence());
   }
+
+  /**
+   * Test the method that realigns protein to match mapped codon alignment.
+   */
+  @Test
+  public void testAlignProteinAsDna()
+  {
+    // seq1 codons are [1,2,3] [4,5,6] [7,8,9] [10,11,12]
+    SequenceI dna1 = new Sequence("Seq1", "TGCCATTACCAG-");
+    // seq2 codons are [1,3,4] [5,6,7] [8,9,10] [11,12,13]
+    SequenceI dna2 = new Sequence("Seq2", "T-GCCATTACCAG");
+    // seq3 codons are [1,2,3] [4,5,7] [8,9,10] [11,12,13]
+    SequenceI dna3 = new Sequence("Seq3", "TGCCA-TTACCAG");
+    AlignmentI dna = new Alignment(new SequenceI[]
+    { dna1, dna2, dna3 });
+    dna.setDataset(null);
+
+    // protein alignment will be realigned like dna
+    SequenceI prot1 = new Sequence("Seq1", "CHYQ");
+    SequenceI prot2 = new Sequence("Seq2", "CHYQ");
+    SequenceI prot3 = new Sequence("Seq3", "CHYQ");
+    AlignmentI protein = new Alignment(new SequenceI[]
+    { prot1, prot2, prot3 });
+    protein.setDataset(null);
+
+    MapList map = new MapList(new int[]
+    { 1, 12 }, new int[]
+    { 1, 4 }, 3, 1);
+    AlignedCodonFrame acf = new AlignedCodonFrame();
+    acf.addMap(dna1.getDatasetSequence(), prot1.getDatasetSequence(), map);
+    acf.addMap(dna2.getDatasetSequence(), prot2.getDatasetSequence(), map);
+    acf.addMap(dna3.getDatasetSequence(), prot3.getDatasetSequence(), map);
+    protein.setCodonFrames(Collections.singleton(acf));
+
+    /*
+     * Translated codon order is [1,2,3] [1,3,4] [4,5,6] [4,5,7] [5,6,7] [7,8,9]
+     * [8,9,10] [10,11,12] [11,12,13]
+     */
+    AlignmentUtils.alignProteinAsDna(protein, dna);
+    assertEquals("C-H--Y-Q-", prot1.getSequenceAsString());
+    assertEquals("-C--H-Y-Q", prot2.getSequenceAsString());
+    assertEquals("C--H--Y-Q", prot3.getSequenceAsString());
+  }
+
 }
diff --git a/test/jalview/datamodel/AlignedCodonIteratorTest.java b/test/jalview/datamodel/AlignedCodonIteratorTest.java
new file mode 100644 (file)
index 0000000..671c51d
--- /dev/null
@@ -0,0 +1,135 @@
+package jalview.datamodel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+import jalview.util.MapList;
+
+import java.util.Iterator;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for Mapping$AlignedCodonIterator
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class AlignedCodonIteratorTest
+{
+  /**
+   * Test normal case for iterating over aligned codons.
+   */
+  @Test
+  public void testNext()
+  {
+    SequenceI from = new Sequence("Seq1", "-CgC-C-cCtAG-AtG-Gc");
+    from.createDatasetSequence();
+    SequenceI to = new Sequence("Seq1", "-PQ-R-");
+    to.createDatasetSequence();
+    MapList map = new MapList(new int[]
+    { 1, 1, 3, 4, 6, 6, 8, 10, 12, 13 }, new int[]
+    { 1, 3 }, 3, 1);
+    Mapping m = new Mapping(to.getDatasetSequence(), map);
+
+    Iterator<AlignedCodon> codons = m.getCodonIterator(from, '-');
+    AlignedCodon codon = codons.next();
+    assertEquals("[1, 3, 5]", codon.toString());
+    assertEquals("P", codon.product);
+    codon = codons.next();
+    assertEquals("[8, 10, 11]", codon.toString());
+    assertEquals("Q", codon.product);
+    codon = codons.next();
+    assertEquals("[13, 15, 17]", codon.toString());
+    assertEquals("R", codon.product);
+    assertFalse(codons.hasNext());
+  }
+
+  /**
+   * Test weird case where the mapping skips over a peptide.
+   */
+  @Test
+  public void testNext_unmappedPeptide()
+  {
+    SequenceI from = new Sequence("Seq1", "-CgC-C-cCtAG-AtG-Gc");
+    from.createDatasetSequence();
+    SequenceI to = new Sequence("Seq1", "-PQ-TR-");
+    to.createDatasetSequence();
+    MapList map = new MapList(new int[]
+    { 1, 1, 3, 4, 6, 6, 8, 10, 12, 13 }, new int[]
+    { 1, 2, 4, 4 }, 3, 1);
+    Mapping m = new Mapping(to.getDatasetSequence(), map);
+
+    Iterator<AlignedCodon> codons = m.getCodonIterator(from, '-');
+    AlignedCodon codon = codons.next();
+    assertEquals("[1, 3, 5]", codon.toString());
+    assertEquals("P", codon.product);
+    codon = codons.next();
+    assertEquals("[8, 10, 11]", codon.toString());
+    assertEquals("Q", codon.product);
+    codon = codons.next();
+    assertEquals("[13, 15, 17]", codon.toString());
+    assertEquals("R", codon.product);
+    assertFalse(codons.hasNext());
+  }
+  
+  /**
+   * Test for exception thrown for an incomplete codon.
+   */
+  @Test
+  public void testNext_incompleteCodon()
+  {
+    SequenceI from = new Sequence("Seq1", "-CgC-C-cCgTt");
+    from.createDatasetSequence();
+    SequenceI to = new Sequence("Seq1", "-PQ-R-");
+    to.createDatasetSequence();
+    MapList map = new MapList(new int[]
+    { 1, 1, 3, 4, 6, 6, 8, 8 }, new int[]
+    { 1, 3 }, 3, 1);
+    Mapping m = new Mapping(to.getDatasetSequence(), map);
+
+    Iterator<AlignedCodon> codons = m.getCodonIterator(from, '-');
+    AlignedCodon codon = codons.next();
+    assertEquals("[1, 3, 5]", codon.toString());
+    assertEquals("P", codon.product);
+    try
+    {
+      codon = codons.next();
+      fail("expected exception");
+    } catch (IncompleteCodonException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Test normal case for iterating over aligned codons.
+   */
+  @Test
+  public void testAnother()
+  {
+    SequenceI from = new Sequence("Seq1", "TGCCATTACCAG-");
+    from.createDatasetSequence();
+    SequenceI to = new Sequence("Seq1", "CHYQ");
+    to.createDatasetSequence();
+    MapList map = new MapList(new int[]
+    { 1, 12 }, new int[]
+    { 1, 4 }, 3, 1);
+    Mapping m = new Mapping(to.getDatasetSequence(), map);
+  
+    Iterator<AlignedCodon> codons = m.getCodonIterator(from, '-');
+    AlignedCodon codon = codons.next();
+    assertEquals("[0, 1, 2]", codon.toString());
+    assertEquals("C", codon.product);
+    codon = codons.next();
+    assertEquals("[3, 4, 5]", codon.toString());
+    assertEquals("H", codon.product);
+    codon = codons.next();
+    assertEquals("[6, 7, 8]", codon.toString());
+    assertEquals("Y", codon.product);
+    codon = codons.next();
+    assertEquals("[9, 10, 11]", codon.toString());
+    assertEquals("Q", codon.product);
+    assertFalse(codons.hasNext());
+  }
+}
index 3738614..15b0314 100644 (file)
@@ -12,13 +12,13 @@ public class SearchResultsTest
   {
     SequenceI seq = new Sequence("", "abcdefghijklm");
     SearchResults sr = new SearchResults();
-    sr.addResult(seq, 0, 0);
+    sr.addResult(seq, 1, 1);
     assertEquals("a", sr.toString());
-    sr.addResult(seq, 2, 4);
+    sr.addResult(seq, 3, 5);
     assertEquals("acde", sr.toString());
 
     seq = new Sequence("", "pqrstuvwxy");
-    sr.addResult(seq, 5, 6);
+    sr.addResult(seq, 6, 7);
     assertEquals("acdeuv", sr.toString());
   }
 }
index 1913a70..f69fe40 100644 (file)
@@ -434,9 +434,9 @@ public class MapListTest
     assertEquals(18, ml.getFromHighest());
     assertEquals(1, ml.getToLowest());
     assertEquals(6, ml.getToHighest());
-    assertEquals("[2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 16, 18]",
-            Arrays.toString(ml.getFromRanges()));
-    assertEquals("[1, 1, 3, 4, 6, 6]", Arrays.toString(ml.getToRanges()));
+    assertEquals("{[2, 3], [5, 7], [9, 10], [12, 12], [14, 14], [16, 18]}",
+            prettyPrint(ml.getFromRanges()));
+    assertEquals("{[1, 1], [3, 4], [6, 6]}", prettyPrint(ml.getToRanges()));
 
     /*
      * Also copy constructor
@@ -447,9 +447,33 @@ public class MapListTest
     assertEquals(18, ml2.getFromHighest());
     assertEquals(1, ml2.getToLowest());
     assertEquals(6, ml2.getToHighest());
-    assertEquals("[2, 3, 5, 7, 9, 10, 12, 12, 14, 14, 16, 18]",
-            Arrays.toString(ml2.getFromRanges()));
-    assertEquals("[1, 1, 3, 4, 6, 6]", Arrays.toString(ml2.getToRanges()));
+    assertEquals("{[2, 3], [5, 7], [9, 10], [12, 12], [14, 14], [16, 18]}",
+            prettyPrint(ml2.getFromRanges()));
+    assertEquals("{[1, 1], [3, 4], [6, 6]}", prettyPrint(ml2.getToRanges()));
+  }
+
+  /**
+   * Convert a List of {[i, j], [k, l], ...} to "[[i, j], [k, l], ...]"
+   * 
+   * @param ranges
+   * @return
+   */
+  private String prettyPrint(List<int[]> ranges)
+  {
+    StringBuilder sb = new StringBuilder(ranges.size() * 5);
+    boolean first = true;
+    sb.append("{");
+    for (int[] range : ranges)
+    {
+      if (!first)
+      {
+        sb.append(", ");
+      }
+      sb.append(Arrays.toString(range));
+      first = false;
+    }
+    sb.append("}");
+    return sb.toString();
   }
 
   /**
@@ -469,9 +493,9 @@ public class MapListTest
     assertEquals(ml.getFromRatio(), ml2.getToRatio());
     assertEquals(ml.getToHighest(), ml2.getFromHighest());
     assertEquals(ml.getFromHighest(), ml2.getToHighest());
-    assertEquals(Arrays.toString(ml.getFromRanges()),
-            Arrays.toString(ml2.getToRanges()));
-    assertEquals(Arrays.toString(ml.getToRanges()),
-            Arrays.toString(ml2.getFromRanges()));
+    assertEquals(prettyPrint(ml.getFromRanges()),
+            prettyPrint(ml2.getToRanges()));
+    assertEquals(prettyPrint(ml.getToRanges()),
+            prettyPrint(ml2.getFromRanges()));
   }
 }