457688160ac3a62cbde7005e3be741c0563d0d7e
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.analysis;
22
23 import jalview.datamodel.AlignedCodon;
24 import jalview.datamodel.AlignedCodonFrame;
25 import jalview.datamodel.AlignmentAnnotation;
26 import jalview.datamodel.AlignmentI;
27 import jalview.datamodel.Mapping;
28 import jalview.datamodel.SearchResults;
29 import jalview.datamodel.Sequence;
30 import jalview.datamodel.SequenceI;
31 import jalview.schemes.ResidueProperties;
32 import jalview.util.MapList;
33
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.Iterator;
39 import java.util.LinkedHashMap;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Set;
44 import java.util.TreeMap;
45
46 /**
47  * grab bag of useful alignment manipulation operations Expect these to be
48  * refactored elsewhere at some point.
49  * 
50  * @author jimp
51  * 
52  */
53 public class AlignmentUtils
54 {
55
56   /**
57    * given an existing alignment, create a new alignment including all, or up to
58    * flankSize additional symbols from each sequence's dataset sequence
59    * 
60    * @param core
61    * @param flankSize
62    * @return AlignmentI
63    */
64   public static AlignmentI expandContext(AlignmentI core, int flankSize)
65   {
66     List<SequenceI> sq = new ArrayList<SequenceI>();
67     int maxoffset = 0;
68     for (SequenceI s : core.getSequences())
69     {
70       SequenceI newSeq = s.deriveSequence();
71       if (newSeq.getStart() > maxoffset
72               && newSeq.getDatasetSequence().getStart() < s.getStart())
73       {
74         maxoffset = newSeq.getStart();
75       }
76       sq.add(newSeq);
77     }
78     if (flankSize > -1)
79     {
80       maxoffset = flankSize;
81     }
82     // now add offset to create a new expanded alignment
83     for (SequenceI s : sq)
84     {
85       SequenceI ds = s;
86       while (ds.getDatasetSequence() != null)
87       {
88         ds = ds.getDatasetSequence();
89       }
90       int s_end = s.findPosition(s.getStart() + s.getLength());
91       // find available flanking residues for sequence
92       int ustream_ds = s.getStart() - ds.getStart(), dstream_ds = ds
93               .getEnd() - s_end;
94
95       // build new flanked sequence
96
97       // compute gap padding to start of flanking sequence
98       int offset = maxoffset - ustream_ds;
99
100       // padding is gapChar x ( maxoffset - min(ustream_ds, flank)
101       if (flankSize >= 0)
102       {
103         if (flankSize < ustream_ds)
104         {
105           // take up to flankSize residues
106           offset = maxoffset - flankSize;
107           ustream_ds = flankSize;
108         }
109         if (flankSize < dstream_ds)
110         {
111           dstream_ds = flankSize;
112         }
113       }
114       char[] upstream = new String(ds.getSequence(s.getStart() - 1
115               - ustream_ds, s.getStart() - 1)).toLowerCase().toCharArray();
116       char[] downstream = new String(ds.getSequence(s_end - 1, s_end + 1
117               + dstream_ds)).toLowerCase().toCharArray();
118       char[] coreseq = s.getSequence();
119       char[] nseq = new char[offset + upstream.length + downstream.length
120               + coreseq.length];
121       char c = core.getGapCharacter();
122       // TODO could lowercase the flanking regions
123       int p = 0;
124       for (; p < offset; p++)
125       {
126         nseq[p] = c;
127       }
128       // s.setSequence(new String(upstream).toLowerCase()+new String(coreseq) +
129       // new String(downstream).toLowerCase());
130       System.arraycopy(upstream, 0, nseq, p, upstream.length);
131       System.arraycopy(coreseq, 0, nseq, p + upstream.length,
132               coreseq.length);
133       System.arraycopy(downstream, 0, nseq, p + coreseq.length
134               + upstream.length, downstream.length);
135       s.setSequence(new String(nseq));
136       s.setStart(s.getStart() - ustream_ds);
137       s.setEnd(s_end + downstream.length);
138     }
139     AlignmentI newAl = new jalview.datamodel.Alignment(
140             sq.toArray(new SequenceI[0]));
141     for (SequenceI s : sq)
142     {
143       if (s.getAnnotation() != null)
144       {
145         for (AlignmentAnnotation aa : s.getAnnotation())
146         {
147           newAl.addAnnotation(aa);
148         }
149       }
150     }
151     newAl.setDataset(core.getDataset());
152     return newAl;
153   }
154
155   /**
156    * Returns the index (zero-based position) of a sequence in an alignment, or
157    * -1 if not found.
158    * 
159    * @param al
160    * @param seq
161    * @return
162    */
163   public static int getSequenceIndex(AlignmentI al, SequenceI seq)
164   {
165     int result = -1;
166     int pos = 0;
167     for (SequenceI alSeq : al.getSequences())
168     {
169       if (alSeq == seq)
170       {
171         result = pos;
172         break;
173       }
174       pos++;
175     }
176     return result;
177   }
178
179   /**
180    * Returns a map of lists of sequences in the alignment, keyed by sequence
181    * name. For use in mapping between different alignment views of the same
182    * sequences.
183    * 
184    * @see jalview.datamodel.AlignmentI#getSequencesByName()
185    */
186   public static Map<String, List<SequenceI>> getSequencesByName(
187           AlignmentI al)
188   {
189     Map<String, List<SequenceI>> theMap = new LinkedHashMap<String, List<SequenceI>>();
190     for (SequenceI seq : al.getSequences())
191     {
192       String name = seq.getName();
193       if (name != null)
194       {
195         List<SequenceI> seqs = theMap.get(name);
196         if (seqs == null)
197         {
198           seqs = new ArrayList<SequenceI>();
199           theMap.put(name, seqs);
200         }
201         seqs.add(seq);
202       }
203     }
204     return theMap;
205   }
206
207   /**
208    * Build mapping of protein to cDNA alignment. Mappings are made between
209    * sequences where the cDNA translates to the protein sequence. Any new
210    * mappings are added to the protein alignment. Returns true if any mappings
211    * either already exist or were added, else false.
212    * 
213    * @param proteinAlignment
214    * @param cdnaAlignment
215    * @return
216    */
217   public static boolean mapProteinToCdna(
218           final AlignmentI proteinAlignment,
219           final AlignmentI cdnaAlignment)
220   {
221     if (proteinAlignment == null || cdnaAlignment == null)
222     {
223       return false;
224     }
225
226     Set<SequenceI> mappedDna = new HashSet<SequenceI>();
227     Set<SequenceI> mappedProtein = new HashSet<SequenceI>();
228
229     /*
230      * First pass - map sequences where cross-references exist. This include
231      * 1-to-many mappings to support, for example, variant cDNA.
232      */
233     boolean mappingPerformed = mapProteinToCdna(proteinAlignment,
234             cdnaAlignment, mappedDna, mappedProtein, true);
235
236     /*
237      * Second pass - map sequences where no cross-references exist. This only
238      * does 1-to-1 mappings and assumes corresponding sequences are in the same
239      * order in the alignments.
240      */
241     mappingPerformed |= mapProteinToCdna(proteinAlignment, cdnaAlignment,
242             mappedDna, mappedProtein, false);
243     return mappingPerformed;
244   }
245
246   /**
247    * Make mappings between compatible sequences (where the cDNA translation
248    * matches the protein).
249    * 
250    * @param proteinAlignment
251    * @param cdnaAlignment
252    * @param mappedDna
253    *          a set of mapped DNA sequences (to add to)
254    * @param mappedProtein
255    *          a set of mapped Protein sequences (to add to)
256    * @param xrefsOnly
257    *          if true, only map sequences where xrefs exist
258    * @return
259    */
260   protected static boolean mapProteinToCdna(
261           final AlignmentI proteinAlignment,
262           final AlignmentI cdnaAlignment, Set<SequenceI> mappedDna,
263           Set<SequenceI> mappedProtein, boolean xrefsOnly)
264   {
265     boolean mappingPerformed = false;
266     List<SequenceI> thisSeqs = proteinAlignment.getSequences();
267     for (SequenceI aaSeq : thisSeqs)
268     {
269       boolean proteinMapped = false;
270       AlignedCodonFrame acf = new AlignedCodonFrame();
271
272       for (SequenceI cdnaSeq : cdnaAlignment.getSequences())
273       {
274         /*
275          * Always try to map if sequences have xref to each other; this supports
276          * variant cDNA or alternative splicing for a protein sequence.
277          * 
278          * If no xrefs, try to map progressively, assuming that alignments have
279          * mappable sequences in corresponding order. These are not
280          * many-to-many, as that would risk mixing species with similar cDNA
281          * sequences.
282          */
283         if (xrefsOnly && !CrossRef.haveCrossRef(aaSeq, cdnaSeq))
284         {
285           continue;
286         }
287
288         /*
289          * Don't map non-xrefd sequences more than once each. This heuristic
290          * allows us to pair up similar sequences in ordered alignments.
291          */
292         if (!xrefsOnly
293                 && (mappedProtein.contains(aaSeq) || mappedDna
294                         .contains(cdnaSeq)))
295         {
296           continue;
297         }
298         if (!mappingExists(proteinAlignment.getCodonFrames(),
299                 aaSeq.getDatasetSequence(), cdnaSeq.getDatasetSequence()))
300         {
301           MapList map = mapProteinToCdna(aaSeq, cdnaSeq);
302           if (map != null)
303           {
304             acf.addMap(cdnaSeq, aaSeq, map);
305             mappingPerformed = true;
306             proteinMapped = true;
307             mappedDna.add(cdnaSeq);
308             mappedProtein.add(aaSeq);
309           }
310         }
311       }
312       if (proteinMapped)
313       {
314         proteinAlignment.addCodonFrame(acf);
315       }
316     }
317     return mappingPerformed;
318   }
319
320   /**
321    * Answers true if the mappings include one between the given (dataset)
322    * sequences.
323    */
324   public static boolean mappingExists(Set<AlignedCodonFrame> set,
325           SequenceI aaSeq, SequenceI cdnaSeq)
326   {
327     if (set != null)
328     {
329       for (AlignedCodonFrame acf : set)
330       {
331         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
332         {
333           return true;
334         }
335       }
336     }
337     return false;
338   }
339
340   /**
341    * Build a mapping (if possible) of a protein to a cDNA sequence. The cDNA
342    * must be three times the length of the protein, possibly after ignoring
343    * start and/or stop codons, and must translate to the protein. Returns null
344    * if no mapping is determined.
345    * 
346    * @param proteinSeqs
347    * @param cdnaSeq
348    * @return
349    */
350   public static MapList mapProteinToCdna(SequenceI proteinSeq,
351           SequenceI cdnaSeq)
352   {
353     /*
354      * Here we handle either dataset sequence set (desktop) or absent (applet).
355      * Use only the char[] form of the sequence to avoid creating possibly large
356      * String objects.
357      */
358     final SequenceI proteinDataset = proteinSeq.getDatasetSequence();
359     char[] aaSeqChars = proteinDataset != null ? proteinDataset
360             .getSequence() : proteinSeq.getSequence();
361     final SequenceI cdnaDataset = cdnaSeq.getDatasetSequence();
362     char[] cdnaSeqChars = cdnaDataset != null ? cdnaDataset.getSequence()
363             : cdnaSeq.getSequence();
364     if (aaSeqChars == null || cdnaSeqChars == null)
365     {
366       return null;
367     }
368
369     /*
370      * cdnaStart/End, proteinStartEnd are base 1 (for dataset sequence mapping)
371      */
372     final int mappedLength = 3 * aaSeqChars.length;
373     int cdnaLength = cdnaSeqChars.length;
374     int cdnaStart = 1;
375     int cdnaEnd = cdnaLength;
376     final int proteinStart = 1;
377     final int proteinEnd = aaSeqChars.length;
378
379     /*
380      * If lengths don't match, try ignoring stop codon.
381      */
382     if (cdnaLength != mappedLength && cdnaLength > 2)
383     {
384       String lastCodon = String.valueOf(cdnaSeqChars, cdnaLength - 3, 3)
385               .toUpperCase();
386       for (String stop : ResidueProperties.STOP)
387       {
388         if (lastCodon.equals(stop))
389         {
390           cdnaEnd -= 3;
391           cdnaLength -= 3;
392           break;
393         }
394       }
395     }
396
397     /*
398      * If lengths still don't match, try ignoring start codon.
399      */
400     if (cdnaLength != mappedLength
401             && cdnaLength > 2
402             && String.valueOf(cdnaSeqChars, 0, 3).toUpperCase()
403                     .equals(
404                     ResidueProperties.START))
405     {
406       cdnaStart += 3;
407       cdnaLength -= 3;
408     }
409
410     if (cdnaLength != mappedLength)
411     {
412       return null;
413     }
414     if (!translatesAs(cdnaSeqChars, cdnaStart - 1, aaSeqChars))
415     {
416       return null;
417     }
418     MapList map = new MapList(new int[]
419     { cdnaStart, cdnaEnd }, new int[]
420     { proteinStart, proteinEnd }, 3, 1);
421     return map;
422   }
423
424   /**
425    * Test whether the given cdna sequence, starting at the given offset,
426    * translates to the given amino acid sequence, using the standard translation
427    * table. Designed to fail fast i.e. as soon as a mismatch position is found.
428    * 
429    * @param cdnaSeqChars
430    * @param cdnaStart
431    * @param aaSeqChars
432    * @return
433    */
434   protected static boolean translatesAs(char[] cdnaSeqChars, int cdnaStart,
435           char[] aaSeqChars)
436   {
437     int aaResidue = 0;
438     for (int i = cdnaStart; i < cdnaSeqChars.length - 2
439             && aaResidue < aaSeqChars.length; i += 3, aaResidue++)
440     {
441       String codon = String.valueOf(cdnaSeqChars, i, 3);
442       final String translated = ResidueProperties.codonTranslate(
443               codon);
444       /*
445        * ? allow X in protein to match untranslatable in dna ?
446        */
447       final char aaRes = aaSeqChars[aaResidue];
448       if ((translated == null || "STOP".equals(translated)) && aaRes == 'X')
449       {
450         continue;
451       }
452       if (translated == null
453               || !(aaRes == translated.charAt(0)))
454       {
455         // debug
456         System.out.println(("Mismatch at " + i + "/" + aaResidue + ": "
457                 + codon + "(" + translated + ") != " + aaRes));
458         return false;
459       }
460     }
461     // fail if we didn't match all of the aa sequence
462     return (aaResidue == aaSeqChars.length);
463   }
464
465   /**
466    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
467    * currently assumes that we are aligning cDNA to match protein.
468    * 
469    * @param seq
470    *          the sequence to be realigned
471    * @param al
472    *          the alignment whose sequence alignment is to be 'copied'
473    * @param gap
474    *          character string represent a gap in the realigned sequence
475    * @param preserveUnmappedGaps
476    * @param preserveMappedGaps
477    * @return true if the sequence was realigned, false if it could not be
478    */
479   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
480           String gap, boolean preserveMappedGaps,
481           boolean preserveUnmappedGaps)
482   {
483     /*
484      * Get any mappings from the source alignment to the target (dataset) sequence.
485      */
486     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
487     // all mappings. Would it help to constrain this?
488     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
489     if (mappings == null || mappings.isEmpty())
490     {
491       return false;
492     }
493   
494     /*
495      * Locate the aligned source sequence whose dataset sequence is mapped. We
496      * just take the first match here (as we can't align cDNA like more than one
497      * protein sequence).
498      */
499     SequenceI alignFrom = null;
500     AlignedCodonFrame mapping = null;
501     for (AlignedCodonFrame mp : mappings)
502     {
503       alignFrom = mp.findAlignedSequence(seq.getDatasetSequence(), al);
504       if (alignFrom != null)
505       {
506         mapping = mp;
507         break;
508       }
509     }
510   
511     if (alignFrom == null)
512     {
513       return false;
514     }
515     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
516             preserveMappedGaps, preserveUnmappedGaps);
517     return true;
518   }
519
520   /**
521    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
522    * match residues and codons. Flags control whether existing gaps in unmapped
523    * (intron) and mapped (exon) regions are preserved or not. Gaps linking intro
524    * and exon are only retained if both flags are set.
525    * 
526    * @param alignTo
527    * @param alignFrom
528    * @param mapping
529    * @param myGap
530    * @param sourceGap
531    * @param preserveUnmappedGaps
532    * @param preserveMappedGaps
533    */
534   public static void alignSequenceAs(SequenceI alignTo,
535           SequenceI alignFrom,
536           AlignedCodonFrame mapping, String myGap, char sourceGap,
537           boolean preserveMappedGaps, boolean preserveUnmappedGaps)
538   {
539     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
540     final char[] thisSeq = alignTo.getSequence();
541     final char[] thatAligned = alignFrom.getSequence();
542     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
543   
544     // aligned and dataset sequence positions, all base zero
545     int thisSeqPos = 0;
546     int sourceDsPos = 0;
547
548     int basesWritten = 0;
549     char myGapChar = myGap.charAt(0);
550     int ratio = myGap.length();
551
552     /*
553      * Traverse the aligned protein sequence.
554      */
555     int sourceGapMappedLength = 0;
556     boolean inExon = false;
557     for (char sourceChar : thatAligned)
558     {
559       if (sourceChar == sourceGap)
560       {
561         sourceGapMappedLength += ratio;
562         continue;
563       }
564
565       /*
566        * Found a residue. Locate its mapped codon (start) position.
567        */
568       sourceDsPos++;
569       // Note mapping positions are base 1, our sequence positions base 0
570       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
571               sourceDsPos);
572       if (mappedPos == null)
573       {
574         /*
575          * Abort realignment if unmapped protein. Or could ignore it??
576          */
577         System.err.println("Can't align: no codon mapping to residue "
578                 + sourceDsPos + "(" + sourceChar + ")");
579         return;
580       }
581
582       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
583       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
584       StringBuilder trailingCopiedGap = new StringBuilder();
585
586       /*
587        * Copy dna sequence up to and including this codon. Optionally, include
588        * gaps before the codon starts (in introns) and/or after the codon starts
589        * (in exons).
590        * 
591        * Note this only works for 'linear' splicing, not reverse or interleaved.
592        * But then 'align dna as protein' doesn't make much sense otherwise.
593        */
594       int intronLength = 0;
595       while (basesWritten < mappedCodonEnd && thisSeqPos < thisSeq.length)
596       {
597         final char c = thisSeq[thisSeqPos++];
598         if (c != myGapChar)
599         {
600           basesWritten++;
601
602           if (basesWritten < mappedCodonStart)
603           {
604             /*
605              * Found an unmapped (intron) base. First add in any preceding gaps
606              * (if wanted).
607              */
608             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
609             {
610               thisAligned.append(trailingCopiedGap.toString());
611               intronLength += trailingCopiedGap.length();
612               trailingCopiedGap = new StringBuilder();
613             }
614             intronLength++;
615             inExon = false;
616           }
617           else
618           {
619             final boolean startOfCodon = basesWritten == mappedCodonStart;
620             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
621                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
622                     trailingCopiedGap.length(), intronLength, startOfCodon);
623             for (int i = 0; i < gapsToAdd; i++)
624             {
625               thisAligned.append(myGapChar);
626             }
627             sourceGapMappedLength = 0;
628             inExon = true;
629           }
630           thisAligned.append(c);
631           trailingCopiedGap = new StringBuilder();
632         }
633         else
634         {
635           if (inExon && preserveMappedGaps)
636           {
637             trailingCopiedGap.append(myGapChar);
638           }
639           else if (!inExon && preserveUnmappedGaps)
640           {
641             trailingCopiedGap.append(myGapChar);
642           }
643         }
644       }
645     }
646
647     /*
648      * At end of protein sequence. Copy any remaining dna sequence, optionally
649      * including (intron) gaps. We do not copy trailing gaps in protein.
650      */
651     while (thisSeqPos < thisSeq.length)
652     {
653       final char c = thisSeq[thisSeqPos++];
654       if (c != myGapChar || preserveUnmappedGaps)
655       {
656         thisAligned.append(c);
657       }
658     }
659
660     /*
661      * All done aligning, set the aligned sequence.
662      */
663     alignTo.setSequence(new String(thisAligned));
664   }
665
666   /**
667    * Helper method to work out how many gaps to insert when realigning.
668    * 
669    * @param preserveMappedGaps
670    * @param preserveUnmappedGaps
671    * @param sourceGapMappedLength
672    * @param inExon
673    * @param trailingCopiedGap
674    * @param intronLength
675    * @param startOfCodon
676    * @return
677    */
678   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
679           boolean preserveUnmappedGaps, int sourceGapMappedLength,
680           boolean inExon, int trailingGapLength,
681           int intronLength, final boolean startOfCodon)
682   {
683     int gapsToAdd = 0;
684     if (startOfCodon)
685     {
686       /*
687        * Reached start of codon. Ignore trailing gaps in intron unless we are
688        * preserving gaps in both exon and intron. Ignore them anyway if the
689        * protein alignment introduces a gap at least as large as the intronic
690        * region.
691        */
692       if (inExon && !preserveMappedGaps)
693       {
694         trailingGapLength = 0;
695       }
696       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
697       {
698         trailingGapLength = 0;
699       }
700       if (inExon)
701       {
702         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
703       }
704       else
705       {
706         if (intronLength + trailingGapLength <= sourceGapMappedLength)
707         {
708           gapsToAdd = sourceGapMappedLength - intronLength;
709         }
710         else
711         {
712           gapsToAdd = Math.min(intronLength + trailingGapLength
713                   - sourceGapMappedLength, trailingGapLength);
714         }
715       }
716     }
717     else
718     {
719       /*
720        * second or third base of codon; check for any gaps in dna
721        */
722       if (!preserveMappedGaps)
723       {
724         trailingGapLength = 0;
725       }
726       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
727     }
728     return gapsToAdd;
729   }
730
731   /**
732    * Returns a list of sequences mapped from the given sequences and aligned
733    * (gapped) in the same way. For example, the cDNA for aligned protein, where
734    * a single gap in protein generates three gaps in cDNA.
735    * 
736    * @param sequences
737    * @param gapCharacter
738    * @param mappings
739    * @return
740    */
741   public static List<SequenceI> getAlignedTranslation(
742           List<SequenceI> sequences, char gapCharacter,
743           Set<AlignedCodonFrame> mappings)
744   {
745     List<SequenceI> alignedSeqs = new ArrayList<SequenceI>();
746
747     for (SequenceI seq : sequences)
748     {
749       List<SequenceI> mapped = getAlignedTranslation(seq, gapCharacter,
750               mappings);
751       alignedSeqs.addAll(mapped);
752     }
753     return alignedSeqs;
754   }
755
756   /**
757    * Returns sequences aligned 'like' the source sequence, as mapped by the
758    * given mappings. Normally we expect zero or one 'mapped' sequences, but this
759    * will support 1-to-many as well.
760    * 
761    * @param seq
762    * @param gapCharacter
763    * @param mappings
764    * @return
765    */
766   protected static List<SequenceI> getAlignedTranslation(SequenceI seq,
767           char gapCharacter, Set<AlignedCodonFrame> mappings)
768   {
769     List<SequenceI> result = new ArrayList<SequenceI>();
770     for (AlignedCodonFrame mapping : mappings)
771     {
772       if (mapping.involvesSequence(seq))
773       {
774         SequenceI mapped = getAlignedTranslation(seq, gapCharacter, mapping);
775         if (mapped != null)
776         {
777           result.add(mapped);
778         }
779       }
780     }
781     return result;
782   }
783
784   /**
785    * Returns the translation of 'seq' (as held in the mapping) with
786    * corresponding alignment (gaps).
787    * 
788    * @param seq
789    * @param gapCharacter
790    * @param mapping
791    * @return
792    */
793   protected static SequenceI getAlignedTranslation(SequenceI seq,
794           char gapCharacter, AlignedCodonFrame mapping)
795   {
796     String gap = String.valueOf(gapCharacter);
797     boolean toDna = false;
798     int fromRatio = 1;
799     SequenceI mapTo = mapping.getDnaForAaSeq(seq);
800     if (mapTo != null)
801     {
802       // mapping is from protein to nucleotide
803       toDna = true;
804       // should ideally get gap count ratio from mapping
805       gap = String.valueOf(new char[]
806       { gapCharacter, gapCharacter, gapCharacter });
807     }
808     else
809     {
810       // mapping is from nucleotide to protein
811       mapTo = mapping.getAaForDnaSeq(seq);
812       fromRatio = 3;
813     }
814     StringBuilder newseq = new StringBuilder(seq.getLength()
815             * (toDna ? 3 : 1));
816
817     int residueNo = 0; // in seq, base 1
818     int[] phrase = new int[fromRatio];
819     int phraseOffset = 0;
820     int gapWidth = 0;
821     boolean first = true;
822     final Sequence alignedSeq = new Sequence("", "");
823
824     for (char c : seq.getSequence())
825     {
826       if (c == gapCharacter)
827       {
828         gapWidth++;
829         if (gapWidth >= fromRatio)
830         {
831           newseq.append(gap);
832           gapWidth = 0;
833         }
834       }
835       else
836       {
837         phrase[phraseOffset++] = residueNo + 1;
838         if (phraseOffset == fromRatio)
839         {
840           /*
841            * Have read a whole codon (or protein residue), now translate: map
842            * source phrase to positions in target sequence add characters at
843            * these positions to newseq Note mapping positions are base 1, our
844            * sequence positions base 0.
845            */
846           SearchResults sr = new SearchResults();
847           for (int pos : phrase)
848           {
849             mapping.markMappedRegion(seq, pos, sr);
850           }
851           newseq.append(sr.toString());
852           if (first)
853           {
854             first = false;
855             // Hack: Copy sequence dataset, name and description from
856             // SearchResults.match[0].sequence
857             // TODO? carry over sequence names from original 'complement'
858             // alignment
859             SequenceI mappedTo = sr.getResultSequence(0);
860             alignedSeq.setName(mappedTo.getName());
861             alignedSeq.setDescription(mappedTo.getDescription());
862             alignedSeq.setDatasetSequence(mappedTo);
863           }
864           phraseOffset = 0;
865         }
866         residueNo++;
867       }
868     }
869     alignedSeq.setSequence(newseq.toString());
870     return alignedSeq;
871   }
872
873   /**
874    * Realigns the given protein to match the alignment of the dna, using codon
875    * mappings to translate aligned codon positions to protein residues.
876    * 
877    * @param protein
878    *          the alignment whose sequences are realigned by this method
879    * @param dna
880    *          the dna alignment whose alignment we are 'copying'
881    * @return the number of sequences that were realigned
882    */
883   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
884   {
885     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
886
887     /*
888      * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
889      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
890      * comparator keeps the codon positions ordered.
891      */
892     Map<AlignedCodon, Map<SequenceI, String>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, String>>(
893             new CodonComparator());
894     for (SequenceI dnaSeq : dna.getSequences())
895     {
896       for (AlignedCodonFrame mapping : mappings)
897       {
898         Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
899         SequenceI prot = mapping.findAlignedSequence(
900                 dnaSeq.getDatasetSequence(), protein);
901         if (prot != null)
902         {
903           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
904                   seqMap, alignedCodons);
905         }
906       }
907     }
908     return alignProteinAs(protein, alignedCodons);
909   }
910
911   /**
912    * Update the aligned protein sequences to match the codon alignments given in
913    * the map.
914    * 
915    * @param protein
916    * @param alignedCodons
917    *          an ordered map of codon positions (columns), with sequence/peptide
918    *          values present in each column
919    * @return
920    */
921   protected static int alignProteinAs(AlignmentI protein,
922           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
923   {
924     /*
925      * Prefill aligned sequences with gaps before inserting aligned protein
926      * residues.
927      */
928     int alignedWidth = alignedCodons.size();
929     char[] gaps = new char[alignedWidth];
930     Arrays.fill(gaps, protein.getGapCharacter());
931     String allGaps = String.valueOf(gaps);
932     for (SequenceI seq : protein.getSequences())
933     {
934       seq.setSequence(allGaps);
935     }
936
937     int column = 0;
938     for (AlignedCodon codon : alignedCodons.keySet())
939     {
940       final Map<SequenceI, String> columnResidues = alignedCodons.get(codon);
941       for (Entry<SequenceI, String> entry : columnResidues
942               .entrySet())
943       {
944         // place translated codon at its column position in sequence
945         entry.getKey().getSequence()[column] = entry.getValue().charAt(0);
946       }
947       column++;
948     }
949     return 0;
950   }
951
952   /**
953    * Populate the map of aligned codons by traversing the given sequence
954    * mapping, locating the aligned positions of mapped codons, and adding those
955    * positions and their translation products to the map.
956    * 
957    * @param dna
958    *          the aligned sequence we are mapping from
959    * @param protein
960    *          the sequence to be aligned to the codons
961    * @param gapChar
962    *          the gap character in the dna sequence
963    * @param seqMap
964    *          a mapping to a sequence translation
965    * @param alignedCodons
966    *          the map we are building up
967    */
968   static void addCodonPositions(SequenceI dna, SequenceI protein,
969           char gapChar,
970           Mapping seqMap,
971           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
972   {
973     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
974     while (codons.hasNext())
975     {
976       AlignedCodon codon = codons.next();
977       Map<SequenceI, String> seqProduct = alignedCodons.get(codon);
978       if (seqProduct == null)
979       {
980         seqProduct = new HashMap<SequenceI, String>();
981         alignedCodons.put(codon, seqProduct);
982       }
983       seqProduct.put(protein, codon.product);
984     }
985   }
986
987   /**
988    * Returns true if a cDNA/Protein mapping either exists, or could be made,
989    * between at least one pair of sequences in the two alignments. Currently,
990    * the logic is:
991    * <ul>
992    * <li>One alignment must be nucleotide, and the other protein</li>
993    * <li>At least one pair of sequences must be already mapped, or mappable</li>
994    * <li>Mappable means the nucleotide translation matches the protein sequence</li>
995    * <li>The translation may ignore start and stop codons if present in the
996    * nucleotide</li>
997    * </ul>
998    * 
999    * @param al1
1000    * @param al2
1001    * @return
1002    */
1003   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
1004   {
1005     /*
1006      * Require one nucleotide and one protein
1007      */
1008     if (al1.isNucleotide() == al2.isNucleotide())
1009     {
1010       return false;
1011     }
1012     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
1013     AlignmentI protein = dna == al1 ? al2 : al1;
1014     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
1015     for (SequenceI dnaSeq : dna.getSequences())
1016     {
1017       for (SequenceI proteinSeq : protein.getSequences())
1018       {
1019         if (isMappable(dnaSeq, proteinSeq, mappings))
1020         {
1021           return true;
1022         }
1023       }
1024     }
1025     return false;
1026   }
1027
1028   /**
1029    * Returns true if the dna sequence is mapped, or could be mapped, to the
1030    * protein sequence.
1031    * 
1032    * @param dnaSeq
1033    * @param proteinSeq
1034    * @param mappings
1035    * @return
1036    */
1037   public static boolean isMappable(SequenceI dnaSeq, SequenceI proteinSeq,
1038           Set<AlignedCodonFrame> mappings)
1039   {
1040     SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq.getDatasetSequence();
1041     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
1042             : proteinSeq.getDatasetSequence();
1043     
1044     /*
1045      * Already mapped?
1046      */
1047     for (AlignedCodonFrame mapping : mappings) {
1048       if ( proteinDs == mapping.getAaForDnaSeq(dnaDs)) {
1049         return true;
1050       }
1051     }
1052
1053     /*
1054      * Just try to make a mapping (it is not yet stored), test whether
1055      * successful.
1056      */
1057     return mapProteinToCdna(proteinDs, dnaDs) != null;
1058   }
1059 }