JAL-1705 include any unmapped protein start 'X' when aligning to dna
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
index d8cb9a2..e624ce7 100644 (file)
@@ -28,13 +28,17 @@ 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;
@@ -42,6 +46,8 @@ import jalview.util.MappingUtils;
 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;
@@ -924,6 +930,34 @@ 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());
 
     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
@@ -933,24 +967,114 @@ public class AlignmentUtils
      * {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 there must be an easier way! root problem is that our mapping data
+    // model does not include phase so can't map part of a codon to a peptide
+    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());
+    }
   }
 
   /**
@@ -965,7 +1089,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)
   {
     /*
@@ -987,12 +1111,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++;
     }
@@ -1017,23 +1142,48 @@ 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
       }
-      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:
@@ -1301,7 +1451,7 @@ public class AlignmentUtils
       return false;
     }
     String name = seq2.getName();
-    final DBRefEntry[] xrefs = seq1.getDBRef();
+    final DBRefEntry[] xrefs = seq1.getDBRefs();
     if (xrefs != null)
     {
       for (DBRefEntry xref : xrefs)
@@ -1318,19 +1468,27 @@ public class AlignmentUtils
   }
 
   /**
-   * Constructs an alignment consisting of the mapped cds 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 sequences (with gapped columns omitted).
    * 
    * @param dna
    *          aligned dna sequences
    * @param mappings
    *          from dna to protein; these are replaced with new mappings
+   * @param gapChar
    * @return an alignment whose sequences are the cds-only parts of the dna
-   *         sequences (or null if no cds are found)
+   *         sequences (or null if no mappings are found)
    */
   public static AlignmentI makeCdsAlignment(SequenceI[] dna,
-          List<AlignedCodonFrame> mappings)
+          List<AlignedCodonFrame> mappings, char gapChar)
   {
+    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>();
 
@@ -1342,8 +1500,8 @@ public class AlignmentUtils
       for (AlignedCodonFrame acf : seqMappings)
       {
         AlignedCodonFrame newMapping = new AlignedCodonFrame();
-        final List<SequenceI> mappedCds = makeCdsSequences(ds, acf,
-                newMapping);
+        final List<SequenceI> mappedCds = makeCdsSequences(dnaSeq, acf,
+                cdsColumns, newMapping, gapChar);
         if (!mappedCds.isEmpty())
         {
           cdsSequences.addAll(mappedCds);
@@ -1353,6 +1511,7 @@ public class AlignmentUtils
     }
     AlignmentI al = new Alignment(
             cdsSequences.toArray(new SequenceI[cdsSequences.size()]));
+    al.setGapCharacter(gapChar);
     al.setDataset(null);
 
     /*
@@ -1365,6 +1524,127 @@ public class AlignmentUtils
   }
 
   /**
+   * 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 cds-only sequences and populate their mappings to
    * protein products
    * <p>
@@ -1378,58 +1658,77 @@ public class AlignmentUtils
    * (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 ungappedCdsColumns
    * @param newMappings
    *          the new mapping to populate, from the cds-only sequences to their
    *          mapped protein sequences
    * @return
    */
   protected static List<SequenceI> makeCdsSequences(SequenceI dnaSeq,
-          AlignedCodonFrame mapping, AlignedCodonFrame newMappings)
+          AlignedCodonFrame mapping, List<int[]> ungappedCdsColumns,
+          AlignedCodonFrame newMappings, char gapChar)
   {
     List<SequenceI> cdsSequences = new ArrayList<SequenceI>();
     List<Mapping> seqMappings = mapping.getMappingsForSequence(dnaSeq);
 
     for (Mapping seqMapping : seqMappings)
     {
-      SequenceI cds = makeCdsSequence(dnaSeq, seqMapping, newMappings);
+      SequenceI cds = makeCdsSequence(dnaSeq, seqMapping,
+              ungappedCdsColumns, gapChar);
       cdsSequences.add(cds);
 
       /*
        * add new mappings, from dna to cds, and from cds to peptide 
        */
-      MapList dnaToCds = addCdsMappings(dnaSeq, cds, seqMapping,
-              newMappings);
+      MapList dnaToCds = addCdsMappings(dnaSeq.getDatasetSequence(), cds,
+              seqMapping, newMappings);
 
       /*
        * transfer any features on dna that overlap the CDS
        */
-      transferFeatures(dnaSeq, cds, dnaToCds, "CDS" /* SequenceOntology.CDS */);
+      transferFeatures(dnaSeq, cds, dnaToCds, null, SequenceOntologyI.CDS);
     }
     return cdsSequences;
   }
 
   /**
-   * Transfers any co-located features on 'fromSeq' to 'toSeq', adjusting the
+   * 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
    */
-  protected static void transferFeatures(SequenceI fromSeq,
-          SequenceI toSeq, MapList mapping, String... 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)
       {
         String type = sf.getType();
+        if (select != null && !so.isA(type, select))
+        {
+          continue;
+        }
         boolean omit = false;
         for (String toOmit : omitting)
         {
@@ -1447,16 +1746,48 @@ public class AlignmentUtils
          * locate the mapped range - null if either start or end is
          * not mapped (no partial overlaps are calculated)
          */
-        int[] mappedTo = mapping.locateInTo(sf.getBegin(), sf.getEnd());
+        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]));
-          toSeq.addSequenceFeature(copy);
+          copyTo.addSequenceFeature(copy);
+          count++;
         }
       }
     }
+    return count;
   }
 
   /**
@@ -1474,8 +1805,8 @@ public class AlignmentUtils
    * @return
    */
   protected static MapList addCdsMappings(SequenceI dnaSeq,
-          SequenceI cdsSeq,
-          Mapping dnaMapping, AlignedCodonFrame newMappings)
+          SequenceI cdsSeq, Mapping dnaMapping,
+          AlignedCodonFrame newMappings)
   {
     cdsSeq.createDatasetSequence();
 
@@ -1484,59 +1815,75 @@ public class AlignmentUtils
      * the peptide ranges taken unchanged from the dna mapping
      */
     List<int[]> cdsRanges = new ArrayList<int[]>();
-    cdsRanges.add(new int[] { 1, cdsSeq.getLength() });
+    SequenceI cdsDataset = cdsSeq.getDatasetSequence();
+    cdsRanges.add(new int[] { 1, cdsDataset.getLength() });
     MapList cdsToPeptide = new MapList(cdsRanges, dnaMapping.getMap()
             .getToRanges(), 3, 1);
-    newMappings.addMap(cdsSeq.getDatasetSequence(), dnaMapping.getTo(),
-            cdsToPeptide);
+    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, cdsSeq.getDatasetSequence(), dnaToCds);
+    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. Any sequence features on
-   * the dna which overlap the CDS regions are copied to the new sequence.
+   * as the 'from' ranges of the mapping on the dna.
    * 
    * @param dnaSeq
    *          nucleotide sequence
    * @param seqMapping
    *          mappings from CDS regions of nucleotide
-   * @param exonMappings
-   *          CDS-to-peptide mapping (to add to)
+   * @param ungappedCdsColumns
    * @return
    */
   protected static SequenceI makeCdsSequence(SequenceI dnaSeq,
-          Mapping seqMapping, AlignedCodonFrame exonMappings)
+          Mapping seqMapping, List<int[]> ungappedCdsColumns, char gapChar)
   {
-    StringBuilder newSequence = new StringBuilder(dnaSeq.getLength());
-    final char[] dna = dnaSeq.getSequence();
-    int offset = dnaSeq.getStart() - 1;
+    int cdsWidth = MappingUtils.getLength(ungappedCdsColumns);
 
     /*
-     * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
+     * 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
      */
-    final List<int[]> dnaCdsRanges = seqMapping.getMap().getFromRanges();
-    for (int[] range : dnaCdsRanges)
+    List<int[]> fromRanges = seqMapping.getMap().getFromRanges();
+    char[] cdsChars = new char[cdsWidth];
+    int pos = 0;
+    for (int[] columns : ungappedCdsColumns)
     {
-      // TODO handle reverse mapping as well (range[1] < range[0])
-      for (int pos = range[0]; pos <= range[1]; pos++)
+      for (int i = columns[0]; i <= columns[1]; i++)
       {
-        newSequence.append(dna[pos - offset - 1]);
+        char dnaChar = dnaSeq.getCharAt(i - 1);
+        if (Comparison.isGap(dnaChar))
+        {
+          cdsChars[pos] = gapChar;
+        }
+        else
+        {
+          int seqPos = dnaSeq.findPosition(i - 1);
+          if (MappingUtils.contains(fromRanges, seqPos))
+          {
+            cdsChars[pos] = dnaChar;
+          }
+          else
+          {
+            cdsChars[pos] = gapChar;
+          }
+        }
+        pos++;
       }
     }
+    SequenceI cdsSequence = new Sequence(dnaSeq.getName(),
+            String.valueOf(cdsChars));
 
-    SequenceI cds = new Sequence(dnaSeq.getName(),
-            newSequence.toString());
+    transferDbRefs(seqMapping.getTo(), cdsSequence);
 
-    transferDbRefs(seqMapping.getTo(), cds);
-    return cds;
+    return cdsSequence;
   }
 
   /**
@@ -1549,7 +1896,7 @@ public class AlignmentUtils
   protected static void transferDbRefs(SequenceI from, SequenceI to)
   {
     String cdsAccId = FeatureProperties.getCodingFeature(DBRefSource.EMBL);
-    DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(from.getDBRef(),
+    DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(from.getDBRefs(),
             DBRefSource.CODINGDBS);
     if (cdsRefs != null)
     {