JAL-845 applet colour by tree; translate as cDNA; pull up history list
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
3  * Copyright (C) 2014 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.AlignedCodonFrame;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.SequenceI;
27 import jalview.schemes.ResidueProperties;
28 import jalview.util.MapList;
29
30 import java.util.ArrayList;
31 import java.util.LinkedHashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35
36 /**
37  * grab bag of useful alignment manipulation operations Expect these to be
38  * refactored elsewhere at some point.
39  * 
40  * @author jimp
41  * 
42  */
43 public class AlignmentUtils
44 {
45
46   /**
47    * Represents the 3 possible results of trying to map one alignment to
48    * another.
49    */
50   public enum MappingResult
51   {
52     Mapped, NotMapped, AlreadyMapped
53   }
54
55   /**
56    * given an existing alignment, create a new alignment including all, or up to
57    * flankSize additional symbols from each sequence's dataset sequence
58    * 
59    * @param core
60    * @param flankSize
61    * @return AlignmentI
62    */
63   public static AlignmentI expandContext(AlignmentI core, int flankSize)
64   {
65     List<SequenceI> sq = new ArrayList<SequenceI>();
66     int maxoffset = 0;
67     for (SequenceI s : core.getSequences())
68     {
69       SequenceI newSeq = s.deriveSequence();
70       if (newSeq.getStart() > maxoffset
71               && newSeq.getDatasetSequence().getStart() < s.getStart())
72       {
73         maxoffset = newSeq.getStart();
74       }
75       sq.add(newSeq);
76     }
77     if (flankSize > -1)
78     {
79       maxoffset = flankSize;
80     }
81     // now add offset to create a new expanded alignment
82     for (SequenceI s : sq)
83     {
84       SequenceI ds = s;
85       while (ds.getDatasetSequence() != null)
86       {
87         ds = ds.getDatasetSequence();
88       }
89       int s_end = s.findPosition(s.getStart() + s.getLength());
90       // find available flanking residues for sequence
91       int ustream_ds = s.getStart() - ds.getStart(), dstream_ds = ds
92               .getEnd() - s_end;
93
94       // build new flanked sequence
95
96       // compute gap padding to start of flanking sequence
97       int offset = maxoffset - ustream_ds;
98
99       // padding is gapChar x ( maxoffset - min(ustream_ds, flank)
100       if (flankSize >= 0)
101       {
102         if (flankSize < ustream_ds)
103         {
104           // take up to flankSize residues
105           offset = maxoffset - flankSize;
106           ustream_ds = flankSize;
107         }
108         if (flankSize < dstream_ds)
109         {
110           dstream_ds = flankSize;
111         }
112       }
113       char[] upstream = new String(ds.getSequence(s.getStart() - 1
114               - ustream_ds, s.getStart() - 1)).toLowerCase().toCharArray();
115       char[] downstream = new String(ds.getSequence(s_end - 1, s_end + 1
116               + dstream_ds)).toLowerCase().toCharArray();
117       char[] coreseq = s.getSequence();
118       char[] nseq = new char[offset + upstream.length + downstream.length
119               + coreseq.length];
120       char c = core.getGapCharacter();
121       // TODO could lowercase the flanking regions
122       int p = 0;
123       for (; p < offset; p++)
124       {
125         nseq[p] = c;
126       }
127       // s.setSequence(new String(upstream).toLowerCase()+new String(coreseq) +
128       // new String(downstream).toLowerCase());
129       System.arraycopy(upstream, 0, nseq, p, upstream.length);
130       System.arraycopy(coreseq, 0, nseq, p + upstream.length,
131               coreseq.length);
132       System.arraycopy(downstream, 0, nseq, p + coreseq.length
133               + upstream.length, downstream.length);
134       s.setSequence(new String(nseq));
135       s.setStart(s.getStart() - ustream_ds);
136       s.setEnd(s_end + downstream.length);
137     }
138     AlignmentI newAl = new jalview.datamodel.Alignment(
139             sq.toArray(new SequenceI[0]));
140     for (SequenceI s : sq)
141     {
142       if (s.getAnnotation() != null)
143       {
144         for (AlignmentAnnotation aa : s.getAnnotation())
145         {
146           newAl.addAnnotation(aa);
147         }
148       }
149     }
150     newAl.setDataset(core.getDataset());
151     return newAl;
152   }
153
154   /**
155    * Returns the index (zero-based position) of a sequence in an alignment, or
156    * -1 if not found.
157    * 
158    * @param al
159    * @param seq
160    * @return
161    */
162   public static int getSequenceIndex(AlignmentI al, SequenceI seq)
163   {
164     int result = -1;
165     int pos = 0;
166     for (SequenceI alSeq : al.getSequences())
167     {
168       if (alSeq == seq)
169       {
170         result = pos;
171         break;
172       }
173       pos++;
174     }
175     return result;
176   }
177
178   /**
179    * Returns a map of lists of sequences in the alignment, keyed by sequence
180    * name. For use in mapping between different alignment views of the same
181    * sequences.
182    * 
183    * @see jalview.datamodel.AlignmentI#getSequencesByName()
184    */
185   public static Map<String, List<SequenceI>> getSequencesByName(
186           AlignmentI al)
187   {
188     Map<String, List<SequenceI>> theMap = new LinkedHashMap<String, List<SequenceI>>();
189     for (SequenceI seq : al.getSequences())
190     {
191       String name = seq.getName();
192       if (name != null)
193       {
194         List<SequenceI> seqs = theMap.get(name);
195         if (seqs == null)
196         {
197           seqs = new ArrayList<SequenceI>();
198           theMap.put(name, seqs);
199         }
200         seqs.add(seq);
201       }
202     }
203     return theMap;
204   }
205
206   /**
207    * Build mapping of protein to cDNA alignment. Mappings are made between
208    * sequences which have the same name and compatible lengths. Has a 3-valued
209    * result: either Mapped (at least one sequence mapping was created),
210    * AlreadyMapped (all possible sequence mappings already exist), or NotMapped
211    * (no possible sequence mappings exist).
212    * 
213    * @param proteinAlignment
214    * @param cdnaAlignment
215    * @return
216    */
217   public static MappingResult mapProteinToCdna(
218           final AlignmentI proteinAlignment,
219           final AlignmentI cdnaAlignment)
220   {
221     if (proteinAlignment == null || cdnaAlignment == null)
222     {
223       return MappingResult.NotMapped;
224     }
225
226     boolean mappingPossible = false;
227     boolean mappingPerformed = false;
228
229     List<SequenceI> thisSeqs = proteinAlignment.getSequences();
230   
231     /*
232      * Build a look-up of cDNA sequences by name, for matching purposes.
233      */
234     Map<String, List<SequenceI>> cdnaSeqs = cdnaAlignment
235             .getSequencesByName();
236   
237     for (SequenceI aaSeq : thisSeqs)
238     {
239       AlignedCodonFrame acf = new AlignedCodonFrame();
240       List<SequenceI> candidates = cdnaSeqs.get(aaSeq.getName());
241       if (candidates == null)
242       {
243         /*
244          * No cDNA sequence with matching name, so no mapping possible for this
245          * protein sequence
246          */
247         continue;
248       }
249       mappingPossible = true;
250       for (SequenceI cdnaSeq : candidates)
251       {
252         if (!mappingExists(proteinAlignment.getCodonFrames(),
253                 aaSeq.getDatasetSequence(), cdnaSeq.getDatasetSequence()))
254         {
255           MapList map = mapProteinToCdna(aaSeq, cdnaSeq);
256           if (map != null)
257           {
258             acf.addMap(cdnaSeq, aaSeq, map);
259             mappingPerformed = true;
260           }
261         }
262       }
263       proteinAlignment.addCodonFrame(acf);
264     }
265
266     /*
267      * If at least one mapping was possible but none was done, then the
268      * alignments are already as mapped as they can be.
269      */
270     if (mappingPossible && !mappingPerformed)
271     {
272       return MappingResult.AlreadyMapped;
273     }
274     else
275     {
276       return mappingPerformed ? MappingResult.Mapped
277               : MappingResult.NotMapped;
278     }
279   }
280
281   /**
282    * Answers true if the mappings include one between the given (dataset)
283    * sequences.
284    */
285   public static boolean mappingExists(Set<AlignedCodonFrame> set,
286           SequenceI aaSeq, SequenceI cdnaSeq)
287   {
288     if (set != null)
289     {
290       for (AlignedCodonFrame acf : set)
291       {
292         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
293         {
294           return true;
295         }
296       }
297     }
298     return false;
299   }
300
301   /**
302    * Build a mapping (if possible) of a protein to a cDNA sequence. The cDNA
303    * must be three times the length of the protein, possibly after ignoring
304    * start and/or stop codons. Returns null if no mapping is determined.
305    * 
306    * @param proteinSeqs
307    * @param cdnaSeq
308    * @return
309    */
310   public static MapList mapProteinToCdna(SequenceI proteinSeq,
311           SequenceI cdnaSeq)
312   {
313     /*
314      * Here we handle either dataset sequence set (desktop) or absent (applet)
315      */
316     final SequenceI proteinDataset = proteinSeq.getDatasetSequence();
317     String aaSeqString = proteinDataset != null ? proteinDataset
318             .getSequenceAsString() : proteinSeq.getSequenceAsString();
319     final SequenceI cdnaDataset = cdnaSeq.getDatasetSequence();
320     String cdnaSeqString = cdnaDataset != null ? cdnaDataset
321             .getSequenceAsString() : cdnaSeq.getSequenceAsString();
322     if (aaSeqString == null || cdnaSeqString == null)
323     {
324       return null;
325     }
326
327     final int mappedLength = 3 * aaSeqString.length();
328     int cdnaLength = cdnaSeqString.length();
329     int cdnaStart = 1;
330     int cdnaEnd = cdnaLength;
331     final int proteinStart = 1;
332     final int proteinEnd = aaSeqString.length();
333
334     /*
335      * If lengths don't match, try ignoring stop codon.
336      */
337     if (cdnaLength != mappedLength)
338     {
339       for (Object stop : ResidueProperties.STOP)
340       {
341         if (cdnaSeqString.toUpperCase().endsWith((String) stop))
342         {
343           cdnaEnd -= 3;
344           cdnaLength -= 3;
345           break;
346         }
347       }
348     }
349
350     /*
351      * If lengths still don't match, try ignoring start codon.
352      */
353     if (cdnaLength != mappedLength
354             && cdnaSeqString.toUpperCase().startsWith(
355                     ResidueProperties.START))
356     {
357       cdnaStart += 3;
358       cdnaLength -= 3;
359     }
360
361     if (cdnaLength == mappedLength)
362     {
363       MapList map = new MapList(new int[]
364       { cdnaStart, cdnaEnd }, new int[]
365       { proteinStart, proteinEnd }, 3, 1);
366       return map;
367     }
368     else
369     {
370       return null;
371     }
372   }
373
374   /**
375    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
376    * currently assumes that we are aligning cDNA to match protein.
377    * 
378    * @param seq
379    *          the sequence to be realigned
380    * @param al
381    *          the alignment whose sequence alignment is to be 'copied'
382    * @param gap
383    *          character string represent a gap in the realigned sequence
384    * @param preserveUnmappedGaps
385    * @param preserveMappedGaps
386    * @return true if the sequence was realigned, false if it could not be
387    */
388   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
389           String gap, boolean preserveMappedGaps,
390           boolean preserveUnmappedGaps)
391   {
392     /*
393      * Get any mappings from the source alignment to the target (dataset) sequence.
394      */
395     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
396     // all mappings. Would it help to constrain this?
397     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
398     if (mappings == null)
399     {
400       return false;
401     }
402   
403     /*
404      * Locate the aligned source sequence whose dataset sequence is mapped. We
405      * just take the first match here (as we can't align cDNA like more than one
406      * protein sequence).
407      */
408     SequenceI alignFrom = null;
409     AlignedCodonFrame mapping = null;
410     for (AlignedCodonFrame mp : mappings)
411     {
412       alignFrom = mp.findAlignedSequence(seq.getDatasetSequence(), al);
413       if (alignFrom != null)
414       {
415         mapping = mp;
416         break;
417       }
418     }
419   
420     if (alignFrom == null)
421     {
422       return false;
423     }
424     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
425             preserveMappedGaps, preserveUnmappedGaps);
426     return true;
427   }
428
429   /**
430    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
431    * match residues and codons. Flags control whether existing gaps in unmapped
432    * (intron) and mapped (exon) regions are preserved or not. Gaps linking intro
433    * and exon are only retained if both flags are set.
434    * 
435    * @param alignTo
436    * @param alignFrom
437    * @param mapping
438    * @param myGap
439    * @param sourceGap
440    * @param preserveUnmappedGaps
441    * @param preserveMappedGaps
442    */
443   public static void alignSequenceAs(SequenceI alignTo,
444           SequenceI alignFrom,
445           AlignedCodonFrame mapping, String myGap, char sourceGap,
446           boolean preserveMappedGaps, boolean preserveUnmappedGaps)
447   {
448     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
449     final char[] thisSeq = alignTo.getSequence();
450     final char[] thatAligned = alignFrom.getSequence();
451     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
452   
453     // aligned and dataset sequence positions, all base zero
454     int thisSeqPos = 0;
455     int sourceDsPos = 0;
456
457     int basesWritten = 0;
458     char myGapChar = myGap.charAt(0);
459     int ratio = myGap.length();
460
461     /*
462      * Traverse the aligned protein sequence.
463      */
464     int sourceGapMappedLength = 0;
465     boolean inExon = false;
466     for (char sourceChar : thatAligned)
467     {
468       if (sourceChar == sourceGap)
469       {
470         sourceGapMappedLength += ratio;
471         continue;
472       }
473
474       /*
475        * Found a residue. Locate its mapped codon (start) position.
476        */
477       sourceDsPos++;
478       // Note mapping positions are base 1, our sequence positions base 0
479       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
480               sourceDsPos);
481       if (mappedPos == null)
482       {
483         /*
484          * Abort realignment if unmapped protein. Or could ignore it??
485          */
486         System.err.println("Can't align: no codon mapping to residue "
487                 + sourceDsPos + "(" + sourceChar + ")");
488         return;
489       }
490
491       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
492       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
493       StringBuilder trailingCopiedGap = new StringBuilder();
494
495       /*
496        * Copy dna sequence up to and including this codon. Optionally, include
497        * gaps before the codon starts (in introns) and/or after the codon starts
498        * (in exons).
499        * 
500        * Note this only works for 'linear' splicing, not reverse or interleaved.
501        * But then 'align dna as protein' doesn't make much sense otherwise.
502        */
503       int intronLength = 0;
504       while (basesWritten < mappedCodonEnd && thisSeqPos < thisSeq.length)
505       {
506         final char c = thisSeq[thisSeqPos++];
507         if (c != myGapChar)
508         {
509           basesWritten++;
510
511           if (basesWritten < mappedCodonStart)
512           {
513             /*
514              * Found an unmapped (intron) base. First add in any preceding gaps
515              * (if wanted).
516              */
517             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
518             {
519               thisAligned.append(trailingCopiedGap.toString());
520               intronLength += trailingCopiedGap.length();
521               trailingCopiedGap = new StringBuilder();
522             }
523             intronLength++;
524             inExon = false;
525           }
526           else
527           {
528             final boolean startOfCodon = basesWritten == mappedCodonStart;
529             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
530                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
531                     trailingCopiedGap.length(), intronLength, startOfCodon);
532             for (int i = 0; i < gapsToAdd; i++)
533             {
534               thisAligned.append(myGapChar);
535             }
536             sourceGapMappedLength = 0;
537             inExon = true;
538           }
539           thisAligned.append(c);
540           trailingCopiedGap = new StringBuilder();
541         }
542         else
543         {
544           if (inExon && preserveMappedGaps)
545           {
546             trailingCopiedGap.append(myGapChar);
547           }
548           else if (!inExon && preserveUnmappedGaps)
549           {
550             trailingCopiedGap.append(myGapChar);
551           }
552         }
553       }
554     }
555
556     /*
557      * At end of protein sequence. Copy any remaining dna sequence, optionally
558      * including (intron) gaps. We do not copy trailing gaps in protein.
559      */
560     while (thisSeqPos < thisSeq.length)
561     {
562       final char c = thisSeq[thisSeqPos++];
563       if (c != myGapChar || preserveUnmappedGaps)
564       {
565         thisAligned.append(c);
566       }
567     }
568
569     /*
570      * All done aligning, set the aligned sequence.
571      */
572     alignTo.setSequence(new String(thisAligned));
573   }
574
575   /**
576    * Helper method to work out how many gaps to insert when realigning.
577    * 
578    * @param preserveMappedGaps
579    * @param preserveUnmappedGaps
580    * @param sourceGapMappedLength
581    * @param inExon
582    * @param trailingCopiedGap
583    * @param intronLength
584    * @param startOfCodon
585    * @return
586    */
587   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
588           boolean preserveUnmappedGaps, int sourceGapMappedLength,
589           boolean inExon, int trailingGapLength,
590           int intronLength, final boolean startOfCodon)
591   {
592     int gapsToAdd = 0;
593     if (startOfCodon)
594     {
595       /*
596        * Reached start of codon. Ignore trailing gaps in intron unless we are
597        * preserving gaps in both exon and intron. Ignore them anyway if the
598        * protein alignment introduces a gap at least as large as the intronic
599        * region.
600        */
601       if (inExon && !preserveMappedGaps)
602       {
603         trailingGapLength = 0;
604       }
605       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
606       {
607         trailingGapLength = 0;
608       }
609       if (inExon)
610       {
611         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
612       }
613       else
614       {
615         if (intronLength + trailingGapLength <= sourceGapMappedLength)
616         {
617           gapsToAdd = sourceGapMappedLength - intronLength;
618         }
619         else
620         {
621           gapsToAdd = Math.min(intronLength + trailingGapLength
622                   - sourceGapMappedLength, trailingGapLength);
623         }
624       }
625     }
626     else
627     {
628       /*
629        * second or third base of codon; check for any gaps in dna
630        */
631       if (!preserveMappedGaps)
632       {
633         trailingGapLength = 0;
634       }
635       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
636     }
637     return gapsToAdd;
638   }
639 }