988f2d1118a3184fe5ebc9a140b4ea333a9be1db
[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 && aaRes == 'X')
449       {
450         continue;
451       }
452       if (translated == null
453               || !(aaRes == translated.charAt(0)))
454       {
455         return false;
456       }
457     }
458     // fail if we didn't match all of the aa sequence
459     return (aaResidue == aaSeqChars.length);
460   }
461
462   /**
463    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
464    * currently assumes that we are aligning cDNA to match protein.
465    * 
466    * @param seq
467    *          the sequence to be realigned
468    * @param al
469    *          the alignment whose sequence alignment is to be 'copied'
470    * @param gap
471    *          character string represent a gap in the realigned sequence
472    * @param preserveUnmappedGaps
473    * @param preserveMappedGaps
474    * @return true if the sequence was realigned, false if it could not be
475    */
476   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
477           String gap, boolean preserveMappedGaps,
478           boolean preserveUnmappedGaps)
479   {
480     /*
481      * Get any mappings from the source alignment to the target (dataset) sequence.
482      */
483     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
484     // all mappings. Would it help to constrain this?
485     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
486     if (mappings == null || mappings.isEmpty())
487     {
488       return false;
489     }
490   
491     /*
492      * Locate the aligned source sequence whose dataset sequence is mapped. We
493      * just take the first match here (as we can't align cDNA like more than one
494      * protein sequence).
495      */
496     SequenceI alignFrom = null;
497     AlignedCodonFrame mapping = null;
498     for (AlignedCodonFrame mp : mappings)
499     {
500       alignFrom = mp.findAlignedSequence(seq.getDatasetSequence(), al);
501       if (alignFrom != null)
502       {
503         mapping = mp;
504         break;
505       }
506     }
507   
508     if (alignFrom == null)
509     {
510       return false;
511     }
512     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
513             preserveMappedGaps, preserveUnmappedGaps);
514     return true;
515   }
516
517   /**
518    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
519    * match residues and codons. Flags control whether existing gaps in unmapped
520    * (intron) and mapped (exon) regions are preserved or not. Gaps linking intro
521    * and exon are only retained if both flags are set.
522    * 
523    * @param alignTo
524    * @param alignFrom
525    * @param mapping
526    * @param myGap
527    * @param sourceGap
528    * @param preserveUnmappedGaps
529    * @param preserveMappedGaps
530    */
531   public static void alignSequenceAs(SequenceI alignTo,
532           SequenceI alignFrom,
533           AlignedCodonFrame mapping, String myGap, char sourceGap,
534           boolean preserveMappedGaps, boolean preserveUnmappedGaps)
535   {
536     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
537     final char[] thisSeq = alignTo.getSequence();
538     final char[] thatAligned = alignFrom.getSequence();
539     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
540   
541     // aligned and dataset sequence positions, all base zero
542     int thisSeqPos = 0;
543     int sourceDsPos = 0;
544
545     int basesWritten = 0;
546     char myGapChar = myGap.charAt(0);
547     int ratio = myGap.length();
548
549     /*
550      * Traverse the aligned protein sequence.
551      */
552     int sourceGapMappedLength = 0;
553     boolean inExon = false;
554     for (char sourceChar : thatAligned)
555     {
556       if (sourceChar == sourceGap)
557       {
558         sourceGapMappedLength += ratio;
559         continue;
560       }
561
562       /*
563        * Found a residue. Locate its mapped codon (start) position.
564        */
565       sourceDsPos++;
566       // Note mapping positions are base 1, our sequence positions base 0
567       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
568               sourceDsPos);
569       if (mappedPos == null)
570       {
571         /*
572          * Abort realignment if unmapped protein. Or could ignore it??
573          */
574         System.err.println("Can't align: no codon mapping to residue "
575                 + sourceDsPos + "(" + sourceChar + ")");
576         return;
577       }
578
579       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
580       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
581       StringBuilder trailingCopiedGap = new StringBuilder();
582
583       /*
584        * Copy dna sequence up to and including this codon. Optionally, include
585        * gaps before the codon starts (in introns) and/or after the codon starts
586        * (in exons).
587        * 
588        * Note this only works for 'linear' splicing, not reverse or interleaved.
589        * But then 'align dna as protein' doesn't make much sense otherwise.
590        */
591       int intronLength = 0;
592       while (basesWritten < mappedCodonEnd && thisSeqPos < thisSeq.length)
593       {
594         final char c = thisSeq[thisSeqPos++];
595         if (c != myGapChar)
596         {
597           basesWritten++;
598
599           if (basesWritten < mappedCodonStart)
600           {
601             /*
602              * Found an unmapped (intron) base. First add in any preceding gaps
603              * (if wanted).
604              */
605             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
606             {
607               thisAligned.append(trailingCopiedGap.toString());
608               intronLength += trailingCopiedGap.length();
609               trailingCopiedGap = new StringBuilder();
610             }
611             intronLength++;
612             inExon = false;
613           }
614           else
615           {
616             final boolean startOfCodon = basesWritten == mappedCodonStart;
617             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
618                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
619                     trailingCopiedGap.length(), intronLength, startOfCodon);
620             for (int i = 0; i < gapsToAdd; i++)
621             {
622               thisAligned.append(myGapChar);
623             }
624             sourceGapMappedLength = 0;
625             inExon = true;
626           }
627           thisAligned.append(c);
628           trailingCopiedGap = new StringBuilder();
629         }
630         else
631         {
632           if (inExon && preserveMappedGaps)
633           {
634             trailingCopiedGap.append(myGapChar);
635           }
636           else if (!inExon && preserveUnmappedGaps)
637           {
638             trailingCopiedGap.append(myGapChar);
639           }
640         }
641       }
642     }
643
644     /*
645      * At end of protein sequence. Copy any remaining dna sequence, optionally
646      * including (intron) gaps. We do not copy trailing gaps in protein.
647      */
648     while (thisSeqPos < thisSeq.length)
649     {
650       final char c = thisSeq[thisSeqPos++];
651       if (c != myGapChar || preserveUnmappedGaps)
652       {
653         thisAligned.append(c);
654       }
655     }
656
657     /*
658      * All done aligning, set the aligned sequence.
659      */
660     alignTo.setSequence(new String(thisAligned));
661   }
662
663   /**
664    * Helper method to work out how many gaps to insert when realigning.
665    * 
666    * @param preserveMappedGaps
667    * @param preserveUnmappedGaps
668    * @param sourceGapMappedLength
669    * @param inExon
670    * @param trailingCopiedGap
671    * @param intronLength
672    * @param startOfCodon
673    * @return
674    */
675   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
676           boolean preserveUnmappedGaps, int sourceGapMappedLength,
677           boolean inExon, int trailingGapLength,
678           int intronLength, final boolean startOfCodon)
679   {
680     int gapsToAdd = 0;
681     if (startOfCodon)
682     {
683       /*
684        * Reached start of codon. Ignore trailing gaps in intron unless we are
685        * preserving gaps in both exon and intron. Ignore them anyway if the
686        * protein alignment introduces a gap at least as large as the intronic
687        * region.
688        */
689       if (inExon && !preserveMappedGaps)
690       {
691         trailingGapLength = 0;
692       }
693       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
694       {
695         trailingGapLength = 0;
696       }
697       if (inExon)
698       {
699         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
700       }
701       else
702       {
703         if (intronLength + trailingGapLength <= sourceGapMappedLength)
704         {
705           gapsToAdd = sourceGapMappedLength - intronLength;
706         }
707         else
708         {
709           gapsToAdd = Math.min(intronLength + trailingGapLength
710                   - sourceGapMappedLength, trailingGapLength);
711         }
712       }
713     }
714     else
715     {
716       /*
717        * second or third base of codon; check for any gaps in dna
718        */
719       if (!preserveMappedGaps)
720       {
721         trailingGapLength = 0;
722       }
723       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
724     }
725     return gapsToAdd;
726   }
727
728   /**
729    * Returns a list of sequences mapped from the given sequences and aligned
730    * (gapped) in the same way. For example, the cDNA for aligned protein, where
731    * a single gap in protein generates three gaps in cDNA.
732    * 
733    * @param sequences
734    * @param gapCharacter
735    * @param mappings
736    * @return
737    */
738   public static List<SequenceI> getAlignedTranslation(
739           List<SequenceI> sequences, char gapCharacter,
740           Set<AlignedCodonFrame> mappings)
741   {
742     List<SequenceI> alignedSeqs = new ArrayList<SequenceI>();
743
744     for (SequenceI seq : sequences)
745     {
746       List<SequenceI> mapped = getAlignedTranslation(seq, gapCharacter,
747               mappings);
748       alignedSeqs.addAll(mapped);
749     }
750     return alignedSeqs;
751   }
752
753   /**
754    * Returns sequences aligned 'like' the source sequence, as mapped by the
755    * given mappings. Normally we expect zero or one 'mapped' sequences, but this
756    * will support 1-to-many as well.
757    * 
758    * @param seq
759    * @param gapCharacter
760    * @param mappings
761    * @return
762    */
763   protected static List<SequenceI> getAlignedTranslation(SequenceI seq,
764           char gapCharacter, Set<AlignedCodonFrame> mappings)
765   {
766     List<SequenceI> result = new ArrayList<SequenceI>();
767     for (AlignedCodonFrame mapping : mappings)
768     {
769       if (mapping.involvesSequence(seq))
770       {
771         SequenceI mapped = getAlignedTranslation(seq, gapCharacter, mapping);
772         if (mapped != null)
773         {
774           result.add(mapped);
775         }
776       }
777     }
778     return result;
779   }
780
781   /**
782    * Returns the translation of 'seq' (as held in the mapping) with
783    * corresponding alignment (gaps).
784    * 
785    * @param seq
786    * @param gapCharacter
787    * @param mapping
788    * @return
789    */
790   protected static SequenceI getAlignedTranslation(SequenceI seq,
791           char gapCharacter, AlignedCodonFrame mapping)
792   {
793     String gap = String.valueOf(gapCharacter);
794     boolean toDna = false;
795     int fromRatio = 1;
796     SequenceI mapTo = mapping.getDnaForAaSeq(seq);
797     if (mapTo != null)
798     {
799       // mapping is from protein to nucleotide
800       toDna = true;
801       // should ideally get gap count ratio from mapping
802       gap = String.valueOf(new char[]
803       { gapCharacter, gapCharacter, gapCharacter });
804     }
805     else
806     {
807       // mapping is from nucleotide to protein
808       mapTo = mapping.getAaForDnaSeq(seq);
809       fromRatio = 3;
810     }
811     StringBuilder newseq = new StringBuilder(seq.getLength()
812             * (toDna ? 3 : 1));
813
814     int residueNo = 0; // in seq, base 1
815     int[] phrase = new int[fromRatio];
816     int phraseOffset = 0;
817     int gapWidth = 0;
818     boolean first = true;
819     final Sequence alignedSeq = new Sequence("", "");
820
821     for (char c : seq.getSequence())
822     {
823       if (c == gapCharacter)
824       {
825         gapWidth++;
826         if (gapWidth >= fromRatio)
827         {
828           newseq.append(gap);
829           gapWidth = 0;
830         }
831       }
832       else
833       {
834         phrase[phraseOffset++] = residueNo + 1;
835         if (phraseOffset == fromRatio)
836         {
837           /*
838            * Have read a whole codon (or protein residue), now translate: map
839            * source phrase to positions in target sequence add characters at
840            * these positions to newseq Note mapping positions are base 1, our
841            * sequence positions base 0.
842            */
843           SearchResults sr = new SearchResults();
844           for (int pos : phrase)
845           {
846             mapping.markMappedRegion(seq, pos, sr);
847           }
848           newseq.append(sr.toString());
849           if (first)
850           {
851             first = false;
852             // Hack: Copy sequence dataset, name and description from
853             // SearchResults.match[0].sequence
854             // TODO? carry over sequence names from original 'complement'
855             // alignment
856             SequenceI mappedTo = sr.getResultSequence(0);
857             alignedSeq.setName(mappedTo.getName());
858             alignedSeq.setDescription(mappedTo.getDescription());
859             alignedSeq.setDatasetSequence(mappedTo);
860           }
861           phraseOffset = 0;
862         }
863         residueNo++;
864       }
865     }
866     alignedSeq.setSequence(newseq.toString());
867     return alignedSeq;
868   }
869
870   /**
871    * Realigns the given protein to match the alignment of the dna, using codon
872    * mappings to translate aligned codon positions to protein residues.
873    * 
874    * @param protein
875    *          the alignment whose sequences are realigned by this method
876    * @param dna
877    *          the dna alignment whose alignment we are 'copying'
878    * @return the number of sequences that were realigned
879    */
880   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
881   {
882     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
883
884     /*
885      * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
886      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
887      * comparator keeps the codon positions ordered.
888      */
889     Map<AlignedCodon, Map<SequenceI, String>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, String>>(
890             new CodonComparator());
891     for (SequenceI dnaSeq : dna.getSequences())
892     {
893       for (AlignedCodonFrame mapping : mappings)
894       {
895         Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
896         SequenceI prot = mapping.findAlignedSequence(
897                 dnaSeq.getDatasetSequence(), protein);
898         if (prot != null)
899         {
900           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
901                   seqMap, alignedCodons);
902         }
903       }
904     }
905     return alignProteinAs(protein, alignedCodons);
906   }
907
908   /**
909    * Update the aligned protein sequences to match the codon alignments given in
910    * the map.
911    * 
912    * @param protein
913    * @param alignedCodons
914    *          an ordered map of codon positions (columns), with sequence/peptide
915    *          values present in each column
916    * @return
917    */
918   protected static int alignProteinAs(AlignmentI protein,
919           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
920   {
921     /*
922      * Prefill aligned sequences with gaps before inserting aligned protein
923      * residues.
924      */
925     int alignedWidth = alignedCodons.size();
926     char[] gaps = new char[alignedWidth];
927     Arrays.fill(gaps, protein.getGapCharacter());
928     String allGaps = String.valueOf(gaps);
929     for (SequenceI seq : protein.getSequences())
930     {
931       seq.setSequence(allGaps);
932     }
933
934     int column = 0;
935     for (AlignedCodon codon : alignedCodons.keySet())
936     {
937       final Map<SequenceI, String> columnResidues = alignedCodons.get(codon);
938       for (Entry<SequenceI, String> entry : columnResidues
939               .entrySet())
940       {
941         // place translated codon at its column position in sequence
942         entry.getKey().getSequence()[column] = entry.getValue().charAt(0);
943       }
944       column++;
945     }
946     return 0;
947   }
948
949   /**
950    * Populate the map of aligned codons by traversing the given sequence
951    * mapping, locating the aligned positions of mapped codons, and adding those
952    * positions and their translation products to the map.
953    * 
954    * @param dna
955    *          the aligned sequence we are mapping from
956    * @param protein
957    *          the sequence to be aligned to the codons
958    * @param gapChar
959    *          the gap character in the dna sequence
960    * @param seqMap
961    *          a mapping to a sequence translation
962    * @param alignedCodons
963    *          the map we are building up
964    */
965   static void addCodonPositions(SequenceI dna, SequenceI protein,
966           char gapChar,
967           Mapping seqMap,
968           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
969   {
970     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
971     while (codons.hasNext())
972     {
973       AlignedCodon codon = codons.next();
974       Map<SequenceI, String> seqProduct = alignedCodons.get(codon);
975       if (seqProduct == null)
976       {
977         seqProduct = new HashMap<SequenceI, String>();
978         alignedCodons.put(codon, seqProduct);
979       }
980       seqProduct.put(protein, codon.product);
981     }
982   }
983
984   /**
985    * Returns true if a cDNA/Protein mapping either exists, or could be made,
986    * between at least one pair of sequences in the two alignments. Currently,
987    * the logic is:
988    * <ul>
989    * <li>One alignment must be nucleotide, and the other protein</li>
990    * <li>At least one pair of sequences must be already mapped, or mappable</li>
991    * <li>Mappable means the nucleotide translation matches the protein sequence</li>
992    * <li>The translation may ignore start and stop codons if present in the
993    * nucleotide</li>
994    * </ul>
995    * 
996    * @param al1
997    * @param al2
998    * @return
999    */
1000   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
1001   {
1002     /*
1003      * Require one nucleotide and one protein
1004      */
1005     if (al1.isNucleotide() == al2.isNucleotide())
1006     {
1007       return false;
1008     }
1009     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
1010     AlignmentI protein = dna == al1 ? al2 : al1;
1011     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
1012     for (SequenceI dnaSeq : dna.getSequences())
1013     {
1014       for (SequenceI proteinSeq : protein.getSequences())
1015       {
1016         if (isMappable(dnaSeq, proteinSeq, mappings))
1017         {
1018           return true;
1019         }
1020       }
1021     }
1022     return false;
1023   }
1024
1025   /**
1026    * Returns true if the dna sequence is mapped, or could be mapped, to the
1027    * protein sequence.
1028    * 
1029    * @param dnaSeq
1030    * @param proteinSeq
1031    * @param mappings
1032    * @return
1033    */
1034   public static boolean isMappable(SequenceI dnaSeq, SequenceI proteinSeq,
1035           Set<AlignedCodonFrame> mappings)
1036   {
1037     SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq.getDatasetSequence();
1038     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
1039             : proteinSeq.getDatasetSequence();
1040     
1041     /*
1042      * Already mapped?
1043      */
1044     for (AlignedCodonFrame mapping : mappings) {
1045       if ( proteinDs == mapping.getAaForDnaSeq(dnaDs)) {
1046         return true;
1047       }
1048     }
1049
1050     /*
1051      * Just try to make a mapping (it is not yet stored), test whether
1052      * successful.
1053      */
1054     return mapProteinToCdna(proteinDs, dnaDs) != null;
1055   }
1056 }