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