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