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