05e844c896a6740891239ab38f4b6feab2ca1136
[jalview.git] / src / jalview / analysis / AlignmentUtils.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.analysis;
22
23 import jalview.datamodel.AlignedCodon;
24 import jalview.datamodel.AlignedCodonFrame;
25 import jalview.datamodel.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 mapProteinAlignmentToCdna(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 mappingExistsOrAdded = 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           mappingExistsOrAdded = true;
315         }
316         else
317         {
318           MapList map = mapProteinSequenceToCdna(aaSeq, cdnaSeq);
319           if (map != null)
320           {
321             acf.addMap(cdnaSeq, aaSeq, map);
322             mappingExistsOrAdded = true;
323             proteinMapped = true;
324             mappedDna.add(cdnaSeq);
325             mappedProtein.add(aaSeq);
326           }
327         }
328       }
329       if (proteinMapped)
330       {
331         proteinAlignment.addCodonFrame(acf);
332       }
333     }
334     return mappingExistsOrAdded;
335   }
336
337   /**
338    * Answers true if the mappings include one between the given (dataset)
339    * sequences.
340    */
341   public static boolean mappingExists(Set<AlignedCodonFrame> set,
342           SequenceI aaSeq, SequenceI cdnaSeq)
343   {
344     if (set != null)
345     {
346       for (AlignedCodonFrame acf : set)
347       {
348         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
349         {
350           return true;
351         }
352       }
353     }
354     return false;
355   }
356
357   /**
358    * Build a mapping (if possible) of a protein to a cDNA sequence. The cDNA
359    * must be three times the length of the protein, possibly after ignoring
360    * start and/or stop codons, and must translate to the protein. Returns null
361    * if no mapping is determined.
362    * 
363    * @param proteinSeqs
364    * @param cdnaSeq
365    * @return
366    */
367   public static MapList mapProteinSequenceToCdna(SequenceI proteinSeq,
368           SequenceI cdnaSeq)
369   {
370     /*
371      * Here we handle either dataset sequence set (desktop) or absent (applet).
372      * Use only the char[] form of the sequence to avoid creating possibly large
373      * String objects.
374      */
375     final SequenceI proteinDataset = proteinSeq.getDatasetSequence();
376     char[] aaSeqChars = proteinDataset != null ? proteinDataset
377             .getSequence() : proteinSeq.getSequence();
378     final SequenceI cdnaDataset = cdnaSeq.getDatasetSequence();
379     char[] cdnaSeqChars = cdnaDataset != null ? cdnaDataset.getSequence()
380             : cdnaSeq.getSequence();
381     if (aaSeqChars == null || cdnaSeqChars == null)
382     {
383       return null;
384     }
385
386     /*
387      * cdnaStart/End, proteinStartEnd are base 1 (for dataset sequence mapping)
388      */
389     final int mappedLength = 3 * aaSeqChars.length;
390     int cdnaLength = cdnaSeqChars.length;
391     int cdnaStart = cdnaSeq.getStart();
392     int cdnaEnd = cdnaSeq.getEnd();
393     final int proteinStart = proteinSeq.getStart();
394     final int proteinEnd = proteinSeq.getEnd();
395
396     /*
397      * If lengths don't match, try ignoring stop codon.
398      */
399     if (cdnaLength != mappedLength && cdnaLength > 2)
400     {
401       String lastCodon = String.valueOf(cdnaSeqChars, cdnaLength - 3, 3)
402               .toUpperCase();
403       for (String stop : ResidueProperties.STOP)
404       {
405         if (lastCodon.equals(stop))
406         {
407           cdnaEnd -= 3;
408           cdnaLength -= 3;
409           break;
410         }
411       }
412     }
413
414     /*
415      * If lengths still don't match, try ignoring start codon.
416      */
417     int startOffset = 0;
418     if (cdnaLength != mappedLength
419             && cdnaLength > 2
420             && String.valueOf(cdnaSeqChars, 0, 3).toUpperCase()
421                     .equals(ResidueProperties.START))
422     {
423       startOffset += 3;
424       cdnaStart += 3;
425       cdnaLength -= 3;
426     }
427
428     if (cdnaLength != mappedLength)
429     {
430       return null;
431     }
432     if (!translatesAs(cdnaSeqChars, startOffset,
433             aaSeqChars))
434     {
435       return null;
436     }
437     MapList map = new MapList(new int[] { cdnaStart, cdnaEnd }, new int[] {
438         proteinStart, proteinEnd }, 3, 1);
439     return map;
440   }
441
442   /**
443    * Test whether the given cdna sequence, starting at the given offset,
444    * translates to the given amino acid sequence, using the standard translation
445    * table. Designed to fail fast i.e. as soon as a mismatch position is found.
446    * 
447    * @param cdnaSeqChars
448    * @param cdnaStart
449    * @param aaSeqChars
450    * @return
451    */
452   protected static boolean translatesAs(char[] cdnaSeqChars, int cdnaStart,
453           char[] aaSeqChars)
454   {
455     if (cdnaSeqChars == null || aaSeqChars == null)
456     {
457       return false;
458     }
459
460     int aaResidue = 0;
461     for (int i = cdnaStart; i < cdnaSeqChars.length - 2
462             && aaResidue < aaSeqChars.length; i += 3, aaResidue++)
463     {
464       String codon = String.valueOf(cdnaSeqChars, i, 3);
465       final String translated = ResidueProperties.codonTranslate(codon);
466       /*
467        * allow * in protein to match untranslatable in dna
468        */
469       final char aaRes = aaSeqChars[aaResidue];
470       if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
471       {
472         continue;
473       }
474       if (translated == null || !(aaRes == translated.charAt(0)))
475       {
476         // debug
477         // System.out.println(("Mismatch at " + i + "/" + aaResidue + ": "
478         // + codon + "(" + translated + ") != " + aaRes));
479         return false;
480       }
481     }
482     // fail if we didn't match all of the aa sequence
483     return (aaResidue == aaSeqChars.length);
484   }
485
486   /**
487    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
488    * currently assumes that we are aligning cDNA to match protein.
489    * 
490    * @param seq
491    *          the sequence to be realigned
492    * @param al
493    *          the alignment whose sequence alignment is to be 'copied'
494    * @param gap
495    *          character string represent a gap in the realigned sequence
496    * @param preserveUnmappedGaps
497    * @param preserveMappedGaps
498    * @return true if the sequence was realigned, false if it could not be
499    */
500   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
501           String gap, boolean preserveMappedGaps,
502           boolean preserveUnmappedGaps)
503   {
504     /*
505      * Get any mappings from the source alignment to the target (dataset)
506      * sequence.
507      */
508     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
509     // all mappings. Would it help to constrain this?
510     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
511     if (mappings == null || mappings.isEmpty())
512     {
513       return false;
514     }
515
516     /*
517      * Locate the aligned source sequence whose dataset sequence is mapped. We
518      * just take the first match here (as we can't align cDNA like more than one
519      * protein sequence).
520      */
521     SequenceI alignFrom = null;
522     AlignedCodonFrame mapping = null;
523     for (AlignedCodonFrame mp : mappings)
524     {
525       alignFrom = mp.findAlignedSequence(seq.getDatasetSequence(), al);
526       if (alignFrom != null)
527       {
528         mapping = mp;
529         break;
530       }
531     }
532
533     if (alignFrom == null)
534     {
535       return false;
536     }
537     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
538             preserveMappedGaps, preserveUnmappedGaps);
539     return true;
540   }
541
542   /**
543    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
544    * match residues and codons. Flags control whether existing gaps in unmapped
545    * (intron) and mapped (exon) regions are preserved or not. Gaps linking intro
546    * and exon are only retained if both flags are set.
547    * 
548    * @param alignTo
549    * @param alignFrom
550    * @param mapping
551    * @param myGap
552    * @param sourceGap
553    * @param preserveUnmappedGaps
554    * @param preserveMappedGaps
555    */
556   public static void alignSequenceAs(SequenceI alignTo,
557           SequenceI alignFrom, AlignedCodonFrame mapping, String myGap,
558           char sourceGap, boolean preserveMappedGaps,
559           boolean preserveUnmappedGaps)
560   {
561     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
562     final char[] thisSeq = alignTo.getSequence();
563     final char[] thatAligned = alignFrom.getSequence();
564     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
565
566     // aligned and dataset sequence positions, all base zero
567     int thisSeqPos = 0;
568     int sourceDsPos = 0;
569
570     int basesWritten = 0;
571     char myGapChar = myGap.charAt(0);
572     int ratio = myGap.length();
573
574     /*
575      * Traverse the aligned protein sequence.
576      */
577     int fromOffset = alignFrom.getStart() - 1;
578     int toOffset = alignTo.getStart() - 1;
579     int sourceGapMappedLength = 0;
580     boolean inExon = false;
581     for (char sourceChar : thatAligned)
582     {
583       if (sourceChar == sourceGap)
584       {
585         sourceGapMappedLength += ratio;
586         continue;
587       }
588
589       /*
590        * Found a residue. Locate its mapped codon (start) position.
591        */
592       sourceDsPos++;
593       // Note mapping positions are base 1, our sequence positions base 0
594       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
595               sourceDsPos + fromOffset);
596       if (mappedPos == null)
597       {
598         /*
599          * Abort realignment if unmapped protein. Or could ignore it??
600          */
601         System.err.println("Can't align: no codon mapping to residue "
602                 + sourceDsPos + "(" + sourceChar + ")");
603         return;
604       }
605
606       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
607       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
608       StringBuilder trailingCopiedGap = new StringBuilder();
609
610       /*
611        * Copy dna sequence up to and including this codon. Optionally, include
612        * gaps before the codon starts (in introns) and/or after the codon starts
613        * (in exons).
614        * 
615        * Note this only works for 'linear' splicing, not reverse or interleaved.
616        * But then 'align dna as protein' doesn't make much sense otherwise.
617        */
618       int intronLength = 0;
619       while (basesWritten + toOffset < mappedCodonEnd
620               && thisSeqPos < thisSeq.length)
621       {
622         final char c = thisSeq[thisSeqPos++];
623         if (c != myGapChar)
624         {
625           basesWritten++;
626           int sourcePosition = basesWritten + toOffset;
627           if (sourcePosition < mappedCodonStart)
628           {
629             /*
630              * Found an unmapped (intron) base. First add in any preceding gaps
631              * (if wanted).
632              */
633             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
634             {
635               thisAligned.append(trailingCopiedGap.toString());
636               intronLength += trailingCopiedGap.length();
637               trailingCopiedGap = new StringBuilder();
638             }
639             intronLength++;
640             inExon = false;
641           }
642           else
643           {
644             final boolean startOfCodon = sourcePosition == mappedCodonStart;
645             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
646                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
647                     trailingCopiedGap.length(), intronLength, startOfCodon);
648             for (int i = 0; i < gapsToAdd; i++)
649             {
650               thisAligned.append(myGapChar);
651             }
652             sourceGapMappedLength = 0;
653             inExon = true;
654           }
655           thisAligned.append(c);
656           trailingCopiedGap = new StringBuilder();
657         }
658         else
659         {
660           if (inExon && preserveMappedGaps)
661           {
662             trailingCopiedGap.append(myGapChar);
663           }
664           else if (!inExon && preserveUnmappedGaps)
665           {
666             trailingCopiedGap.append(myGapChar);
667           }
668         }
669       }
670     }
671
672     /*
673      * At end of protein sequence. Copy any remaining dna sequence, optionally
674      * including (intron) gaps. We do not copy trailing gaps in protein.
675      */
676     while (thisSeqPos < thisSeq.length)
677     {
678       final char c = thisSeq[thisSeqPos++];
679       if (c != myGapChar || preserveUnmappedGaps)
680       {
681         thisAligned.append(c);
682       }
683     }
684
685     /*
686      * All done aligning, set the aligned sequence.
687      */
688     alignTo.setSequence(new String(thisAligned));
689   }
690
691   /**
692    * Helper method to work out how many gaps to insert when realigning.
693    * 
694    * @param preserveMappedGaps
695    * @param preserveUnmappedGaps
696    * @param sourceGapMappedLength
697    * @param inExon
698    * @param trailingCopiedGap
699    * @param intronLength
700    * @param startOfCodon
701    * @return
702    */
703   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
704           boolean preserveUnmappedGaps, int sourceGapMappedLength,
705           boolean inExon, int trailingGapLength, int intronLength,
706           final boolean startOfCodon)
707   {
708     int gapsToAdd = 0;
709     if (startOfCodon)
710     {
711       /*
712        * Reached start of codon. Ignore trailing gaps in intron unless we are
713        * preserving gaps in both exon and intron. Ignore them anyway if the
714        * protein alignment introduces a gap at least as large as the intronic
715        * region.
716        */
717       if (inExon && !preserveMappedGaps)
718       {
719         trailingGapLength = 0;
720       }
721       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
722       {
723         trailingGapLength = 0;
724       }
725       if (inExon)
726       {
727         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
728       }
729       else
730       {
731         if (intronLength + trailingGapLength <= sourceGapMappedLength)
732         {
733           gapsToAdd = sourceGapMappedLength - intronLength;
734         }
735         else
736         {
737           gapsToAdd = Math.min(intronLength + trailingGapLength
738                   - sourceGapMappedLength, trailingGapLength);
739         }
740       }
741     }
742     else
743     {
744       /*
745        * second or third base of codon; check for any gaps in dna
746        */
747       if (!preserveMappedGaps)
748       {
749         trailingGapLength = 0;
750       }
751       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
752     }
753     return gapsToAdd;
754   }
755
756   /**
757    * Returns a list of sequences mapped from the given sequences and aligned
758    * (gapped) in the same way. For example, the cDNA for aligned protein, where
759    * a single gap in protein generates three gaps in cDNA.
760    * 
761    * @param sequences
762    * @param gapCharacter
763    * @param mappings
764    * @return
765    */
766   public static List<SequenceI> getAlignedTranslation(
767           List<SequenceI> sequences, char gapCharacter,
768           Set<AlignedCodonFrame> mappings)
769   {
770     List<SequenceI> alignedSeqs = new ArrayList<SequenceI>();
771
772     for (SequenceI seq : sequences)
773     {
774       List<SequenceI> mapped = getAlignedTranslation(seq, gapCharacter,
775               mappings);
776       alignedSeqs.addAll(mapped);
777     }
778     return alignedSeqs;
779   }
780
781   /**
782    * Returns sequences aligned 'like' the source sequence, as mapped by the
783    * given mappings. Normally we expect zero or one 'mapped' sequences, but this
784    * will support 1-to-many as well.
785    * 
786    * @param seq
787    * @param gapCharacter
788    * @param mappings
789    * @return
790    */
791   protected static List<SequenceI> getAlignedTranslation(SequenceI seq,
792           char gapCharacter, Set<AlignedCodonFrame> mappings)
793   {
794     List<SequenceI> result = new ArrayList<SequenceI>();
795     for (AlignedCodonFrame mapping : mappings)
796     {
797       if (mapping.involvesSequence(seq))
798       {
799         SequenceI mapped = getAlignedTranslation(seq, gapCharacter, mapping);
800         if (mapped != null)
801         {
802           result.add(mapped);
803         }
804       }
805     }
806     return result;
807   }
808
809   /**
810    * Returns the translation of 'seq' (as held in the mapping) with
811    * corresponding alignment (gaps).
812    * 
813    * @param seq
814    * @param gapCharacter
815    * @param mapping
816    * @return
817    */
818   protected static SequenceI getAlignedTranslation(SequenceI seq,
819           char gapCharacter, AlignedCodonFrame mapping)
820   {
821     String gap = String.valueOf(gapCharacter);
822     boolean toDna = false;
823     int fromRatio = 1;
824     SequenceI mapTo = mapping.getDnaForAaSeq(seq);
825     if (mapTo != null)
826     {
827       // mapping is from protein to nucleotide
828       toDna = true;
829       // should ideally get gap count ratio from mapping
830       gap = String.valueOf(new char[] { gapCharacter, gapCharacter,
831           gapCharacter });
832     }
833     else
834     {
835       // mapping is from nucleotide to protein
836       mapTo = mapping.getAaForDnaSeq(seq);
837       fromRatio = 3;
838     }
839     StringBuilder newseq = new StringBuilder(seq.getLength()
840             * (toDna ? 3 : 1));
841
842     int residueNo = 0; // in seq, base 1
843     int[] phrase = new int[fromRatio];
844     int phraseOffset = 0;
845     int gapWidth = 0;
846     boolean first = true;
847     final Sequence alignedSeq = new Sequence("", "");
848
849     for (char c : seq.getSequence())
850     {
851       if (c == gapCharacter)
852       {
853         gapWidth++;
854         if (gapWidth >= fromRatio)
855         {
856           newseq.append(gap);
857           gapWidth = 0;
858         }
859       }
860       else
861       {
862         phrase[phraseOffset++] = residueNo + 1;
863         if (phraseOffset == fromRatio)
864         {
865           /*
866            * Have read a whole codon (or protein residue), now translate: map
867            * source phrase to positions in target sequence add characters at
868            * these positions to newseq Note mapping positions are base 1, our
869            * sequence positions base 0.
870            */
871           SearchResults sr = new SearchResults();
872           for (int pos : phrase)
873           {
874             mapping.markMappedRegion(seq, pos, sr);
875           }
876           newseq.append(sr.getCharacters());
877           if (first)
878           {
879             first = false;
880             // Hack: Copy sequence dataset, name and description from
881             // SearchResults.match[0].sequence
882             // TODO? carry over sequence names from original 'complement'
883             // alignment
884             SequenceI mappedTo = sr.getResultSequence(0);
885             alignedSeq.setName(mappedTo.getName());
886             alignedSeq.setDescription(mappedTo.getDescription());
887             alignedSeq.setDatasetSequence(mappedTo);
888           }
889           phraseOffset = 0;
890         }
891         residueNo++;
892       }
893     }
894     alignedSeq.setSequence(newseq.toString());
895     return alignedSeq;
896   }
897
898   /**
899    * Realigns the given protein to match the alignment of the dna, using codon
900    * mappings to translate aligned codon positions to protein residues.
901    * 
902    * @param protein
903    *          the alignment whose sequences are realigned by this method
904    * @param dna
905    *          the dna alignment whose alignment we are 'copying'
906    * @return the number of sequences that were realigned
907    */
908   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
909   {
910     List<SequenceI> unmappedProtein = new ArrayList<SequenceI>();
911     unmappedProtein.addAll(protein.getSequences());
912
913     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
914
915     /*
916      * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
917      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
918      * comparator keeps the codon positions ordered.
919      */
920     Map<AlignedCodon, Map<SequenceI, String>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, String>>(
921             new CodonComparator());
922     for (SequenceI dnaSeq : dna.getSequences())
923     {
924       for (AlignedCodonFrame mapping : mappings)
925       {
926         Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
927         SequenceI prot = mapping.findAlignedSequence(
928                 dnaSeq.getDatasetSequence(), protein);
929         if (prot != null)
930         {
931           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
932                   seqMap, alignedCodons);
933           unmappedProtein.remove(prot);
934         }
935       }
936     }
937     return alignProteinAs(protein, alignedCodons, unmappedProtein);
938   }
939
940   /**
941    * Update the aligned protein sequences to match the codon alignments given in
942    * the map.
943    * 
944    * @param protein
945    * @param alignedCodons
946    *          an ordered map of codon positions (columns), with sequence/peptide
947    *          values present in each column
948    * @param unmappedProtein
949    * @return
950    */
951   protected static int alignProteinAs(AlignmentI protein,
952           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons,
953           List<SequenceI> unmappedProtein)
954   {
955     /*
956      * Prefill aligned sequences with gaps before inserting aligned protein
957      * residues.
958      */
959     int alignedWidth = alignedCodons.size();
960     char[] gaps = new char[alignedWidth];
961     Arrays.fill(gaps, protein.getGapCharacter());
962     String allGaps = String.valueOf(gaps);
963     for (SequenceI seq : protein.getSequences())
964     {
965       if (!unmappedProtein.contains(seq))
966       {
967         seq.setSequence(allGaps);
968       }
969     }
970
971     int column = 0;
972     for (AlignedCodon codon : alignedCodons.keySet())
973     {
974       final Map<SequenceI, String> columnResidues = alignedCodons
975               .get(codon);
976       for (Entry<SequenceI, String> entry : columnResidues.entrySet())
977       {
978         // place translated codon at its column position in sequence
979         entry.getKey().getSequence()[column] = entry.getValue().charAt(0);
980       }
981       column++;
982     }
983     return 0;
984   }
985
986   /**
987    * Populate the map of aligned codons by traversing the given sequence
988    * mapping, locating the aligned positions of mapped codons, and adding those
989    * positions and their translation products to the map.
990    * 
991    * @param dna
992    *          the aligned sequence we are mapping from
993    * @param protein
994    *          the sequence to be aligned to the codons
995    * @param gapChar
996    *          the gap character in the dna sequence
997    * @param seqMap
998    *          a mapping to a sequence translation
999    * @param alignedCodons
1000    *          the map we are building up
1001    */
1002   static void addCodonPositions(SequenceI dna, SequenceI protein,
1003           char gapChar, Mapping seqMap,
1004           Map<AlignedCodon, Map<SequenceI, String>> alignedCodons)
1005   {
1006     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
1007     while (codons.hasNext())
1008     {
1009       AlignedCodon codon = codons.next();
1010       Map<SequenceI, String> seqProduct = alignedCodons.get(codon);
1011       if (seqProduct == null)
1012       {
1013         seqProduct = new HashMap<SequenceI, String>();
1014         alignedCodons.put(codon, seqProduct);
1015       }
1016       seqProduct.put(protein, codon.product);
1017     }
1018   }
1019
1020   /**
1021    * Returns true if a cDNA/Protein mapping either exists, or could be made,
1022    * between at least one pair of sequences in the two alignments. Currently,
1023    * the logic is:
1024    * <ul>
1025    * <li>One alignment must be nucleotide, and the other protein</li>
1026    * <li>At least one pair of sequences must be already mapped, or mappable</li>
1027    * <li>Mappable means the nucleotide translation matches the protein sequence</li>
1028    * <li>The translation may ignore start and stop codons if present in the
1029    * nucleotide</li>
1030    * </ul>
1031    * 
1032    * @param al1
1033    * @param al2
1034    * @return
1035    */
1036   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
1037   {
1038     if (al1 == null || al2 == null)
1039     {
1040       return false;
1041     }
1042
1043     /*
1044      * Require one nucleotide and one protein
1045      */
1046     if (al1.isNucleotide() == al2.isNucleotide())
1047     {
1048       return false;
1049     }
1050     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
1051     AlignmentI protein = dna == al1 ? al2 : al1;
1052     Set<AlignedCodonFrame> mappings = protein.getCodonFrames();
1053     for (SequenceI dnaSeq : dna.getSequences())
1054     {
1055       for (SequenceI proteinSeq : protein.getSequences())
1056       {
1057         if (isMappable(dnaSeq, proteinSeq, mappings))
1058         {
1059           return true;
1060         }
1061       }
1062     }
1063     return false;
1064   }
1065
1066   /**
1067    * Returns true if the dna sequence is mapped, or could be mapped, to the
1068    * protein sequence.
1069    * 
1070    * @param dnaSeq
1071    * @param proteinSeq
1072    * @param mappings
1073    * @return
1074    */
1075   protected static boolean isMappable(SequenceI dnaSeq,
1076           SequenceI proteinSeq, Set<AlignedCodonFrame> mappings)
1077   {
1078     if (dnaSeq == null || proteinSeq == null)
1079     {
1080       return false;
1081     }
1082
1083     SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq
1084             .getDatasetSequence();
1085     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
1086             : proteinSeq.getDatasetSequence();
1087
1088     /*
1089      * Already mapped?
1090      */
1091     for (AlignedCodonFrame mapping : mappings)
1092     {
1093       if (proteinDs == mapping.getAaForDnaSeq(dnaDs))
1094       {
1095         return true;
1096       }
1097     }
1098
1099     /*
1100      * Just try to make a mapping (it is not yet stored), test whether
1101      * successful.
1102      */
1103     return mapProteinSequenceToCdna(proteinDs, dnaDs) != null;
1104   }
1105
1106   /**
1107    * Finds any reference annotations associated with the sequences in
1108    * sequenceScope, that are not already added to the alignment, and adds them
1109    * to the 'candidates' map. Also populates a lookup table of annotation
1110    * labels, keyed by calcId, for use in constructing tooltips or the like.
1111    * 
1112    * @param sequenceScope
1113    *          the sequences to scan for reference annotations
1114    * @param labelForCalcId
1115    *          (optional) map to populate with label for calcId
1116    * @param candidates
1117    *          map to populate with annotations for sequence
1118    * @param al
1119    *          the alignment to check for presence of annotations
1120    */
1121   public static void findAddableReferenceAnnotations(
1122           List<SequenceI> sequenceScope,
1123           Map<String, String> labelForCalcId,
1124           final Map<SequenceI, List<AlignmentAnnotation>> candidates,
1125           AlignmentI al)
1126   {
1127     if (sequenceScope == null)
1128     {
1129       return;
1130     }
1131
1132     /*
1133      * For each sequence in scope, make a list of any annotations on the
1134      * underlying dataset sequence which are not already on the alignment.
1135      * 
1136      * Add to a map of { alignmentSequence, <List of annotations to add> }
1137      */
1138     for (SequenceI seq : sequenceScope)
1139     {
1140       SequenceI dataset = seq.getDatasetSequence();
1141       if (dataset == null)
1142       {
1143         continue;
1144       }
1145       AlignmentAnnotation[] datasetAnnotations = dataset.getAnnotation();
1146       if (datasetAnnotations == null)
1147       {
1148         continue;
1149       }
1150       final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
1151       for (AlignmentAnnotation dsann : datasetAnnotations)
1152       {
1153         /*
1154          * Find matching annotations on the alignment. If none is found, then
1155          * add this annotation to the list of 'addable' annotations for this
1156          * sequence.
1157          */
1158         final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
1159                 .findAnnotations(seq, dsann.getCalcId(), dsann.label);
1160         if (!matchedAlignmentAnnotations.iterator().hasNext())
1161         {
1162           result.add(dsann);
1163           if (labelForCalcId != null)
1164           {
1165             labelForCalcId.put(dsann.getCalcId(), dsann.label);
1166           }
1167         }
1168       }
1169       /*
1170        * Save any addable annotations for this sequence
1171        */
1172       if (!result.isEmpty())
1173       {
1174         candidates.put(seq, result);
1175       }
1176     }
1177   }
1178
1179   /**
1180    * Adds annotations to the top of the alignment annotations, in the same order
1181    * as their related sequences.
1182    * 
1183    * @param annotations
1184    *          the annotations to add
1185    * @param alignment
1186    *          the alignment to add them to
1187    * @param selectionGroup
1188    *          current selection group (or null if none)
1189    */
1190   public static void addReferenceAnnotations(
1191           Map<SequenceI, List<AlignmentAnnotation>> annotations,
1192           final AlignmentI alignment, final SequenceGroup selectionGroup)
1193   {
1194     for (SequenceI seq : annotations.keySet())
1195     {
1196       for (AlignmentAnnotation ann : annotations.get(seq))
1197       {
1198         AlignmentAnnotation copyAnn = new AlignmentAnnotation(ann);
1199         int startRes = 0;
1200         int endRes = ann.annotations.length;
1201         if (selectionGroup != null)
1202         {
1203           startRes = selectionGroup.getStartRes();
1204           endRes = selectionGroup.getEndRes();
1205         }
1206         copyAnn.restrict(startRes, endRes);
1207
1208         /*
1209          * Add to the sequence (sets copyAnn.datasetSequence), unless the
1210          * original annotation is already on the sequence.
1211          */
1212         if (!seq.hasAnnotation(ann))
1213         {
1214           seq.addAlignmentAnnotation(copyAnn);
1215         }
1216         // adjust for gaps
1217         copyAnn.adjustForAlignment();
1218         // add to the alignment and set visible
1219         alignment.addAnnotation(copyAnn);
1220         copyAnn.visible = true;
1221       }
1222     }
1223   }
1224
1225   /**
1226    * Set visibility of alignment annotations of specified types (labels), for
1227    * specified sequences. This supports controls like
1228    * "Show all secondary structure", "Hide all Temp factor", etc.
1229    * 
1230    * @al the alignment to scan for annotations
1231    * @param types
1232    *          the types (labels) of annotations to be updated
1233    * @param forSequences
1234    *          if not null, only annotations linked to one of these sequences are
1235    *          in scope for update; if null, acts on all sequence annotations
1236    * @param anyType
1237    *          if this flag is true, 'types' is ignored (label not checked)
1238    * @param doShow
1239    *          if true, set visibility on, else set off
1240    */
1241   public static void showOrHideSequenceAnnotations(AlignmentI al,
1242           Collection<String> types, List<SequenceI> forSequences,
1243           boolean anyType, boolean doShow)
1244   {
1245     for (AlignmentAnnotation aa : al.getAlignmentAnnotation())
1246     {
1247       if (anyType || types.contains(aa.label))
1248       {
1249         if ((aa.sequenceRef != null)
1250                 && (forSequences == null || forSequences
1251                         .contains(aa.sequenceRef)))
1252         {
1253           aa.visible = doShow;
1254         }
1255       }
1256     }
1257   }
1258
1259   /**
1260    * Returns true if either sequence has a cross-reference to the other
1261    * 
1262    * @param seq1
1263    * @param seq2
1264    * @return
1265    */
1266   public static boolean haveCrossRef(SequenceI seq1, SequenceI seq2)
1267   {
1268     // Note: moved here from class CrossRef as the latter class has dependencies
1269     // not availability to the applet's classpath
1270     return hasCrossRef(seq1, seq2) || hasCrossRef(seq2, seq1);
1271   }
1272
1273   /**
1274    * Returns true if seq1 has a cross-reference to seq2. Currently this assumes
1275    * that sequence name is structured as Source|AccessionId.
1276    * 
1277    * @param seq1
1278    * @param seq2
1279    * @return
1280    */
1281   public static boolean hasCrossRef(SequenceI seq1, SequenceI seq2)
1282   {
1283     if (seq1 == null || seq2 == null)
1284     {
1285       return false;
1286     }
1287     String name = seq2.getName();
1288     final DBRefEntry[] xrefs = seq1.getDBRef();
1289     if (xrefs != null)
1290     {
1291       for (DBRefEntry xref : xrefs)
1292       {
1293         String xrefName = xref.getSource() + "|" + xref.getAccessionId();
1294         // case-insensitive test, consistent with DBRefEntry.equalRef()
1295         if (xrefName.equalsIgnoreCase(name))
1296         {
1297           return true;
1298         }
1299       }
1300     }
1301     return false;
1302   }
1303
1304   /**
1305    * Constructs an alignment consisting of the mapped exon regions in the given
1306    * nucleotide sequences, and updates mappings to match.
1307    * 
1308    * @param dna
1309    *          aligned dna sequences
1310    * @param mappings
1311    *          from dna to protein; these are replaced with new mappings
1312    * @return an alignment whose sequences are the exon-only parts of the dna
1313    *         sequences (or null if no exons are found)
1314    */
1315   public static AlignmentI makeExonAlignment(SequenceI[] dna,
1316           Set<AlignedCodonFrame> mappings)
1317   {
1318     Set<AlignedCodonFrame> newMappings = new LinkedHashSet<AlignedCodonFrame>();
1319     List<SequenceI> exonSequences = new ArrayList<SequenceI>();
1320
1321     for (SequenceI dnaSeq : dna)
1322     {
1323       final SequenceI ds = dnaSeq.getDatasetSequence();
1324       List<AlignedCodonFrame> seqMappings = MappingUtils
1325               .findMappingsForSequence(ds, mappings);
1326       for (AlignedCodonFrame acf : seqMappings)
1327       {
1328         AlignedCodonFrame newMapping = new AlignedCodonFrame();
1329         final List<SequenceI> mappedExons = makeExonSequences(ds, acf,
1330                 newMapping);
1331         if (!mappedExons.isEmpty())
1332         {
1333           exonSequences.addAll(mappedExons);
1334           newMappings.add(newMapping);
1335         }
1336       }
1337     }
1338     AlignmentI al = new Alignment(
1339             exonSequences.toArray(new SequenceI[exonSequences.size()]));
1340     al.setDataset(null);
1341
1342     /*
1343      * Replace the old mappings with the new ones
1344      */
1345     mappings.clear();
1346     mappings.addAll(newMappings);
1347
1348     return al;
1349   }
1350
1351   /**
1352    * Helper method to make exon-only sequences and populate their mappings to
1353    * protein products
1354    * <p>
1355    * For example, if ggCCaTTcGAg has mappings [3, 4, 6, 7, 9, 10] to protein
1356    * then generate a sequence CCTTGA with mapping [1, 6] to the same protein
1357    * residues
1358    * <p>
1359    * Typically eukaryotic dna will include exons encoding for a single peptide
1360    * sequence i.e. return a single result. Bacterial dna may have overlapping
1361    * exon mappings coding for multiple peptides so return multiple results
1362    * (example EMBL KF591215).
1363    * 
1364    * @param dnaSeq
1365    *          a dna dataset sequence
1366    * @param mapping
1367    *          containing one or more mappings of the sequence to protein
1368    * @param newMapping
1369    *          the new mapping to populate, from the exon-only sequences to their
1370    *          mapped protein sequences
1371    * @return
1372    */
1373   protected static List<SequenceI> makeExonSequences(SequenceI dnaSeq,
1374           AlignedCodonFrame mapping, AlignedCodonFrame newMapping)
1375   {
1376     List<SequenceI> exonSequences = new ArrayList<SequenceI>();
1377     List<Mapping> seqMappings = mapping.getMappingsForSequence(dnaSeq);
1378     final char[] dna = dnaSeq.getSequence();
1379     for (Mapping seqMapping : seqMappings)
1380     {
1381       StringBuilder newSequence = new StringBuilder(dnaSeq.getLength());
1382
1383       /*
1384        * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
1385        */
1386       final List<int[]> dnaExonRanges = seqMapping.getMap().getFromRanges();
1387       for (int[] range : dnaExonRanges)
1388       {
1389         for (int pos = range[0]; pos <= range[1]; pos++)
1390         {
1391           newSequence.append(dna[pos - 1]);
1392         }
1393       }
1394
1395       SequenceI exon = new Sequence(dnaSeq.getName(),
1396               newSequence.toString());
1397
1398       /*
1399        * Locate any xrefs to CDS database on the protein product and attach to
1400        * the CDS sequence. Also add as a sub-token of the sequence name.
1401        */
1402       // default to "CDS" if we can't locate an actual gene id
1403       String cdsAccId = FeatureProperties
1404               .getCodingFeature(DBRefSource.EMBL);
1405       DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(seqMapping.getTo()
1406               .getDBRef(), DBRefSource.CODINGDBS);
1407       if (cdsRefs != null)
1408       {
1409         for (DBRefEntry cdsRef : cdsRefs)
1410         {
1411           exon.addDBRef(new DBRefEntry(cdsRef));
1412           cdsAccId = cdsRef.getAccessionId();
1413         }
1414       }
1415       exon.setName(exon.getName() + "|" + cdsAccId);
1416       exon.createDatasetSequence();
1417
1418       /*
1419        * Build new mappings - from the same protein regions, but now to
1420        * contiguous exons
1421        */
1422       List<int[]> exonRange = new ArrayList<int[]>();
1423       exonRange.add(new int[] { 1, newSequence.length() });
1424       MapList map = new MapList(exonRange, seqMapping.getMap()
1425               .getToRanges(), 3, 1);
1426       newMapping.addMap(exon.getDatasetSequence(), seqMapping.getTo(), map);
1427       MapList cdsToDnaMap = new MapList(dnaExonRanges, exonRange, 1, 1);
1428       newMapping.addMap(dnaSeq, exon.getDatasetSequence(), cdsToDnaMap);
1429
1430       exonSequences.add(exon);
1431     }
1432     return exonSequences;
1433   }
1434 }