JAL-2154 JAL-2106 transfer primary refs to CDS for makeCDS
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
index 35aa91d..cc80384 100644 (file)
@@ -22,7 +22,6 @@ package jalview.analysis;
 
 import static jalview.io.gff.GffConstants.CLINICAL_SIGNIFICANCE;
 
-import jalview.api.DBRefEntryI;
 import jalview.datamodel.AlignedCodon;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
@@ -867,6 +866,8 @@ public class AlignmentUtils
    * Realigns the given dna to match the alignment of the protein, using codon
    * mappings to translate aligned peptide positions to codons.
    * 
+   * Always produces a padded CDS alignment.
+   * 
    * @param dna
    *          the alignment whose sequences are realigned by this method
    * @param protein
@@ -883,42 +884,148 @@ public class AlignmentUtils
     // todo: implement this
     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
     int alignedCount = 0;
+    int width = 0; // alignment width for padding CDS
     for (SequenceI dnaSeq : dna.getSequences())
     {
-      if (alignCdsSequenceAsProtein(dnaSeq, protein, mappings))
+      if (alignCdsSequenceAsProtein(dnaSeq, protein, mappings,
+              dna.getGapCharacter()))
       {
         alignedCount++;
       }
+      width = Math.max(dnaSeq.getLength(), width);
+    }
+    int oldwidth, diff;
+    for (SequenceI dnaSeq : dna.getSequences())
+    {
+      oldwidth = dnaSeq.getLength();
+      diff = width - oldwidth;
+      if (diff > 0)
+      {
+        dnaSeq.insertCharAt(oldwidth, diff, dna.getGapCharacter());
+      }
     }
     return alignedCount;
   }
 
   /**
-   * Helper method to align (if possible) the dna sequence (cds only) to match
-   * the alignment of a mapped protein sequence
+   * Helper method to align (if possible) the dna sequence to match the
+   * alignment of a mapped protein sequence. This is currently limited to
+   * handling coding sequence only.
    * 
-   * @param dnaSeq
+   * @param cdsSeq
    * @param protein
    * @param mappings
+   * @param gapChar
    * @return
    */
-  static boolean alignCdsSequenceAsProtein(SequenceI dnaSeq,
-          AlignmentI protein, List<AlignedCodonFrame> mappings)
+  static boolean alignCdsSequenceAsProtein(SequenceI cdsSeq,
+          AlignmentI protein, List<AlignedCodonFrame> mappings, char gapChar)
   {
+    SequenceI cdsDss = cdsSeq.getDatasetSequence();
+    if (cdsDss == null)
+    {
+      System.err
+              .println("alignCdsSequenceAsProtein needs aligned sequence!");
+      return false;
+    }
+    
     List<AlignedCodonFrame> dnaMappings = MappingUtils
-            .findMappingsForSequence(dnaSeq, mappings);
+            .findMappingsForSequence(cdsSeq, mappings);
     for (AlignedCodonFrame mapping : dnaMappings)
     {
-      SequenceI peptide = mapping.findAlignedSequence(dnaSeq, protein);
+      SequenceI peptide = mapping.findAlignedSequence(cdsSeq, protein);
+      int peptideLength = peptide.getLength();
       if (peptide != null)
       {
-        Mapping map = mapping.getMappingBetween(dnaSeq, peptide);
+        Mapping map = mapping.getMappingBetween(cdsSeq, peptide);
         if (map != null)
         {
+          MapList mapList = map.getMap();
+          if (map.getTo() == peptide.getDatasetSequence())
+          {
+            mapList = mapList.getInverse();
+          }
+          int cdsLength = cdsDss.getLength();
+          int mappedFromLength = MappingUtils.getLength(mapList
+                  .getFromRanges());
+          int mappedToLength = MappingUtils
+                  .getLength(mapList.getToRanges());
+          boolean addStopCodon = (cdsLength == mappedFromLength * 3 + 3)
+                  || (peptide.getDatasetSequence().getLength() == mappedFromLength - 1);
+          if (cdsLength != mappedToLength && !addStopCodon)
+          {
+            System.err
+                    .println(String
+                            .format("Can't align cds as protein (length mismatch %d/%d): %s",
+                                    cdsLength, mappedToLength,
+                                    cdsSeq.getName()));
+          }
+
           /*
-           * traverse peptide adding gaps or codons to new cds sequence
+           * pre-fill the aligned cds sequence with gaps
            */
-          MapList mapList = map.getMap();
+          char[] alignedCds = new char[peptideLength * 3
+                  + (addStopCodon ? 3 : 0)];
+          Arrays.fill(alignedCds, gapChar);
+
+          /*
+           * walk over the aligned peptide sequence and insert mapped 
+           * codons for residues in the aligned cds sequence 
+           */
+          char[] alignedPeptide = peptide.getSequence();
+          char[] nucleotides = cdsDss.getSequence();
+          int copiedBases = 0;
+          int cdsStart = cdsDss.getStart();
+          int proteinPos = peptide.getStart() - 1;
+          int cdsCol = 0;
+          for (char residue : alignedPeptide)
+          {
+            if (Comparison.isGap(residue))
+            {
+              cdsCol += 3;
+            }
+            else
+            {
+              proteinPos++;
+              int[] codon = mapList.locateInTo(proteinPos, proteinPos);
+              if (codon == null)
+              {
+                // e.g. incomplete start codon, X in peptide
+                cdsCol += 3;
+              }
+              else
+              {
+                for (int j = codon[0]; j <= codon[1]; j++)
+                {
+                  char mappedBase = nucleotides[j - cdsStart];
+                  alignedCds[cdsCol++] = mappedBase;
+                  copiedBases++;
+                }
+              }
+            }
+          }
+
+          /*
+           * append stop codon if not mapped from protein,
+           * closing it up to the end of the mapped sequence
+           */
+          if (copiedBases == nucleotides.length - 3)
+          {
+            for (int i = alignedCds.length - 1; i >= 0; i--)
+            {
+              if (!Comparison.isGap(alignedCds[i]))
+              {
+                cdsCol = i + 1; // gap just after end of sequence
+                break;
+              }
+            }
+            for (int i = nucleotides.length - 3; i < nucleotides.length; i++)
+            {
+              alignedCds[cdsCol++] = nucleotides[i];
+            }
+          }
+          cdsSeq.setSequence(new String(alignedCds));
+          return true;
         }
       }
     }
@@ -1574,6 +1681,10 @@ public class AlignmentUtils
            * its dataset sequence to the dataset
            */
           cdsSeq = makeCdsSequence(dnaSeq.getDatasetSequence(), aMapping);
+          // cdsSeq has a name constructed as CDS|<dbref>
+          // <dbref> will be either the accession for the coding sequence,
+          // marked in the /via/ dbref to the protein product accession
+          // or it will be the original nucleotide accession.
           SequenceI cdsSeqDss = cdsSeq.createDatasetSequence();
           cdsSeqs.add(cdsSeq);
           if (!dataset.getSequences().contains(cdsSeqDss))
@@ -1589,7 +1700,8 @@ public class AlignmentUtils
           MapList cdsToProteinMap = new MapList(cdsRange, mapList.getToRanges(),
                   mapList.getFromRatio(), mapList.getToRatio());
           AlignedCodonFrame cdsToProteinMapping = new AlignedCodonFrame();
-          cdsToProteinMapping.addMap(cdsSeq, proteinProduct, cdsToProteinMap);
+          cdsToProteinMapping.addMap(cdsSeqDss, proteinProduct,
+                  cdsToProteinMap);
 
           /*
            * guard against duplicating the mapping if repeating this action
@@ -1599,23 +1711,8 @@ public class AlignmentUtils
             mappings.add(cdsToProteinMapping);
           }
 
-          /*
-           * copy protein's dbrefs to CDS sequence
-           * this enables Get Cross-References from CDS alignment
-           */
-          DBRefEntry[] proteinRefs = DBRefUtils.selectDbRefs(false,
-                  proteinProduct.getDBRefs());
-          if (proteinRefs != null)
-          {
-            for (DBRefEntry ref : proteinRefs)
-            {
-              DBRefEntry cdsToProteinRef = new DBRefEntry(ref);
-              cdsToProteinRef.setMap(new Mapping(proteinProduct,
-                      cdsToProteinMap));
-              cdsSeqDss.addDBRef(cdsToProteinRef);
-            }
-          }
-
+          propagateDBRefsToCDS(cdsSeqDss, dnaSeq.getDatasetSequence(),
+                  proteinProduct, aMapping);
           /*
            * add another mapping from original 'from' range to CDS
            */
@@ -1623,7 +1720,7 @@ public class AlignmentUtils
           MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
                   cdsRange, 1,
                   1);
-          dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeq,
+          dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeqDss,
                   dnaToCdsMap);
           if (!mappings.contains(dnaToCdsMapping))
           {
@@ -1637,12 +1734,37 @@ public class AlignmentUtils
            * same source and accession, so need a different accession for
            * the CDS from the dna sequence
            */
-          DBRefEntryI dnaRef = dnaDss.getSourceDBRef();
-          if (dnaRef != null)
+          
+          // specific use case:
+          // Genomic contig ENSCHR:1, contains coding regions for ENSG01,
+          // ENSG02, ENSG03, with transcripts and products similarly named.
+          // cannot add distinct dbrefs mapping location on ENSCHR:1 to ENSG01
+          
+          // JBPNote: ?? can't actually create an example that demonstrates we
+          // need to
+          // synthesize an xref.
+          
+          for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
           {
+            // creates a complementary cross-reference to the source sequence's
+            // primary reference.
+
+            DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(),
+                    primRef.getSource() + ":" + primRef.getVersion(),
+                    primRef.getAccessionId());
+            cdsCrossRef
+                    .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap)));
+            cdsSeqDss.addDBRef(cdsCrossRef);
+
+            // problem here is that the cross-reference is synthesized -
+            // cdsSeq.getName() may be like 'CDS|dnaaccession' or
+            // 'CDS|emblcdsacc'
             // assuming cds version same as dna ?!?
-            DBRefEntry proteinToCdsRef = new DBRefEntry(dnaRef.getSource(),
-                    dnaRef.getVersion(), cdsSeq.getName());
+
+            DBRefEntry proteinToCdsRef = new DBRefEntry(
+                    primRef.getSource(), primRef.getVersion(),
+                    cdsSeq.getName());
+            //
             proteinToCdsRef.setMap(new Mapping(cdsSeqDss, cdsToProteinMap
                     .getInverse()));
             proteinProduct.addDBRef(proteinToCdsRef);
@@ -1777,7 +1899,7 @@ public class AlignmentUtils
         }
       }
     }
-
+    
     /*
      * assign 'from id' held in the mapping if set (e.g. EMBL protein_id),
      * else generate a sequence name
@@ -1791,6 +1913,84 @@ public class AlignmentUtils
   }
 
   /**
+   * add any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
+   * the given mapping.
+   * 
+   * @param cdsSeq
+   * @param contig
+   * @param mapping
+   * @return list of DBRefEntrys added.
+   */
+  public static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
+          SequenceI contig, SequenceI proteinProduct, Mapping mapping)
+  {
+
+    // gather direct refs from contig congrent with mapping
+    List<DBRefEntry> direct = new ArrayList<DBRefEntry>();
+    HashSet<String> directSources = new HashSet<String>();
+    if (contig.getDBRefs() != null)
+    {
+      for (DBRefEntry dbr : contig.getDBRefs())
+      {
+        if (dbr.hasMap() && dbr.getMap().getMap().isTripletMap())
+        {
+          MapList map = dbr.getMap().getMap();
+          // check if map is the CDS mapping
+          if (mapping.getMap().equals(map))
+          {
+            direct.add(dbr);
+            directSources.add(dbr.getSource());
+          }
+        }
+      }
+    }
+    DBRefEntry[] onSource = DBRefUtils.selectRefs(
+            proteinProduct.getDBRefs(),
+            directSources.toArray(new String[0]));
+    List<DBRefEntry> propagated = new ArrayList<DBRefEntry>();
+
+    // and generate appropriate mappings
+    for (DBRefEntry cdsref : direct)
+    {
+      // clone maplist and mapping
+      MapList cdsposmap = new MapList(Arrays.asList(new int[][] { new int[]
+      { cdsSeq.getStart(), cdsSeq.getEnd() } }), cdsref.getMap().getMap()
+              .getToRanges(), 3, 1);
+      Mapping cdsmap = new Mapping(cdsref.getMap().getTo(), cdsref.getMap()
+              .getMap());
+
+      // create dbref
+      DBRefEntry newref = new DBRefEntry(cdsref.getSource(),
+              cdsref.getVersion(), cdsref.getAccessionId(), new Mapping(
+                      cdsmap.getTo(), cdsposmap));
+
+      // and see if we can map to the protein product for this mapping.
+      // onSource is the filtered set of accessions on protein that we are
+      // tranferring, so we assume accession is the same.
+      if (cdsmap.getTo() == null && onSource != null)
+      {
+        List<DBRefEntry> sourceRefs = DBRefUtils.searchRefs(onSource,
+                cdsref.getAccessionId());
+        if (sourceRefs != null)
+        {
+          for (DBRefEntry srcref : sourceRefs)
+          {
+            if (srcref.getSource().equalsIgnoreCase(cdsref.getSource()))
+            {
+              // we have found a complementary dbref on the protein product, so
+              // update mapping's getTo
+              newref.getMap().setTo(proteinProduct);
+            }
+          }
+        }
+      }
+      cdsSeq.addDBRef(newref);
+      propagated.add(newref);
+    }
+    return propagated;
+  }
+
+  /**
    * 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.
@@ -2441,19 +2641,32 @@ public class AlignmentUtils
    */
   public static int alignAs(AlignmentI unaligned, AlignmentI aligned)
   {
+    /*
+     * easy case - aligning a copy of aligned sequences
+     */
+    if (alignAsSameSequences(unaligned, aligned))
+    {
+      return unaligned.getHeight();
+    }
+
+    /*
+     * fancy case - aligning via mappings between sequences
+     */
     List<SequenceI> unmapped = new ArrayList<SequenceI>();
     Map<Integer, Map<SequenceI, Character>> columnMap = buildMappedColumnsMap(
             unaligned, aligned, unmapped);
     int width = columnMap.size();
     char gap = unaligned.getGapCharacter();
     int realignedCount = 0;
+    // TODO: verify this loop scales sensibly for very wide/high alignments
 
     for (SequenceI seq : unaligned.getSequences())
     {
       if (!unmapped.contains(seq))
       {
         char[] newSeq = new char[width];
-        Arrays.fill(newSeq, gap);
+        Arrays.fill(newSeq, gap); // JBPComment - doubt this is faster than the
+                                  // Integer iteration below
         int newCol = 0;
         int lastCol = 0;
 
@@ -2485,6 +2698,7 @@ public class AlignmentUtils
           System.arraycopy(newSeq, 0, tmp, 0, lastCol + 1);
           newSeq = tmp;
         }
+        // TODO: optimise SequenceI to avoid char[]->String->char[]
         seq.setSequence(String.valueOf(newSeq));
         realignedCount++;
       }
@@ -2493,6 +2707,72 @@ public class AlignmentUtils
   }
 
   /**
+   * If unaligned and aligned sequences share the same dataset sequences, then
+   * simply copies the aligned sequences to the unaligned sequences and returns
+   * true; else returns false
+   * 
+   * @param unaligned
+   *          - sequences to be aligned based on aligned
+   * @param aligned
+   *          - 'guide' alignment containing sequences derived from same dataset
+   *          as unaligned
+   * @return
+   */
+  static boolean alignAsSameSequences(AlignmentI unaligned,
+          AlignmentI aligned)
+  {
+    if (aligned.getDataset() == null || unaligned.getDataset() == null)
+    {
+      return false; // should only pass alignments with datasets here
+    }
+
+    // map from dataset sequence to alignment sequence(s)
+    Map<SequenceI, List<SequenceI>> alignedDatasets = new HashMap<SequenceI, List<SequenceI>>();
+    for (SequenceI seq : aligned.getSequences())
+    {
+      SequenceI ds = seq.getDatasetSequence();
+      if (alignedDatasets.get(ds) == null)
+      {
+        alignedDatasets.put(ds, new ArrayList<SequenceI>());
+      }
+      alignedDatasets.get(ds).add(seq);
+    }
+
+    /*
+     * first pass - check whether all sequences to be aligned share a dataset
+     * sequence with an aligned sequence
+     */
+    for (SequenceI seq : unaligned.getSequences())
+    {
+      if (!alignedDatasets.containsKey(seq.getDatasetSequence()))
+      {
+        return false;
+      }
+    }
+
+    /*
+     * second pass - copy aligned sequences;
+     * heuristic rule: pair off sequences in order for the case where 
+     * more than one shares the same dataset sequence 
+     */
+    for (SequenceI seq : unaligned.getSequences())
+    {
+      List<SequenceI> alignedSequences = alignedDatasets.get(seq
+              .getDatasetSequence());
+      // TODO: getSequenceAsString() will be deprecated in the future
+      // TODO: need to leave to SequenceI implementor to update gaps
+      seq.setSequence(alignedSequences.get(0).getSequenceAsString());
+      if (alignedSequences.size() > 0)
+      {
+        // pop off aligned sequences (except the last one)
+        alignedSequences.remove(0);
+      }
+    }
+
+    return true;
+  }
+
+  /**
    * Returns a map whose key is alignment column number (base 1), and whose
    * values are a map of sequence characters in that column.
    *