mungo merge
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
index da5bc2f..db69823 100644 (file)
@@ -28,27 +28,35 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.FeatureProperties;
+import jalview.datamodel.IncompleteCodonException;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.io.gff.SequenceOntologyFactory;
+import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
+import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
+import jalview.util.StringUtils;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 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.NoSuchElementException;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -315,7 +323,7 @@ public class AlignmentUtils
         }
         else
         {
-          MapList map = mapProteinSequenceToCdna(aaSeq, cdnaSeq);
+          MapList map = mapCdnaToProtein(aaSeq, cdnaSeq);
           if (map != null)
           {
             acf.addMap(cdnaSeq, aaSeq, map);
@@ -338,12 +346,12 @@ public class AlignmentUtils
    * Answers true if the mappings include one between the given (dataset)
    * sequences.
    */
-  public static boolean mappingExists(Set<AlignedCodonFrame> set,
+  public static boolean mappingExists(List<AlignedCodonFrame> mappings,
           SequenceI aaSeq, SequenceI cdnaSeq)
   {
-    if (set != null)
+    if (mappings != null)
     {
-      for (AlignedCodonFrame acf : set)
+      for (AlignedCodonFrame acf : mappings)
       {
         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
         {
@@ -355,16 +363,22 @@ public class AlignmentUtils
   }
 
   /**
-   * Build a mapping (if possible) of a protein to a cDNA sequence. The cDNA
-   * must be three times the length of the protein, possibly after ignoring
-   * start and/or stop codons, and must translate to the protein. Returns null
-   * if no mapping is determined.
+   * Builds a mapping (if possible) of a cDNA to a protein sequence.
+   * <ul>
+   * <li>first checks if the cdna translates exactly to the protein sequence</li>
+   * <li>else checks for translation after removing a STOP codon</li>
+   * <li>else checks for translation after removing a START codon</li>
+   * <li>if that fails, inspect CDS features on the cDNA sequence</li>
+   * </ul>
+   * Returns null if no mapping is determined.
    * 
-   * @param proteinSeqs
+   * @param proteinSeq
+   *          the aligned protein sequence
    * @param cdnaSeq
+   *          the aligned cdna sequence
    * @return
    */
-  public static MapList mapProteinSequenceToCdna(SequenceI proteinSeq,
+  public static MapList mapCdnaToProtein(SequenceI proteinSeq,
           SequenceI cdnaSeq)
   {
     /*
@@ -394,7 +408,7 @@ public class AlignmentUtils
     final int proteinEnd = proteinSeq.getEnd();
 
     /*
-     * If lengths don't match, try ignoring stop codon.
+     * If lengths don't match, try ignoring stop codon (if present)
      */
     if (cdnaLength != mappedLength && cdnaLength > 2)
     {
@@ -425,17 +439,20 @@ public class AlignmentUtils
       cdnaLength -= 3;
     }
 
-    if (cdnaLength != mappedLength)
+    if (translatesAs(cdnaSeqChars, startOffset, aaSeqChars))
     {
-      return null;
-    }
-    if (!translatesAs(cdnaSeqChars, startOffset, aaSeqChars))
-    {
-      return null;
+      /*
+       * protein is translation of dna (+/- start/stop codons)
+       */
+      MapList map = new MapList(new int[] { cdnaStart, cdnaEnd }, new int[]
+      { proteinStart, proteinEnd }, 3, 1);
+      return map;
     }
-    MapList map = new MapList(new int[] { cdnaStart, cdnaEnd }, new int[] {
-        proteinStart, proteinEnd }, 3, 1);
-    return map;
+
+    /*
+     * translation failed - try mapping CDS annotated regions of dna
+     */
+    return mapCdsToProtein(cdnaSeq, proteinSeq);
   }
 
   /**
@@ -456,16 +473,18 @@ public class AlignmentUtils
       return false;
     }
 
-    int aaResidue = 0;
-    for (int i = cdnaStart; i < cdnaSeqChars.length - 2
-            && aaResidue < aaSeqChars.length; i += 3, aaResidue++)
+    int aaPos = 0;
+    int dnaPos = cdnaStart;
+    for (; dnaPos < cdnaSeqChars.length - 2
+            && aaPos < aaSeqChars.length; dnaPos += 3, aaPos++)
     {
-      String codon = String.valueOf(cdnaSeqChars, i, 3);
+      String codon = String.valueOf(cdnaSeqChars, dnaPos, 3);
       final String translated = ResidueProperties.codonTranslate(codon);
+
       /*
        * allow * in protein to match untranslatable in dna
        */
-      final char aaRes = aaSeqChars[aaResidue];
+      final char aaRes = aaSeqChars[aaPos];
       if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
       {
         continue;
@@ -478,8 +497,32 @@ public class AlignmentUtils
         return false;
       }
     }
-    // fail if we didn't match all of the aa sequence
-    return (aaResidue == aaSeqChars.length);
+
+    /*
+     * check we matched all of the protein sequence
+     */
+    if (aaPos != aaSeqChars.length)
+    {
+      return false;
+    }
+
+    /*
+     * check we matched all of the dna except
+     * for optional trailing STOP codon
+     */
+    if (dnaPos == cdnaSeqChars.length)
+    {
+      return true;
+    }
+    if (dnaPos == cdnaSeqChars.length - 3)
+    {
+      String codon = String.valueOf(cdnaSeqChars, dnaPos, 3);
+      if ("STOP".equals(ResidueProperties.codonTranslate(codon)))
+      {
+        return true;
+      }
+    }
+    return false;
   }
 
   /**
@@ -514,8 +557,8 @@ public class AlignmentUtils
 
     /*
      * 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
-     * protein sequence).
+     * just take the first match here (as we can't align like more than one
+     * sequence).
      */
     SequenceI alignFrom = null;
     AlignedCodonFrame mapping = null;
@@ -541,8 +584,8 @@ public class AlignmentUtils
   /**
    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
    * match residues and codons. Flags control whether existing gaps in unmapped
-   * (intron) and mapped (exon) regions are preserved or not. Gaps linking intro
-   * and exon are only retained if both flags are set.
+   * (intron) and mapped (exon) regions are preserved or not. Gaps between
+   * intron and exon are only retained if both flags are set.
    * 
    * @param alignTo
    * @param alignFrom
@@ -558,9 +601,6 @@ public class AlignmentUtils
           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;
@@ -570,13 +610,17 @@ public class AlignmentUtils
     char myGapChar = myGap.charAt(0);
     int ratio = myGap.length();
 
-    /*
-     * Traverse the aligned protein sequence.
-     */
     int fromOffset = alignFrom.getStart() - 1;
     int toOffset = alignTo.getStart() - 1;
     int sourceGapMappedLength = 0;
     boolean inExon = false;
+    final char[] thisSeq = alignTo.getSequence();
+    final char[] thatAligned = alignFrom.getSequence();
+    StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
+
+    /*
+     * Traverse the 'model' aligned sequence
+     */
     for (char sourceChar : thatAligned)
     {
       if (sourceChar == sourceGap)
@@ -586,7 +630,7 @@ public class AlignmentUtils
       }
 
       /*
-       * Found a residue. Locate its mapped codon (start) position.
+       * Found a non-gap character. Locate its mapped region if any.
        */
       sourceDsPos++;
       // Note mapping positions are base 1, our sequence positions base 0
@@ -595,11 +639,13 @@ public class AlignmentUtils
       if (mappedPos == null)
       {
         /*
-         * Abort realignment if unmapped protein. Or could ignore it??
+         * unmapped position; treat like a gap
          */
-        System.err.println("Can't align: no codon mapping to residue "
-                + sourceDsPos + "(" + sourceChar + ")");
-        return;
+        sourceGapMappedLength += ratio;
+        // System.err.println("Can't align: no codon mapping to residue "
+        // + sourceDsPos + "(" + sourceChar + ")");
+        // return;
+        continue;
       }
 
       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
@@ -669,8 +715,8 @@ public class AlignmentUtils
     }
 
     /*
-     * At end of protein sequence. Copy any remaining dna sequence, optionally
-     * including (intron) gaps. We do not copy trailing gaps in protein.
+     * At end of model aligned sequence. Copy any remaining target sequence, optionally
+     * including (intron) gaps.
      */
     while (thisSeqPos < thisSeq.length)
     {
@@ -679,6 +725,20 @@ public class AlignmentUtils
       {
         thisAligned.append(c);
       }
+      sourceGapMappedLength--;
+    }
+
+    /*
+     * finally add gaps to pad for any trailing source gaps or
+     * unmapped characters
+     */
+    if (preserveUnmappedGaps)
+    {
+      while (sourceGapMappedLength > 0)
+      {
+        thisAligned.append(myGapChar);
+        sourceGapMappedLength--;
+      }
     }
 
     /*
@@ -907,33 +967,152 @@ public class AlignmentUtils
   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
   {
     List<SequenceI> unmappedProtein = new ArrayList<SequenceI>();
+    Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = buildCodonColumnsMap(
+            protein, dna, unmappedProtein);
+    return alignProteinAs(protein, alignedCodons, unmappedProtein);
+  }
+
+  /**
+   * Builds a map whose key is an aligned codon position (3 alignment column
+   * numbers base 0), and whose value is a map from protein sequence to each
+   * protein's peptide residue for that codon. The map generates an ordering of
+   * the codons, and allows us to read off the peptides at each position in
+   * order to assemble 'aligned' protein sequences.
+   * 
+   * @param protein
+   *          the protein alignment
+   * @param dna
+   *          the coding dna alignment
+   * @param unmappedProtein
+   *          any unmapped proteins are added to this list
+   * @return
+   */
+  protected static Map<AlignedCodon, Map<SequenceI, AlignedCodon>> buildCodonColumnsMap(
+          AlignmentI protein, AlignmentI dna,
+          List<SequenceI> unmappedProtein)
+  {
+    /*
+     * maintain a list of any proteins with no mappings - these will be
+     * rendered 'as is' in the protein alignment as we can't align them
+     */
     unmappedProtein.addAll(protein.getSequences());
 
-    Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
+    List<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>>(
+    Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, AlignedCodon>>(
             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)
         {
+          Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
                   seqMap, alignedCodons);
           unmappedProtein.remove(prot);
         }
       }
     }
-    return alignProteinAs(protein, alignedCodons, unmappedProtein);
+
+    /*
+     * Finally add any unmapped peptide start residues (e.g. for incomplete
+     * codons) as if at the codon position before the second residue
+     */
+    int mappedSequenceCount = protein.getHeight() - unmappedProtein.size();
+    addUnmappedPeptideStarts(alignedCodons, mappedSequenceCount);
+    
+    return alignedCodons;
+  }
+
+  /**
+   * Scans for any protein mapped from position 2 (meaning unmapped start
+   * position e.g. an incomplete codon), and synthesizes a 'codon' for it at the
+   * preceding position in the alignment
+   * 
+   * @param alignedCodons
+   *          the codon-to-peptide map
+   * @param mappedSequenceCount
+   *          the number of distinct sequences in the map
+   */
+  protected static void addUnmappedPeptideStarts(
+          Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
+          int mappedSequenceCount)
+  {
+    // TODO delete this ugly hack once JAL-2022 is resolved
+    // i.e. we can model startPhase > 0 (incomplete start codon)
+
+    List<SequenceI> sequencesChecked = new ArrayList<SequenceI>();
+    AlignedCodon lastCodon = null;
+    Map<SequenceI, AlignedCodon> toAdd = new HashMap<SequenceI, AlignedCodon>();
+
+    for (Entry<AlignedCodon, Map<SequenceI, AlignedCodon>> entry : alignedCodons
+            .entrySet())
+    {
+      for (Entry<SequenceI, AlignedCodon> sequenceCodon : entry.getValue()
+              .entrySet())
+      {
+        SequenceI seq = sequenceCodon.getKey();
+        if (sequencesChecked.contains(seq))
+        {
+          continue;
+        }
+        sequencesChecked.add(seq);
+        AlignedCodon codon = sequenceCodon.getValue();
+        if (codon.peptideCol > 1)
+        {
+          System.err
+                  .println("Problem mapping protein with >1 unmapped start positions: "
+                          + seq.getName());
+        }
+        else if (codon.peptideCol == 1)
+        {
+          /*
+           * first position (peptideCol == 0) was unmapped - add it
+           */
+          if (lastCodon != null)
+          {
+            AlignedCodon firstPeptide = new AlignedCodon(lastCodon.pos1,
+                    lastCodon.pos2, lastCodon.pos3, String.valueOf(seq
+                            .getCharAt(0)), 0);
+            toAdd.put(seq, firstPeptide);
+          }
+          else
+          {
+            /*
+             * unmapped residue at start of alignment (no prior column) -
+             * 'insert' at nominal codon [0, 0, 0]
+             */
+            AlignedCodon firstPeptide = new AlignedCodon(0, 0, 0,
+                    String.valueOf(seq.getCharAt(0)), 0);
+            toAdd.put(seq, firstPeptide);
+          }
+        }
+        if (sequencesChecked.size() == mappedSequenceCount)
+        {
+          // no need to check past first mapped position in all sequences
+          break;
+        }
+      }
+      lastCodon = entry.getKey();
+    }
+
+    /*
+     * add any new codons safely after iterating over the map
+     */
+    for (Entry<SequenceI, AlignedCodon> startCodon : toAdd.entrySet())
+    {
+      addCodonToMap(alignedCodons, startCodon.getValue(),
+              startCodon.getKey());
+    }
   }
 
   /**
@@ -948,7 +1127,7 @@ public class AlignmentUtils
    * @return
    */
   protected static int alignProteinAs(AlignmentI protein,
-          Map<AlignedCodon, Map<SequenceI, String>> alignedCodons,
+          Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
           List<SequenceI> unmappedProtein)
   {
     /*
@@ -970,12 +1149,13 @@ public class AlignmentUtils
     int column = 0;
     for (AlignedCodon codon : alignedCodons.keySet())
     {
-      final Map<SequenceI, String> columnResidues = alignedCodons
+      final Map<SequenceI, AlignedCodon> columnResidues = alignedCodons
               .get(codon);
-      for (Entry<SequenceI, String> entry : columnResidues.entrySet())
+      for (Entry<SequenceI, AlignedCodon> entry : columnResidues.entrySet())
       {
         // place translated codon at its column position in sequence
-        entry.getKey().getSequence()[column] = entry.getValue().charAt(0);
+        entry.getKey().getSequence()[column] = entry.getValue().product
+                .charAt(0);
       }
       column++;
     }
@@ -1000,23 +1180,51 @@ public class AlignmentUtils
    */
   static void addCodonPositions(SequenceI dna, SequenceI protein,
           char gapChar, Mapping seqMap,
-          Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
+          Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons)
   {
     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
+
+    /*
+     * add codon positions, and their peptide translations, to the alignment
+     * map, while remembering the first codon mapped
+     */
     while (codons.hasNext())
     {
-      AlignedCodon codon = codons.next();
-      Map<SequenceI, String> seqProduct = alignedCodons.get(codon);
-      if (seqProduct == null)
+      try
+      {
+        AlignedCodon codon = codons.next();
+        addCodonToMap(alignedCodons, codon, protein);
+      } catch (IncompleteCodonException e)
       {
-        seqProduct = new HashMap<SequenceI, String>();
-        alignedCodons.put(codon, seqProduct);
+        // possible incomplete trailing codon - ignore
+      } catch (NoSuchElementException e)
+      {
+        // possibly peptide lacking STOP
       }
-      seqProduct.put(protein, codon.product);
     }
   }
 
   /**
+   * Helper method to add a codon-to-peptide entry to the aligned codons map
+   * 
+   * @param alignedCodons
+   * @param codon
+   * @param protein
+   */
+  protected static void addCodonToMap(
+          Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
+          AlignedCodon codon, SequenceI protein)
+  {
+    Map<SequenceI, AlignedCodon> seqProduct = alignedCodons.get(codon);
+    if (seqProduct == null)
+    {
+      seqProduct = new HashMap<SequenceI, AlignedCodon>();
+      alignedCodons.put(codon, seqProduct);
+    }
+    seqProduct.put(protein, codon);
+  }
+
+  /**
    * Returns true if a cDNA/Protein mapping either exists, or could be made,
    * between at least one pair of sequences in the two alignments. Currently,
    * the logic is:
@@ -1048,7 +1256,7 @@ public class AlignmentUtils
     }
     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
     AlignmentI protein = dna == al1 ? al2 : al1;
-    Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
+    List<AlignedCodonFrame> mappings = protein.getCodonFrames();
     for (SequenceI dnaSeq : dna.getSequences())
     {
       for (SequenceI proteinSeq : protein.getSequences())
@@ -1072,7 +1280,7 @@ public class AlignmentUtils
    * @return
    */
   protected static boolean isMappable(SequenceI dnaSeq,
-          SequenceI proteinSeq, Set<AlignedCodonFrame> mappings)
+          SequenceI proteinSeq, List<AlignedCodonFrame> mappings)
   {
     if (dnaSeq == null || proteinSeq == null)
     {
@@ -1084,13 +1292,13 @@ public class AlignmentUtils
     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
             : proteinSeq.getDatasetSequence();
 
-    /*
-     * Already mapped?
-     */
     for (AlignedCodonFrame mapping : mappings)
     {
       if (proteinDs == mapping.getAaForDnaSeq(dnaDs))
       {
+        /*
+         * already mapped
+         */
         return true;
       }
     }
@@ -1099,7 +1307,7 @@ public class AlignmentUtils
      * Just try to make a mapping (it is not yet stored), test whether
      * successful.
      */
-    return mapProteinSequenceToCdna(proteinDs, dnaDs) != null;
+    return mapCdnaToProtein(proteinDs, dnaDs) != null;
   }
 
   /**
@@ -1301,21 +1509,31 @@ public class AlignmentUtils
   }
 
   /**
-   * Constructs an alignment consisting of the mapped exon regions in the given
-   * nucleotide sequences, and updates mappings to match.
+   * Constructs an alignment consisting of the mapped (CDS) regions in the given
+   * nucleotide sequences, and updates mappings to match. The new sequences are
+   * aligned as per the original sequence, with entirely gapped columns (codon
+   * interrupted by intron) omitted.
    * 
    * @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)
+   * @param al
+   * @return an alignment whose sequences are the cds-only parts of the dna
+   *         sequences (or null if no mappings are found)
    */
-  public static AlignmentI makeExonAlignment(SequenceI[] dna,
-          Set<AlignedCodonFrame> mappings)
+  public static AlignmentI makeCdsAlignment(SequenceI[] dna,
+          List<AlignedCodonFrame> mappings, AlignmentI al)
   {
-    Set<AlignedCodonFrame> newMappings = new LinkedHashSet<AlignedCodonFrame>();
-    List<SequenceI> exonSequences = new ArrayList<SequenceI>();
+    List<int[]> cdsColumns = findCdsColumns(dna);
+
+    /*
+     * create CDS sequences and new mappings 
+     * (from cdna to cds, and cds to peptide)
+     */
+    List<AlignedCodonFrame> newMappings = new ArrayList<AlignedCodonFrame>();
+    List<SequenceI> cdsSequences = new ArrayList<SequenceI>();
+    char gap = al.getGapCharacter();
 
     for (SequenceI dnaSeq : dna)
     {
@@ -1325,18 +1543,30 @@ public class AlignmentUtils
       for (AlignedCodonFrame acf : seqMappings)
       {
         AlignedCodonFrame newMapping = new AlignedCodonFrame();
-        final List<SequenceI> mappedExons = makeExonSequences(ds, acf,
-                newMapping);
-        if (!mappedExons.isEmpty())
+        final List<SequenceI> mappedCds = makeCdsSequences(dnaSeq, acf,
+                cdsColumns, newMapping, gap);
+        if (!mappedCds.isEmpty())
         {
-          exonSequences.addAll(mappedExons);
+          cdsSequences.addAll(mappedCds);
           newMappings.add(newMapping);
         }
       }
     }
-    AlignmentI al = new Alignment(
-            exonSequences.toArray(new SequenceI[exonSequences.size()]));
-    al.setDataset(null);
+    AlignmentI newAl = new Alignment(
+            cdsSequences.toArray(new SequenceI[cdsSequences.size()]));
+
+    /*
+     * add new sequences to the shared dataset, set it on the new alignment
+     */
+    List<SequenceI> dsseqs = al.getDataset().getSequences();
+    for (SequenceI seq : newAl.getSequences())
+    {
+      if (!dsseqs.contains(seq.getDatasetSequence()))
+      {
+        dsseqs.add(seq.getDatasetSequence());
+      }
+    }
+    newAl.setDataset(al.getDataset());
 
     /*
      * Replace the old mappings with the new ones
@@ -1344,90 +1574,741 @@ public class AlignmentUtils
     mappings.clear();
     mappings.addAll(newMappings);
 
-    return al;
+    return newAl;
+  }
+
+  /**
+   * Returns a consolidated list of column ranges where at least one sequence
+   * has a CDS feature. This assumes CDS features are on genomic sequence i.e.
+   * are for contiguous CDS ranges (no gaps).
+   * 
+   * @param seqs
+   * @return
+   */
+  public static List<int[]> findCdsColumns(SequenceI[] seqs)
+  {
+    // TODO use refactored code from AlignViewController
+    // markColumnsContainingFeatures, not reinvent the wheel!
+
+    List<int[]> result = new ArrayList<int[]>();
+    for (SequenceI seq : seqs)
+    {
+      result.addAll(findCdsColumns(seq));
+    }
+
+    /*
+     * sort and compact the list into ascending, non-overlapping ranges
+     */
+    Collections.sort(result, new Comparator<int[]>()
+    {
+      @Override
+      public int compare(int[] o1, int[] o2)
+      {
+        return Integer.compare(o1[0], o2[0]);
+      }
+    });
+    result = MapList.coalesceRanges(result);
+
+    return result;
+  }
+
+  public static List<int[]> findCdsColumns(SequenceI seq)
+  {
+    List<int[]> result = new ArrayList<int[]>();
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    SequenceFeature[] sfs = seq.getSequenceFeatures();
+    if (sfs != null)
+    {
+      for (SequenceFeature sf : sfs)
+      {
+        if (so.isA(sf.getType(), SequenceOntologyI.CDS))
+        {
+          int colStart = seq.findIndex(sf.getBegin());
+          int colEnd = seq.findIndex(sf.getEnd());
+          result.add(new int[] { colStart, colEnd });
+        }
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Answers true if all sequences have a gap at (or do not extend to) the
+   * specified column position (base 1)
+   * 
+   * @param seqs
+   * @param col
+   * @return
+   */
+  public static boolean isGappedColumn(List<SequenceI> seqs, int col)
+  {
+    if (seqs != null)
+    {
+      for (SequenceI seq : seqs)
+      {
+        if (!Comparison.isGap(seq.getCharAt(col - 1)))
+        {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Returns the column ranges (base 1) of each aligned sequence that are
+   * involved in any mapping. This is a helper method for aligning protein
+   * products of aligned transcripts.
+   * 
+   * @param mappedSequences
+   *          (possibly gapped) dna sequences
+   * @param mappings
+   * @return
+   */
+  protected static List<List<int[]>> getMappedColumns(
+          List<SequenceI> mappedSequences, List<AlignedCodonFrame> mappings)
+  {
+    List<List<int[]>> result = new ArrayList<List<int[]>>();
+    for (SequenceI seq : mappedSequences)
+    {
+      List<int[]> columns = new ArrayList<int[]>();
+      List<AlignedCodonFrame> seqMappings = MappingUtils
+              .findMappingsForSequence(seq, mappings);
+      for (AlignedCodonFrame mapping : seqMappings)
+      {
+        List<Mapping> maps = mapping.getMappingsForSequence(seq);
+        for (Mapping map : maps)
+        {
+          /*
+           * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
+           * Find and add the overall aligned column range for each
+           */
+          for (int[] cdsRange : map.getMap().getFromRanges())
+          {
+            int startPos = cdsRange[0];
+            int endPos = cdsRange[1];
+            int startCol = seq.findIndex(startPos);
+            int endCol = seq.findIndex(endPos);
+            columns.add(new int[] { startCol, endCol });
+          }
+        }
+      }
+      result.add(columns);
+    }
+    return result;
   }
 
   /**
-   * Helper method to make exon-only sequences and populate their mappings to
+   * Helper method to make cds-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
+   * Typically eukaryotic dna will include cds 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
+   * cds mappings coding for multiple peptides so return multiple results
    * (example EMBL KF591215).
    * 
    * @param dnaSeq
-   *          a dna dataset sequence
+   *          a dna aligned 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
+   * @param ungappedCdsColumns
+   * @param newMappings
+   *          the new mapping to populate, from the cds-only sequences to their
    *          mapped protein sequences
    * @return
    */
-  protected static List<SequenceI> makeExonSequences(SequenceI dnaSeq,
-          AlignedCodonFrame mapping, AlignedCodonFrame newMapping)
+  protected static List<SequenceI> makeCdsSequences(SequenceI dnaSeq,
+          AlignedCodonFrame mapping, List<int[]> ungappedCdsColumns,
+          AlignedCodonFrame newMappings, char gapChar)
   {
-    List<SequenceI> exonSequences = new ArrayList<SequenceI>();
+    List<SequenceI> cdsSequences = new ArrayList<SequenceI>();
     List<Mapping> seqMappings = mapping.getMappingsForSequence(dnaSeq);
-    final char[] dna = dnaSeq.getSequence();
+
     for (Mapping seqMapping : seqMappings)
     {
-      StringBuilder newSequence = new StringBuilder(dnaSeq.getLength());
+      SequenceI cds = makeCdsSequence(dnaSeq, seqMapping,
+              ungappedCdsColumns, gapChar);
+      cds.createDatasetSequence();
+      cdsSequences.add(cds);
 
       /*
-       * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
+       * add new mappings, from dna to cds, and from cds to peptide 
        */
-      final List<int[]> dnaExonRanges = seqMapping.getMap().getFromRanges();
-      for (int[] range : dnaExonRanges)
+      MapList dnaToCds = addCdsMappings(dnaSeq.getDatasetSequence(), cds,
+              seqMapping, newMappings);
+
+      /*
+       * transfer any features on dna that overlap the CDS
+       */
+      transferFeatures(dnaSeq, cds, dnaToCds, null, SequenceOntologyI.CDS);
+    }
+    return cdsSequences;
+  }
+
+  /**
+   * Transfers co-located features on 'fromSeq' to 'toSeq', adjusting the
+   * feature start/end ranges, optionally omitting specified feature types.
+   * Returns the number of features copied.
+   * 
+   * @param fromSeq
+   * @param toSeq
+   * @param select
+   *          if not null, only features of this type are copied (including
+   *          subtypes in the Sequence Ontology)
+   * @param mapping
+   *          the mapping from 'fromSeq' to 'toSeq'
+   * @param omitting
+   */
+  public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
+          MapList mapping, String select, String... omitting)
+  {
+    SequenceI copyTo = toSeq;
+    while (copyTo.getDatasetSequence() != null)
+    {
+      copyTo = copyTo.getDatasetSequence();
+    }
+
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    int count = 0;
+    SequenceFeature[] sfs = fromSeq.getSequenceFeatures();
+    if (sfs != null)
+    {
+      for (SequenceFeature sf : sfs)
       {
-        for (int pos = range[0]; pos <= range[1]; pos++)
+        String type = sf.getType();
+        if (select != null && !so.isA(type, select))
         {
-          newSequence.append(dna[pos - 1]);
+          continue;
+        }
+        boolean omit = false;
+        for (String toOmit : omitting)
+        {
+          if (type.equals(toOmit))
+          {
+            omit = true;
+          }
+        }
+        if (omit)
+        {
+          continue;
+        }
+
+        /*
+         * locate the mapped range - null if either start or end is
+         * not mapped (no partial overlaps are calculated)
+         */
+        int start = sf.getBegin();
+        int end = sf.getEnd();
+        int[] mappedTo = mapping.locateInTo(start, end);
+        /*
+         * if whole exon range doesn't map, try interpreting it
+         * as 5' or 3' exon overlapping the CDS range
+         */
+        if (mappedTo == null)
+        {
+          mappedTo = mapping.locateInTo(end, end);
+          if (mappedTo != null)
+          {
+            /*
+             * end of exon is in CDS range - 5' overlap
+             * to a range from the start of the peptide
+             */
+            mappedTo[0] = 1;
+          }
+        }
+        if (mappedTo == null)
+        {
+          mappedTo = mapping.locateInTo(start, start);
+          if (mappedTo != null)
+          {
+            /*
+             * start of exon is in CDS range - 3' overlap
+             * to a range up to the end of the peptide
+             */
+            mappedTo[1] = toSeq.getLength();
+          }
+        }
+        if (mappedTo != null)
+        {
+          SequenceFeature copy = new SequenceFeature(sf);
+          copy.setBegin(Math.min(mappedTo[0], mappedTo[1]));
+          copy.setEnd(Math.max(mappedTo[0], mappedTo[1]));
+          copyTo.addSequenceFeature(copy);
+          count++;
         }
       }
+    }
+    return count;
+  }
 
-      SequenceI exon = new Sequence(dnaSeq.getName(),
-              newSequence.toString());
+  /**
+   * Creates and adds mappings
+   * <ul>
+   * <li>from cds to peptide</li>
+   * <li>from dna to cds</li>
+   * </ul>
+   * and returns the dna-to-cds mapping
+   * 
+   * @param dnaSeq
+   * @param cdsSeq
+   * @param dnaMapping
+   * @param newMappings
+   * @return
+   */
+  protected static MapList addCdsMappings(SequenceI dnaSeq,
+          SequenceI cdsSeq, Mapping dnaMapping,
+          AlignedCodonFrame newMappings)
+  {
+    cdsSeq.createDatasetSequence();
 
-      /*
-       * 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()
-              .getDBRefs(), DBRefSource.CODINGDBS);
-      if (cdsRefs != null)
+    /*
+     * CDS to peptide is just a contiguous 3:1 mapping, with
+     * the peptide ranges taken unchanged from the dna mapping
+     */
+    List<int[]> cdsRanges = new ArrayList<int[]>();
+    SequenceI cdsDataset = cdsSeq.getDatasetSequence();
+    cdsRanges.add(new int[] { 1, cdsDataset.getLength() });
+    MapList cdsToPeptide = new MapList(cdsRanges, dnaMapping.getMap()
+            .getToRanges(), 3, 1);
+    newMappings.addMap(cdsDataset, dnaMapping.getTo(), cdsToPeptide);
+
+    /*
+     * dna 'from' ranges map 1:1 to the contiguous extracted CDS 
+     */
+    MapList dnaToCds = new MapList(dnaMapping.getMap().getFromRanges(),
+            cdsRanges, 1, 1);
+    newMappings.addMap(dnaSeq, cdsDataset, dnaToCds);
+    return dnaToCds;
+  }
+
+  /**
+   * Makes and returns a CDS-only sequence, where the CDS regions are identified
+   * as the 'from' ranges of the mapping on the dna.
+   * 
+   * @param dnaSeq
+   *          nucleotide sequence
+   * @param seqMapping
+   *          mappings from CDS regions of nucleotide
+   * @param ungappedCdsColumns
+   * @return
+   */
+  protected static SequenceI makeCdsSequence(SequenceI dnaSeq,
+          Mapping seqMapping, List<int[]> ungappedCdsColumns, char gapChar)
+  {
+    int cdsWidth = MappingUtils.getLength(ungappedCdsColumns);
+
+    /*
+     * populate CDS columns with the aligned
+     * column character if that column is mapped (which may be a gap 
+     * if an intron interrupts a codon), else with a gap
+     */
+    List<int[]> fromRanges = seqMapping.getMap().getFromRanges();
+    char[] cdsChars = new char[cdsWidth];
+    int pos = 0;
+    for (int[] columns : ungappedCdsColumns)
+    {
+      for (int i = columns[0]; i <= columns[1]; i++)
       {
-        for (DBRefEntry cdsRef : cdsRefs)
+        char dnaChar = dnaSeq.getCharAt(i - 1);
+        if (Comparison.isGap(dnaChar))
         {
-          exon.addDBRef(new DBRefEntry(cdsRef));
-          cdsAccId = cdsRef.getAccessionId();
+          cdsChars[pos] = gapChar;
+        }
+        else
+        {
+          int seqPos = dnaSeq.findPosition(i - 1);
+          if (MappingUtils.contains(fromRanges, seqPos))
+          {
+            cdsChars[pos] = dnaChar;
+          }
+          else
+          {
+            cdsChars[pos] = gapChar;
+          }
         }
+        pos++;
       }
-      exon.setName(exon.getName() + "|" + cdsAccId);
-      exon.createDatasetSequence();
+    }
+    SequenceI cdsSequence = new Sequence(dnaSeq.getName(),
+            String.valueOf(cdsChars));
+
+    transferDbRefs(seqMapping.getTo(), cdsSequence);
+
+    return cdsSequence;
+  }
+
+  /**
+   * Locate any xrefs to CDS databases on the protein product and attach to the
+   * CDS sequence. Also add as a sub-token of the sequence name.
+   * 
+   * @param from
+   * @param to
+   */
+  protected static void transferDbRefs(SequenceI from, SequenceI to)
+  {
+    String cdsAccId = FeatureProperties.getCodingFeature(DBRefSource.EMBL);
+    DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(from.getDBRefs(),
+            DBRefSource.CODINGDBS);
+    if (cdsRefs != null)
+    {
+      for (DBRefEntry cdsRef : cdsRefs)
+      {
+        to.addDBRef(new DBRefEntry(cdsRef));
+        cdsAccId = cdsRef.getAccessionId();
+      }
+    }
+    if (!to.getName().contains(cdsAccId))
+    {
+      to.setName(to.getName() + "|" + cdsAccId);
+    }
+  }
 
+  /**
+   * Returns a mapping from dna to protein by inspecting sequence features of
+   * type "CDS" on the dna.
+   * 
+   * @param dnaSeq
+   * @param proteinSeq
+   * @return
+   */
+  public static MapList mapCdsToProtein(SequenceI dnaSeq,
+          SequenceI proteinSeq)
+  {
+    List<int[]> ranges = findCdsPositions(dnaSeq);
+    int mappedDnaLength = MappingUtils.getLength(ranges);
+  
+    int proteinLength = proteinSeq.getLength();
+    int proteinStart = proteinSeq.getStart();
+    int proteinEnd = proteinSeq.getEnd();
+  
+    /*
+     * incomplete start codon may mean X at start of peptide
+     * we ignore both for mapping purposes
+     */
+    if (proteinSeq.getCharAt(0) == 'X')
+    {
+      // todo JAL-2022 support startPhase > 0
+      proteinStart++;
+      proteinLength--;
+    }
+    List<int[]> proteinRange = new ArrayList<int[]>();
+  
+    /*
+     * dna length should map to protein (or protein plus stop codon)
+     */
+    int codesForResidues = mappedDnaLength / 3;
+    if (codesForResidues == (proteinLength + 1))
+    {
+      // assuming extra codon is for STOP and not in peptide
+      codesForResidues--;
+    }
+    if (codesForResidues == proteinLength)
+    {
+      proteinRange.add(new int[] { proteinStart, proteinEnd });
+      return new MapList(ranges, proteinRange, 3, 1);
+    }
+    return null;
+  }
+
+  /**
+   * Returns a list of CDS ranges found (as sequence positions base 1), i.e. of
+   * start/end positions of sequence features of type "CDS" (or a sub-type of
+   * CDS in the Sequence Ontology)
+   * 
+   * @param dnaSeq
+   * @return
+   */
+  public static List<int[]> findCdsPositions(SequenceI dnaSeq)
+  {
+    List<int[]> result = new ArrayList<int[]>();
+    SequenceFeature[] sfs = dnaSeq.getSequenceFeatures();
+    if (sfs == null)
+    {
+      return result;
+    }
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    for (SequenceFeature sf : sfs)
+    {
       /*
-       * Build new mappings - from the same protein regions, but now to
-       * contiguous exons
+       * process a CDS feature (or a sub-type of CDS)
        */
-      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);
+      if (so.isA(sf.getType(), SequenceOntologyI.CDS))
+      {
+        int phase = 0;
+        try {
+          phase = Integer.parseInt(sf.getPhase());
+        } catch (NumberFormatException e)
+        {
+          // ignore
+        }
+        /*
+         * phase > 0 on first codon means 5' incomplete - skip to the start
+         * of the next codon; example ENST00000496384
+         */
+        int begin = sf.getBegin();
+        int end = sf.getEnd();
+        if (result.isEmpty())
+        {
+          // TODO JAL-2022 support start phase > 0
+          begin += phase;
+          if (begin > end)
+          {
+            continue; // shouldn't happen?
+          }
+        }
+        result.add(new int[] { begin, end });
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Maps exon features from dna to protein, and computes variants in peptide
+   * product generated by variants in dna, and adds them as sequence_variant
+   * features on the protein sequence. Returns the number of variant features
+   * added.
+   * 
+   * @param dnaSeq
+   * @param peptide
+   * @param dnaToProtein
+   */
+  public static int computeProteinFeatures(SequenceI dnaSeq,
+          SequenceI peptide, MapList dnaToProtein)
+  {
+    while (dnaSeq.getDatasetSequence() != null)
+    {
+      dnaSeq = dnaSeq.getDatasetSequence();
+    }
+    while (peptide.getDatasetSequence() != null)
+    {
+      peptide = peptide.getDatasetSequence();
+    }
+  
+    transferFeatures(dnaSeq, peptide, dnaToProtein,
+            SequenceOntologyI.EXON);
+  
+    LinkedHashMap<Integer, String[][]> variants = buildDnaVariantsMap(
+            dnaSeq, dnaToProtein);
+  
+    /*
+     * scan codon variations, compute peptide variants and add to peptide sequence
+     */
+    int count = 0;
+    for (Entry<Integer, String[][]> variant : variants.entrySet())
+    {
+      int peptidePos = variant.getKey();
+      String[][] codonVariants = variant.getValue();
+      String residue = String.valueOf(peptide.getCharAt(peptidePos - 1)); // 0-based
+      List<String> peptideVariants = computePeptideVariants(codonVariants,
+              residue);
+      if (!peptideVariants.isEmpty())
+      {
+        String desc = StringUtils.listToDelimitedString(peptideVariants,
+                ", ");
+        SequenceFeature sf = new SequenceFeature(
+                SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
+                peptidePos, 0f, null);
+        peptide.addSequenceFeature(sf);
+        count++;
+      }
+    }
+  
+    /*
+     * ugly sort to get sequence features in start position order
+     * - would be better to store in Sequence as a TreeSet instead?
+     */
+    Arrays.sort(peptide.getSequenceFeatures(),
+            new Comparator<SequenceFeature>()
+            {
+              @Override
+              public int compare(SequenceFeature o1, SequenceFeature o2)
+              {
+                int c = Integer.compare(o1.getBegin(), o2.getBegin());
+                return c == 0 ? Integer.compare(o1.getEnd(), o2.getEnd())
+                        : c;
+              }
+            });
+    return count;
+  }
+
+  /**
+   * Builds a map whose key is position in the protein sequence, and value is an
+   * array of all variants for the coding codon positions
+   * 
+   * @param dnaSeq
+   * @param dnaToProtein
+   * @return
+   */
+  static LinkedHashMap<Integer, String[][]> buildDnaVariantsMap(
+          SequenceI dnaSeq, MapList dnaToProtein)
+  {
+    /*
+     * map from peptide position to all variant features of the codon for it
+     * LinkedHashMap ensures we add the peptide features in sequence order
+     */
+    LinkedHashMap<Integer, String[][]> variants = new LinkedHashMap<Integer, String[][]>();
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+  
+    SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures();
+    if (dnaFeatures == null)
+    {
+      return variants;
+    }
+  
+    int dnaStart = dnaSeq.getStart();
+    int[] lastCodon = null;
+    int lastPeptidePostion = 0;
+  
+    /*
+     * build a map of codon variations for peptides
+     */
+    for (SequenceFeature sf : dnaFeatures)
+    {
+      int dnaCol = sf.getBegin();
+      if (dnaCol != sf.getEnd())
+      {
+        // not handling multi-locus variant features
+        continue;
+      }
+      if (so.isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT))
+      {
+        int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
+        if (mapsTo == null)
+        {
+          // feature doesn't lie within coding region
+          continue;
+        }
+        int peptidePosition = mapsTo[0];
+        String[][] codonVariants = variants.get(peptidePosition);
+        if (codonVariants == null)
+        {
+          codonVariants = new String[3][];
+          variants.put(peptidePosition, codonVariants);
+        }
+  
+        /*
+         * extract dna variants to a string array
+         */
+        String alls = (String) sf.getValue("alleles");
+        if (alls == null)
+        {
+          continue;
+        }
+        String[] alleles = alls.toUpperCase().split(",");
+        int i = 0;
+        for (String allele : alleles)
+        {
+          alleles[i++] = allele.trim(); // lose any space characters "A, G"
+        }
+  
+        /*
+         * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
+         */
+        int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
+                : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
+                        peptidePosition, peptidePosition));
+        lastPeptidePostion = peptidePosition;
+        lastCodon = codon;
+  
+        /*
+         * save nucleotide (and this variant) for each codon position
+         */
+        for (int codonPos = 0; codonPos < 3; codonPos++)
+        {
+          String nucleotide = String.valueOf(
+                  dnaSeq.getCharAt(codon[codonPos] - dnaStart))
+                  .toUpperCase();
+          if (codonVariants[codonPos] == null)
+          {
+            /*
+             * record current dna base
+             */
+            codonVariants[codonPos] = new String[] { nucleotide };
+          }
+          if (codon[codonPos] == dnaCol)
+          {
+            /*
+             * add alleles to dna base (and any previously found alleles)
+             */
+            String[] known = codonVariants[codonPos];
+            String[] dnaVariants = new String[alleles.length + known.length];
+            System.arraycopy(known, 0, dnaVariants, 0, known.length);
+            System.arraycopy(alleles, 0, dnaVariants, known.length,
+                    alleles.length);
+            codonVariants[codonPos] = dnaVariants;
+          }
+        }
+      }
+    }
+    return variants;
+  }
 
-      exonSequences.add(exon);
+  /**
+   * Returns a sorted, non-redundant list of all peptide translations generated
+   * by the given dna variants, excluding the current residue value
+   * 
+   * @param codonVariants
+   *          an array of base values (acgtACGT) for codon positions 1, 2, 3
+   * @param residue
+   *          the current residue translation
+   * @return
+   */
+  static List<String> computePeptideVariants(
+          String[][] codonVariants, String residue)
+  {
+    List<String> result = new ArrayList<String>();
+    for (String base1 : codonVariants[0])
+    {
+      for (String base2 : codonVariants[1])
+      {
+        for (String base3 : codonVariants[2])
+        {
+          String codon = base1 + base2 + base3;
+          /*
+           * get peptide translation of codon e.g. GAT -> D
+           * note that variants which are not single alleles,
+           * e.g. multibase variants or HGMD_MUTATION etc
+           * are ignored here
+           */
+          String peptide = codon.contains("-") ? "-"
+                  : (codon.length() > 3 ? null : ResidueProperties
+                          .codonTranslate(codon));
+          if (peptide != null && !result.contains(peptide)
+                  && !peptide.equalsIgnoreCase(residue))
+          {
+            result.add(peptide);
+          }
+        }
+      }
     }
-    return exonSequences;
+  
+    /*
+     * sort alphabetically with STOP at the end
+     */
+    Collections.sort(result, new Comparator<String>()
+    {
+  
+      @Override
+      public int compare(String o1, String o2)
+      {
+        if ("STOP".equals(o1))
+        {
+          return 1;
+        }
+        else if ("STOP".equals(o2))
+        {
+          return -1;
+        }
+        else
+        {
+          return o1.compareTo(o2);
+        }
+      }
+    });
+    return result;
   }
 }