JAL-1925 update source version in license
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
index 6f0125d..f2262fb 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9.0b2)
+ * Copyright (C) 2015 The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
  */
 package jalview.analysis;
 
+import jalview.datamodel.AlignedCodon;
+import jalview.datamodel.AlignedCodonFrame;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.FeatureProperties;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.ResidueProperties;
+import jalview.util.DBRefUtils;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -27,24 +45,13 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeMap;
 
-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.SequenceGroup;
-import jalview.datamodel.SequenceI;
-import jalview.schemes.ResidueProperties;
-import jalview.util.MapList;
-
 /**
  * grab bag of useful alignment manipulation operations Expect these to be
  * refactored elsewhere at some point.
@@ -70,18 +77,22 @@ public class AlignmentUtils
     for (SequenceI s : core.getSequences())
     {
       SequenceI newSeq = s.deriveSequence();
-      if (newSeq.getStart() > maxoffset
+      final int newSeqStart = newSeq.getStart() - 1;
+      if (newSeqStart > maxoffset
               && newSeq.getDatasetSequence().getStart() < s.getStart())
       {
-        maxoffset = newSeq.getStart();
+        maxoffset = newSeqStart;
       }
       sq.add(newSeq);
     }
     if (flankSize > -1)
     {
-      maxoffset = flankSize;
+      maxoffset = Math.min(maxoffset, flankSize);
     }
-    // now add offset to create a new expanded alignment
+
+    /*
+     * now add offset left and right to create an expanded alignment
+     */
     for (SequenceI s : sq)
     {
       SequenceI ds = s;
@@ -91,8 +102,8 @@ public class AlignmentUtils
       }
       int s_end = s.findPosition(s.getStart() + s.getLength());
       // find available flanking residues for sequence
-      int ustream_ds = s.getStart() - ds.getStart(), dstream_ds = ds
-              .getEnd() - s_end;
+      int ustream_ds = s.getStart() - ds.getStart();
+      int dstream_ds = ds.getEnd() - s_end;
 
       // build new flanked sequence
 
@@ -108,27 +119,27 @@ public class AlignmentUtils
           offset = maxoffset - flankSize;
           ustream_ds = flankSize;
         }
-        if (flankSize < dstream_ds)
+        if (flankSize <= dstream_ds)
         {
-          dstream_ds = flankSize;
+          dstream_ds = flankSize - 1;
         }
       }
+      // 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();
-      char[] downstream = new String(ds.getSequence(s_end - 1, s_end + 1
+      char[] downstream = new String(ds.getSequence(s_end - 1, s_end
               + dstream_ds)).toLowerCase().toCharArray();
       char[] coreseq = s.getSequence();
       char[] nseq = new char[offset + upstream.length + downstream.length
               + coreseq.length];
       char c = core.getGapCharacter();
-      // TODO could lowercase the flanking regions
+
       int p = 0;
       for (; p < offset; p++)
       {
         nseq[p] = c;
       }
-      // s.setSequence(new String(upstream).toLowerCase()+new String(coreseq) +
-      // new String(downstream).toLowerCase());
+
       System.arraycopy(upstream, 0, nseq, p, upstream.length);
       System.arraycopy(coreseq, 0, nseq, p + upstream.length,
               coreseq.length);
@@ -146,6 +157,7 @@ public class AlignmentUtils
       {
         for (AlignmentAnnotation aa : s.getAnnotation())
         {
+          aa.adjustForAlignment(); // JAL-1712 fix
           newAl.addAnnotation(aa);
         }
       }
@@ -216,9 +228,8 @@ public class AlignmentUtils
    * @param cdnaAlignment
    * @return
    */
-  public static boolean mapProteinToCdna(
-          final AlignmentI proteinAlignment,
-          final AlignmentI cdnaAlignment)
+  public static boolean mapProteinAlignmentToCdna(
+          final AlignmentI proteinAlignment, final AlignmentI cdnaAlignment)
   {
     if (proteinAlignment == null || cdnaAlignment == null)
     {
@@ -264,7 +275,7 @@ public class AlignmentUtils
           final AlignmentI cdnaAlignment, Set<SequenceI> mappedDna,
           Set<SequenceI> mappedProtein, boolean xrefsOnly)
   {
-    boolean mappingPerformed = false;
+    boolean mappingExistsOrAdded = false;
     List<SequenceI> thisSeqs = proteinAlignment.getSequences();
     for (SequenceI aaSeq : thisSeqs)
     {
@@ -282,7 +293,7 @@ public class AlignmentUtils
          * many-to-many, as that would risk mixing species with similar cDNA
          * sequences.
          */
-        if (xrefsOnly && !CrossRef.haveCrossRef(aaSeq, cdnaSeq))
+        if (xrefsOnly && !AlignmentUtils.haveCrossRef(aaSeq, cdnaSeq))
         {
           continue;
         }
@@ -297,14 +308,18 @@ public class AlignmentUtils
         {
           continue;
         }
-        if (!mappingExists(proteinAlignment.getCodonFrames(),
+        if (mappingExists(proteinAlignment.getCodonFrames(),
                 aaSeq.getDatasetSequence(), cdnaSeq.getDatasetSequence()))
         {
-          MapList map = mapProteinToCdna(aaSeq, cdnaSeq);
+          mappingExistsOrAdded = true;
+        }
+        else
+        {
+          MapList map = mapProteinSequenceToCdna(aaSeq, cdnaSeq);
           if (map != null)
           {
             acf.addMap(cdnaSeq, aaSeq, map);
-            mappingPerformed = true;
+            mappingExistsOrAdded = true;
             proteinMapped = true;
             mappedDna.add(cdnaSeq);
             mappedProtein.add(aaSeq);
@@ -316,7 +331,7 @@ public class AlignmentUtils
         proteinAlignment.addCodonFrame(acf);
       }
     }
-    return mappingPerformed;
+    return mappingExistsOrAdded;
   }
 
   /**
@@ -349,7 +364,7 @@ public class AlignmentUtils
    * @param cdnaSeq
    * @return
    */
-  public static MapList mapProteinToCdna(SequenceI proteinSeq,
+  public static MapList mapProteinSequenceToCdna(SequenceI proteinSeq,
           SequenceI cdnaSeq)
   {
     /*
@@ -373,10 +388,10 @@ public class AlignmentUtils
      */
     final int mappedLength = 3 * aaSeqChars.length;
     int cdnaLength = cdnaSeqChars.length;
-    int cdnaStart = 1;
-    int cdnaEnd = cdnaLength;
-    final int proteinStart = 1;
-    final int proteinEnd = aaSeqChars.length;
+    int cdnaStart = cdnaSeq.getStart();
+    int cdnaEnd = cdnaSeq.getEnd();
+    final int proteinStart = proteinSeq.getStart();
+    final int proteinEnd = proteinSeq.getEnd();
 
     /*
      * If lengths don't match, try ignoring stop codon.
@@ -399,12 +414,13 @@ public class AlignmentUtils
     /*
      * If lengths still don't match, try ignoring start codon.
      */
+    int startOffset = 0;
     if (cdnaLength != mappedLength
             && cdnaLength > 2
             && String.valueOf(cdnaSeqChars, 0, 3).toUpperCase()
-                    .equals(
-                    ResidueProperties.START))
+                    .equals(ResidueProperties.START))
     {
+      startOffset += 3;
       cdnaStart += 3;
       cdnaLength -= 3;
     }
@@ -413,13 +429,12 @@ public class AlignmentUtils
     {
       return null;
     }
-    if (!translatesAs(cdnaSeqChars, cdnaStart - 1, aaSeqChars))
+    if (!translatesAs(cdnaSeqChars, startOffset, aaSeqChars))
     {
       return null;
     }
-    MapList map = new MapList(new int[]
-    { cdnaStart, cdnaEnd }, new int[]
-    { proteinStart, proteinEnd }, 3, 1);
+    MapList map = new MapList(new int[] { cdnaStart, cdnaEnd }, new int[] {
+        proteinStart, proteinEnd }, 3, 1);
     return map;
   }
 
@@ -436,23 +451,26 @@ public class AlignmentUtils
   protected static boolean translatesAs(char[] cdnaSeqChars, int cdnaStart,
           char[] aaSeqChars)
   {
+    if (cdnaSeqChars == null || aaSeqChars == null)
+    {
+      return false;
+    }
+
     int aaResidue = 0;
     for (int i = cdnaStart; i < cdnaSeqChars.length - 2
             && aaResidue < aaSeqChars.length; i += 3, aaResidue++)
     {
       String codon = String.valueOf(cdnaSeqChars, i, 3);
-      final String translated = ResidueProperties.codonTranslate(
-              codon);
+      final String translated = ResidueProperties.codonTranslate(codon);
       /*
-       * ? allow X in protein to match untranslatable in dna ?
+       * allow * in protein to match untranslatable in dna
        */
       final char aaRes = aaSeqChars[aaResidue];
-      if ((translated == null || "STOP".equals(translated)) && aaRes == 'X')
+      if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
       {
         continue;
       }
-      if (translated == null
-              || !(aaRes == translated.charAt(0)))
+      if (translated == null || !(aaRes == translated.charAt(0)))
       {
         // debug
         // System.out.println(("Mismatch at " + i + "/" + aaResidue + ": "
@@ -483,7 +501,8 @@ public class AlignmentUtils
           boolean preserveUnmappedGaps)
   {
     /*
-     * Get any mappings from the source alignment to the target (dataset) sequence.
+     * Get any mappings from the source alignment to the target (dataset)
+     * sequence.
      */
     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
     // all mappings. Would it help to constrain this?
@@ -492,7 +511,7 @@ public class AlignmentUtils
     {
       return false;
     }
-  
+
     /*
      * Locate the aligned source sequence whose dataset sequence is mapped. We
      * just take the first match here (as we can't align cDNA like more than one
@@ -509,7 +528,7 @@ public class AlignmentUtils
         break;
       }
     }
-  
+
     if (alignFrom == null)
     {
       return false;
@@ -534,15 +553,15 @@ public class AlignmentUtils
    * @param preserveMappedGaps
    */
   public static void alignSequenceAs(SequenceI alignTo,
-          SequenceI alignFrom,
-          AlignedCodonFrame mapping, String myGap, char sourceGap,
-          boolean preserveMappedGaps, boolean preserveUnmappedGaps)
+          SequenceI alignFrom, AlignedCodonFrame mapping, String myGap,
+          char sourceGap, boolean preserveMappedGaps,
+          boolean preserveUnmappedGaps)
   {
     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
     final char[] thisSeq = alignTo.getSequence();
     final char[] thatAligned = alignFrom.getSequence();
     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
-  
+
     // aligned and dataset sequence positions, all base zero
     int thisSeqPos = 0;
     int sourceDsPos = 0;
@@ -554,6 +573,8 @@ public class AlignmentUtils
     /*
      * Traverse the aligned protein sequence.
      */
+    int fromOffset = alignFrom.getStart() - 1;
+    int toOffset = alignTo.getStart() - 1;
     int sourceGapMappedLength = 0;
     boolean inExon = false;
     for (char sourceChar : thatAligned)
@@ -570,7 +591,7 @@ public class AlignmentUtils
       sourceDsPos++;
       // Note mapping positions are base 1, our sequence positions base 0
       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
-              sourceDsPos);
+              sourceDsPos + fromOffset);
       if (mappedPos == null)
       {
         /*
@@ -594,14 +615,15 @@ public class AlignmentUtils
        * But then 'align dna as protein' doesn't make much sense otherwise.
        */
       int intronLength = 0;
-      while (basesWritten < mappedCodonEnd && thisSeqPos < thisSeq.length)
+      while (basesWritten + toOffset < mappedCodonEnd
+              && thisSeqPos < thisSeq.length)
       {
         final char c = thisSeq[thisSeqPos++];
         if (c != myGapChar)
         {
           basesWritten++;
-
-          if (basesWritten < mappedCodonStart)
+          int sourcePosition = basesWritten + toOffset;
+          if (sourcePosition < mappedCodonStart)
           {
             /*
              * Found an unmapped (intron) base. First add in any preceding gaps
@@ -618,7 +640,7 @@ public class AlignmentUtils
           }
           else
           {
-            final boolean startOfCodon = basesWritten == mappedCodonStart;
+            final boolean startOfCodon = sourcePosition == mappedCodonStart;
             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
                     trailingCopiedGap.length(), intronLength, startOfCodon);
@@ -679,8 +701,8 @@ public class AlignmentUtils
    */
   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
           boolean preserveUnmappedGaps, int sourceGapMappedLength,
-          boolean inExon, int trailingGapLength,
-          int intronLength, final boolean startOfCodon)
+          boolean inExon, int trailingGapLength, int intronLength,
+          final boolean startOfCodon)
   {
     int gapsToAdd = 0;
     if (startOfCodon)
@@ -804,8 +826,8 @@ public class AlignmentUtils
       // mapping is from protein to nucleotide
       toDna = true;
       // should ideally get gap count ratio from mapping
-      gap = String.valueOf(new char[]
-      { gapCharacter, gapCharacter, gapCharacter });
+      gap = String.valueOf(new char[] { gapCharacter, gapCharacter,
+          gapCharacter });
     }
     else
     {
@@ -850,7 +872,7 @@ public class AlignmentUtils
           {
             mapping.markMappedRegion(seq, pos, sr);
           }
-          newseq.append(sr.toString());
+          newseq.append(sr.getCharacters());
           if (first)
           {
             first = false;
@@ -884,6 +906,9 @@ public class AlignmentUtils
    */
   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
   {
+    List<SequenceI> unmappedProtein = new ArrayList<SequenceI>();
+    unmappedProtein.addAll(protein.getSequences());
+
     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
 
     /*
@@ -904,10 +929,11 @@ public class AlignmentUtils
         {
           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
                   seqMap, alignedCodons);
+          unmappedProtein.remove(prot);
         }
       }
     }
-    return alignProteinAs(protein, alignedCodons);
+    return alignProteinAs(protein, alignedCodons, unmappedProtein);
   }
 
   /**
@@ -918,10 +944,12 @@ public class AlignmentUtils
    * @param alignedCodons
    *          an ordered map of codon positions (columns), with sequence/peptide
    *          values present in each column
+   * @param unmappedProtein
    * @return
    */
   protected static int alignProteinAs(AlignmentI protein,
-          Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
+          Map<AlignedCodon, Map<SequenceI, String>> alignedCodons,
+          List<SequenceI> unmappedProtein)
   {
     /*
      * Prefill aligned sequences with gaps before inserting aligned protein
@@ -933,15 +961,18 @@ public class AlignmentUtils
     String allGaps = String.valueOf(gaps);
     for (SequenceI seq : protein.getSequences())
     {
-      seq.setSequence(allGaps);
+      if (!unmappedProtein.contains(seq))
+      {
+        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())
+      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);
@@ -968,8 +999,7 @@ public class AlignmentUtils
    *          the map we are building up
    */
   static void addCodonPositions(SequenceI dna, SequenceI protein,
-          char gapChar,
-          Mapping seqMap,
+          char gapChar, Mapping seqMap,
           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
   {
     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
@@ -1004,6 +1034,11 @@ public class AlignmentUtils
    */
   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
   {
+    if (al1 == null || al2 == null)
+    {
+      return false;
+    }
+
     /*
      * Require one nucleotide and one protein
      */
@@ -1036,18 +1071,26 @@ public class AlignmentUtils
    * @param mappings
    * @return
    */
-  public static boolean isMappable(SequenceI dnaSeq, SequenceI proteinSeq,
-          Set<AlignedCodonFrame> mappings)
+  protected static boolean isMappable(SequenceI dnaSeq,
+          SequenceI proteinSeq, Set<AlignedCodonFrame> mappings)
   {
-    SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq.getDatasetSequence();
+    if (dnaSeq == null || proteinSeq == null)
+    {
+      return false;
+    }
+
+    SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq
+            .getDatasetSequence();
     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
             : proteinSeq.getDatasetSequence();
-    
+
     /*
      * Already mapped?
      */
-    for (AlignedCodonFrame mapping : mappings) {
-      if ( proteinDs == mapping.getAaForDnaSeq(dnaDs)) {
+    for (AlignedCodonFrame mapping : mappings)
+    {
+      if (proteinDs == mapping.getAaForDnaSeq(dnaDs))
+      {
         return true;
       }
     }
@@ -1056,7 +1099,7 @@ public class AlignmentUtils
      * Just try to make a mapping (it is not yet stored), test whether
      * successful.
      */
-    return mapProteinToCdna(proteinDs, dnaDs) != null;
+    return mapProteinSequenceToCdna(proteinDs, dnaDs) != null;
   }
 
   /**
@@ -1075,7 +1118,8 @@ public class AlignmentUtils
    *          the alignment to check for presence of annotations
    */
   public static void findAddableReferenceAnnotations(
-          List<SequenceI> sequenceScope, Map<String, String> labelForCalcId,
+          List<SequenceI> sequenceScope,
+          Map<String, String> labelForCalcId,
           final Map<SequenceI, List<AlignmentAnnotation>> candidates,
           AlignmentI al)
   {
@@ -1083,7 +1127,7 @@ public class AlignmentUtils
     {
       return;
     }
-  
+
     /*
      * For each sequence in scope, make a list of any annotations on the
      * underlying dataset sequence which are not already on the alignment.
@@ -1111,8 +1155,7 @@ public class AlignmentUtils
          * sequence.
          */
         final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
-                .findAnnotations(seq, dsann.getCalcId(),
-                        dsann.label);
+                .findAnnotations(seq, dsann.getCalcId(), dsann.label);
         if (!matchedAlignmentAnnotations.iterator().hasNext())
         {
           result.add(dsann);
@@ -1160,7 +1203,7 @@ public class AlignmentUtils
           endRes = selectionGroup.getEndRes();
         }
         copyAnn.restrict(startRes, endRes);
-  
+
         /*
          * Add to the sequence (sets copyAnn.datasetSequence), unless the
          * original annotation is already on the sequence.
@@ -1198,8 +1241,7 @@ public class AlignmentUtils
           Collection<String> types, List<SequenceI> forSequences,
           boolean anyType, boolean doShow)
   {
-    for (AlignmentAnnotation aa : al
-            .getAlignmentAnnotation())
+    for (AlignmentAnnotation aa : al.getAlignmentAnnotation())
     {
       if (anyType || types.contains(aa.label))
       {
@@ -1212,4 +1254,180 @@ public class AlignmentUtils
       }
     }
   }
+
+  /**
+   * Returns true if either sequence has a cross-reference to the other
+   * 
+   * @param seq1
+   * @param seq2
+   * @return
+   */
+  public static boolean haveCrossRef(SequenceI seq1, SequenceI seq2)
+  {
+    // Note: moved here from class CrossRef as the latter class has dependencies
+    // not availability to the applet's classpath
+    return hasCrossRef(seq1, seq2) || hasCrossRef(seq2, seq1);
+  }
+
+  /**
+   * Returns true if seq1 has a cross-reference to seq2. Currently this assumes
+   * that sequence name is structured as Source|AccessionId.
+   * 
+   * @param seq1
+   * @param seq2
+   * @return
+   */
+  public static boolean hasCrossRef(SequenceI seq1, SequenceI seq2)
+  {
+    if (seq1 == null || seq2 == null)
+    {
+      return false;
+    }
+    String name = seq2.getName();
+    final DBRefEntry[] xrefs = seq1.getDBRef();
+    if (xrefs != null)
+    {
+      for (DBRefEntry xref : xrefs)
+      {
+        String xrefName = xref.getSource() + "|" + xref.getAccessionId();
+        // case-insensitive test, consistent with DBRefEntry.equalRef()
+        if (xrefName.equalsIgnoreCase(name))
+        {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Constructs an alignment consisting of the mapped exon regions in the given
+   * nucleotide sequences, and updates mappings to match.
+   * 
+   * @param dna
+   *          aligned dna sequences
+   * @param mappings
+   *          from dna to protein; these are replaced with new mappings
+   * @return an alignment whose sequences are the exon-only parts of the dna
+   *         sequences (or null if no exons are found)
+   */
+  public static AlignmentI makeExonAlignment(SequenceI[] dna,
+          Set<AlignedCodonFrame> mappings)
+  {
+    Set<AlignedCodonFrame> newMappings = new LinkedHashSet<AlignedCodonFrame>();
+    List<SequenceI> exonSequences = new ArrayList<SequenceI>();
+
+    for (SequenceI dnaSeq : dna)
+    {
+      final SequenceI ds = dnaSeq.getDatasetSequence();
+      List<AlignedCodonFrame> seqMappings = MappingUtils
+              .findMappingsForSequence(ds, mappings);
+      for (AlignedCodonFrame acf : seqMappings)
+      {
+        AlignedCodonFrame newMapping = new AlignedCodonFrame();
+        final List<SequenceI> mappedExons = makeExonSequences(ds, acf,
+                newMapping);
+        if (!mappedExons.isEmpty())
+        {
+          exonSequences.addAll(mappedExons);
+          newMappings.add(newMapping);
+        }
+      }
+    }
+    AlignmentI al = new Alignment(
+            exonSequences.toArray(new SequenceI[exonSequences.size()]));
+    al.setDataset(null);
+
+    /*
+     * Replace the old mappings with the new ones
+     */
+    mappings.clear();
+    mappings.addAll(newMappings);
+
+    return al;
+  }
+
+  /**
+   * Helper method to make exon-only sequences and populate their mappings to
+   * protein products
+   * <p>
+   * For example, if ggCCaTTcGAg has mappings [3, 4, 6, 7, 9, 10] to protein
+   * then generate a sequence CCTTGA with mapping [1, 6] to the same protein
+   * residues
+   * <p>
+   * Typically eukaryotic dna will include exons encoding for a single peptide
+   * sequence i.e. return a single result. Bacterial dna may have overlapping
+   * exon mappings coding for multiple peptides so return multiple results
+   * (example EMBL KF591215).
+   * 
+   * @param dnaSeq
+   *          a dna dataset sequence
+   * @param mapping
+   *          containing one or more mappings of the sequence to protein
+   * @param newMapping
+   *          the new mapping to populate, from the exon-only sequences to their
+   *          mapped protein sequences
+   * @return
+   */
+  protected static List<SequenceI> makeExonSequences(SequenceI dnaSeq,
+          AlignedCodonFrame mapping, AlignedCodonFrame newMapping)
+  {
+    List<SequenceI> exonSequences = new ArrayList<SequenceI>();
+    List<Mapping> seqMappings = mapping.getMappingsForSequence(dnaSeq);
+    final char[] dna = dnaSeq.getSequence();
+    for (Mapping seqMapping : seqMappings)
+    {
+      StringBuilder newSequence = new StringBuilder(dnaSeq.getLength());
+
+      /*
+       * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
+       */
+      final List<int[]> dnaExonRanges = seqMapping.getMap().getFromRanges();
+      for (int[] range : dnaExonRanges)
+      {
+        for (int pos = range[0]; pos <= range[1]; pos++)
+        {
+          newSequence.append(dna[pos - 1]);
+        }
+      }
+
+      SequenceI exon = new Sequence(dnaSeq.getName(),
+              newSequence.toString());
+
+      /*
+       * Locate any xrefs to CDS database on the protein product and attach to
+       * the CDS sequence. Also add as a sub-token of the sequence name.
+       */
+      // default to "CDS" if we can't locate an actual gene id
+      String cdsAccId = FeatureProperties
+              .getCodingFeature(DBRefSource.EMBL);
+      DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(seqMapping.getTo()
+              .getDBRef(), DBRefSource.CODINGDBS);
+      if (cdsRefs != null)
+      {
+        for (DBRefEntry cdsRef : cdsRefs)
+        {
+          exon.addDBRef(new DBRefEntry(cdsRef));
+          cdsAccId = cdsRef.getAccessionId();
+        }
+      }
+      exon.setName(exon.getName() + "|" + cdsAccId);
+      exon.createDatasetSequence();
+
+      /*
+       * Build new mappings - from the same protein regions, but now to
+       * contiguous exons
+       */
+      List<int[]> exonRange = new ArrayList<int[]>();
+      exonRange.add(new int[] { 1, newSequence.length() });
+      MapList map = new MapList(exonRange, seqMapping.getMap()
+              .getToRanges(), 3, 1);
+      newMapping.addMap(exon.getDatasetSequence(), seqMapping.getTo(), map);
+      MapList cdsToDnaMap = new MapList(dnaExonRanges, exonRange, 1, 1);
+      newMapping.addMap(dnaSeq, exon.getDatasetSequence(), cdsToDnaMap);
+
+      exonSequences.add(exon);
+    }
+    return exonSequences;
+  }
 }