JAL-1705 refactoring etc for fetching Ensembl --> Uniprot
[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.IncompleteCodonException;
32 import jalview.datamodel.Mapping;
33 import jalview.datamodel.SearchResults;
34 import jalview.datamodel.Sequence;
35 import jalview.datamodel.SequenceFeature;
36 import jalview.datamodel.SequenceGroup;
37 import jalview.datamodel.SequenceI;
38 import jalview.io.gff.SequenceOntologyFactory;
39 import jalview.io.gff.SequenceOntologyI;
40 import jalview.schemes.ResidueProperties;
41 import jalview.util.Comparison;
42 import jalview.util.DBRefUtils;
43 import jalview.util.MapList;
44 import jalview.util.MappingUtils;
45 import jalview.util.StringUtils;
46
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.Comparator;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.Iterator;
55 import java.util.LinkedHashMap;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Map.Entry;
59 import java.util.NoSuchElementException;
60 import java.util.Set;
61 import java.util.TreeMap;
62
63 /**
64  * grab bag of useful alignment manipulation operations Expect these to be
65  * refactored elsewhere at some point.
66  * 
67  * @author jimp
68  * 
69  */
70 public class AlignmentUtils
71 {
72
73   /**
74    * given an existing alignment, create a new alignment including all, or up to
75    * flankSize additional symbols from each sequence's dataset sequence
76    * 
77    * @param core
78    * @param flankSize
79    * @return AlignmentI
80    */
81   public static AlignmentI expandContext(AlignmentI core, int flankSize)
82   {
83     List<SequenceI> sq = new ArrayList<SequenceI>();
84     int maxoffset = 0;
85     for (SequenceI s : core.getSequences())
86     {
87       SequenceI newSeq = s.deriveSequence();
88       final int newSeqStart = newSeq.getStart() - 1;
89       if (newSeqStart > maxoffset
90               && newSeq.getDatasetSequence().getStart() < s.getStart())
91       {
92         maxoffset = newSeqStart;
93       }
94       sq.add(newSeq);
95     }
96     if (flankSize > -1)
97     {
98       maxoffset = Math.min(maxoffset, flankSize);
99     }
100
101     /*
102      * now add offset left and right to create an expanded alignment
103      */
104     for (SequenceI s : sq)
105     {
106       SequenceI ds = s;
107       while (ds.getDatasetSequence() != null)
108       {
109         ds = ds.getDatasetSequence();
110       }
111       int s_end = s.findPosition(s.getStart() + s.getLength());
112       // find available flanking residues for sequence
113       int ustream_ds = s.getStart() - ds.getStart();
114       int dstream_ds = ds.getEnd() - s_end;
115
116       // build new flanked sequence
117
118       // compute gap padding to start of flanking sequence
119       int offset = maxoffset - ustream_ds;
120
121       // padding is gapChar x ( maxoffset - min(ustream_ds, flank)
122       if (flankSize >= 0)
123       {
124         if (flankSize < ustream_ds)
125         {
126           // take up to flankSize residues
127           offset = maxoffset - flankSize;
128           ustream_ds = flankSize;
129         }
130         if (flankSize <= dstream_ds)
131         {
132           dstream_ds = flankSize - 1;
133         }
134       }
135       // TODO use Character.toLowerCase to avoid creating String objects?
136       char[] upstream = new String(ds.getSequence(s.getStart() - 1
137               - ustream_ds, s.getStart() - 1)).toLowerCase().toCharArray();
138       char[] downstream = new String(ds.getSequence(s_end - 1, s_end
139               + dstream_ds)).toLowerCase().toCharArray();
140       char[] coreseq = s.getSequence();
141       char[] nseq = new char[offset + upstream.length + downstream.length
142               + coreseq.length];
143       char c = core.getGapCharacter();
144
145       int p = 0;
146       for (; p < offset; p++)
147       {
148         nseq[p] = c;
149       }
150
151       System.arraycopy(upstream, 0, nseq, p, upstream.length);
152       System.arraycopy(coreseq, 0, nseq, p + upstream.length,
153               coreseq.length);
154       System.arraycopy(downstream, 0, nseq, p + coreseq.length
155               + upstream.length, downstream.length);
156       s.setSequence(new String(nseq));
157       s.setStart(s.getStart() - ustream_ds);
158       s.setEnd(s_end + downstream.length);
159     }
160     AlignmentI newAl = new jalview.datamodel.Alignment(
161             sq.toArray(new SequenceI[0]));
162     for (SequenceI s : sq)
163     {
164       if (s.getAnnotation() != null)
165       {
166         for (AlignmentAnnotation aa : s.getAnnotation())
167         {
168           aa.adjustForAlignment(); // JAL-1712 fix
169           newAl.addAnnotation(aa);
170         }
171       }
172     }
173     newAl.setDataset(core.getDataset());
174     return newAl;
175   }
176
177   /**
178    * Returns the index (zero-based position) of a sequence in an alignment, or
179    * -1 if not found.
180    * 
181    * @param al
182    * @param seq
183    * @return
184    */
185   public static int getSequenceIndex(AlignmentI al, SequenceI seq)
186   {
187     int result = -1;
188     int pos = 0;
189     for (SequenceI alSeq : al.getSequences())
190     {
191       if (alSeq == seq)
192       {
193         result = pos;
194         break;
195       }
196       pos++;
197     }
198     return result;
199   }
200
201   /**
202    * Returns a map of lists of sequences in the alignment, keyed by sequence
203    * name. For use in mapping between different alignment views of the same
204    * sequences.
205    * 
206    * @see jalview.datamodel.AlignmentI#getSequencesByName()
207    */
208   public static Map<String, List<SequenceI>> getSequencesByName(
209           AlignmentI al)
210   {
211     Map<String, List<SequenceI>> theMap = new LinkedHashMap<String, List<SequenceI>>();
212     for (SequenceI seq : al.getSequences())
213     {
214       String name = seq.getName();
215       if (name != null)
216       {
217         List<SequenceI> seqs = theMap.get(name);
218         if (seqs == null)
219         {
220           seqs = new ArrayList<SequenceI>();
221           theMap.put(name, seqs);
222         }
223         seqs.add(seq);
224       }
225     }
226     return theMap;
227   }
228
229   /**
230    * Build mapping of protein to cDNA alignment. Mappings are made between
231    * sequences where the cDNA translates to the protein sequence. Any new
232    * mappings are added to the protein alignment. Returns true if any mappings
233    * either already exist or were added, else false.
234    * 
235    * @param proteinAlignment
236    * @param cdnaAlignment
237    * @return
238    */
239   public static boolean mapProteinAlignmentToCdna(
240           final AlignmentI proteinAlignment, final AlignmentI cdnaAlignment)
241   {
242     if (proteinAlignment == null || cdnaAlignment == null)
243     {
244       return false;
245     }
246
247     Set<SequenceI> mappedDna = new HashSet<SequenceI>();
248     Set<SequenceI> mappedProtein = new HashSet<SequenceI>();
249
250     /*
251      * First pass - map sequences where cross-references exist. This include
252      * 1-to-many mappings to support, for example, variant cDNA.
253      */
254     boolean mappingPerformed = mapProteinToCdna(proteinAlignment,
255             cdnaAlignment, mappedDna, mappedProtein, true);
256
257     /*
258      * Second pass - map sequences where no cross-references exist. This only
259      * does 1-to-1 mappings and assumes corresponding sequences are in the same
260      * order in the alignments.
261      */
262     mappingPerformed |= mapProteinToCdna(proteinAlignment, cdnaAlignment,
263             mappedDna, mappedProtein, false);
264     return mappingPerformed;
265   }
266
267   /**
268    * Make mappings between compatible sequences (where the cDNA translation
269    * matches the protein).
270    * 
271    * @param proteinAlignment
272    * @param cdnaAlignment
273    * @param mappedDna
274    *          a set of mapped DNA sequences (to add to)
275    * @param mappedProtein
276    *          a set of mapped Protein sequences (to add to)
277    * @param xrefsOnly
278    *          if true, only map sequences where xrefs exist
279    * @return
280    */
281   protected static boolean mapProteinToCdna(
282           final AlignmentI proteinAlignment,
283           final AlignmentI cdnaAlignment, Set<SequenceI> mappedDna,
284           Set<SequenceI> mappedProtein, boolean xrefsOnly)
285   {
286     boolean mappingExistsOrAdded = false;
287     List<SequenceI> thisSeqs = proteinAlignment.getSequences();
288     for (SequenceI aaSeq : thisSeqs)
289     {
290       boolean proteinMapped = false;
291       AlignedCodonFrame acf = new AlignedCodonFrame();
292
293       for (SequenceI cdnaSeq : cdnaAlignment.getSequences())
294       {
295         /*
296          * Always try to map if sequences have xref to each other; this supports
297          * variant cDNA or alternative splicing for a protein sequence.
298          * 
299          * If no xrefs, try to map progressively, assuming that alignments have
300          * mappable sequences in corresponding order. These are not
301          * many-to-many, as that would risk mixing species with similar cDNA
302          * sequences.
303          */
304         if (xrefsOnly && !AlignmentUtils.haveCrossRef(aaSeq, cdnaSeq))
305         {
306           continue;
307         }
308
309         /*
310          * Don't map non-xrefd sequences more than once each. This heuristic
311          * allows us to pair up similar sequences in ordered alignments.
312          */
313         if (!xrefsOnly
314                 && (mappedProtein.contains(aaSeq) || mappedDna
315                         .contains(cdnaSeq)))
316         {
317           continue;
318         }
319         if (mappingExists(proteinAlignment.getCodonFrames(),
320                 aaSeq.getDatasetSequence(), cdnaSeq.getDatasetSequence()))
321         {
322           mappingExistsOrAdded = true;
323         }
324         else
325         {
326           MapList map = mapCdnaToProtein(aaSeq, cdnaSeq);
327           if (map != null)
328           {
329             acf.addMap(cdnaSeq, aaSeq, map);
330             mappingExistsOrAdded = true;
331             proteinMapped = true;
332             mappedDna.add(cdnaSeq);
333             mappedProtein.add(aaSeq);
334           }
335         }
336       }
337       if (proteinMapped)
338       {
339         proteinAlignment.addCodonFrame(acf);
340       }
341     }
342     return mappingExistsOrAdded;
343   }
344
345   /**
346    * Answers true if the mappings include one between the given (dataset)
347    * sequences.
348    */
349   public static boolean mappingExists(List<AlignedCodonFrame> mappings,
350           SequenceI aaSeq, SequenceI cdnaSeq)
351   {
352     if (mappings != null)
353     {
354       for (AlignedCodonFrame acf : mappings)
355       {
356         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
357         {
358           return true;
359         }
360       }
361     }
362     return false;
363   }
364
365   /**
366    * Builds a mapping (if possible) of a cDNA to a protein sequence.
367    * <ul>
368    * <li>first checks if the cdna translates exactly to the protein sequence</li>
369    * <li>else checks for translation after removing a STOP codon</li>
370    * <li>else checks for translation after removing a START codon</li>
371    * <li>if that fails, inspect CDS features on the cDNA sequence</li>
372    * </ul>
373    * Returns null if no mapping is determined.
374    * 
375    * @param proteinSeq
376    *          the aligned protein sequence
377    * @param cdnaSeq
378    *          the aligned cdna sequence
379    * @return
380    */
381   public static MapList mapCdnaToProtein(SequenceI proteinSeq,
382           SequenceI cdnaSeq)
383   {
384     /*
385      * Here we handle either dataset sequence set (desktop) or absent (applet).
386      * Use only the char[] form of the sequence to avoid creating possibly large
387      * String objects.
388      */
389     final SequenceI proteinDataset = proteinSeq.getDatasetSequence();
390     char[] aaSeqChars = proteinDataset != null ? proteinDataset
391             .getSequence() : proteinSeq.getSequence();
392     final SequenceI cdnaDataset = cdnaSeq.getDatasetSequence();
393     char[] cdnaSeqChars = cdnaDataset != null ? cdnaDataset.getSequence()
394             : cdnaSeq.getSequence();
395     if (aaSeqChars == null || cdnaSeqChars == null)
396     {
397       return null;
398     }
399
400     /*
401      * cdnaStart/End, proteinStartEnd are base 1 (for dataset sequence mapping)
402      */
403     final int mappedLength = 3 * aaSeqChars.length;
404     int cdnaLength = cdnaSeqChars.length;
405     int cdnaStart = cdnaSeq.getStart();
406     int cdnaEnd = cdnaSeq.getEnd();
407     final int proteinStart = proteinSeq.getStart();
408     final int proteinEnd = proteinSeq.getEnd();
409
410     /*
411      * If lengths don't match, try ignoring stop codon (if present)
412      */
413     if (cdnaLength != mappedLength && cdnaLength > 2)
414     {
415       String lastCodon = String.valueOf(cdnaSeqChars, cdnaLength - 3, 3)
416               .toUpperCase();
417       for (String stop : ResidueProperties.STOP)
418       {
419         if (lastCodon.equals(stop))
420         {
421           cdnaEnd -= 3;
422           cdnaLength -= 3;
423           break;
424         }
425       }
426     }
427
428     /*
429      * If lengths still don't match, try ignoring start codon.
430      */
431     int startOffset = 0;
432     if (cdnaLength != mappedLength
433             && cdnaLength > 2
434             && String.valueOf(cdnaSeqChars, 0, 3).toUpperCase()
435                     .equals(ResidueProperties.START))
436     {
437       startOffset += 3;
438       cdnaStart += 3;
439       cdnaLength -= 3;
440     }
441
442     if (translatesAs(cdnaSeqChars, startOffset, aaSeqChars))
443     {
444       /*
445        * protein is translation of dna (+/- start/stop codons)
446        */
447       MapList map = new MapList(new int[] { cdnaStart, cdnaEnd }, new int[]
448       { proteinStart, proteinEnd }, 3, 1);
449       return map;
450     }
451
452     /*
453      * translation failed - try mapping CDS annotated regions of dna
454      */
455     return mapCdsToProtein(cdnaSeq, proteinSeq);
456   }
457
458   /**
459    * Test whether the given cdna sequence, starting at the given offset,
460    * translates to the given amino acid sequence, using the standard translation
461    * table. Designed to fail fast i.e. as soon as a mismatch position is found.
462    * 
463    * @param cdnaSeqChars
464    * @param cdnaStart
465    * @param aaSeqChars
466    * @return
467    */
468   protected static boolean translatesAs(char[] cdnaSeqChars, int cdnaStart,
469           char[] aaSeqChars)
470   {
471     if (cdnaSeqChars == null || aaSeqChars == null)
472     {
473       return false;
474     }
475
476     int aaPos = 0;
477     int dnaPos = cdnaStart;
478     for (; dnaPos < cdnaSeqChars.length - 2
479             && aaPos < aaSeqChars.length; dnaPos += 3, aaPos++)
480     {
481       String codon = String.valueOf(cdnaSeqChars, dnaPos, 3);
482       final String translated = ResidueProperties.codonTranslate(codon);
483
484       /*
485        * allow * in protein to match untranslatable in dna
486        */
487       final char aaRes = aaSeqChars[aaPos];
488       if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
489       {
490         continue;
491       }
492       if (translated == null || !(aaRes == translated.charAt(0)))
493       {
494         // debug
495         // System.out.println(("Mismatch at " + i + "/" + aaResidue + ": "
496         // + codon + "(" + translated + ") != " + aaRes));
497         return false;
498       }
499     }
500
501     /*
502      * check we matched all of the protein sequence
503      */
504     if (aaPos != aaSeqChars.length)
505     {
506       return false;
507     }
508
509     /*
510      * check we matched all of the dna except
511      * for optional trailing STOP codon
512      */
513     if (dnaPos == cdnaSeqChars.length)
514     {
515       return true;
516     }
517     if (dnaPos == cdnaSeqChars.length - 3)
518     {
519       String codon = String.valueOf(cdnaSeqChars, dnaPos, 3);
520       if ("STOP".equals(ResidueProperties.codonTranslate(codon)))
521       {
522         return true;
523       }
524     }
525     return false;
526   }
527
528   /**
529    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
530    * currently assumes that we are aligning cDNA to match protein.
531    * 
532    * @param seq
533    *          the sequence to be realigned
534    * @param al
535    *          the alignment whose sequence alignment is to be 'copied'
536    * @param gap
537    *          character string represent a gap in the realigned sequence
538    * @param preserveUnmappedGaps
539    * @param preserveMappedGaps
540    * @return true if the sequence was realigned, false if it could not be
541    */
542   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
543           String gap, boolean preserveMappedGaps,
544           boolean preserveUnmappedGaps)
545   {
546     /*
547      * Get any mappings from the source alignment to the target (dataset)
548      * sequence.
549      */
550     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
551     // all mappings. Would it help to constrain this?
552     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
553     if (mappings == null || mappings.isEmpty())
554     {
555       return false;
556     }
557
558     /*
559      * Locate the aligned source sequence whose dataset sequence is mapped. We
560      * just take the first match here (as we can't align like more than one
561      * sequence).
562      */
563     SequenceI alignFrom = null;
564     AlignedCodonFrame mapping = null;
565     for (AlignedCodonFrame mp : mappings)
566     {
567       alignFrom = mp.findAlignedSequence(seq.getDatasetSequence(), al);
568       if (alignFrom != null)
569       {
570         mapping = mp;
571         break;
572       }
573     }
574
575     if (alignFrom == null)
576     {
577       return false;
578     }
579     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
580             preserveMappedGaps, preserveUnmappedGaps);
581     return true;
582   }
583
584   /**
585    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
586    * match residues and codons. Flags control whether existing gaps in unmapped
587    * (intron) and mapped (exon) regions are preserved or not. Gaps between
588    * intron and exon are only retained if both flags are set.
589    * 
590    * @param alignTo
591    * @param alignFrom
592    * @param mapping
593    * @param myGap
594    * @param sourceGap
595    * @param preserveUnmappedGaps
596    * @param preserveMappedGaps
597    */
598   public static void alignSequenceAs(SequenceI alignTo,
599           SequenceI alignFrom, AlignedCodonFrame mapping, String myGap,
600           char sourceGap, boolean preserveMappedGaps,
601           boolean preserveUnmappedGaps)
602   {
603     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
604
605     // aligned and dataset sequence positions, all base zero
606     int thisSeqPos = 0;
607     int sourceDsPos = 0;
608
609     int basesWritten = 0;
610     char myGapChar = myGap.charAt(0);
611     int ratio = myGap.length();
612
613     int fromOffset = alignFrom.getStart() - 1;
614     int toOffset = alignTo.getStart() - 1;
615     int sourceGapMappedLength = 0;
616     boolean inExon = false;
617     final char[] thisSeq = alignTo.getSequence();
618     final char[] thatAligned = alignFrom.getSequence();
619     StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
620
621     /*
622      * Traverse the 'model' aligned sequence
623      */
624     for (char sourceChar : thatAligned)
625     {
626       if (sourceChar == sourceGap)
627       {
628         sourceGapMappedLength += ratio;
629         continue;
630       }
631
632       /*
633        * Found a non-gap character. Locate its mapped region if any.
634        */
635       sourceDsPos++;
636       // Note mapping positions are base 1, our sequence positions base 0
637       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
638               sourceDsPos + fromOffset);
639       if (mappedPos == null)
640       {
641         /*
642          * unmapped position; treat like a gap
643          */
644         sourceGapMappedLength += ratio;
645         // System.err.println("Can't align: no codon mapping to residue "
646         // + sourceDsPos + "(" + sourceChar + ")");
647         // return;
648         continue;
649       }
650
651       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
652       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
653       StringBuilder trailingCopiedGap = new StringBuilder();
654
655       /*
656        * Copy dna sequence up to and including this codon. Optionally, include
657        * gaps before the codon starts (in introns) and/or after the codon starts
658        * (in exons).
659        * 
660        * Note this only works for 'linear' splicing, not reverse or interleaved.
661        * But then 'align dna as protein' doesn't make much sense otherwise.
662        */
663       int intronLength = 0;
664       while (basesWritten + toOffset < mappedCodonEnd
665               && thisSeqPos < thisSeq.length)
666       {
667         final char c = thisSeq[thisSeqPos++];
668         if (c != myGapChar)
669         {
670           basesWritten++;
671           int sourcePosition = basesWritten + toOffset;
672           if (sourcePosition < mappedCodonStart)
673           {
674             /*
675              * Found an unmapped (intron) base. First add in any preceding gaps
676              * (if wanted).
677              */
678             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
679             {
680               thisAligned.append(trailingCopiedGap.toString());
681               intronLength += trailingCopiedGap.length();
682               trailingCopiedGap = new StringBuilder();
683             }
684             intronLength++;
685             inExon = false;
686           }
687           else
688           {
689             final boolean startOfCodon = sourcePosition == mappedCodonStart;
690             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
691                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
692                     trailingCopiedGap.length(), intronLength, startOfCodon);
693             for (int i = 0; i < gapsToAdd; i++)
694             {
695               thisAligned.append(myGapChar);
696             }
697             sourceGapMappedLength = 0;
698             inExon = true;
699           }
700           thisAligned.append(c);
701           trailingCopiedGap = new StringBuilder();
702         }
703         else
704         {
705           if (inExon && preserveMappedGaps)
706           {
707             trailingCopiedGap.append(myGapChar);
708           }
709           else if (!inExon && preserveUnmappedGaps)
710           {
711             trailingCopiedGap.append(myGapChar);
712           }
713         }
714       }
715     }
716
717     /*
718      * At end of model aligned sequence. Copy any remaining target sequence, optionally
719      * including (intron) gaps.
720      */
721     while (thisSeqPos < thisSeq.length)
722     {
723       final char c = thisSeq[thisSeqPos++];
724       if (c != myGapChar || preserveUnmappedGaps)
725       {
726         thisAligned.append(c);
727       }
728       sourceGapMappedLength--;
729     }
730
731     /*
732      * finally add gaps to pad for any trailing source gaps or
733      * unmapped characters
734      */
735     if (preserveUnmappedGaps)
736     {
737       while (sourceGapMappedLength > 0)
738       {
739         thisAligned.append(myGapChar);
740         sourceGapMappedLength--;
741       }
742     }
743
744     /*
745      * All done aligning, set the aligned sequence.
746      */
747     alignTo.setSequence(new String(thisAligned));
748   }
749
750   /**
751    * Helper method to work out how many gaps to insert when realigning.
752    * 
753    * @param preserveMappedGaps
754    * @param preserveUnmappedGaps
755    * @param sourceGapMappedLength
756    * @param inExon
757    * @param trailingCopiedGap
758    * @param intronLength
759    * @param startOfCodon
760    * @return
761    */
762   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
763           boolean preserveUnmappedGaps, int sourceGapMappedLength,
764           boolean inExon, int trailingGapLength, int intronLength,
765           final boolean startOfCodon)
766   {
767     int gapsToAdd = 0;
768     if (startOfCodon)
769     {
770       /*
771        * Reached start of codon. Ignore trailing gaps in intron unless we are
772        * preserving gaps in both exon and intron. Ignore them anyway if the
773        * protein alignment introduces a gap at least as large as the intronic
774        * region.
775        */
776       if (inExon && !preserveMappedGaps)
777       {
778         trailingGapLength = 0;
779       }
780       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
781       {
782         trailingGapLength = 0;
783       }
784       if (inExon)
785       {
786         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
787       }
788       else
789       {
790         if (intronLength + trailingGapLength <= sourceGapMappedLength)
791         {
792           gapsToAdd = sourceGapMappedLength - intronLength;
793         }
794         else
795         {
796           gapsToAdd = Math.min(intronLength + trailingGapLength
797                   - sourceGapMappedLength, trailingGapLength);
798         }
799       }
800     }
801     else
802     {
803       /*
804        * second or third base of codon; check for any gaps in dna
805        */
806       if (!preserveMappedGaps)
807       {
808         trailingGapLength = 0;
809       }
810       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
811     }
812     return gapsToAdd;
813   }
814
815   /**
816    * Returns a list of sequences mapped from the given sequences and aligned
817    * (gapped) in the same way. For example, the cDNA for aligned protein, where
818    * a single gap in protein generates three gaps in cDNA.
819    * 
820    * @param sequences
821    * @param gapCharacter
822    * @param mappings
823    * @return
824    */
825   public static List<SequenceI> getAlignedTranslation(
826           List<SequenceI> sequences, char gapCharacter,
827           Set<AlignedCodonFrame> mappings)
828   {
829     List<SequenceI> alignedSeqs = new ArrayList<SequenceI>();
830
831     for (SequenceI seq : sequences)
832     {
833       List<SequenceI> mapped = getAlignedTranslation(seq, gapCharacter,
834               mappings);
835       alignedSeqs.addAll(mapped);
836     }
837     return alignedSeqs;
838   }
839
840   /**
841    * Returns sequences aligned 'like' the source sequence, as mapped by the
842    * given mappings. Normally we expect zero or one 'mapped' sequences, but this
843    * will support 1-to-many as well.
844    * 
845    * @param seq
846    * @param gapCharacter
847    * @param mappings
848    * @return
849    */
850   protected static List<SequenceI> getAlignedTranslation(SequenceI seq,
851           char gapCharacter, Set<AlignedCodonFrame> mappings)
852   {
853     List<SequenceI> result = new ArrayList<SequenceI>();
854     for (AlignedCodonFrame mapping : mappings)
855     {
856       if (mapping.involvesSequence(seq))
857       {
858         SequenceI mapped = getAlignedTranslation(seq, gapCharacter, mapping);
859         if (mapped != null)
860         {
861           result.add(mapped);
862         }
863       }
864     }
865     return result;
866   }
867
868   /**
869    * Returns the translation of 'seq' (as held in the mapping) with
870    * corresponding alignment (gaps).
871    * 
872    * @param seq
873    * @param gapCharacter
874    * @param mapping
875    * @return
876    */
877   protected static SequenceI getAlignedTranslation(SequenceI seq,
878           char gapCharacter, AlignedCodonFrame mapping)
879   {
880     String gap = String.valueOf(gapCharacter);
881     boolean toDna = false;
882     int fromRatio = 1;
883     SequenceI mapTo = mapping.getDnaForAaSeq(seq);
884     if (mapTo != null)
885     {
886       // mapping is from protein to nucleotide
887       toDna = true;
888       // should ideally get gap count ratio from mapping
889       gap = String.valueOf(new char[] { gapCharacter, gapCharacter,
890           gapCharacter });
891     }
892     else
893     {
894       // mapping is from nucleotide to protein
895       mapTo = mapping.getAaForDnaSeq(seq);
896       fromRatio = 3;
897     }
898     StringBuilder newseq = new StringBuilder(seq.getLength()
899             * (toDna ? 3 : 1));
900
901     int residueNo = 0; // in seq, base 1
902     int[] phrase = new int[fromRatio];
903     int phraseOffset = 0;
904     int gapWidth = 0;
905     boolean first = true;
906     final Sequence alignedSeq = new Sequence("", "");
907
908     for (char c : seq.getSequence())
909     {
910       if (c == gapCharacter)
911       {
912         gapWidth++;
913         if (gapWidth >= fromRatio)
914         {
915           newseq.append(gap);
916           gapWidth = 0;
917         }
918       }
919       else
920       {
921         phrase[phraseOffset++] = residueNo + 1;
922         if (phraseOffset == fromRatio)
923         {
924           /*
925            * Have read a whole codon (or protein residue), now translate: map
926            * source phrase to positions in target sequence add characters at
927            * these positions to newseq Note mapping positions are base 1, our
928            * sequence positions base 0.
929            */
930           SearchResults sr = new SearchResults();
931           for (int pos : phrase)
932           {
933             mapping.markMappedRegion(seq, pos, sr);
934           }
935           newseq.append(sr.getCharacters());
936           if (first)
937           {
938             first = false;
939             // Hack: Copy sequence dataset, name and description from
940             // SearchResults.match[0].sequence
941             // TODO? carry over sequence names from original 'complement'
942             // alignment
943             SequenceI mappedTo = sr.getResultSequence(0);
944             alignedSeq.setName(mappedTo.getName());
945             alignedSeq.setDescription(mappedTo.getDescription());
946             alignedSeq.setDatasetSequence(mappedTo);
947           }
948           phraseOffset = 0;
949         }
950         residueNo++;
951       }
952     }
953     alignedSeq.setSequence(newseq.toString());
954     return alignedSeq;
955   }
956
957   /**
958    * Realigns the given protein to match the alignment of the dna, using codon
959    * mappings to translate aligned codon positions to protein residues.
960    * 
961    * @param protein
962    *          the alignment whose sequences are realigned by this method
963    * @param dna
964    *          the dna alignment whose alignment we are 'copying'
965    * @return the number of sequences that were realigned
966    */
967   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
968   {
969     List<SequenceI> unmappedProtein = new ArrayList<SequenceI>();
970     Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = buildCodonColumnsMap(
971             protein, dna, unmappedProtein);
972     return alignProteinAs(protein, alignedCodons, unmappedProtein);
973   }
974
975   /**
976    * Builds a map whose key is an aligned codon position (3 alignment column
977    * numbers base 0), and whose value is a map from protein sequence to each
978    * protein's peptide residue for that codon. The map generates an ordering of
979    * the codons, and allows us to read off the peptides at each position in
980    * order to assemble 'aligned' protein sequences.
981    * 
982    * @param protein
983    *          the protein alignment
984    * @param dna
985    *          the coding dna alignment
986    * @param unmappedProtein
987    *          any unmapped proteins are added to this list
988    * @return
989    */
990   protected static Map<AlignedCodon, Map<SequenceI, AlignedCodon>> buildCodonColumnsMap(
991           AlignmentI protein, AlignmentI dna,
992           List<SequenceI> unmappedProtein)
993   {
994     /*
995      * maintain a list of any proteins with no mappings - these will be
996      * rendered 'as is' in the protein alignment as we can't align them
997      */
998     unmappedProtein.addAll(protein.getSequences());
999
1000     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
1001
1002     /*
1003      * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
1004      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
1005      * comparator keeps the codon positions ordered.
1006      */
1007     Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, AlignedCodon>>(
1008             new CodonComparator());
1009
1010     for (SequenceI dnaSeq : dna.getSequences())
1011     {
1012       for (AlignedCodonFrame mapping : mappings)
1013       {
1014         SequenceI prot = mapping.findAlignedSequence(
1015                 dnaSeq.getDatasetSequence(), protein);
1016         if (prot != null)
1017         {
1018           Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
1019           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(),
1020                   seqMap, alignedCodons);
1021           unmappedProtein.remove(prot);
1022         }
1023       }
1024     }
1025
1026     /*
1027      * Finally add any unmapped peptide start residues (e.g. for incomplete
1028      * codons) as if at the codon position before the second residue
1029      */
1030     int mappedSequenceCount = protein.getHeight() - unmappedProtein.size();
1031     addUnmappedPeptideStarts(alignedCodons, mappedSequenceCount);
1032     
1033     return alignedCodons;
1034   }
1035
1036   /**
1037    * Scans for any protein mapped from position 2 (meaning unmapped start
1038    * position e.g. an incomplete codon), and synthesizes a 'codon' for it at the
1039    * preceding position in the alignment
1040    * 
1041    * @param alignedCodons
1042    *          the codon-to-peptide map
1043    * @param mappedSequenceCount
1044    *          the number of distinct sequences in the map
1045    */
1046   protected static void addUnmappedPeptideStarts(
1047           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
1048           int mappedSequenceCount)
1049   {
1050     // TODO delete this ugly hack once JAL-2022 is resolved
1051     // i.e. we can model startPhase > 0 (incomplete start codon)
1052
1053     List<SequenceI> sequencesChecked = new ArrayList<SequenceI>();
1054     AlignedCodon lastCodon = null;
1055     Map<SequenceI, AlignedCodon> toAdd = new HashMap<SequenceI, AlignedCodon>();
1056
1057     for (Entry<AlignedCodon, Map<SequenceI, AlignedCodon>> entry : alignedCodons
1058             .entrySet())
1059     {
1060       for (Entry<SequenceI, AlignedCodon> sequenceCodon : entry.getValue()
1061               .entrySet())
1062       {
1063         SequenceI seq = sequenceCodon.getKey();
1064         if (sequencesChecked.contains(seq))
1065         {
1066           continue;
1067         }
1068         sequencesChecked.add(seq);
1069         AlignedCodon codon = sequenceCodon.getValue();
1070         if (codon.peptideCol > 1)
1071         {
1072           System.err
1073                   .println("Problem mapping protein with >1 unmapped start positions: "
1074                           + seq.getName());
1075         }
1076         else if (codon.peptideCol == 1)
1077         {
1078           /*
1079            * first position (peptideCol == 0) was unmapped - add it
1080            */
1081           if (lastCodon != null)
1082           {
1083             AlignedCodon firstPeptide = new AlignedCodon(lastCodon.pos1,
1084                     lastCodon.pos2, lastCodon.pos3, String.valueOf(seq
1085                             .getCharAt(0)), 0);
1086             toAdd.put(seq, firstPeptide);
1087           }
1088           else
1089           {
1090             /*
1091              * unmapped residue at start of alignment (no prior column) -
1092              * 'insert' at nominal codon [0, 0, 0]
1093              */
1094             AlignedCodon firstPeptide = new AlignedCodon(0, 0, 0,
1095                     String.valueOf(seq.getCharAt(0)), 0);
1096             toAdd.put(seq, firstPeptide);
1097           }
1098         }
1099         if (sequencesChecked.size() == mappedSequenceCount)
1100         {
1101           // no need to check past first mapped position in all sequences
1102           break;
1103         }
1104       }
1105       lastCodon = entry.getKey();
1106     }
1107
1108     /*
1109      * add any new codons safely after iterating over the map
1110      */
1111     for (Entry<SequenceI, AlignedCodon> startCodon : toAdd.entrySet())
1112     {
1113       addCodonToMap(alignedCodons, startCodon.getValue(),
1114               startCodon.getKey());
1115     }
1116   }
1117
1118   /**
1119    * Update the aligned protein sequences to match the codon alignments given in
1120    * the map.
1121    * 
1122    * @param protein
1123    * @param alignedCodons
1124    *          an ordered map of codon positions (columns), with sequence/peptide
1125    *          values present in each column
1126    * @param unmappedProtein
1127    * @return
1128    */
1129   protected static int alignProteinAs(AlignmentI protein,
1130           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
1131           List<SequenceI> unmappedProtein)
1132   {
1133     /*
1134      * Prefill aligned sequences with gaps before inserting aligned protein
1135      * residues.
1136      */
1137     int alignedWidth = alignedCodons.size();
1138     char[] gaps = new char[alignedWidth];
1139     Arrays.fill(gaps, protein.getGapCharacter());
1140     String allGaps = String.valueOf(gaps);
1141     for (SequenceI seq : protein.getSequences())
1142     {
1143       if (!unmappedProtein.contains(seq))
1144       {
1145         seq.setSequence(allGaps);
1146       }
1147     }
1148
1149     int column = 0;
1150     for (AlignedCodon codon : alignedCodons.keySet())
1151     {
1152       final Map<SequenceI, AlignedCodon> columnResidues = alignedCodons
1153               .get(codon);
1154       for (Entry<SequenceI, AlignedCodon> entry : columnResidues.entrySet())
1155       {
1156         // place translated codon at its column position in sequence
1157         entry.getKey().getSequence()[column] = entry.getValue().product
1158                 .charAt(0);
1159       }
1160       column++;
1161     }
1162     return 0;
1163   }
1164
1165   /**
1166    * Populate the map of aligned codons by traversing the given sequence
1167    * mapping, locating the aligned positions of mapped codons, and adding those
1168    * positions and their translation products to the map.
1169    * 
1170    * @param dna
1171    *          the aligned sequence we are mapping from
1172    * @param protein
1173    *          the sequence to be aligned to the codons
1174    * @param gapChar
1175    *          the gap character in the dna sequence
1176    * @param seqMap
1177    *          a mapping to a sequence translation
1178    * @param alignedCodons
1179    *          the map we are building up
1180    */
1181   static void addCodonPositions(SequenceI dna, SequenceI protein,
1182           char gapChar, Mapping seqMap,
1183           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons)
1184   {
1185     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
1186
1187     /*
1188      * add codon positions, and their peptide translations, to the alignment
1189      * map, while remembering the first codon mapped
1190      */
1191     while (codons.hasNext())
1192     {
1193       try
1194       {
1195         AlignedCodon codon = codons.next();
1196         addCodonToMap(alignedCodons, codon, protein);
1197       } catch (IncompleteCodonException e)
1198       {
1199         // possible incomplete trailing codon - ignore
1200       } catch (NoSuchElementException e)
1201       {
1202         // possibly peptide lacking STOP
1203       }
1204     }
1205   }
1206
1207   /**
1208    * Helper method to add a codon-to-peptide entry to the aligned codons map
1209    * 
1210    * @param alignedCodons
1211    * @param codon
1212    * @param protein
1213    */
1214   protected static void addCodonToMap(
1215           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
1216           AlignedCodon codon, SequenceI protein)
1217   {
1218     Map<SequenceI, AlignedCodon> seqProduct = alignedCodons.get(codon);
1219     if (seqProduct == null)
1220     {
1221       seqProduct = new HashMap<SequenceI, AlignedCodon>();
1222       alignedCodons.put(codon, seqProduct);
1223     }
1224     seqProduct.put(protein, codon);
1225   }
1226
1227   /**
1228    * Returns true if a cDNA/Protein mapping either exists, or could be made,
1229    * between at least one pair of sequences in the two alignments. Currently,
1230    * the logic is:
1231    * <ul>
1232    * <li>One alignment must be nucleotide, and the other protein</li>
1233    * <li>At least one pair of sequences must be already mapped, or mappable</li>
1234    * <li>Mappable means the nucleotide translation matches the protein sequence</li>
1235    * <li>The translation may ignore start and stop codons if present in the
1236    * nucleotide</li>
1237    * </ul>
1238    * 
1239    * @param al1
1240    * @param al2
1241    * @return
1242    */
1243   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
1244   {
1245     if (al1 == null || al2 == null)
1246     {
1247       return false;
1248     }
1249
1250     /*
1251      * Require one nucleotide and one protein
1252      */
1253     if (al1.isNucleotide() == al2.isNucleotide())
1254     {
1255       return false;
1256     }
1257     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
1258     AlignmentI protein = dna == al1 ? al2 : al1;
1259     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
1260     for (SequenceI dnaSeq : dna.getSequences())
1261     {
1262       for (SequenceI proteinSeq : protein.getSequences())
1263       {
1264         if (isMappable(dnaSeq, proteinSeq, mappings))
1265         {
1266           return true;
1267         }
1268       }
1269     }
1270     return false;
1271   }
1272
1273   /**
1274    * Returns true if the dna sequence is mapped, or could be mapped, to the
1275    * protein sequence.
1276    * 
1277    * @param dnaSeq
1278    * @param proteinSeq
1279    * @param mappings
1280    * @return
1281    */
1282   protected static boolean isMappable(SequenceI dnaSeq,
1283           SequenceI proteinSeq, List<AlignedCodonFrame> mappings)
1284   {
1285     if (dnaSeq == null || proteinSeq == null)
1286     {
1287       return false;
1288     }
1289
1290     SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq : dnaSeq
1291             .getDatasetSequence();
1292     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null ? proteinSeq
1293             : proteinSeq.getDatasetSequence();
1294
1295     for (AlignedCodonFrame mapping : mappings)
1296     {
1297       if (proteinDs == mapping.getAaForDnaSeq(dnaDs))
1298       {
1299         /*
1300          * already mapped
1301          */
1302         return true;
1303       }
1304     }
1305
1306     /*
1307      * Just try to make a mapping (it is not yet stored), test whether
1308      * successful.
1309      */
1310     return mapCdnaToProtein(proteinDs, dnaDs) != null;
1311   }
1312
1313   /**
1314    * Finds any reference annotations associated with the sequences in
1315    * sequenceScope, that are not already added to the alignment, and adds them
1316    * to the 'candidates' map. Also populates a lookup table of annotation
1317    * labels, keyed by calcId, for use in constructing tooltips or the like.
1318    * 
1319    * @param sequenceScope
1320    *          the sequences to scan for reference annotations
1321    * @param labelForCalcId
1322    *          (optional) map to populate with label for calcId
1323    * @param candidates
1324    *          map to populate with annotations for sequence
1325    * @param al
1326    *          the alignment to check for presence of annotations
1327    */
1328   public static void findAddableReferenceAnnotations(
1329           List<SequenceI> sequenceScope,
1330           Map<String, String> labelForCalcId,
1331           final Map<SequenceI, List<AlignmentAnnotation>> candidates,
1332           AlignmentI al)
1333   {
1334     if (sequenceScope == null)
1335     {
1336       return;
1337     }
1338
1339     /*
1340      * For each sequence in scope, make a list of any annotations on the
1341      * underlying dataset sequence which are not already on the alignment.
1342      * 
1343      * Add to a map of { alignmentSequence, <List of annotations to add> }
1344      */
1345     for (SequenceI seq : sequenceScope)
1346     {
1347       SequenceI dataset = seq.getDatasetSequence();
1348       if (dataset == null)
1349       {
1350         continue;
1351       }
1352       AlignmentAnnotation[] datasetAnnotations = dataset.getAnnotation();
1353       if (datasetAnnotations == null)
1354       {
1355         continue;
1356       }
1357       final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
1358       for (AlignmentAnnotation dsann : datasetAnnotations)
1359       {
1360         /*
1361          * Find matching annotations on the alignment. If none is found, then
1362          * add this annotation to the list of 'addable' annotations for this
1363          * sequence.
1364          */
1365         final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
1366                 .findAnnotations(seq, dsann.getCalcId(), dsann.label);
1367         if (!matchedAlignmentAnnotations.iterator().hasNext())
1368         {
1369           result.add(dsann);
1370           if (labelForCalcId != null)
1371           {
1372             labelForCalcId.put(dsann.getCalcId(), dsann.label);
1373           }
1374         }
1375       }
1376       /*
1377        * Save any addable annotations for this sequence
1378        */
1379       if (!result.isEmpty())
1380       {
1381         candidates.put(seq, result);
1382       }
1383     }
1384   }
1385
1386   /**
1387    * Adds annotations to the top of the alignment annotations, in the same order
1388    * as their related sequences.
1389    * 
1390    * @param annotations
1391    *          the annotations to add
1392    * @param alignment
1393    *          the alignment to add them to
1394    * @param selectionGroup
1395    *          current selection group (or null if none)
1396    */
1397   public static void addReferenceAnnotations(
1398           Map<SequenceI, List<AlignmentAnnotation>> annotations,
1399           final AlignmentI alignment, final SequenceGroup selectionGroup)
1400   {
1401     for (SequenceI seq : annotations.keySet())
1402     {
1403       for (AlignmentAnnotation ann : annotations.get(seq))
1404       {
1405         AlignmentAnnotation copyAnn = new AlignmentAnnotation(ann);
1406         int startRes = 0;
1407         int endRes = ann.annotations.length;
1408         if (selectionGroup != null)
1409         {
1410           startRes = selectionGroup.getStartRes();
1411           endRes = selectionGroup.getEndRes();
1412         }
1413         copyAnn.restrict(startRes, endRes);
1414
1415         /*
1416          * Add to the sequence (sets copyAnn.datasetSequence), unless the
1417          * original annotation is already on the sequence.
1418          */
1419         if (!seq.hasAnnotation(ann))
1420         {
1421           seq.addAlignmentAnnotation(copyAnn);
1422         }
1423         // adjust for gaps
1424         copyAnn.adjustForAlignment();
1425         // add to the alignment and set visible
1426         alignment.addAnnotation(copyAnn);
1427         copyAnn.visible = true;
1428       }
1429     }
1430   }
1431
1432   /**
1433    * Set visibility of alignment annotations of specified types (labels), for
1434    * specified sequences. This supports controls like
1435    * "Show all secondary structure", "Hide all Temp factor", etc.
1436    * 
1437    * @al the alignment to scan for annotations
1438    * @param types
1439    *          the types (labels) of annotations to be updated
1440    * @param forSequences
1441    *          if not null, only annotations linked to one of these sequences are
1442    *          in scope for update; if null, acts on all sequence annotations
1443    * @param anyType
1444    *          if this flag is true, 'types' is ignored (label not checked)
1445    * @param doShow
1446    *          if true, set visibility on, else set off
1447    */
1448   public static void showOrHideSequenceAnnotations(AlignmentI al,
1449           Collection<String> types, List<SequenceI> forSequences,
1450           boolean anyType, boolean doShow)
1451   {
1452     for (AlignmentAnnotation aa : al.getAlignmentAnnotation())
1453     {
1454       if (anyType || types.contains(aa.label))
1455       {
1456         if ((aa.sequenceRef != null)
1457                 && (forSequences == null || forSequences
1458                         .contains(aa.sequenceRef)))
1459         {
1460           aa.visible = doShow;
1461         }
1462       }
1463     }
1464   }
1465
1466   /**
1467    * Returns true if either sequence has a cross-reference to the other
1468    * 
1469    * @param seq1
1470    * @param seq2
1471    * @return
1472    */
1473   public static boolean haveCrossRef(SequenceI seq1, SequenceI seq2)
1474   {
1475     // Note: moved here from class CrossRef as the latter class has dependencies
1476     // not availability to the applet's classpath
1477     return hasCrossRef(seq1, seq2) || hasCrossRef(seq2, seq1);
1478   }
1479
1480   /**
1481    * Returns true if seq1 has a cross-reference to seq2. Currently this assumes
1482    * that sequence name is structured as Source|AccessionId.
1483    * 
1484    * @param seq1
1485    * @param seq2
1486    * @return
1487    */
1488   public static boolean hasCrossRef(SequenceI seq1, SequenceI seq2)
1489   {
1490     if (seq1 == null || seq2 == null)
1491     {
1492       return false;
1493     }
1494     String name = seq2.getName();
1495     final DBRefEntry[] xrefs = seq1.getDBRefs();
1496     if (xrefs != null)
1497     {
1498       for (DBRefEntry xref : xrefs)
1499       {
1500         String xrefName = xref.getSource() + "|" + xref.getAccessionId();
1501         // case-insensitive test, consistent with DBRefEntry.equalRef()
1502         if (xrefName.equalsIgnoreCase(name))
1503         {
1504           return true;
1505         }
1506       }
1507     }
1508     return false;
1509   }
1510
1511   /**
1512    * Constructs an alignment consisting of the mapped (CDS) regions in the given
1513    * nucleotide sequences, and updates mappings to match. The new sequences are
1514    * aligned as per the original sequence, with entirely gapped columns (codon
1515    * interrupted by intron) omitted.
1516    * 
1517    * @param dna
1518    *          aligned dna sequences
1519    * @param mappings
1520    *          from dna to protein; these are replaced with new mappings
1521    * @param al
1522    * @return an alignment whose sequences are the cds-only parts of the dna
1523    *         sequences (or null if no mappings are found)
1524    */
1525   public static AlignmentI makeCdsAlignment(SequenceI[] dna,
1526           List<AlignedCodonFrame> mappings, AlignmentI al)
1527   {
1528     List<int[]> cdsColumns = findCdsColumns(dna);
1529
1530     /*
1531      * create CDS sequences and new mappings 
1532      * (from cdna to cds, and cds to peptide)
1533      */
1534     List<AlignedCodonFrame> newMappings = new ArrayList<AlignedCodonFrame>();
1535     List<SequenceI> cdsSequences = new ArrayList<SequenceI>();
1536     char gap = al.getGapCharacter();
1537
1538     for (SequenceI dnaSeq : dna)
1539     {
1540       final SequenceI ds = dnaSeq.getDatasetSequence();
1541       List<AlignedCodonFrame> seqMappings = MappingUtils
1542               .findMappingsForSequence(ds, mappings);
1543       for (AlignedCodonFrame acf : seqMappings)
1544       {
1545         AlignedCodonFrame newMapping = new AlignedCodonFrame();
1546         final List<SequenceI> mappedCds = makeCdsSequences(dnaSeq, acf,
1547                 cdsColumns, newMapping, gap);
1548         if (!mappedCds.isEmpty())
1549         {
1550           cdsSequences.addAll(mappedCds);
1551           newMappings.add(newMapping);
1552         }
1553       }
1554     }
1555     AlignmentI newAl = new Alignment(
1556             cdsSequences.toArray(new SequenceI[cdsSequences.size()]));
1557
1558     /*
1559      * add new sequences to the shared dataset, set it on the new alignment
1560      */
1561     List<SequenceI> dsseqs = al.getDataset().getSequences();
1562     for (SequenceI seq : newAl.getSequences())
1563     {
1564       if (!dsseqs.contains(seq.getDatasetSequence()))
1565       {
1566         dsseqs.add(seq.getDatasetSequence());
1567       }
1568     }
1569     newAl.setDataset(al.getDataset());
1570
1571     /*
1572      * Replace the old mappings with the new ones
1573      */
1574     mappings.clear();
1575     mappings.addAll(newMappings);
1576
1577     return newAl;
1578   }
1579
1580   /**
1581    * Returns a consolidated list of column ranges where at least one sequence
1582    * has a CDS feature. This assumes CDS features are on genomic sequence i.e.
1583    * are for contiguous CDS ranges (no gaps).
1584    * 
1585    * @param seqs
1586    * @return
1587    */
1588   public static List<int[]> findCdsColumns(SequenceI[] seqs)
1589   {
1590     // TODO use refactored code from AlignViewController
1591     // markColumnsContainingFeatures, not reinvent the wheel!
1592
1593     List<int[]> result = new ArrayList<int[]>();
1594     for (SequenceI seq : seqs)
1595     {
1596       result.addAll(findCdsColumns(seq));
1597     }
1598
1599     /*
1600      * sort and compact the list into ascending, non-overlapping ranges
1601      */
1602     Collections.sort(result, new Comparator<int[]>()
1603     {
1604       @Override
1605       public int compare(int[] o1, int[] o2)
1606       {
1607         return Integer.compare(o1[0], o2[0]);
1608       }
1609     });
1610     result = MapList.coalesceRanges(result);
1611
1612     return result;
1613   }
1614
1615   public static List<int[]> findCdsColumns(SequenceI seq)
1616   {
1617     List<int[]> result = new ArrayList<int[]>();
1618     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
1619     SequenceFeature[] sfs = seq.getSequenceFeatures();
1620     if (sfs != null)
1621     {
1622       for (SequenceFeature sf : sfs)
1623       {
1624         if (so.isA(sf.getType(), SequenceOntologyI.CDS))
1625         {
1626           int colStart = seq.findIndex(sf.getBegin());
1627           int colEnd = seq.findIndex(sf.getEnd());
1628           result.add(new int[] { colStart, colEnd });
1629         }
1630       }
1631     }
1632     return result;
1633   }
1634
1635   /**
1636    * Answers true if all sequences have a gap at (or do not extend to) the
1637    * specified column position (base 1)
1638    * 
1639    * @param seqs
1640    * @param col
1641    * @return
1642    */
1643   public static boolean isGappedColumn(List<SequenceI> seqs, int col)
1644   {
1645     if (seqs != null)
1646     {
1647       for (SequenceI seq : seqs)
1648       {
1649         if (!Comparison.isGap(seq.getCharAt(col - 1)))
1650         {
1651           return false;
1652         }
1653       }
1654     }
1655     return true;
1656   }
1657
1658   /**
1659    * Returns the column ranges (base 1) of each aligned sequence that are
1660    * involved in any mapping. This is a helper method for aligning protein
1661    * products of aligned transcripts.
1662    * 
1663    * @param mappedSequences
1664    *          (possibly gapped) dna sequences
1665    * @param mappings
1666    * @return
1667    */
1668   protected static List<List<int[]>> getMappedColumns(
1669           List<SequenceI> mappedSequences, List<AlignedCodonFrame> mappings)
1670   {
1671     List<List<int[]>> result = new ArrayList<List<int[]>>();
1672     for (SequenceI seq : mappedSequences)
1673     {
1674       List<int[]> columns = new ArrayList<int[]>();
1675       List<AlignedCodonFrame> seqMappings = MappingUtils
1676               .findMappingsForSequence(seq, mappings);
1677       for (AlignedCodonFrame mapping : seqMappings)
1678       {
1679         List<Mapping> maps = mapping.getMappingsForSequence(seq);
1680         for (Mapping map : maps)
1681         {
1682           /*
1683            * Get the codon regions as { [2, 5], [7, 12], [14, 14] etc }
1684            * Find and add the overall aligned column range for each
1685            */
1686           for (int[] cdsRange : map.getMap().getFromRanges())
1687           {
1688             int startPos = cdsRange[0];
1689             int endPos = cdsRange[1];
1690             int startCol = seq.findIndex(startPos);
1691             int endCol = seq.findIndex(endPos);
1692             columns.add(new int[] { startCol, endCol });
1693           }
1694         }
1695       }
1696       result.add(columns);
1697     }
1698     return result;
1699   }
1700
1701   /**
1702    * Helper method to make cds-only sequences and populate their mappings to
1703    * protein products
1704    * <p>
1705    * For example, if ggCCaTTcGAg has mappings [3, 4, 6, 7, 9, 10] to protein
1706    * then generate a sequence CCTTGA with mapping [1, 6] to the same protein
1707    * residues
1708    * <p>
1709    * Typically eukaryotic dna will include cds encoding for a single peptide
1710    * sequence i.e. return a single result. Bacterial dna may have overlapping
1711    * cds mappings coding for multiple peptides so return multiple results
1712    * (example EMBL KF591215).
1713    * 
1714    * @param dnaSeq
1715    *          a dna aligned sequence
1716    * @param mapping
1717    *          containing one or more mappings of the sequence to protein
1718    * @param ungappedCdsColumns
1719    * @param newMappings
1720    *          the new mapping to populate, from the cds-only sequences to their
1721    *          mapped protein sequences
1722    * @return
1723    */
1724   protected static List<SequenceI> makeCdsSequences(SequenceI dnaSeq,
1725           AlignedCodonFrame mapping, List<int[]> ungappedCdsColumns,
1726           AlignedCodonFrame newMappings, char gapChar)
1727   {
1728     List<SequenceI> cdsSequences = new ArrayList<SequenceI>();
1729     List<Mapping> seqMappings = mapping.getMappingsForSequence(dnaSeq);
1730
1731     for (Mapping seqMapping : seqMappings)
1732     {
1733       SequenceI cds = makeCdsSequence(dnaSeq, seqMapping,
1734               ungappedCdsColumns, gapChar);
1735       cds.createDatasetSequence();
1736       cdsSequences.add(cds);
1737
1738       /*
1739        * add new mappings, from dna to cds, and from cds to peptide 
1740        */
1741       MapList dnaToCds = addCdsMappings(dnaSeq.getDatasetSequence(), cds,
1742               seqMapping, newMappings);
1743
1744       /*
1745        * transfer any features on dna that overlap the CDS
1746        */
1747       transferFeatures(dnaSeq, cds, dnaToCds, null, SequenceOntologyI.CDS);
1748     }
1749     return cdsSequences;
1750   }
1751
1752   /**
1753    * Transfers co-located features on 'fromSeq' to 'toSeq', adjusting the
1754    * feature start/end ranges, optionally omitting specified feature types.
1755    * Returns the number of features copied.
1756    * 
1757    * @param fromSeq
1758    * @param toSeq
1759    * @param select
1760    *          if not null, only features of this type are copied (including
1761    *          subtypes in the Sequence Ontology)
1762    * @param mapping
1763    *          the mapping from 'fromSeq' to 'toSeq'
1764    * @param omitting
1765    */
1766   public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
1767           MapList mapping, String select, String... omitting)
1768   {
1769     SequenceI copyTo = toSeq;
1770     while (copyTo.getDatasetSequence() != null)
1771     {
1772       copyTo = copyTo.getDatasetSequence();
1773     }
1774
1775     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
1776     int count = 0;
1777     SequenceFeature[] sfs = fromSeq.getSequenceFeatures();
1778     if (sfs != null)
1779     {
1780       for (SequenceFeature sf : sfs)
1781       {
1782         String type = sf.getType();
1783         if (select != null && !so.isA(type, select))
1784         {
1785           continue;
1786         }
1787         boolean omit = false;
1788         for (String toOmit : omitting)
1789         {
1790           if (type.equals(toOmit))
1791           {
1792             omit = true;
1793           }
1794         }
1795         if (omit)
1796         {
1797           continue;
1798         }
1799
1800         /*
1801          * locate the mapped range - null if either start or end is
1802          * not mapped (no partial overlaps are calculated)
1803          */
1804         int start = sf.getBegin();
1805         int end = sf.getEnd();
1806         int[] mappedTo = mapping.locateInTo(start, end);
1807         /*
1808          * if whole exon range doesn't map, try interpreting it
1809          * as 5' or 3' exon overlapping the CDS range
1810          */
1811         if (mappedTo == null)
1812         {
1813           mappedTo = mapping.locateInTo(end, end);
1814           if (mappedTo != null)
1815           {
1816             /*
1817              * end of exon is in CDS range - 5' overlap
1818              * to a range from the start of the peptide
1819              */
1820             mappedTo[0] = 1;
1821           }
1822         }
1823         if (mappedTo == null)
1824         {
1825           mappedTo = mapping.locateInTo(start, start);
1826           if (mappedTo != null)
1827           {
1828             /*
1829              * start of exon is in CDS range - 3' overlap
1830              * to a range up to the end of the peptide
1831              */
1832             mappedTo[1] = toSeq.getLength();
1833           }
1834         }
1835         if (mappedTo != null)
1836         {
1837           SequenceFeature copy = new SequenceFeature(sf);
1838           copy.setBegin(Math.min(mappedTo[0], mappedTo[1]));
1839           copy.setEnd(Math.max(mappedTo[0], mappedTo[1]));
1840           copyTo.addSequenceFeature(copy);
1841           count++;
1842         }
1843       }
1844     }
1845     return count;
1846   }
1847
1848   /**
1849    * Creates and adds mappings
1850    * <ul>
1851    * <li>from cds to peptide</li>
1852    * <li>from dna to cds</li>
1853    * </ul>
1854    * and returns the dna-to-cds mapping
1855    * 
1856    * @param dnaSeq
1857    * @param cdsSeq
1858    * @param dnaMapping
1859    * @param newMappings
1860    * @return
1861    */
1862   protected static MapList addCdsMappings(SequenceI dnaSeq,
1863           SequenceI cdsSeq, Mapping dnaMapping,
1864           AlignedCodonFrame newMappings)
1865   {
1866     cdsSeq.createDatasetSequence();
1867
1868     /*
1869      * CDS to peptide is just a contiguous 3:1 mapping, with
1870      * the peptide ranges taken unchanged from the dna mapping
1871      */
1872     List<int[]> cdsRanges = new ArrayList<int[]>();
1873     SequenceI cdsDataset = cdsSeq.getDatasetSequence();
1874     cdsRanges.add(new int[] { 1, cdsDataset.getLength() });
1875     MapList cdsToPeptide = new MapList(cdsRanges, dnaMapping.getMap()
1876             .getToRanges(), 3, 1);
1877     newMappings.addMap(cdsDataset, dnaMapping.getTo(), cdsToPeptide);
1878
1879     /*
1880      * dna 'from' ranges map 1:1 to the contiguous extracted CDS 
1881      */
1882     MapList dnaToCds = new MapList(dnaMapping.getMap().getFromRanges(),
1883             cdsRanges, 1, 1);
1884     newMappings.addMap(dnaSeq, cdsDataset, dnaToCds);
1885     return dnaToCds;
1886   }
1887
1888   /**
1889    * Makes and returns a CDS-only sequence, where the CDS regions are identified
1890    * as the 'from' ranges of the mapping on the dna.
1891    * 
1892    * @param dnaSeq
1893    *          nucleotide sequence
1894    * @param seqMapping
1895    *          mappings from CDS regions of nucleotide
1896    * @param ungappedCdsColumns
1897    * @return
1898    */
1899   protected static SequenceI makeCdsSequence(SequenceI dnaSeq,
1900           Mapping seqMapping, List<int[]> ungappedCdsColumns, char gapChar)
1901   {
1902     int cdsWidth = MappingUtils.getLength(ungappedCdsColumns);
1903
1904     /*
1905      * populate CDS columns with the aligned
1906      * column character if that column is mapped (which may be a gap 
1907      * if an intron interrupts a codon), else with a gap
1908      */
1909     List<int[]> fromRanges = seqMapping.getMap().getFromRanges();
1910     char[] cdsChars = new char[cdsWidth];
1911     int pos = 0;
1912     for (int[] columns : ungappedCdsColumns)
1913     {
1914       for (int i = columns[0]; i <= columns[1]; i++)
1915       {
1916         char dnaChar = dnaSeq.getCharAt(i - 1);
1917         if (Comparison.isGap(dnaChar))
1918         {
1919           cdsChars[pos] = gapChar;
1920         }
1921         else
1922         {
1923           int seqPos = dnaSeq.findPosition(i - 1);
1924           if (MappingUtils.contains(fromRanges, seqPos))
1925           {
1926             cdsChars[pos] = dnaChar;
1927           }
1928           else
1929           {
1930             cdsChars[pos] = gapChar;
1931           }
1932         }
1933         pos++;
1934       }
1935     }
1936     SequenceI cdsSequence = new Sequence(dnaSeq.getName(),
1937             String.valueOf(cdsChars));
1938
1939     transferDbRefs(seqMapping.getTo(), cdsSequence);
1940
1941     return cdsSequence;
1942   }
1943
1944   /**
1945    * Locate any xrefs to CDS databases on the protein product and attach to the
1946    * CDS sequence. Also add as a sub-token of the sequence name.
1947    * 
1948    * @param from
1949    * @param to
1950    */
1951   protected static void transferDbRefs(SequenceI from, SequenceI to)
1952   {
1953     String cdsAccId = FeatureProperties.getCodingFeature(DBRefSource.EMBL);
1954     DBRefEntry[] cdsRefs = DBRefUtils.selectRefs(from.getDBRefs(),
1955             DBRefSource.CODINGDBS);
1956     if (cdsRefs != null)
1957     {
1958       for (DBRefEntry cdsRef : cdsRefs)
1959       {
1960         to.addDBRef(new DBRefEntry(cdsRef));
1961         cdsAccId = cdsRef.getAccessionId();
1962       }
1963     }
1964     if (!to.getName().contains(cdsAccId))
1965     {
1966       to.setName(to.getName() + "|" + cdsAccId);
1967     }
1968   }
1969
1970   /**
1971    * Returns a mapping from dna to protein by inspecting sequence features of
1972    * type "CDS" on the dna.
1973    * 
1974    * @param dnaSeq
1975    * @param proteinSeq
1976    * @return
1977    */
1978   public static MapList mapCdsToProtein(SequenceI dnaSeq,
1979           SequenceI proteinSeq)
1980   {
1981     List<int[]> ranges = findCdsPositions(dnaSeq);
1982     int mappedDnaLength = MappingUtils.getLength(ranges);
1983   
1984     int proteinLength = proteinSeq.getLength();
1985     int proteinStart = proteinSeq.getStart();
1986     int proteinEnd = proteinSeq.getEnd();
1987   
1988     /*
1989      * incomplete start codon may mean X at start of peptide
1990      * we ignore both for mapping purposes
1991      */
1992     if (proteinSeq.getCharAt(0) == 'X')
1993     {
1994       // todo JAL-2022 support startPhase > 0
1995       proteinStart++;
1996       proteinLength--;
1997     }
1998     List<int[]> proteinRange = new ArrayList<int[]>();
1999   
2000     /*
2001      * dna length should map to protein (or protein plus stop codon)
2002      */
2003     int codesForResidues = mappedDnaLength / 3;
2004     if (codesForResidues == (proteinLength + 1))
2005     {
2006       // assuming extra codon is for STOP and not in peptide
2007       codesForResidues--;
2008     }
2009     if (codesForResidues == proteinLength)
2010     {
2011       proteinRange.add(new int[] { proteinStart, proteinEnd });
2012       return new MapList(ranges, proteinRange, 3, 1);
2013     }
2014     return null;
2015   }
2016
2017   /**
2018    * Returns a list of CDS ranges found (as sequence positions base 1), i.e. of
2019    * start/end positions of sequence features of type "CDS" (or a sub-type of
2020    * CDS in the Sequence Ontology)
2021    * 
2022    * @param dnaSeq
2023    * @return
2024    */
2025   public static List<int[]> findCdsPositions(SequenceI dnaSeq)
2026   {
2027     List<int[]> result = new ArrayList<int[]>();
2028     SequenceFeature[] sfs = dnaSeq.getSequenceFeatures();
2029     if (sfs == null)
2030     {
2031       return result;
2032     }
2033     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
2034     for (SequenceFeature sf : sfs)
2035     {
2036       /*
2037        * process a CDS feature (or a sub-type of CDS)
2038        */
2039       if (so.isA(sf.getType(), SequenceOntologyI.CDS))
2040       {
2041         int phase = 0;
2042         try {
2043           phase = Integer.parseInt(sf.getPhase());
2044         } catch (NumberFormatException e)
2045         {
2046           // ignore
2047         }
2048         /*
2049          * phase > 0 on first codon means 5' incomplete - skip to the start
2050          * of the next codon; example ENST00000496384
2051          */
2052         int begin = sf.getBegin();
2053         int end = sf.getEnd();
2054         if (result.isEmpty())
2055         {
2056           // TODO JAL-2022 support start phase > 0
2057           begin += phase;
2058           if (begin > end)
2059           {
2060             continue; // shouldn't happen?
2061           }
2062         }
2063         result.add(new int[] { begin, end });
2064       }
2065     }
2066     return result;
2067   }
2068
2069   /**
2070    * Maps exon features from dna to protein, and computes variants in peptide
2071    * product generated by variants in dna, and adds them as sequence_variant
2072    * features on the protein sequence. Returns the number of variant features
2073    * added.
2074    * 
2075    * @param dnaSeq
2076    * @param peptide
2077    * @param dnaToProtein
2078    */
2079   public static int computeProteinFeatures(SequenceI dnaSeq,
2080           SequenceI peptide, MapList dnaToProtein)
2081   {
2082     while (dnaSeq.getDatasetSequence() != null)
2083     {
2084       dnaSeq = dnaSeq.getDatasetSequence();
2085     }
2086     while (peptide.getDatasetSequence() != null)
2087     {
2088       peptide = peptide.getDatasetSequence();
2089     }
2090   
2091     transferFeatures(dnaSeq, peptide, dnaToProtein,
2092             SequenceOntologyI.EXON);
2093   
2094     LinkedHashMap<Integer, String[][]> variants = buildDnaVariantsMap(
2095             dnaSeq, dnaToProtein);
2096   
2097     /*
2098      * scan codon variations, compute peptide variants and add to peptide sequence
2099      */
2100     int count = 0;
2101     for (Entry<Integer, String[][]> variant : variants.entrySet())
2102     {
2103       int peptidePos = variant.getKey();
2104       String[][] codonVariants = variant.getValue();
2105       String residue = String.valueOf(peptide.getCharAt(peptidePos - 1)); // 0-based
2106       List<String> peptideVariants = computePeptideVariants(codonVariants,
2107               residue);
2108       if (!peptideVariants.isEmpty())
2109       {
2110         String desc = StringUtils.listToDelimitedString(peptideVariants,
2111                 ", ");
2112         SequenceFeature sf = new SequenceFeature(
2113                 SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
2114                 peptidePos, 0f, null);
2115         peptide.addSequenceFeature(sf);
2116         count++;
2117       }
2118     }
2119   
2120     /*
2121      * ugly sort to get sequence features in start position order
2122      * - would be better to store in Sequence as a TreeSet instead?
2123      */
2124     Arrays.sort(peptide.getSequenceFeatures(),
2125             new Comparator<SequenceFeature>()
2126             {
2127               @Override
2128               public int compare(SequenceFeature o1, SequenceFeature o2)
2129               {
2130                 int c = Integer.compare(o1.getBegin(), o2.getBegin());
2131                 return c == 0 ? Integer.compare(o1.getEnd(), o2.getEnd())
2132                         : c;
2133               }
2134             });
2135     return count;
2136   }
2137
2138   /**
2139    * Builds a map whose key is position in the protein sequence, and value is an
2140    * array of all variants for the coding codon positions
2141    * 
2142    * @param dnaSeq
2143    * @param dnaToProtein
2144    * @return
2145    */
2146   static LinkedHashMap<Integer, String[][]> buildDnaVariantsMap(
2147           SequenceI dnaSeq, MapList dnaToProtein)
2148   {
2149     /*
2150      * map from peptide position to all variant features of the codon for it
2151      * LinkedHashMap ensures we add the peptide features in sequence order
2152      */
2153     LinkedHashMap<Integer, String[][]> variants = new LinkedHashMap<Integer, String[][]>();
2154     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
2155   
2156     SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures();
2157     if (dnaFeatures == null)
2158     {
2159       return variants;
2160     }
2161   
2162     int dnaStart = dnaSeq.getStart();
2163     int[] lastCodon = null;
2164     int lastPeptidePostion = 0;
2165   
2166     /*
2167      * build a map of codon variations for peptides
2168      */
2169     for (SequenceFeature sf : dnaFeatures)
2170     {
2171       int dnaCol = sf.getBegin();
2172       if (dnaCol != sf.getEnd())
2173       {
2174         // not handling multi-locus variant features
2175         continue;
2176       }
2177       if (so.isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT))
2178       {
2179         int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
2180         if (mapsTo == null)
2181         {
2182           // feature doesn't lie within coding region
2183           continue;
2184         }
2185         int peptidePosition = mapsTo[0];
2186         String[][] codonVariants = variants.get(peptidePosition);
2187         if (codonVariants == null)
2188         {
2189           codonVariants = new String[3][];
2190           variants.put(peptidePosition, codonVariants);
2191         }
2192   
2193         /*
2194          * extract dna variants to a string array
2195          */
2196         String alls = (String) sf.getValue("alleles");
2197         if (alls == null)
2198         {
2199           continue;
2200         }
2201         String[] alleles = alls.toUpperCase().split(",");
2202         int i = 0;
2203         for (String allele : alleles)
2204         {
2205           alleles[i++] = allele.trim(); // lose any space characters "A, G"
2206         }
2207   
2208         /*
2209          * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
2210          */
2211         int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
2212                 : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
2213                         peptidePosition, peptidePosition));
2214         lastPeptidePostion = peptidePosition;
2215         lastCodon = codon;
2216   
2217         /*
2218          * save nucleotide (and this variant) for each codon position
2219          */
2220         for (int codonPos = 0; codonPos < 3; codonPos++)
2221         {
2222           String nucleotide = String.valueOf(
2223                   dnaSeq.getCharAt(codon[codonPos] - dnaStart))
2224                   .toUpperCase();
2225           if (codonVariants[codonPos] == null)
2226           {
2227             /*
2228              * record current dna base
2229              */
2230             codonVariants[codonPos] = new String[] { nucleotide };
2231           }
2232           if (codon[codonPos] == dnaCol)
2233           {
2234             /*
2235              * add alleles to dna base (and any previously found alleles)
2236              */
2237             String[] known = codonVariants[codonPos];
2238             String[] dnaVariants = new String[alleles.length + known.length];
2239             System.arraycopy(known, 0, dnaVariants, 0, known.length);
2240             System.arraycopy(alleles, 0, dnaVariants, known.length,
2241                     alleles.length);
2242             codonVariants[codonPos] = dnaVariants;
2243           }
2244         }
2245       }
2246     }
2247     return variants;
2248   }
2249
2250   /**
2251    * Returns a sorted, non-redundant list of all peptide translations generated
2252    * by the given dna variants, excluding the current residue value
2253    * 
2254    * @param codonVariants
2255    *          an array of base values (acgtACGT) for codon positions 1, 2, 3
2256    * @param residue
2257    *          the current residue translation
2258    * @return
2259    */
2260   static List<String> computePeptideVariants(
2261           String[][] codonVariants, String residue)
2262   {
2263     List<String> result = new ArrayList<String>();
2264     for (String base1 : codonVariants[0])
2265     {
2266       for (String base2 : codonVariants[1])
2267       {
2268         for (String base3 : codonVariants[2])
2269         {
2270           String codon = base1 + base2 + base3;
2271           /*
2272            * get peptide translation of codon e.g. GAT -> D
2273            * note that variants which are not single alleles,
2274            * e.g. multibase variants or HGMD_MUTATION etc
2275            * are ignored here
2276            */
2277           String peptide = codon.contains("-") ? "-"
2278                   : (codon.length() > 3 ? null : ResidueProperties
2279                           .codonTranslate(codon));
2280           if (peptide != null && !result.contains(peptide)
2281                   && !peptide.equalsIgnoreCase(residue))
2282           {
2283             result.add(peptide);
2284           }
2285         }
2286       }
2287     }
2288   
2289     /*
2290      * sort alphabetically with STOP at the end
2291      */
2292     Collections.sort(result, new Comparator<String>()
2293     {
2294   
2295       @Override
2296       public int compare(String o1, String o2)
2297       {
2298         if ("STOP".equals(o1))
2299         {
2300           return 1;
2301         }
2302         else if ("STOP".equals(o2))
2303         {
2304           return -1;
2305         }
2306         else
2307         {
2308           return o1.compareTo(o2);
2309         }
2310       }
2311     });
2312     return result;
2313   }
2314 }