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