JAL-1693 generate multiple exon sequences for 1-many dna-protein
[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 java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.LinkedHashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.Set;
34 import java.util.TreeMap;
35
36 import jalview.datamodel.AlignedCodon;
37 import jalview.datamodel.AlignedCodonFrame;
38 import jalview.datamodel.Alignment;
39 import jalview.datamodel.AlignmentAnnotation;
40 import jalview.datamodel.AlignmentI;
41 import jalview.datamodel.DBRefEntry;
42 import jalview.datamodel.DBRefSource;
43 import jalview.datamodel.FeatureProperties;
44 import jalview.datamodel.Mapping;
45 import jalview.datamodel.SearchResults;
46 import jalview.datamodel.Sequence;
47 import jalview.datamodel.SequenceGroup;
48 import jalview.datamodel.SequenceI;
49 import jalview.schemes.ResidueProperties;
50 import jalview.util.DBRefUtils;
51 import jalview.util.MapList;
52 import jalview.util.MappingUtils;
53
54 /**
55  * grab bag of useful alignment manipulation operations Expect these to be
56  * refactored elsewhere at some point.
57  * 
58  * @author jimp
59  * 
60  */
61 public class AlignmentUtils
62 {
63
64   /**
65    * given an existing alignment, create a new alignment including all, or up to
66    * flankSize additional symbols from each sequence's dataset sequence
67    * 
68    * @param core
69    * @param flankSize
70    * @return AlignmentI
71    */
72   public static AlignmentI expandContext(AlignmentI core, int flankSize)
73   {
74     List<SequenceI> sq = new ArrayList<SequenceI>();
75     int maxoffset = 0;
76     for (SequenceI s : core.getSequences())
77     {
78       SequenceI newSeq = s.deriveSequence();
79       if (newSeq.getStart() > maxoffset
80               && newSeq.getDatasetSequence().getStart() < s.getStart())
81       {
82         maxoffset = newSeq.getStart();
83       }
84       sq.add(newSeq);
85     }
86     if (flankSize > -1)
87     {
88       maxoffset = flankSize;
89     }
90     // now add offset to create a new expanded alignment
91     for (SequenceI s : sq)
92     {
93       SequenceI ds = s;
94       while (ds.getDatasetSequence() != null)
95       {
96         ds = ds.getDatasetSequence();
97       }
98       int s_end = s.findPosition(s.getStart() + s.getLength());
99       // find available flanking residues for sequence
100       int ustream_ds = s.getStart() - ds.getStart(), dstream_ds = ds
101               .getEnd() - s_end;
102
103       // build new flanked sequence
104
105       // compute gap padding to start of flanking sequence
106       int offset = maxoffset - ustream_ds;
107
108       // padding is gapChar x ( maxoffset - min(ustream_ds, flank)
109       if (flankSize >= 0)
110       {
111         if (flankSize < ustream_ds)
112         {
113           // take up to flankSize residues
114           offset = maxoffset - flankSize;
115           ustream_ds = flankSize;
116         }
117         if (flankSize < dstream_ds)
118         {
119           dstream_ds = flankSize;
120         }
121       }
122       char[] upstream = new String(ds.getSequence(s.getStart() - 1
123               - ustream_ds, s.getStart() - 1)).toLowerCase().toCharArray();
124       char[] downstream = new String(ds.getSequence(s_end - 1, s_end + 1
125               + dstream_ds)).toLowerCase().toCharArray();
126       char[] coreseq = s.getSequence();
127       char[] nseq = new char[offset + upstream.length + downstream.length
128               + coreseq.length];
129       char c = core.getGapCharacter();
130       // TODO could lowercase the flanking regions
131       int p = 0;
132       for (; p < offset; p++)
133       {
134         nseq[p] = c;
135       }
136       // s.setSequence(new String(upstream).toLowerCase()+new String(coreseq) +
137       // new String(downstream).toLowerCase());
138       System.arraycopy(upstream, 0, nseq, p, upstream.length);
139       System.arraycopy(coreseq, 0, nseq, p + upstream.length,
140               coreseq.length);
141       System.arraycopy(downstream, 0, nseq, p + coreseq.length
142               + upstream.length, downstream.length);
143       s.setSequence(new String(nseq));
144       s.setStart(s.getStart() - ustream_ds);
145       s.setEnd(s_end + downstream.length);
146     }
147     AlignmentI newAl = new jalview.datamodel.Alignment(
148             sq.toArray(new SequenceI[0]));
149     for (SequenceI s : sq)
150     {
151       if (s.getAnnotation() != null)
152       {
153         for (AlignmentAnnotation aa : s.getAnnotation())
154         {
155           newAl.addAnnotation(aa);
156         }
157       }
158     }
159     newAl.setDataset(core.getDataset());
160     return newAl;
161   }
162
163   /**
164    * Returns the index (zero-based position) of a sequence in an alignment, or
165    * -1 if not found.
166    * 
167    * @param al
168    * @param seq
169    * @return
170    */
171   public static int getSequenceIndex(AlignmentI al, SequenceI seq)
172   {
173     int result = -1;
174     int pos = 0;
175     for (SequenceI alSeq : al.getSequences())
176     {
177       if (alSeq == seq)
178       {
179         result = pos;
180         break;
181       }
182       pos++;
183     }
184     return result;
185   }
186
187   /**
188    * Returns a map of lists of sequences in the alignment, keyed by sequence
189    * name. For use in mapping between different alignment views of the same
190    * sequences.
191    * 
192    * @see jalview.datamodel.AlignmentI#getSequencesByName()
193    */
194   public static Map<String, List<SequenceI>> getSequencesByName(
195           AlignmentI al)
196   {
197     Map<String, List<SequenceI>> theMap = new LinkedHashMap<String, List<SequenceI>>();
198     for (SequenceI seq : al.getSequences())
199     {
200       String name = seq.getName();
201       if (name != null)
202       {
203         List<SequenceI> seqs = theMap.get(name);
204         if (seqs == null)
205         {
206           seqs = new ArrayList<SequenceI>();
207           theMap.put(name, seqs);
208         }
209         seqs.add(seq);
210       }
211     }
212     return theMap;
213   }
214
215   /**
216    * Build mapping of protein to cDNA alignment. Mappings are made between
217    * sequences where the cDNA translates to the protein sequence. Any new
218    * mappings are added to the protein alignment. Returns true if any mappings
219    * either already exist or were added, else false.
220    * 
221    * @param proteinAlignment
222    * @param cdnaAlignment
223    * @return
224    */
225   public static boolean mapProteinToCdna(
226           final AlignmentI proteinAlignment,
227           final AlignmentI cdnaAlignment)
228   {
229     if (proteinAlignment == null || cdnaAlignment == null)
230     {
231       return false;
232     }
233
234     Set<SequenceI> mappedDna = new HashSet<SequenceI>();
235     Set<SequenceI> mappedProtein = new HashSet<SequenceI>();
236
237     /*
238      * First pass - map sequences where cross-references exist. This include
239      * 1-to-many mappings to support, for example, variant cDNA.
240      */
241     boolean mappingPerformed = mapProteinToCdna(proteinAlignment,
242             cdnaAlignment, mappedDna, mappedProtein, true);
243
244     /*
245      * Second pass - map sequences where no cross-references exist. This only
246      * does 1-to-1 mappings and assumes corresponding sequences are in the same
247      * order in the alignments.
248      */
249     mappingPerformed |= mapProteinToCdna(proteinAlignment, cdnaAlignment,
250             mappedDna, mappedProtein, false);
251     return mappingPerformed;
252   }
253
254   /**
255    * Make mappings between compatible sequences (where the cDNA translation
256    * matches the protein).
257    * 
258    * @param proteinAlignment
259    * @param cdnaAlignment
260    * @param mappedDna
261    *          a set of mapped DNA sequences (to add to)
262    * @param mappedProtein
263    *          a set of mapped Protein sequences (to add to)
264    * @param xrefsOnly
265    *          if true, only map sequences where xrefs exist
266    * @return
267    */
268   protected static boolean mapProteinToCdna(
269           final AlignmentI proteinAlignment,
270           final AlignmentI cdnaAlignment, Set<SequenceI> mappedDna,
271           Set<SequenceI> mappedProtein, boolean xrefsOnly)
272   {
273     boolean mappingPerformed = false;
274     List<SequenceI> thisSeqs = proteinAlignment.getSequences();
275     for (SequenceI aaSeq : thisSeqs)
276     {
277       boolean proteinMapped = false;
278       AlignedCodonFrame acf = new AlignedCodonFrame();
279
280       for (SequenceI cdnaSeq : cdnaAlignment.getSequences())
281       {
282         /*
283          * Always try to map if sequences have xref to each other; this supports
284          * variant cDNA or alternative splicing for a protein sequence.
285          * 
286          * If no xrefs, try to map progressively, assuming that alignments have
287          * mappable sequences in corresponding order. These are not
288          * many-to-many, as that would risk mixing species with similar cDNA
289          * sequences.
290          */
291         if (xrefsOnly && !AlignmentUtils.haveCrossRef(aaSeq, cdnaSeq))
292         {
293           continue;
294         }
295
296         /*
297          * Don't map non-xrefd sequences more than once each. This heuristic
298          * allows us to pair up similar sequences in ordered alignments.
299          */
300         if (!xrefsOnly
301                 && (mappedProtein.contains(aaSeq) || mappedDna
302                         .contains(cdnaSeq)))
303         {
304           continue;
305         }
306         if (!mappingExists(proteinAlignment.getCodonFrames(),
307                 aaSeq.getDatasetSequence(), cdnaSeq.getDatasetSequence()))
308         {
309           MapList map = mapProteinToCdna(aaSeq, cdnaSeq);
310           if (map != null)
311           {
312             acf.addMap(cdnaSeq, aaSeq, map);
313             mappingPerformed = true;
314             proteinMapped = true;
315             mappedDna.add(cdnaSeq);
316             mappedProtein.add(aaSeq);
317           }
318         }
319       }
320       if (proteinMapped)
321       {
322         proteinAlignment.addCodonFrame(acf);
323       }
324     }
325     return mappingPerformed;
326   }
327
328   /**
329    * Answers true if the mappings include one between the given (dataset)
330    * sequences.
331    */
332   public static boolean mappingExists(Set<AlignedCodonFrame> set,
333           SequenceI aaSeq, SequenceI cdnaSeq)
334   {
335     if (set != null)
336     {
337       for (AlignedCodonFrame acf : set)
338       {
339         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
340         {
341           return true;
342         }
343       }
344     }
345     return false;
346   }
347
348   /**
349    * Build a mapping (if possible) of a protein to a cDNA sequence. The cDNA
350    * must be three times the length of the protein, possibly after ignoring
351    * start and/or stop codons, and must translate to the protein. Returns null
352    * if no mapping is determined.
353    * 
354    * @param proteinSeqs
355    * @param cdnaSeq
356    * @return
357    */
358   public static MapList mapProteinToCdna(SequenceI proteinSeq,
359           SequenceI cdnaSeq)
360   {
361     /*
362      * Here we handle either dataset sequence set (desktop) or absent (applet).
363      * Use only the char[] form of the sequence to avoid creating possibly large
364      * String objects.
365      */
366     final SequenceI proteinDataset = proteinSeq.getDatasetSequence();
367     char[] aaSeqChars = proteinDataset != null ? proteinDataset
368             .getSequence() : proteinSeq.getSequence();
369     final SequenceI cdnaDataset = cdnaSeq.getDatasetSequence();
370     char[] cdnaSeqChars = cdnaDataset != null ? cdnaDataset.getSequence()
371             : cdnaSeq.getSequence();
372     if (aaSeqChars == null || cdnaSeqChars == null)
373     {
374       return null;
375     }
376
377     /*
378      * cdnaStart/End, proteinStartEnd are base 1 (for dataset sequence mapping)
379      */
380     final int mappedLength = 3 * aaSeqChars.length;
381     int cdnaLength = cdnaSeqChars.length;
382     int cdnaStart = 1;
383     int cdnaEnd = cdnaLength;
384     final int proteinStart = 1;
385     final int proteinEnd = aaSeqChars.length;
386
387     /*
388      * If lengths don't match, try ignoring stop codon.
389      */
390     if (cdnaLength != mappedLength && cdnaLength > 2)
391     {
392       String lastCodon = String.valueOf(cdnaSeqChars, cdnaLength - 3, 3)
393               .toUpperCase();
394       for (String stop : ResidueProperties.STOP)
395       {
396         if (lastCodon.equals(stop))
397         {
398           cdnaEnd -= 3;
399           cdnaLength -= 3;
400           break;
401         }
402       }
403     }
404
405     /*
406      * If lengths still don't match, try ignoring start codon.
407      */
408     if (cdnaLength != mappedLength
409             && cdnaLength > 2
410             && String.valueOf(cdnaSeqChars, 0, 3).toUpperCase()
411                     .equals(
412                     ResidueProperties.START))
413     {
414       cdnaStart += 3;
415       cdnaLength -= 3;
416     }
417
418     if (cdnaLength != mappedLength)
419     {
420       return null;
421     }
422     if (!translatesAs(cdnaSeqChars, cdnaStart - 1, aaSeqChars))
423     {
424       return null;
425     }
426     MapList map = new MapList(new int[]
427     { cdnaStart, cdnaEnd }, new int[]
428     { proteinStart, proteinEnd }, 3, 1);
429     return map;
430   }
431
432   /**
433    * Test whether the given cdna sequence, starting at the given offset,
434    * translates to the given amino acid sequence, using the standard translation
435    * table. Designed to fail fast i.e. as soon as a mismatch position is found.
436    * 
437    * @param cdnaSeqChars
438    * @param cdnaStart
439    * @param aaSeqChars
440    * @return
441    */
442   protected static boolean translatesAs(char[] cdnaSeqChars, int cdnaStart,
443           char[] aaSeqChars)
444   {
445     int aaResidue = 0;
446     for (int i = cdnaStart; i < cdnaSeqChars.length - 2
447             && aaResidue < aaSeqChars.length; i += 3, aaResidue++)
448     {
449       String codon = String.valueOf(cdnaSeqChars, i, 3);
450       final String translated = ResidueProperties.codonTranslate(
451               codon);
452       /*
453        * ? allow X in protein to match untranslatable in dna ?
454        */
455       final char aaRes = aaSeqChars[aaResidue];
456       if ((translated == null || "STOP".equals(translated)) && aaRes == 'X')
457       {
458         continue;
459       }
460       if (translated == null
461               || !(aaRes == translated.charAt(0)))
462       {
463         // debug
464         // System.out.println(("Mismatch at " + i + "/" + aaResidue + ": "
465         // + codon + "(" + translated + ") != " + aaRes));
466         return false;
467       }
468     }
469     // fail if we didn't match all of the aa sequence
470     return (aaResidue == aaSeqChars.length);
471   }
472
473   /**
474    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
475    * currently assumes that we are aligning cDNA to match protein.
476    * 
477    * @param seq
478    *          the sequence to be realigned
479    * @param al
480    *          the alignment whose sequence alignment is to be 'copied'
481    * @param gap
482    *          character string represent a gap in the realigned sequence
483    * @param preserveUnmappedGaps
484    * @param preserveMappedGaps
485    * @return true if the sequence was realigned, false if it could not be
486    */
487   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
488           String gap, boolean preserveMappedGaps,
489           boolean preserveUnmappedGaps)
490   {
491     /*
492      * Get any mappings from the source alignment to the target (dataset) sequence.
493      */
494     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
495     // all mappings. Would it help to constrain this?
496     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
497     if (mappings == null || mappings.isEmpty())
498     {
499       return false;
500     }
501   
502     /*
503      * Locate the aligned source sequence whose dataset sequence is mapped. We
504      * just take the first match here (as we can't align cDNA like more than one
505      * protein sequence).
506      */
507     SequenceI alignFrom = null;
508     AlignedCodonFrame mapping = null;
509     for (AlignedCodonFrame mp : mappings)
510     {
511       alignFrom = mp.findAlignedSequence(seq.getDatasetSequence(), al);
512       if (alignFrom != null)
513       {
514         mapping = mp;
515         break;
516       }
517     }
518   
519     if (alignFrom == null)
520     {
521       return false;
522     }
523     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
524             preserveMappedGaps, preserveUnmappedGaps);
525     return true;
526   }
527
528   /**
529    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
530    * match residues and codons. Flags control whether existing gaps in unmapped
531    * (intron) and mapped (exon) regions are preserved or not. Gaps linking intro
532    * and exon are only retained if both flags are set.
533    * 
534    * @param alignTo
535    * @param alignFrom
536    * @param mapping
537    * @param myGap
538    * @param sourceGap
539    * @param preserveUnmappedGaps
540    * @param preserveMappedGaps
541    */
542   public static void alignSequenceAs(SequenceI alignTo,
543           SequenceI alignFrom,
544           AlignedCodonFrame mapping, String myGap, char sourceGap,
545           boolean preserveMappedGaps, boolean preserveUnmappedGaps)
546   {
547     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
548     final char[] thisSeq = alignTo.getSequence();
549     final char[] thatAligned = alignFrom.getSequence();
550     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
551   
552     // aligned and dataset sequence positions, all base zero
553     int thisSeqPos = 0;
554     int sourceDsPos = 0;
555
556     int basesWritten = 0;
557     char myGapChar = myGap.charAt(0);
558     int ratio = myGap.length();
559
560     /*
561      * Traverse the aligned protein sequence.
562      */
563     int sourceGapMappedLength = 0;
564     boolean inExon = false;
565     for (char sourceChar : thatAligned)
566     {
567       if (sourceChar == sourceGap)
568       {
569         sourceGapMappedLength += ratio;
570         continue;
571       }
572
573       /*
574        * Found a residue. Locate its mapped codon (start) position.
575        */
576       sourceDsPos++;
577       // Note mapping positions are base 1, our sequence positions base 0
578       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
579               sourceDsPos);
580       if (mappedPos == null)
581       {
582         /*
583          * Abort realignment if unmapped protein. Or could ignore it??
584          */
585         System.err.println("Can't align: no codon mapping to residue "
586                 + sourceDsPos + "(" + sourceChar + ")");
587         return;
588       }
589
590       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
591       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
592       StringBuilder trailingCopiedGap = new StringBuilder();
593
594       /*
595        * Copy dna sequence up to and including this codon. Optionally, include
596        * gaps before the codon starts (in introns) and/or after the codon starts
597        * (in exons).
598        * 
599        * Note this only works for 'linear' splicing, not reverse or interleaved.
600        * But then 'align dna as protein' doesn't make much sense otherwise.
601        */
602       int intronLength = 0;
603       while (basesWritten < mappedCodonEnd && thisSeqPos < thisSeq.length)
604       {
605         final char c = thisSeq[thisSeqPos++];
606         if (c != myGapChar)
607         {
608           basesWritten++;
609
610           if (basesWritten < mappedCodonStart)
611           {
612             /*
613              * Found an unmapped (intron) base. First add in any preceding gaps
614              * (if wanted).
615              */
616             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
617             {
618               thisAligned.append(trailingCopiedGap.toString());
619               intronLength += trailingCopiedGap.length();
620               trailingCopiedGap = new StringBuilder();
621             }
622             intronLength++;
623             inExon = false;
624           }
625           else
626           {
627             final boolean startOfCodon = basesWritten == mappedCodonStart;
628             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
629                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
630                     trailingCopiedGap.length(), intronLength, startOfCodon);
631             for (int i = 0; i < gapsToAdd; i++)
632             {
633               thisAligned.append(myGapChar);
634             }
635             sourceGapMappedLength = 0;
636             inExon = true;
637           }
638           thisAligned.append(c);
639           trailingCopiedGap = new StringBuilder();
640         }
641         else
642         {
643           if (inExon && preserveMappedGaps)
644           {
645             trailingCopiedGap.append(myGapChar);
646           }
647           else if (!inExon && preserveUnmappedGaps)
648           {
649             trailingCopiedGap.append(myGapChar);
650           }
651         }
652       }
653     }
654
655     /*
656      * At end of protein sequence. Copy any remaining dna sequence, optionally
657      * including (intron) gaps. We do not copy trailing gaps in protein.
658      */
659     while (thisSeqPos < thisSeq.length)
660     {
661       final char c = thisSeq[thisSeqPos++];
662       if (c != myGapChar || preserveUnmappedGaps)
663       {
664         thisAligned.append(c);
665       }
666     }
667
668     /*
669      * All done aligning, set the aligned sequence.
670      */
671     alignTo.setSequence(new String(thisAligned));
672   }
673
674   /**
675    * Helper method to work out how many gaps to insert when realigning.
676    * 
677    * @param preserveMappedGaps
678    * @param preserveUnmappedGaps
679    * @param sourceGapMappedLength
680    * @param inExon
681    * @param trailingCopiedGap
682    * @param intronLength
683    * @param startOfCodon
684    * @return
685    */
686   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
687           boolean preserveUnmappedGaps, int sourceGapMappedLength,
688           boolean inExon, int trailingGapLength,
689           int intronLength, final boolean startOfCodon)
690   {
691     int gapsToAdd = 0;
692     if (startOfCodon)
693     {
694       /*
695        * Reached start of codon. Ignore trailing gaps in intron unless we are
696        * preserving gaps in both exon and intron. Ignore them anyway if the
697        * protein alignment introduces a gap at least as large as the intronic
698        * region.
699        */
700       if (inExon && !preserveMappedGaps)
701       {
702         trailingGapLength = 0;
703       }
704       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
705       {
706         trailingGapLength = 0;
707       }
708       if (inExon)
709       {
710         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
711       }
712       else
713       {
714         if (intronLength + trailingGapLength <= sourceGapMappedLength)
715         {
716           gapsToAdd = sourceGapMappedLength - intronLength;
717         }
718         else
719         {
720           gapsToAdd = Math.min(intronLength + trailingGapLength
721                   - sourceGapMappedLength, trailingGapLength);
722         }
723       }
724     }
725     else
726     {
727       /*
728        * second or third base of codon; check for any gaps in dna
729        */
730       if (!preserveMappedGaps)
731       {
732         trailingGapLength = 0;
733       }
734       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
735     }
736     return gapsToAdd;
737   }
738
739   /**
740    * Returns a list of sequences mapped from the given sequences and aligned
741    * (gapped) in the same way. For example, the cDNA for aligned protein, where
742    * a single gap in protein generates three gaps in cDNA.
743    * 
744    * @param sequences
745    * @param gapCharacter
746    * @param mappings
747    * @return
748    */
749   public static List<SequenceI> getAlignedTranslation(
750           List<SequenceI> sequences, char gapCharacter,
751           Set<AlignedCodonFrame> mappings)
752   {
753     List<SequenceI> alignedSeqs = new ArrayList<SequenceI>();
754
755     for (SequenceI seq : sequences)
756     {
757       List<SequenceI> mapped = getAlignedTranslation(seq, gapCharacter,
758               mappings);
759       alignedSeqs.addAll(mapped);
760     }
761     return alignedSeqs;
762   }
763
764   /**
765    * Returns sequences aligned 'like' the source sequence, as mapped by the
766    * given mappings. Normally we expect zero or one 'mapped' sequences, but this
767    * will support 1-to-many as well.
768    * 
769    * @param seq
770    * @param gapCharacter
771    * @param mappings
772    * @return
773    */
774   protected static List<SequenceI> getAlignedTranslation(SequenceI seq,
775           char gapCharacter, Set<AlignedCodonFrame> mappings)
776   {
777     List<SequenceI> result = new ArrayList<SequenceI>();
778     for (AlignedCodonFrame mapping : mappings)
779     {
780       if (mapping.involvesSequence(seq))
781       {
782         SequenceI mapped = getAlignedTranslation(seq, gapCharacter, mapping);
783         if (mapped != null)
784         {
785           result.add(mapped);
786         }
787       }
788     }
789     return result;
790   }
791
792   /**
793    * Returns the translation of 'seq' (as held in the mapping) with
794    * corresponding alignment (gaps).
795    * 
796    * @param seq
797    * @param gapCharacter
798    * @param mapping
799    * @return
800    */
801   protected static SequenceI getAlignedTranslation(SequenceI seq,
802           char gapCharacter, AlignedCodonFrame mapping)
803   {
804     String gap = String.valueOf(gapCharacter);
805     boolean toDna = false;
806     int fromRatio = 1;
807     SequenceI mapTo = mapping.getDnaForAaSeq(seq);
808     if (mapTo != null)
809     {
810       // mapping is from protein to nucleotide
811       toDna = true;
812       // should ideally get gap count ratio from mapping
813       gap = String.valueOf(new char[]
814       { gapCharacter, gapCharacter, gapCharacter });
815     }
816     else
817     {
818       // mapping is from nucleotide to protein
819       mapTo = mapping.getAaForDnaSeq(seq);
820       fromRatio = 3;
821     }
822     StringBuilder newseq = new StringBuilder(seq.getLength()
823             * (toDna ? 3 : 1));
824
825     int residueNo = 0; // in seq, base 1
826     int[] phrase = new int[fromRatio];
827     int phraseOffset = 0;
828     int gapWidth = 0;
829     boolean first = true;
830     final Sequence alignedSeq = new Sequence("", "");
831
832     for (char c : seq.getSequence())
833     {
834       if (c == gapCharacter)
835       {
836         gapWidth++;
837         if (gapWidth >= fromRatio)
838         {
839           newseq.append(gap);
840           gapWidth = 0;
841         }
842       }
843       else
844       {
845         phrase[phraseOffset++] = residueNo + 1;
846         if (phraseOffset == fromRatio)
847         {
848           /*
849            * Have read a whole codon (or protein residue), now translate: map
850            * source phrase to positions in target sequence add characters at
851            * these positions to newseq Note mapping positions are base 1, our
852            * sequence positions base 0.
853            */
854           SearchResults sr = new SearchResults();
855           for (int pos : phrase)
856           {
857             mapping.markMappedRegion(seq, pos, sr);
858           }
859           newseq.append(sr.toString());
860           if (first)
861           {
862             first = false;
863             // Hack: Copy sequence dataset, name and description from
864             // SearchResults.match[0].sequence
865             // TODO? carry over sequence names from original 'complement'
866             // alignment
867             SequenceI mappedTo = sr.getResultSequence(0);
868             alignedSeq.setName(mappedTo.getName());
869             alignedSeq.setDescription(mappedTo.getDescription());
870             alignedSeq.setDatasetSequence(mappedTo);
871           }
872           phraseOffset = 0;
873         }
874         residueNo++;
875       }
876     }
877     alignedSeq.setSequence(newseq.toString());
878     return alignedSeq;
879   }
880
881   /**
882    * Realigns the given protein to match the alignment of the dna, using codon
883    * mappings to translate aligned codon positions to protein residues.
884    * 
885    * @param protein
886    *          the alignment whose sequences are realigned by this method
887    * @param dna
888    *          the dna alignment whose alignment we are 'copying'
889    * @return the number of sequences that were realigned
890    */
891   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
892   {
893     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
894
895     /*
896      * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
897      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
898      * comparator keeps the codon positions ordered.
899      */
900     Map<AlignedCodon, Map<SequenceI, String>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, String>>(
901             new CodonComparator());
902     for (SequenceI dnaSeq : dna.getSequences())
903     {
904       for (AlignedCodonFrame mapping : mappings)
905       {
906         Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
907         SequenceI prot = mapping.findAlignedSequence(
908                 dnaSeq.getDatasetSequence(), protein);
909         if (prot != null)
910         {
911           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
912                   seqMap, alignedCodons);
913         }
914       }
915     }
916     return alignProteinAs(protein, alignedCodons);
917   }
918
919   /**
920    * Update the aligned protein sequences to match the codon alignments given in
921    * the map.
922    * 
923    * @param protein
924    * @param alignedCodons
925    *          an ordered map of codon positions (columns), with sequence/peptide
926    *          values present in each column
927    * @return
928    */
929   protected static int alignProteinAs(AlignmentI protein,
930           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
931   {
932     /*
933      * Prefill aligned sequences with gaps before inserting aligned protein
934      * residues.
935      */
936     int alignedWidth = alignedCodons.size();
937     char[] gaps = new char[alignedWidth];
938     Arrays.fill(gaps, protein.getGapCharacter());
939     String allGaps = String.valueOf(gaps);
940     for (SequenceI seq : protein.getSequences())
941     {
942       seq.setSequence(allGaps);
943     }
944
945     int column = 0;
946     for (AlignedCodon codon : alignedCodons.keySet())
947     {
948       final Map<SequenceI, String> columnResidues = alignedCodons.get(codon);
949       for (Entry<SequenceI, String> entry : columnResidues
950               .entrySet())
951       {
952         // place translated codon at its column position in sequence
953         entry.getKey().getSequence()[column] = entry.getValue().charAt(0);
954       }
955       column++;
956     }
957     return 0;
958   }
959
960   /**
961    * Populate the map of aligned codons by traversing the given sequence
962    * mapping, locating the aligned positions of mapped codons, and adding those
963    * positions and their translation products to the map.
964    * 
965    * @param dna
966    *          the aligned sequence we are mapping from
967    * @param protein
968    *          the sequence to be aligned to the codons
969    * @param gapChar
970    *          the gap character in the dna sequence
971    * @param seqMap
972    *          a mapping to a sequence translation
973    * @param alignedCodons
974    *          the map we are building up
975    */
976   static void addCodonPositions(SequenceI dna, SequenceI protein,
977           char gapChar,
978           Mapping seqMap,
979           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
980   {
981     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
982     while (codons.hasNext())
983     {
984       AlignedCodon codon = codons.next();
985       Map<SequenceI, String> seqProduct = alignedCodons.get(codon);
986       if (seqProduct == null)
987       {
988         seqProduct = new HashMap<SequenceI, String>();
989         alignedCodons.put(codon, seqProduct);
990       }
991       seqProduct.put(protein, codon.product);
992     }
993   }
994
995   /**
996    * Returns true if a cDNA/Protein mapping either exists, or could be made,
997    * between at least one pair of sequences in the two alignments. Currently,
998    * the logic is:
999    * <ul>
1000    * <li>One alignment must be nucleotide, and the other protein</li>
1001    * <li>At least one pair of sequences must be already mapped, or mappable</li>
1002    * <li>Mappable means the nucleotide translation matches the protein sequence</li>
1003    * <li>The translation may ignore start and stop codons if present in the
1004    * nucleotide</li>
1005    * </ul>
1006    * 
1007    * @param al1
1008    * @param al2
1009    * @return
1010    */
1011   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
1012   {
1013     /*
1014      * Require one nucleotide and one protein
1015      */
1016     if (al1.isNucleotide() == al2.isNucleotide())
1017     {
1018       return false;
1019     }
1020     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
1021     AlignmentI protein = dna == al1 ? al2 : al1;
1022     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
1023     for (SequenceI dnaSeq : dna.getSequences())
1024     {
1025       for (SequenceI proteinSeq : protein.getSequences())
1026       {
1027         if (isMappable(dnaSeq, proteinSeq, mappings))
1028         {
1029           return true;
1030         }
1031       }
1032     }
1033     return false;
1034   }
1035
1036   /**
1037    * Returns true if the dna sequence is mapped, or could be mapped, to the
1038    * protein sequence.
1039    * 
1040    * @param dnaSeq
1041    * @param proteinSeq
1042    * @param mappings
1043    * @return
1044    */
1045   public static boolean isMappable(SequenceI dnaSeq, SequenceI proteinSeq,
1046           Set<AlignedCodonFrame> mappings)
1047   {
1048     SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq.getDatasetSequence();
1049     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
1050             : proteinSeq.getDatasetSequence();
1051     
1052     /*
1053      * Already mapped?
1054      */
1055     for (AlignedCodonFrame mapping : mappings) {
1056       if ( proteinDs == mapping.getAaForDnaSeq(dnaDs)) {
1057         return true;
1058       }
1059     }
1060
1061     /*
1062      * Just try to make a mapping (it is not yet stored), test whether
1063      * successful.
1064      */
1065     return mapProteinToCdna(proteinDs, dnaDs) != null;
1066   }
1067
1068   /**
1069    * Finds any reference annotations associated with the sequences in
1070    * sequenceScope, that are not already added to the alignment, and adds them
1071    * to the 'candidates' map. Also populates a lookup table of annotation
1072    * labels, keyed by calcId, for use in constructing tooltips or the like.
1073    * 
1074    * @param sequenceScope
1075    *          the sequences to scan for reference annotations
1076    * @param labelForCalcId
1077    *          (optional) map to populate with label for calcId
1078    * @param candidates
1079    *          map to populate with annotations for sequence
1080    * @param al
1081    *          the alignment to check for presence of annotations
1082    */
1083   public static void findAddableReferenceAnnotations(
1084           List<SequenceI> sequenceScope, Map<String, String> labelForCalcId,
1085           final Map<SequenceI, List<AlignmentAnnotation>> candidates,
1086           AlignmentI al)
1087   {
1088     if (sequenceScope == null)
1089     {
1090       return;
1091     }
1092   
1093     /*
1094      * For each sequence in scope, make a list of any annotations on the
1095      * underlying dataset sequence which are not already on the alignment.
1096      * 
1097      * Add to a map of { alignmentSequence, <List of annotations to add> }
1098      */
1099     for (SequenceI seq : sequenceScope)
1100     {
1101       SequenceI dataset = seq.getDatasetSequence();
1102       if (dataset == null)
1103       {
1104         continue;
1105       }
1106       AlignmentAnnotation[] datasetAnnotations = dataset.getAnnotation();
1107       if (datasetAnnotations == null)
1108       {
1109         continue;
1110       }
1111       final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
1112       for (AlignmentAnnotation dsann : datasetAnnotations)
1113       {
1114         /*
1115          * Find matching annotations on the alignment. If none is found, then
1116          * add this annotation to the list of 'addable' annotations for this
1117          * sequence.
1118          */
1119         final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
1120                 .findAnnotations(seq, dsann.getCalcId(),
1121                         dsann.label);
1122         if (!matchedAlignmentAnnotations.iterator().hasNext())
1123         {
1124           result.add(dsann);
1125           if (labelForCalcId != null)
1126           {
1127             labelForCalcId.put(dsann.getCalcId(), dsann.label);
1128           }
1129         }
1130       }
1131       /*
1132        * Save any addable annotations for this sequence
1133        */
1134       if (!result.isEmpty())
1135       {
1136         candidates.put(seq, result);
1137       }
1138     }
1139   }
1140
1141   /**
1142    * Adds annotations to the top of the alignment annotations, in the same order
1143    * as their related sequences.
1144    * 
1145    * @param annotations
1146    *          the annotations to add
1147    * @param alignment
1148    *          the alignment to add them to
1149    * @param selectionGroup
1150    *          current selection group (or null if none)
1151    */
1152   public static void addReferenceAnnotations(
1153           Map<SequenceI, List<AlignmentAnnotation>> annotations,
1154           final AlignmentI alignment, final SequenceGroup selectionGroup)
1155   {
1156     for (SequenceI seq : annotations.keySet())
1157     {
1158       for (AlignmentAnnotation ann : annotations.get(seq))
1159       {
1160         AlignmentAnnotation copyAnn = new AlignmentAnnotation(ann);
1161         int startRes = 0;
1162         int endRes = ann.annotations.length;
1163         if (selectionGroup != null)
1164         {
1165           startRes = selectionGroup.getStartRes();
1166           endRes = selectionGroup.getEndRes();
1167         }
1168         copyAnn.restrict(startRes, endRes);
1169   
1170         /*
1171          * Add to the sequence (sets copyAnn.datasetSequence), unless the
1172          * original annotation is already on the sequence.
1173          */
1174         if (!seq.hasAnnotation(ann))
1175         {
1176           seq.addAlignmentAnnotation(copyAnn);
1177         }
1178         // adjust for gaps
1179         copyAnn.adjustForAlignment();
1180         // add to the alignment and set visible
1181         alignment.addAnnotation(copyAnn);
1182         copyAnn.visible = true;
1183       }
1184     }
1185   }
1186
1187   /**
1188    * Set visibility of alignment annotations of specified types (labels), for
1189    * specified sequences. This supports controls like
1190    * "Show all secondary structure", "Hide all Temp factor", etc.
1191    * 
1192    * @al the alignment to scan for annotations
1193    * @param types
1194    *          the types (labels) of annotations to be updated
1195    * @param forSequences
1196    *          if not null, only annotations linked to one of these sequences are
1197    *          in scope for update; if null, acts on all sequence annotations
1198    * @param anyType
1199    *          if this flag is true, 'types' is ignored (label not checked)
1200    * @param doShow
1201    *          if true, set visibility on, else set off
1202    */
1203   public static void showOrHideSequenceAnnotations(AlignmentI al,
1204           Collection<String> types, List<SequenceI> forSequences,
1205           boolean anyType, boolean doShow)
1206   {
1207     for (AlignmentAnnotation aa : al
1208             .getAlignmentAnnotation())
1209     {
1210       if (anyType || types.contains(aa.label))
1211       {
1212         if ((aa.sequenceRef != null)
1213                 && (forSequences == null || forSequences
1214                         .contains(aa.sequenceRef)))
1215         {
1216           aa.visible = doShow;
1217         }
1218       }
1219     }
1220   }
1221
1222   /**
1223    * Returns true if either sequence has a cross-reference to the other
1224    * 
1225    * @param seq1
1226    * @param seq2
1227    * @return
1228    */
1229   public static boolean haveCrossRef(SequenceI seq1, SequenceI seq2)
1230   {
1231     // Note: moved here from class CrossRef as the latter class has dependencies
1232     // not availability to the applet's classpath
1233     return hasCrossRef(seq1, seq2) || hasCrossRef(seq2, seq1);
1234   }
1235
1236   /**
1237    * Returns true if seq1 has a cross-reference to seq2. Currently this assumes
1238    * that sequence name is structured as Source|AccessId.
1239    * 
1240    * @param seq1
1241    * @param seq2
1242    * @return
1243    */
1244   public static boolean hasCrossRef(SequenceI seq1, SequenceI seq2)
1245   {
1246     if (seq1 == null || seq2 == null)
1247     {
1248       return false;
1249     }
1250     String name = seq2.getName();
1251     final DBRefEntry[] xrefs = seq1.getDBRef();
1252     if (xrefs != null)
1253     {
1254       for (DBRefEntry xref : xrefs)
1255       {
1256         String xrefName = xref.getSource() + "|" + xref.getAccessionId();
1257         // case-insensitive test, consistent with DBRefEntry.equalRef()
1258         if (xrefName.equalsIgnoreCase(name))
1259         {
1260           return true;
1261         }
1262       }
1263     }
1264     return false;
1265   }
1266
1267   /**
1268    * Constructs an alignment consisting of the mapped exon regions in the given
1269    * nucleotide sequences, and updates mappings to match.
1270    * 
1271    * @param dna
1272    *          aligned dna sequences
1273    * @param mappings
1274    *          from dna to protein; these are replaced with new mappings
1275    * @return an alignment whose sequences are the exon-only parts of the dna
1276    *         sequences (or null if no exons are found)
1277    */
1278   public static AlignmentI makeExonAlignment(SequenceI[] dna,
1279           Set<AlignedCodonFrame> mappings)
1280   {
1281     Set<AlignedCodonFrame> newMappings = new HashSet<AlignedCodonFrame>();
1282     List<SequenceI> exonSequences = new ArrayList<SequenceI>();
1283     
1284     for (SequenceI dnaSeq : dna)
1285     {
1286       final SequenceI ds = dnaSeq.getDatasetSequence();
1287       List<AlignedCodonFrame> seqMappings = MappingUtils
1288               .findMappingsForSequence(ds, mappings);
1289       for (AlignedCodonFrame acf : seqMappings)
1290       {
1291         AlignedCodonFrame newMapping = new AlignedCodonFrame();
1292         final List<SequenceI> mappedExons = makeExonSequences(ds, acf,
1293                 newMapping);
1294         if (!mappedExons.isEmpty())
1295         {
1296           exonSequences.addAll(mappedExons);
1297           newMappings.add(newMapping);
1298         }
1299       }
1300     }
1301     AlignmentI al = new Alignment(
1302             exonSequences.toArray(new SequenceI[exonSequences.size()]));
1303     al.setDataset(null);
1304
1305     /*
1306      * Replace the old mappings with the new ones
1307      */
1308     mappings.clear();
1309     mappings.addAll(newMappings);
1310
1311     return al;
1312   }
1313
1314   /**
1315    * Helper method to make exon-only sequences and populate their mappings to
1316    * protein products
1317    * <p>
1318    * For example, if ggCCaTTcGAg has mappings [3, 4, 6, 7, 9, 10] to protein
1319    * then generate a sequence CCTTGA with mapping [1, 6] to the same protein
1320    * residues
1321    * <p>
1322    * Typically eukaryotic dna will include exons encoding for a single peptide
1323    * sequence i.e. return a single result. Bacterial dna may have overlapping
1324    * exon mappings coding for multiple peptides so return multiple results
1325    * (example EMBL KF591215).
1326    * 
1327    * @param dnaSeq
1328    *          a dna dataset sequence
1329    * @param mapping
1330    *          containing one or more mappings of the sequence to protein
1331    * @param newMapping
1332    *          the new mapping to populate, from the exon-only sequences to their
1333    *          mapped protein sequences
1334    * @return
1335    */
1336   protected static List<SequenceI> makeExonSequences(SequenceI dnaSeq,
1337           AlignedCodonFrame mapping, AlignedCodonFrame newMapping)
1338   {
1339     List<SequenceI> exonSequences = new ArrayList<SequenceI>();
1340     List<Mapping> seqMappings = mapping.getMappingsForSequence(dnaSeq);
1341     final char[] dna = dnaSeq.getSequence();
1342     for (Mapping seqMapping : seqMappings)
1343     {
1344       StringBuilder newSequence = new StringBuilder(dnaSeq.getLength());
1345
1346       /*
1347        * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
1348        */
1349       List<int[]> exonRanges = seqMapping.getMap().getFromRanges();
1350       for (int[] range : exonRanges)
1351       {
1352         for (int pos = range[0]; pos <= range[1]; pos++)
1353         {
1354           newSequence.append(dna[pos - 1]);
1355         }
1356       }
1357
1358       SequenceI exon = new Sequence(dnaSeq.getName(),
1359               newSequence.toString());
1360
1361       /*
1362        * Locate any xrefs to CDS database on the protein product and attach to
1363        * the CDS sequence. Also add as a sub-token of the sequence name.
1364        */
1365       // default to "CDS" if we can't locate an actual gene id
1366       String cdsAccId = FeatureProperties
1367               .getCodingFeature(DBRefSource.EMBL);
1368       DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(seqMapping.getTo()
1369               .getDBRef(), DBRefSource.CODINGDBS);
1370       if (cdsRefs != null)
1371       {
1372         for (DBRefEntry cdsRef : cdsRefs)
1373         {
1374           exon.addDBRef(new DBRefEntry(cdsRef));
1375           cdsAccId = cdsRef.getAccessionId();
1376         }
1377       }
1378       exon.setName(exon.getName() + "|" + cdsAccId);
1379       exon.createDatasetSequence();
1380
1381       /*
1382        * Build new mappings - from the same protein regions, but now to
1383        * contiguous exons
1384        */
1385       List<int[]> exonRange = new ArrayList<int[]>();
1386       exonRange.add(new int[]
1387       { 1, newSequence.length() });
1388       MapList map = new MapList(exonRange, seqMapping.getMap()
1389               .getToRanges(),
1390               3, 1);
1391       newMapping.addMap(exon.getDatasetSequence(), seqMapping.getTo(), map);
1392
1393       exonSequences.add(exon);
1394     }
1395     return exonSequences;
1396   }
1397 }