41fe77ee916faddd17f49df0842f5e30e9884e08
[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 static jalview.io.gff.GffConstants.CLINICAL_SIGNIFICANCE;
24
25 import jalview.datamodel.AlignedCodon;
26 import jalview.datamodel.AlignedCodonFrame;
27 import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
28 import jalview.datamodel.Alignment;
29 import jalview.datamodel.AlignmentAnnotation;
30 import jalview.datamodel.AlignmentI;
31 import jalview.datamodel.DBRefEntry;
32 import jalview.datamodel.IncompleteCodonException;
33 import jalview.datamodel.Mapping;
34 import jalview.datamodel.Sequence;
35 import jalview.datamodel.SequenceFeature;
36 import jalview.datamodel.SequenceGroup;
37 import jalview.datamodel.SequenceI;
38 import jalview.datamodel.features.SequenceFeatures;
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.IntRangeComparator;
44 import jalview.util.MapList;
45 import jalview.util.MappingUtils;
46 import jalview.util.StringUtils;
47
48 import java.io.UnsupportedEncodingException;
49 import java.net.URLEncoder;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.HashMap;
55 import java.util.HashSet;
56 import java.util.Iterator;
57 import java.util.LinkedHashMap;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Map.Entry;
61 import java.util.NoSuchElementException;
62 import java.util.Set;
63 import java.util.SortedMap;
64 import java.util.TreeMap;
65
66 /**
67  * grab bag of useful alignment manipulation operations Expect these to be
68  * refactored elsewhere at some point.
69  * 
70  * @author jimp
71  * 
72  */
73 public class AlignmentUtils
74 {
75
76   private static final int CODON_LENGTH = 3;
77
78   private static final String SEQUENCE_VARIANT = "sequence_variant:";
79
80   private static final String ID = "ID";
81
82   /**
83    * A data model to hold the 'normal' base value at a position, and an optional
84    * sequence variant feature
85    */
86   static final class DnaVariant
87   {
88     final String base;
89
90     SequenceFeature variant;
91
92     DnaVariant(String nuc)
93     {
94       base = nuc;
95       variant = null;
96     }
97
98     DnaVariant(String nuc, SequenceFeature var)
99     {
100       base = nuc;
101       variant = var;
102     }
103
104     public String getSource()
105     {
106       return variant == null ? null : variant.getFeatureGroup();
107     }
108
109     /**
110      * toString for aid in the debugger only
111      */
112     @Override
113     public String toString()
114     {
115       return base + ":" + (variant == null ? "" : variant.getDescription());
116     }
117   }
118
119   /**
120    * given an existing alignment, create a new alignment including all, or up to
121    * flankSize additional symbols from each sequence's dataset sequence
122    * 
123    * @param core
124    * @param flankSize
125    * @return AlignmentI
126    */
127   public static AlignmentI expandContext(AlignmentI core, int flankSize)
128   {
129     List<SequenceI> sq = new ArrayList<SequenceI>();
130     int maxoffset = 0;
131     for (SequenceI s : core.getSequences())
132     {
133       SequenceI newSeq = s.deriveSequence();
134       final int newSeqStart = newSeq.getStart() - 1;
135       if (newSeqStart > maxoffset
136               && newSeq.getDatasetSequence().getStart() < s.getStart())
137       {
138         maxoffset = newSeqStart;
139       }
140       sq.add(newSeq);
141     }
142     if (flankSize > -1)
143     {
144       maxoffset = Math.min(maxoffset, flankSize);
145     }
146
147     /*
148      * now add offset left and right to create an expanded alignment
149      */
150     for (SequenceI s : sq)
151     {
152       SequenceI ds = s;
153       while (ds.getDatasetSequence() != null)
154       {
155         ds = ds.getDatasetSequence();
156       }
157       int s_end = s.findPosition(s.getStart() + s.getLength());
158       // find available flanking residues for sequence
159       int ustream_ds = s.getStart() - ds.getStart();
160       int dstream_ds = ds.getEnd() - s_end;
161
162       // build new flanked sequence
163
164       // compute gap padding to start of flanking sequence
165       int offset = maxoffset - ustream_ds;
166
167       // padding is gapChar x ( maxoffset - min(ustream_ds, flank)
168       if (flankSize >= 0)
169       {
170         if (flankSize < ustream_ds)
171         {
172           // take up to flankSize residues
173           offset = maxoffset - flankSize;
174           ustream_ds = flankSize;
175         }
176         if (flankSize <= dstream_ds)
177         {
178           dstream_ds = flankSize - 1;
179         }
180       }
181       // TODO use Character.toLowerCase to avoid creating String objects?
182       char[] upstream = new String(ds
183               .getSequence(s.getStart() - 1 - ustream_ds, s.getStart() - 1))
184                       .toLowerCase().toCharArray();
185       char[] downstream = new String(
186               ds.getSequence(s_end - 1, s_end + dstream_ds)).toLowerCase()
187                       .toCharArray();
188       char[] coreseq = s.getSequence();
189       char[] nseq = new char[offset + upstream.length + downstream.length
190               + coreseq.length];
191       char c = core.getGapCharacter();
192
193       int p = 0;
194       for (; p < offset; p++)
195       {
196         nseq[p] = c;
197       }
198
199       System.arraycopy(upstream, 0, nseq, p, upstream.length);
200       System.arraycopy(coreseq, 0, nseq, p + upstream.length,
201               coreseq.length);
202       System.arraycopy(downstream, 0, nseq,
203               p + coreseq.length + upstream.length, downstream.length);
204       s.setSequence(new String(nseq));
205       s.setStart(s.getStart() - ustream_ds);
206       s.setEnd(s_end + downstream.length);
207     }
208     AlignmentI newAl = new jalview.datamodel.Alignment(
209             sq.toArray(new SequenceI[0]));
210     for (SequenceI s : sq)
211     {
212       if (s.getAnnotation() != null)
213       {
214         for (AlignmentAnnotation aa : s.getAnnotation())
215         {
216           aa.adjustForAlignment(); // JAL-1712 fix
217           newAl.addAnnotation(aa);
218         }
219       }
220     }
221     newAl.setDataset(core.getDataset());
222     return newAl;
223   }
224
225   /**
226    * Returns the index (zero-based position) of a sequence in an alignment, or
227    * -1 if not found.
228    * 
229    * @param al
230    * @param seq
231    * @return
232    */
233   public static int getSequenceIndex(AlignmentI al, SequenceI seq)
234   {
235     int result = -1;
236     int pos = 0;
237     for (SequenceI alSeq : al.getSequences())
238     {
239       if (alSeq == seq)
240       {
241         result = pos;
242         break;
243       }
244       pos++;
245     }
246     return result;
247   }
248
249   /**
250    * Returns a map of lists of sequences in the alignment, keyed by sequence
251    * name. For use in mapping between different alignment views of the same
252    * sequences.
253    * 
254    * @see jalview.datamodel.AlignmentI#getSequencesByName()
255    */
256   public static Map<String, List<SequenceI>> getSequencesByName(
257           AlignmentI al)
258   {
259     Map<String, List<SequenceI>> theMap = new LinkedHashMap<String, List<SequenceI>>();
260     for (SequenceI seq : al.getSequences())
261     {
262       String name = seq.getName();
263       if (name != null)
264       {
265         List<SequenceI> seqs = theMap.get(name);
266         if (seqs == null)
267         {
268           seqs = new ArrayList<SequenceI>();
269           theMap.put(name, seqs);
270         }
271         seqs.add(seq);
272       }
273     }
274     return theMap;
275   }
276
277   /**
278    * Build mapping of protein to cDNA alignment. Mappings are made between
279    * sequences where the cDNA translates to the protein sequence. Any new
280    * mappings are added to the protein alignment. Returns true if any mappings
281    * either already exist or were added, else false.
282    * 
283    * @param proteinAlignment
284    * @param cdnaAlignment
285    * @return
286    */
287   public static boolean mapProteinAlignmentToCdna(
288           final AlignmentI proteinAlignment, final AlignmentI cdnaAlignment)
289   {
290     if (proteinAlignment == null || cdnaAlignment == null)
291     {
292       return false;
293     }
294
295     Set<SequenceI> mappedDna = new HashSet<SequenceI>();
296     Set<SequenceI> mappedProtein = new HashSet<SequenceI>();
297
298     /*
299      * First pass - map sequences where cross-references exist. This include
300      * 1-to-many mappings to support, for example, variant cDNA.
301      */
302     boolean mappingPerformed = mapProteinToCdna(proteinAlignment,
303             cdnaAlignment, mappedDna, mappedProtein, true);
304
305     /*
306      * Second pass - map sequences where no cross-references exist. This only
307      * does 1-to-1 mappings and assumes corresponding sequences are in the same
308      * order in the alignments.
309      */
310     mappingPerformed |= mapProteinToCdna(proteinAlignment, cdnaAlignment,
311             mappedDna, mappedProtein, false);
312     return mappingPerformed;
313   }
314
315   /**
316    * Make mappings between compatible sequences (where the cDNA translation
317    * matches the protein).
318    * 
319    * @param proteinAlignment
320    * @param cdnaAlignment
321    * @param mappedDna
322    *          a set of mapped DNA sequences (to add to)
323    * @param mappedProtein
324    *          a set of mapped Protein sequences (to add to)
325    * @param xrefsOnly
326    *          if true, only map sequences where xrefs exist
327    * @return
328    */
329   protected static boolean mapProteinToCdna(
330           final AlignmentI proteinAlignment, final AlignmentI cdnaAlignment,
331           Set<SequenceI> mappedDna, Set<SequenceI> mappedProtein,
332           boolean xrefsOnly)
333   {
334     boolean mappingExistsOrAdded = false;
335     List<SequenceI> thisSeqs = proteinAlignment.getSequences();
336     for (SequenceI aaSeq : thisSeqs)
337     {
338       boolean proteinMapped = false;
339       AlignedCodonFrame acf = new AlignedCodonFrame();
340
341       for (SequenceI cdnaSeq : cdnaAlignment.getSequences())
342       {
343         /*
344          * Always try to map if sequences have xref to each other; this supports
345          * variant cDNA or alternative splicing for a protein sequence.
346          * 
347          * If no xrefs, try to map progressively, assuming that alignments have
348          * mappable sequences in corresponding order. These are not
349          * many-to-many, as that would risk mixing species with similar cDNA
350          * sequences.
351          */
352         if (xrefsOnly && !AlignmentUtils.haveCrossRef(aaSeq, cdnaSeq))
353         {
354           continue;
355         }
356
357         /*
358          * Don't map non-xrefd sequences more than once each. This heuristic
359          * allows us to pair up similar sequences in ordered alignments.
360          */
361         if (!xrefsOnly && (mappedProtein.contains(aaSeq)
362                 || mappedDna.contains(cdnaSeq)))
363         {
364           continue;
365         }
366         if (mappingExists(proteinAlignment.getCodonFrames(),
367                 aaSeq.getDatasetSequence(), cdnaSeq.getDatasetSequence()))
368         {
369           mappingExistsOrAdded = true;
370         }
371         else
372         {
373           MapList map = mapCdnaToProtein(aaSeq, cdnaSeq);
374           if (map != null)
375           {
376             acf.addMap(cdnaSeq, aaSeq, map);
377             mappingExistsOrAdded = true;
378             proteinMapped = true;
379             mappedDna.add(cdnaSeq);
380             mappedProtein.add(aaSeq);
381           }
382         }
383       }
384       if (proteinMapped)
385       {
386         proteinAlignment.addCodonFrame(acf);
387       }
388     }
389     return mappingExistsOrAdded;
390   }
391
392   /**
393    * Answers true if the mappings include one between the given (dataset)
394    * sequences.
395    */
396   public static boolean mappingExists(List<AlignedCodonFrame> mappings,
397           SequenceI aaSeq, SequenceI cdnaSeq)
398   {
399     if (mappings != null)
400     {
401       for (AlignedCodonFrame acf : mappings)
402       {
403         if (cdnaSeq == acf.getDnaForAaSeq(aaSeq))
404         {
405           return true;
406         }
407       }
408     }
409     return false;
410   }
411
412   /**
413    * Builds a mapping (if possible) of a cDNA to a protein sequence.
414    * <ul>
415    * <li>first checks if the cdna translates exactly to the protein
416    * sequence</li>
417    * <li>else checks for translation after removing a STOP codon</li>
418    * <li>else checks for translation after removing a START codon</li>
419    * <li>if that fails, inspect CDS features on the cDNA sequence</li>
420    * </ul>
421    * Returns null if no mapping is determined.
422    * 
423    * @param proteinSeq
424    *          the aligned protein sequence
425    * @param cdnaSeq
426    *          the aligned cdna sequence
427    * @return
428    */
429   public static MapList mapCdnaToProtein(SequenceI proteinSeq,
430           SequenceI cdnaSeq)
431   {
432     /*
433      * Here we handle either dataset sequence set (desktop) or absent (applet).
434      * Use only the char[] form of the sequence to avoid creating possibly large
435      * String objects.
436      */
437     final SequenceI proteinDataset = proteinSeq.getDatasetSequence();
438     char[] aaSeqChars = proteinDataset != null
439             ? proteinDataset.getSequence()
440             : proteinSeq.getSequence();
441     final SequenceI cdnaDataset = cdnaSeq.getDatasetSequence();
442     char[] cdnaSeqChars = cdnaDataset != null ? cdnaDataset.getSequence()
443             : cdnaSeq.getSequence();
444     if (aaSeqChars == null || cdnaSeqChars == null)
445     {
446       return null;
447     }
448
449     /*
450      * cdnaStart/End, proteinStartEnd are base 1 (for dataset sequence mapping)
451      */
452     final int mappedLength = CODON_LENGTH * aaSeqChars.length;
453     int cdnaLength = cdnaSeqChars.length;
454     int cdnaStart = cdnaSeq.getStart();
455     int cdnaEnd = cdnaSeq.getEnd();
456     final int proteinStart = proteinSeq.getStart();
457     final int proteinEnd = proteinSeq.getEnd();
458
459     /*
460      * If lengths don't match, try ignoring stop codon (if present)
461      */
462     if (cdnaLength != mappedLength && cdnaLength > 2)
463     {
464       String lastCodon = String.valueOf(cdnaSeqChars,
465               cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase();
466       for (String stop : ResidueProperties.STOP)
467       {
468         if (lastCodon.equals(stop))
469         {
470           cdnaEnd -= CODON_LENGTH;
471           cdnaLength -= CODON_LENGTH;
472           break;
473         }
474       }
475     }
476
477     /*
478      * If lengths still don't match, try ignoring start codon.
479      */
480     int startOffset = 0;
481     if (cdnaLength != mappedLength && cdnaLength > 2
482             && String.valueOf(cdnaSeqChars, 0, CODON_LENGTH).toUpperCase()
483                     .equals(ResidueProperties.START))
484     {
485       startOffset += CODON_LENGTH;
486       cdnaStart += CODON_LENGTH;
487       cdnaLength -= CODON_LENGTH;
488     }
489
490     if (translatesAs(cdnaSeqChars, startOffset, aaSeqChars))
491     {
492       /*
493        * protein is translation of dna (+/- start/stop codons)
494        */
495       MapList map = new MapList(new int[] { cdnaStart, cdnaEnd },
496               new int[]
497               { proteinStart, proteinEnd }, CODON_LENGTH, 1);
498       return map;
499     }
500
501     /*
502      * translation failed - try mapping CDS annotated regions of dna
503      */
504     return mapCdsToProtein(cdnaSeq, proteinSeq);
505   }
506
507   /**
508    * Test whether the given cdna sequence, starting at the given offset,
509    * translates to the given amino acid sequence, using the standard translation
510    * table. Designed to fail fast i.e. as soon as a mismatch position is found.
511    * 
512    * @param cdnaSeqChars
513    * @param cdnaStart
514    * @param aaSeqChars
515    * @return
516    */
517   protected static boolean translatesAs(char[] cdnaSeqChars, int cdnaStart,
518           char[] aaSeqChars)
519   {
520     if (cdnaSeqChars == null || aaSeqChars == null)
521     {
522       return false;
523     }
524
525     int aaPos = 0;
526     int dnaPos = cdnaStart;
527     for (; dnaPos < cdnaSeqChars.length - 2
528             && aaPos < aaSeqChars.length; dnaPos += CODON_LENGTH, aaPos++)
529     {
530       String codon = String.valueOf(cdnaSeqChars, dnaPos, CODON_LENGTH);
531       final String translated = ResidueProperties.codonTranslate(codon);
532
533       /*
534        * allow * in protein to match untranslatable in dna
535        */
536       final char aaRes = aaSeqChars[aaPos];
537       if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
538       {
539         continue;
540       }
541       if (translated == null || !(aaRes == translated.charAt(0)))
542       {
543         // debug
544         // System.out.println(("Mismatch at " + i + "/" + aaResidue + ": "
545         // + codon + "(" + translated + ") != " + aaRes));
546         return false;
547       }
548     }
549
550     /*
551      * check we matched all of the protein sequence
552      */
553     if (aaPos != aaSeqChars.length)
554     {
555       return false;
556     }
557
558     /*
559      * check we matched all of the dna except
560      * for optional trailing STOP codon
561      */
562     if (dnaPos == cdnaSeqChars.length)
563     {
564       return true;
565     }
566     if (dnaPos == cdnaSeqChars.length - CODON_LENGTH)
567     {
568       String codon = String.valueOf(cdnaSeqChars, dnaPos, CODON_LENGTH);
569       if ("STOP".equals(ResidueProperties.codonTranslate(codon)))
570       {
571         return true;
572       }
573     }
574     return false;
575   }
576
577   /**
578    * Align sequence 'seq' to match the alignment of a mapped sequence. Note this
579    * currently assumes that we are aligning cDNA to match protein.
580    * 
581    * @param seq
582    *          the sequence to be realigned
583    * @param al
584    *          the alignment whose sequence alignment is to be 'copied'
585    * @param gap
586    *          character string represent a gap in the realigned sequence
587    * @param preserveUnmappedGaps
588    * @param preserveMappedGaps
589    * @return true if the sequence was realigned, false if it could not be
590    */
591   public static boolean alignSequenceAs(SequenceI seq, AlignmentI al,
592           String gap, boolean preserveMappedGaps,
593           boolean preserveUnmappedGaps)
594   {
595     /*
596      * Get any mappings from the source alignment to the target (dataset)
597      * sequence.
598      */
599     // TODO there may be one AlignedCodonFrame per dataset sequence, or one with
600     // all mappings. Would it help to constrain this?
601     List<AlignedCodonFrame> mappings = al.getCodonFrame(seq);
602     if (mappings == null || mappings.isEmpty())
603     {
604       return false;
605     }
606
607     /*
608      * Locate the aligned source sequence whose dataset sequence is mapped. We
609      * just take the first match here (as we can't align like more than one
610      * sequence).
611      */
612     SequenceI alignFrom = null;
613     AlignedCodonFrame mapping = null;
614     for (AlignedCodonFrame mp : mappings)
615     {
616       alignFrom = mp.findAlignedSequence(seq, al);
617       if (alignFrom != null)
618       {
619         mapping = mp;
620         break;
621       }
622     }
623
624     if (alignFrom == null)
625     {
626       return false;
627     }
628     alignSequenceAs(seq, alignFrom, mapping, gap, al.getGapCharacter(),
629             preserveMappedGaps, preserveUnmappedGaps);
630     return true;
631   }
632
633   /**
634    * Align sequence 'alignTo' the same way as 'alignFrom', using the mapping to
635    * match residues and codons. Flags control whether existing gaps in unmapped
636    * (intron) and mapped (exon) regions are preserved or not. Gaps between
637    * intron and exon are only retained if both flags are set.
638    * 
639    * @param alignTo
640    * @param alignFrom
641    * @param mapping
642    * @param myGap
643    * @param sourceGap
644    * @param preserveUnmappedGaps
645    * @param preserveMappedGaps
646    */
647   public static void alignSequenceAs(SequenceI alignTo, SequenceI alignFrom,
648           AlignedCodonFrame mapping, String myGap, char sourceGap,
649           boolean preserveMappedGaps, boolean preserveUnmappedGaps)
650   {
651     // TODO generalise to work for Protein-Protein, dna-dna, dna-protein
652
653     // aligned and dataset sequence positions, all base zero
654     int thisSeqPos = 0;
655     int sourceDsPos = 0;
656
657     int basesWritten = 0;
658     char myGapChar = myGap.charAt(0);
659     int ratio = myGap.length();
660
661     int fromOffset = alignFrom.getStart() - 1;
662     int toOffset = alignTo.getStart() - 1;
663     int sourceGapMappedLength = 0;
664     boolean inExon = false;
665     final int toLength = alignTo.getLength();
666     final int fromLength = alignFrom.getLength();
667     StringBuilder thisAligned = new StringBuilder(2 * toLength);
668
669     /*
670      * Traverse the 'model' aligned sequence
671      */
672     for (int i = 0; i < fromLength; i++)
673     {
674       char sourceChar = alignFrom.getCharAt(i);
675       if (sourceChar == sourceGap)
676       {
677         sourceGapMappedLength += ratio;
678         continue;
679       }
680
681       /*
682        * Found a non-gap character. Locate its mapped region if any.
683        */
684       sourceDsPos++;
685       // Note mapping positions are base 1, our sequence positions base 0
686       int[] mappedPos = mapping.getMappedRegion(alignTo, alignFrom,
687               sourceDsPos + fromOffset);
688       if (mappedPos == null)
689       {
690         /*
691          * unmapped position; treat like a gap
692          */
693         sourceGapMappedLength += ratio;
694         // System.err.println("Can't align: no codon mapping to residue "
695         // + sourceDsPos + "(" + sourceChar + ")");
696         // return;
697         continue;
698       }
699
700       int mappedCodonStart = mappedPos[0]; // position (1...) of codon start
701       int mappedCodonEnd = mappedPos[mappedPos.length - 1]; // codon end pos
702       StringBuilder trailingCopiedGap = new StringBuilder();
703
704       /*
705        * Copy dna sequence up to and including this codon. Optionally, include
706        * gaps before the codon starts (in introns) and/or after the codon starts
707        * (in exons).
708        * 
709        * Note this only works for 'linear' splicing, not reverse or interleaved.
710        * But then 'align dna as protein' doesn't make much sense otherwise.
711        */
712       int intronLength = 0;
713       while (basesWritten + toOffset < mappedCodonEnd
714               && thisSeqPos < toLength)
715       {
716         final char c = alignTo.getCharAt(thisSeqPos++);
717         if (c != myGapChar)
718         {
719           basesWritten++;
720           int sourcePosition = basesWritten + toOffset;
721           if (sourcePosition < mappedCodonStart)
722           {
723             /*
724              * Found an unmapped (intron) base. First add in any preceding gaps
725              * (if wanted).
726              */
727             if (preserveUnmappedGaps && trailingCopiedGap.length() > 0)
728             {
729               thisAligned.append(trailingCopiedGap.toString());
730               intronLength += trailingCopiedGap.length();
731               trailingCopiedGap = new StringBuilder();
732             }
733             intronLength++;
734             inExon = false;
735           }
736           else
737           {
738             final boolean startOfCodon = sourcePosition == mappedCodonStart;
739             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
740                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
741                     trailingCopiedGap.length(), intronLength, startOfCodon);
742             for (int k = 0; k < gapsToAdd; k++)
743             {
744               thisAligned.append(myGapChar);
745             }
746             sourceGapMappedLength = 0;
747             inExon = true;
748           }
749           thisAligned.append(c);
750           trailingCopiedGap = new StringBuilder();
751         }
752         else
753         {
754           if (inExon && preserveMappedGaps)
755           {
756             trailingCopiedGap.append(myGapChar);
757           }
758           else if (!inExon && preserveUnmappedGaps)
759           {
760             trailingCopiedGap.append(myGapChar);
761           }
762         }
763       }
764     }
765
766     /*
767      * At end of model aligned sequence. Copy any remaining target sequence, optionally
768      * including (intron) gaps.
769      */
770     while (thisSeqPos < toLength)
771     {
772       final char c = alignTo.getCharAt(thisSeqPos++);
773       if (c != myGapChar || preserveUnmappedGaps)
774       {
775         thisAligned.append(c);
776       }
777       sourceGapMappedLength--;
778     }
779
780     /*
781      * finally add gaps to pad for any trailing source gaps or
782      * unmapped characters
783      */
784     if (preserveUnmappedGaps)
785     {
786       while (sourceGapMappedLength > 0)
787       {
788         thisAligned.append(myGapChar);
789         sourceGapMappedLength--;
790       }
791     }
792
793     /*
794      * All done aligning, set the aligned sequence.
795      */
796     alignTo.setSequence(new String(thisAligned));
797   }
798
799   /**
800    * Helper method to work out how many gaps to insert when realigning.
801    * 
802    * @param preserveMappedGaps
803    * @param preserveUnmappedGaps
804    * @param sourceGapMappedLength
805    * @param inExon
806    * @param trailingCopiedGap
807    * @param intronLength
808    * @param startOfCodon
809    * @return
810    */
811   protected static int calculateGapsToInsert(boolean preserveMappedGaps,
812           boolean preserveUnmappedGaps, int sourceGapMappedLength,
813           boolean inExon, int trailingGapLength, int intronLength,
814           final boolean startOfCodon)
815   {
816     int gapsToAdd = 0;
817     if (startOfCodon)
818     {
819       /*
820        * Reached start of codon. Ignore trailing gaps in intron unless we are
821        * preserving gaps in both exon and intron. Ignore them anyway if the
822        * protein alignment introduces a gap at least as large as the intronic
823        * region.
824        */
825       if (inExon && !preserveMappedGaps)
826       {
827         trailingGapLength = 0;
828       }
829       if (!inExon && !(preserveMappedGaps && preserveUnmappedGaps))
830       {
831         trailingGapLength = 0;
832       }
833       if (inExon)
834       {
835         gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
836       }
837       else
838       {
839         if (intronLength + trailingGapLength <= sourceGapMappedLength)
840         {
841           gapsToAdd = sourceGapMappedLength - intronLength;
842         }
843         else
844         {
845           gapsToAdd = Math.min(
846                   intronLength + trailingGapLength - sourceGapMappedLength,
847                   trailingGapLength);
848         }
849       }
850     }
851     else
852     {
853       /*
854        * second or third base of codon; check for any gaps in dna
855        */
856       if (!preserveMappedGaps)
857       {
858         trailingGapLength = 0;
859       }
860       gapsToAdd = Math.max(sourceGapMappedLength, trailingGapLength);
861     }
862     return gapsToAdd;
863   }
864
865   /**
866    * Realigns the given protein to match the alignment of the dna, using codon
867    * mappings to translate aligned codon positions to protein residues.
868    * 
869    * @param protein
870    *          the alignment whose sequences are realigned by this method
871    * @param dna
872    *          the dna alignment whose alignment we are 'copying'
873    * @return the number of sequences that were realigned
874    */
875   public static int alignProteinAsDna(AlignmentI protein, AlignmentI dna)
876   {
877     if (protein.isNucleotide() || !dna.isNucleotide())
878     {
879       System.err.println("Wrong alignment type in alignProteinAsDna");
880       return 0;
881     }
882     List<SequenceI> unmappedProtein = new ArrayList<SequenceI>();
883     Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = buildCodonColumnsMap(
884             protein, dna, unmappedProtein);
885     return alignProteinAs(protein, alignedCodons, unmappedProtein);
886   }
887
888   /**
889    * Realigns the given dna to match the alignment of the protein, using codon
890    * mappings to translate aligned peptide positions to codons.
891    * 
892    * Always produces a padded CDS alignment.
893    * 
894    * @param dna
895    *          the alignment whose sequences are realigned by this method
896    * @param protein
897    *          the protein alignment whose alignment we are 'copying'
898    * @return the number of sequences that were realigned
899    */
900   public static int alignCdsAsProtein(AlignmentI dna, AlignmentI protein)
901   {
902     if (protein.isNucleotide() || !dna.isNucleotide())
903     {
904       System.err.println("Wrong alignment type in alignProteinAsDna");
905       return 0;
906     }
907     // todo: implement this
908     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
909     int alignedCount = 0;
910     int width = 0; // alignment width for padding CDS
911     for (SequenceI dnaSeq : dna.getSequences())
912     {
913       if (alignCdsSequenceAsProtein(dnaSeq, protein, mappings,
914               dna.getGapCharacter()))
915       {
916         alignedCount++;
917       }
918       width = Math.max(dnaSeq.getLength(), width);
919     }
920     int oldwidth;
921     int diff;
922     for (SequenceI dnaSeq : dna.getSequences())
923     {
924       oldwidth = dnaSeq.getLength();
925       diff = width - oldwidth;
926       if (diff > 0)
927       {
928         dnaSeq.insertCharAt(oldwidth, diff, dna.getGapCharacter());
929       }
930     }
931     return alignedCount;
932   }
933
934   /**
935    * Helper method to align (if possible) the dna sequence to match the
936    * alignment of a mapped protein sequence. This is currently limited to
937    * handling coding sequence only.
938    * 
939    * @param cdsSeq
940    * @param protein
941    * @param mappings
942    * @param gapChar
943    * @return
944    */
945   static boolean alignCdsSequenceAsProtein(SequenceI cdsSeq,
946           AlignmentI protein, List<AlignedCodonFrame> mappings,
947           char gapChar)
948   {
949     SequenceI cdsDss = cdsSeq.getDatasetSequence();
950     if (cdsDss == null)
951     {
952       System.err
953               .println("alignCdsSequenceAsProtein needs aligned sequence!");
954       return false;
955     }
956
957     List<AlignedCodonFrame> dnaMappings = MappingUtils
958             .findMappingsForSequence(cdsSeq, mappings);
959     for (AlignedCodonFrame mapping : dnaMappings)
960     {
961       SequenceI peptide = mapping.findAlignedSequence(cdsSeq, protein);
962       if (peptide != null)
963       {
964         final int peptideLength = peptide.getLength();
965         Mapping map = mapping.getMappingBetween(cdsSeq, peptide);
966         if (map != null)
967         {
968           MapList mapList = map.getMap();
969           if (map.getTo() == peptide.getDatasetSequence())
970           {
971             mapList = mapList.getInverse();
972           }
973           final int cdsLength = cdsDss.getLength();
974           int mappedFromLength = MappingUtils.getLength(mapList
975                   .getFromRanges());
976           int mappedToLength = MappingUtils
977                   .getLength(mapList.getToRanges());
978           boolean addStopCodon = (cdsLength == mappedFromLength
979                   * CODON_LENGTH + CODON_LENGTH)
980                   || (peptide.getDatasetSequence()
981                           .getLength() == mappedFromLength - 1);
982           if (cdsLength != mappedToLength && !addStopCodon)
983           {
984             System.err.println(String.format(
985                     "Can't align cds as protein (length mismatch %d/%d): %s",
986                     cdsLength, mappedToLength, cdsSeq.getName()));
987           }
988
989           /*
990            * pre-fill the aligned cds sequence with gaps
991            */
992           char[] alignedCds = new char[peptideLength * CODON_LENGTH
993                   + (addStopCodon ? CODON_LENGTH : 0)];
994           Arrays.fill(alignedCds, gapChar);
995
996           /*
997            * walk over the aligned peptide sequence and insert mapped 
998            * codons for residues in the aligned cds sequence 
999            */
1000           int copiedBases = 0;
1001           int cdsStart = cdsDss.getStart();
1002           int proteinPos = peptide.getStart() - 1;
1003           int cdsCol = 0;
1004
1005           for (int col = 0; col < peptideLength; col++)
1006           {
1007             char residue = peptide.getCharAt(col);
1008
1009             if (Comparison.isGap(residue))
1010             {
1011               cdsCol += CODON_LENGTH;
1012             }
1013             else
1014             {
1015               proteinPos++;
1016               int[] codon = mapList.locateInTo(proteinPos, proteinPos);
1017               if (codon == null)
1018               {
1019                 // e.g. incomplete start codon, X in peptide
1020                 cdsCol += CODON_LENGTH;
1021               }
1022               else
1023               {
1024                 for (int j = codon[0]; j <= codon[1]; j++)
1025                 {
1026                   char mappedBase = cdsDss.getCharAt(j - cdsStart);
1027                   alignedCds[cdsCol++] = mappedBase;
1028                   copiedBases++;
1029                 }
1030               }
1031             }
1032           }
1033
1034           /*
1035            * append stop codon if not mapped from protein,
1036            * closing it up to the end of the mapped sequence
1037            */
1038           if (copiedBases == cdsLength - CODON_LENGTH)
1039           {
1040             for (int i = alignedCds.length - 1; i >= 0; i--)
1041             {
1042               if (!Comparison.isGap(alignedCds[i]))
1043               {
1044                 cdsCol = i + 1; // gap just after end of sequence
1045                 break;
1046               }
1047             }
1048             for (int i = cdsLength - CODON_LENGTH; i < cdsLength; i++)
1049             {
1050               alignedCds[cdsCol++] = cdsDss.getCharAt(i);
1051             }
1052           }
1053           cdsSeq.setSequence(new String(alignedCds));
1054           return true;
1055         }
1056       }
1057     }
1058     return false;
1059   }
1060
1061   /**
1062    * Builds a map whose key is an aligned codon position (3 alignment column
1063    * numbers base 0), and whose value is a map from protein sequence to each
1064    * protein's peptide residue for that codon. The map generates an ordering of
1065    * the codons, and allows us to read off the peptides at each position in
1066    * order to assemble 'aligned' protein sequences.
1067    * 
1068    * @param protein
1069    *          the protein alignment
1070    * @param dna
1071    *          the coding dna alignment
1072    * @param unmappedProtein
1073    *          any unmapped proteins are added to this list
1074    * @return
1075    */
1076   protected static Map<AlignedCodon, Map<SequenceI, AlignedCodon>> buildCodonColumnsMap(
1077           AlignmentI protein, AlignmentI dna,
1078           List<SequenceI> unmappedProtein)
1079   {
1080     /*
1081      * maintain a list of any proteins with no mappings - these will be
1082      * rendered 'as is' in the protein alignment as we can't align them
1083      */
1084     unmappedProtein.addAll(protein.getSequences());
1085
1086     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
1087
1088     /*
1089      * Map will hold, for each aligned codon position e.g. [3, 5, 6], a map of
1090      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
1091      * comparator keeps the codon positions ordered.
1092      */
1093     Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, AlignedCodon>>(
1094             new CodonComparator());
1095
1096     for (SequenceI dnaSeq : dna.getSequences())
1097     {
1098       for (AlignedCodonFrame mapping : mappings)
1099       {
1100         SequenceI prot = mapping.findAlignedSequence(dnaSeq, protein);
1101         if (prot != null)
1102         {
1103           Mapping seqMap = mapping.getMappingForSequence(dnaSeq);
1104           addCodonPositions(dnaSeq, prot, protein.getGapCharacter(), seqMap,
1105                   alignedCodons);
1106           unmappedProtein.remove(prot);
1107         }
1108       }
1109     }
1110
1111     /*
1112      * Finally add any unmapped peptide start residues (e.g. for incomplete
1113      * codons) as if at the codon position before the second residue
1114      */
1115     // TODO resolve JAL-2022 so this fudge can be removed
1116     int mappedSequenceCount = protein.getHeight() - unmappedProtein.size();
1117     addUnmappedPeptideStarts(alignedCodons, mappedSequenceCount);
1118
1119     return alignedCodons;
1120   }
1121
1122   /**
1123    * Scans for any protein mapped from position 2 (meaning unmapped start
1124    * position e.g. an incomplete codon), and synthesizes a 'codon' for it at the
1125    * preceding position in the alignment
1126    * 
1127    * @param alignedCodons
1128    *          the codon-to-peptide map
1129    * @param mappedSequenceCount
1130    *          the number of distinct sequences in the map
1131    */
1132   protected static void addUnmappedPeptideStarts(
1133           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
1134           int mappedSequenceCount)
1135   {
1136     // TODO delete this ugly hack once JAL-2022 is resolved
1137     // i.e. we can model startPhase > 0 (incomplete start codon)
1138
1139     List<SequenceI> sequencesChecked = new ArrayList<SequenceI>();
1140     AlignedCodon lastCodon = null;
1141     Map<SequenceI, AlignedCodon> toAdd = new HashMap<SequenceI, AlignedCodon>();
1142
1143     for (Entry<AlignedCodon, Map<SequenceI, AlignedCodon>> entry : alignedCodons
1144             .entrySet())
1145     {
1146       for (Entry<SequenceI, AlignedCodon> sequenceCodon : entry.getValue()
1147               .entrySet())
1148       {
1149         SequenceI seq = sequenceCodon.getKey();
1150         if (sequencesChecked.contains(seq))
1151         {
1152           continue;
1153         }
1154         sequencesChecked.add(seq);
1155         AlignedCodon codon = sequenceCodon.getValue();
1156         if (codon.peptideCol > 1)
1157         {
1158           System.err.println(
1159                   "Problem mapping protein with >1 unmapped start positions: "
1160                           + seq.getName());
1161         }
1162         else if (codon.peptideCol == 1)
1163         {
1164           /*
1165            * first position (peptideCol == 0) was unmapped - add it
1166            */
1167           if (lastCodon != null)
1168           {
1169             AlignedCodon firstPeptide = new AlignedCodon(lastCodon.pos1,
1170                     lastCodon.pos2, lastCodon.pos3,
1171                     String.valueOf(seq.getCharAt(0)), 0);
1172             toAdd.put(seq, firstPeptide);
1173           }
1174           else
1175           {
1176             /*
1177              * unmapped residue at start of alignment (no prior column) -
1178              * 'insert' at nominal codon [0, 0, 0]
1179              */
1180             AlignedCodon firstPeptide = new AlignedCodon(0, 0, 0,
1181                     String.valueOf(seq.getCharAt(0)), 0);
1182             toAdd.put(seq, firstPeptide);
1183           }
1184         }
1185         if (sequencesChecked.size() == mappedSequenceCount)
1186         {
1187           // no need to check past first mapped position in all sequences
1188           break;
1189         }
1190       }
1191       lastCodon = entry.getKey();
1192     }
1193
1194     /*
1195      * add any new codons safely after iterating over the map
1196      */
1197     for (Entry<SequenceI, AlignedCodon> startCodon : toAdd.entrySet())
1198     {
1199       addCodonToMap(alignedCodons, startCodon.getValue(),
1200               startCodon.getKey());
1201     }
1202   }
1203
1204   /**
1205    * Update the aligned protein sequences to match the codon alignments given in
1206    * the map.
1207    * 
1208    * @param protein
1209    * @param alignedCodons
1210    *          an ordered map of codon positions (columns), with sequence/peptide
1211    *          values present in each column
1212    * @param unmappedProtein
1213    * @return
1214    */
1215   protected static int alignProteinAs(AlignmentI protein,
1216           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
1217           List<SequenceI> unmappedProtein)
1218   {
1219     /*
1220      * prefill peptide sequences with gaps 
1221      */
1222     int alignedWidth = alignedCodons.size();
1223     char[] gaps = new char[alignedWidth];
1224     Arrays.fill(gaps, protein.getGapCharacter());
1225     Map<SequenceI, char[]> peptides = new HashMap<>();
1226     for (SequenceI seq : protein.getSequences())
1227     {
1228       if (!unmappedProtein.contains(seq))
1229       {
1230         peptides.put(seq, Arrays.copyOf(gaps, gaps.length));
1231       }
1232     }
1233
1234     /*
1235      * Traverse the codons left to right (as defined by CodonComparator)
1236      * and insert peptides in each column where the sequence is mapped.
1237      * This gives a peptide 'alignment' where residues are aligned if their
1238      * corresponding codons occupy the same columns in the cdna alignment.
1239      */
1240     int column = 0;
1241     for (AlignedCodon codon : alignedCodons.keySet())
1242     {
1243       final Map<SequenceI, AlignedCodon> columnResidues = alignedCodons
1244               .get(codon);
1245       for (Entry<SequenceI, AlignedCodon> entry : columnResidues.entrySet())
1246       {
1247         char residue = entry.getValue().product.charAt(0);
1248         peptides.get(entry.getKey())[column] = residue;
1249       }
1250       column++;
1251     }
1252
1253     /*
1254      * and finally set the constructed sequences
1255      */
1256     for (Entry<SequenceI, char[]> entry : peptides.entrySet())
1257     {
1258       entry.getKey().setSequence(new String(entry.getValue()));
1259     }
1260
1261     return 0;
1262   }
1263
1264   /**
1265    * Populate the map of aligned codons by traversing the given sequence
1266    * mapping, locating the aligned positions of mapped codons, and adding those
1267    * positions and their translation products to the map.
1268    * 
1269    * @param dna
1270    *          the aligned sequence we are mapping from
1271    * @param protein
1272    *          the sequence to be aligned to the codons
1273    * @param gapChar
1274    *          the gap character in the dna sequence
1275    * @param seqMap
1276    *          a mapping to a sequence translation
1277    * @param alignedCodons
1278    *          the map we are building up
1279    */
1280   static void addCodonPositions(SequenceI dna, SequenceI protein,
1281           char gapChar, Mapping seqMap,
1282           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons)
1283   {
1284     Iterator<AlignedCodon> codons = seqMap.getCodonIterator(dna, gapChar);
1285
1286     /*
1287      * add codon positions, and their peptide translations, to the alignment
1288      * map, while remembering the first codon mapped
1289      */
1290     while (codons.hasNext())
1291     {
1292       try
1293       {
1294         AlignedCodon codon = codons.next();
1295         addCodonToMap(alignedCodons, codon, protein);
1296       } catch (IncompleteCodonException e)
1297       {
1298         // possible incomplete trailing codon - ignore
1299       } catch (NoSuchElementException e)
1300       {
1301         // possibly peptide lacking STOP
1302       }
1303     }
1304   }
1305
1306   /**
1307    * Helper method to add a codon-to-peptide entry to the aligned codons map
1308    * 
1309    * @param alignedCodons
1310    * @param codon
1311    * @param protein
1312    */
1313   protected static void addCodonToMap(
1314           Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons,
1315           AlignedCodon codon, SequenceI protein)
1316   {
1317     Map<SequenceI, AlignedCodon> seqProduct = alignedCodons.get(codon);
1318     if (seqProduct == null)
1319     {
1320       seqProduct = new HashMap<SequenceI, AlignedCodon>();
1321       alignedCodons.put(codon, seqProduct);
1322     }
1323     seqProduct.put(protein, codon);
1324   }
1325
1326   /**
1327    * Returns true if a cDNA/Protein mapping either exists, or could be made,
1328    * between at least one pair of sequences in the two alignments. Currently,
1329    * the logic is:
1330    * <ul>
1331    * <li>One alignment must be nucleotide, and the other protein</li>
1332    * <li>At least one pair of sequences must be already mapped, or mappable</li>
1333    * <li>Mappable means the nucleotide translation matches the protein
1334    * sequence</li>
1335    * <li>The translation may ignore start and stop codons if present in the
1336    * nucleotide</li>
1337    * </ul>
1338    * 
1339    * @param al1
1340    * @param al2
1341    * @return
1342    */
1343   public static boolean isMappable(AlignmentI al1, AlignmentI al2)
1344   {
1345     if (al1 == null || al2 == null)
1346     {
1347       return false;
1348     }
1349
1350     /*
1351      * Require one nucleotide and one protein
1352      */
1353     if (al1.isNucleotide() == al2.isNucleotide())
1354     {
1355       return false;
1356     }
1357     AlignmentI dna = al1.isNucleotide() ? al1 : al2;
1358     AlignmentI protein = dna == al1 ? al2 : al1;
1359     List<AlignedCodonFrame> mappings = protein.getCodonFrames();
1360     for (SequenceI dnaSeq : dna.getSequences())
1361     {
1362       for (SequenceI proteinSeq : protein.getSequences())
1363       {
1364         if (isMappable(dnaSeq, proteinSeq, mappings))
1365         {
1366           return true;
1367         }
1368       }
1369     }
1370     return false;
1371   }
1372
1373   /**
1374    * Returns true if the dna sequence is mapped, or could be mapped, to the
1375    * protein sequence.
1376    * 
1377    * @param dnaSeq
1378    * @param proteinSeq
1379    * @param mappings
1380    * @return
1381    */
1382   protected static boolean isMappable(SequenceI dnaSeq,
1383           SequenceI proteinSeq, List<AlignedCodonFrame> mappings)
1384   {
1385     if (dnaSeq == null || proteinSeq == null)
1386     {
1387       return false;
1388     }
1389
1390     SequenceI dnaDs = dnaSeq.getDatasetSequence() == null ? dnaSeq
1391             : dnaSeq.getDatasetSequence();
1392     SequenceI proteinDs = proteinSeq.getDatasetSequence() == null
1393             ? proteinSeq
1394             : proteinSeq.getDatasetSequence();
1395
1396     for (AlignedCodonFrame mapping : mappings)
1397     {
1398       if (proteinDs == mapping.getAaForDnaSeq(dnaDs))
1399       {
1400         /*
1401          * already mapped
1402          */
1403         return true;
1404       }
1405     }
1406
1407     /*
1408      * Just try to make a mapping (it is not yet stored), test whether
1409      * successful.
1410      */
1411     return mapCdnaToProtein(proteinDs, dnaDs) != null;
1412   }
1413
1414   /**
1415    * Finds any reference annotations associated with the sequences in
1416    * sequenceScope, that are not already added to the alignment, and adds them
1417    * to the 'candidates' map. Also populates a lookup table of annotation
1418    * labels, keyed by calcId, for use in constructing tooltips or the like.
1419    * 
1420    * @param sequenceScope
1421    *          the sequences to scan for reference annotations
1422    * @param labelForCalcId
1423    *          (optional) map to populate with label for calcId
1424    * @param candidates
1425    *          map to populate with annotations for sequence
1426    * @param al
1427    *          the alignment to check for presence of annotations
1428    */
1429   public static void findAddableReferenceAnnotations(
1430           List<SequenceI> sequenceScope, Map<String, String> labelForCalcId,
1431           final Map<SequenceI, List<AlignmentAnnotation>> candidates,
1432           AlignmentI al)
1433   {
1434     if (sequenceScope == null)
1435     {
1436       return;
1437     }
1438
1439     /*
1440      * For each sequence in scope, make a list of any annotations on the
1441      * underlying dataset sequence which are not already on the alignment.
1442      * 
1443      * Add to a map of { alignmentSequence, <List of annotations to add> }
1444      */
1445     for (SequenceI seq : sequenceScope)
1446     {
1447       SequenceI dataset = seq.getDatasetSequence();
1448       if (dataset == null)
1449       {
1450         continue;
1451       }
1452       AlignmentAnnotation[] datasetAnnotations = dataset.getAnnotation();
1453       if (datasetAnnotations == null)
1454       {
1455         continue;
1456       }
1457       final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
1458       for (AlignmentAnnotation dsann : datasetAnnotations)
1459       {
1460         /*
1461          * Find matching annotations on the alignment. If none is found, then
1462          * add this annotation to the list of 'addable' annotations for this
1463          * sequence.
1464          */
1465         final Iterable<AlignmentAnnotation> matchedAlignmentAnnotations = al
1466                 .findAnnotations(seq, dsann.getCalcId(), dsann.label);
1467         if (!matchedAlignmentAnnotations.iterator().hasNext())
1468         {
1469           result.add(dsann);
1470           if (labelForCalcId != null)
1471           {
1472             labelForCalcId.put(dsann.getCalcId(), dsann.label);
1473           }
1474         }
1475       }
1476       /*
1477        * Save any addable annotations for this sequence
1478        */
1479       if (!result.isEmpty())
1480       {
1481         candidates.put(seq, result);
1482       }
1483     }
1484   }
1485
1486   /**
1487    * Adds annotations to the top of the alignment annotations, in the same order
1488    * as their related sequences.
1489    * 
1490    * @param annotations
1491    *          the annotations to add
1492    * @param alignment
1493    *          the alignment to add them to
1494    * @param selectionGroup
1495    *          current selection group (or null if none)
1496    */
1497   public static void addReferenceAnnotations(
1498           Map<SequenceI, List<AlignmentAnnotation>> annotations,
1499           final AlignmentI alignment, final SequenceGroup selectionGroup)
1500   {
1501     for (SequenceI seq : annotations.keySet())
1502     {
1503       for (AlignmentAnnotation ann : annotations.get(seq))
1504       {
1505         AlignmentAnnotation copyAnn = new AlignmentAnnotation(ann);
1506         int startRes = 0;
1507         int endRes = ann.annotations.length;
1508         if (selectionGroup != null)
1509         {
1510           startRes = selectionGroup.getStartRes();
1511           endRes = selectionGroup.getEndRes();
1512         }
1513         copyAnn.restrict(startRes, endRes);
1514
1515         /*
1516          * Add to the sequence (sets copyAnn.datasetSequence), unless the
1517          * original annotation is already on the sequence.
1518          */
1519         if (!seq.hasAnnotation(ann))
1520         {
1521           seq.addAlignmentAnnotation(copyAnn);
1522         }
1523         // adjust for gaps
1524         copyAnn.adjustForAlignment();
1525         // add to the alignment and set visible
1526         alignment.addAnnotation(copyAnn);
1527         copyAnn.visible = true;
1528       }
1529     }
1530   }
1531
1532   /**
1533    * Set visibility of alignment annotations of specified types (labels), for
1534    * specified sequences. This supports controls like "Show all secondary
1535    * structure", "Hide all Temp factor", etc.
1536    * 
1537    * @al the alignment to scan for annotations
1538    * @param types
1539    *          the types (labels) of annotations to be updated
1540    * @param forSequences
1541    *          if not null, only annotations linked to one of these sequences are
1542    *          in scope for update; if null, acts on all sequence annotations
1543    * @param anyType
1544    *          if this flag is true, 'types' is ignored (label not checked)
1545    * @param doShow
1546    *          if true, set visibility on, else set off
1547    */
1548   public static void showOrHideSequenceAnnotations(AlignmentI al,
1549           Collection<String> types, List<SequenceI> forSequences,
1550           boolean anyType, boolean doShow)
1551   {
1552     AlignmentAnnotation[] anns = al.getAlignmentAnnotation();
1553     if (anns != null)
1554     {
1555       for (AlignmentAnnotation aa : anns)
1556       {
1557         if (anyType || types.contains(aa.label))
1558         {
1559           if ((aa.sequenceRef != null) && (forSequences == null
1560                   || forSequences.contains(aa.sequenceRef)))
1561           {
1562             aa.visible = doShow;
1563           }
1564         }
1565       }
1566     }
1567   }
1568
1569   /**
1570    * Returns true if either sequence has a cross-reference to the other
1571    * 
1572    * @param seq1
1573    * @param seq2
1574    * @return
1575    */
1576   public static boolean haveCrossRef(SequenceI seq1, SequenceI seq2)
1577   {
1578     // Note: moved here from class CrossRef as the latter class has dependencies
1579     // not availability to the applet's classpath
1580     return hasCrossRef(seq1, seq2) || hasCrossRef(seq2, seq1);
1581   }
1582
1583   /**
1584    * Returns true if seq1 has a cross-reference to seq2. Currently this assumes
1585    * that sequence name is structured as Source|AccessionId.
1586    * 
1587    * @param seq1
1588    * @param seq2
1589    * @return
1590    */
1591   public static boolean hasCrossRef(SequenceI seq1, SequenceI seq2)
1592   {
1593     if (seq1 == null || seq2 == null)
1594     {
1595       return false;
1596     }
1597     String name = seq2.getName();
1598     final DBRefEntry[] xrefs = seq1.getDBRefs();
1599     if (xrefs != null)
1600     {
1601       for (DBRefEntry xref : xrefs)
1602       {
1603         String xrefName = xref.getSource() + "|" + xref.getAccessionId();
1604         // case-insensitive test, consistent with DBRefEntry.equalRef()
1605         if (xrefName.equalsIgnoreCase(name))
1606         {
1607           return true;
1608         }
1609       }
1610     }
1611     return false;
1612   }
1613
1614   /**
1615    * Constructs an alignment consisting of the mapped (CDS) regions in the given
1616    * nucleotide sequences, and updates mappings to match. The CDS sequences are
1617    * added to the original alignment's dataset, which is shared by the new
1618    * alignment. Mappings from nucleotide to CDS, and from CDS to protein, are
1619    * added to the alignment dataset.
1620    * 
1621    * @param dna
1622    *          aligned nucleotide (dna or cds) sequences
1623    * @param dataset
1624    *          the alignment dataset the sequences belong to
1625    * @param products
1626    *          (optional) to restrict results to CDS that map to specified
1627    *          protein products
1628    * @return an alignment whose sequences are the cds-only parts of the dna
1629    *         sequences (or null if no mappings are found)
1630    */
1631   public static AlignmentI makeCdsAlignment(SequenceI[] dna,
1632           AlignmentI dataset, SequenceI[] products)
1633   {
1634     if (dataset == null || dataset.getDataset() != null)
1635     {
1636       throw new IllegalArgumentException(
1637               "IMPLEMENTATION ERROR: dataset.getDataset() must be null!");
1638     }
1639     List<SequenceI> foundSeqs = new ArrayList<SequenceI>();
1640     List<SequenceI> cdsSeqs = new ArrayList<SequenceI>();
1641     List<AlignedCodonFrame> mappings = dataset.getCodonFrames();
1642     HashSet<SequenceI> productSeqs = null;
1643     if (products != null)
1644     {
1645       productSeqs = new HashSet<SequenceI>();
1646       for (SequenceI seq : products)
1647       {
1648         productSeqs.add(seq.getDatasetSequence() == null ? seq
1649                 : seq.getDatasetSequence());
1650       }
1651     }
1652
1653     /*
1654      * Construct CDS sequences from mappings on the alignment dataset.
1655      * The logic is:
1656      * - find the protein product(s) mapped to from each dna sequence
1657      * - if the mapping covers the whole dna sequence (give or take start/stop
1658      *   codon), take the dna as the CDS sequence
1659      * - else search dataset mappings for a suitable dna sequence, i.e. one
1660      *   whose whole sequence is mapped to the protein 
1661      * - if no sequence found, construct one from the dna sequence and mapping
1662      *   (and add it to dataset so it is found if this is repeated)
1663      */
1664     for (SequenceI dnaSeq : dna)
1665     {
1666       SequenceI dnaDss = dnaSeq.getDatasetSequence() == null ? dnaSeq
1667               : dnaSeq.getDatasetSequence();
1668
1669       List<AlignedCodonFrame> seqMappings = MappingUtils
1670               .findMappingsForSequence(dnaSeq, mappings);
1671       for (AlignedCodonFrame mapping : seqMappings)
1672       {
1673         List<Mapping> mappingsFromSequence = mapping
1674                 .getMappingsFromSequence(dnaSeq);
1675
1676         for (Mapping aMapping : mappingsFromSequence)
1677         {
1678           MapList mapList = aMapping.getMap();
1679           if (mapList.getFromRatio() == 1)
1680           {
1681             /*
1682              * not a dna-to-protein mapping (likely dna-to-cds)
1683              */
1684             continue;
1685           }
1686
1687           /*
1688            * skip if mapping is not to one of the target set of proteins
1689            */
1690           SequenceI proteinProduct = aMapping.getTo();
1691           if (productSeqs != null && !productSeqs.contains(proteinProduct))
1692           {
1693             continue;
1694           }
1695
1696           /*
1697            * try to locate the CDS from the dataset mappings;
1698            * guard against duplicate results (for the case that protein has
1699            * dbrefs to both dna and cds sequences)
1700            */
1701           SequenceI cdsSeq = findCdsForProtein(mappings, dnaSeq,
1702                   seqMappings, aMapping);
1703           if (cdsSeq != null)
1704           {
1705             if (!foundSeqs.contains(cdsSeq))
1706             {
1707               foundSeqs.add(cdsSeq);
1708               SequenceI derivedSequence = cdsSeq.deriveSequence();
1709               cdsSeqs.add(derivedSequence);
1710               if (!dataset.getSequences().contains(cdsSeq))
1711               {
1712                 dataset.addSequence(cdsSeq);
1713               }
1714             }
1715             continue;
1716           }
1717
1718           /*
1719            * didn't find mapped CDS sequence - construct it and add
1720            * its dataset sequence to the dataset
1721            */
1722           cdsSeq = makeCdsSequence(dnaSeq.getDatasetSequence(), aMapping,
1723                   dataset).deriveSequence();
1724           // cdsSeq has a name constructed as CDS|<dbref>
1725           // <dbref> will be either the accession for the coding sequence,
1726           // marked in the /via/ dbref to the protein product accession
1727           // or it will be the original nucleotide accession.
1728           SequenceI cdsSeqDss = cdsSeq.getDatasetSequence();
1729
1730           cdsSeqs.add(cdsSeq);
1731
1732           if (!dataset.getSequences().contains(cdsSeqDss))
1733           {
1734             // check if this sequence is a newly created one
1735             // so needs adding to the dataset
1736             dataset.addSequence(cdsSeqDss);
1737           }
1738
1739           /*
1740            * add a mapping from CDS to the (unchanged) mapped to range
1741            */
1742           List<int[]> cdsRange = Collections
1743                   .singletonList(new int[]
1744                   { 1, cdsSeq.getLength() });
1745           MapList cdsToProteinMap = new MapList(cdsRange,
1746                   mapList.getToRanges(), mapList.getFromRatio(),
1747                   mapList.getToRatio());
1748           AlignedCodonFrame cdsToProteinMapping = new AlignedCodonFrame();
1749           cdsToProteinMapping.addMap(cdsSeqDss, proteinProduct,
1750                   cdsToProteinMap);
1751
1752           /*
1753            * guard against duplicating the mapping if repeating this action
1754            */
1755           if (!mappings.contains(cdsToProteinMapping))
1756           {
1757             mappings.add(cdsToProteinMapping);
1758           }
1759
1760           propagateDBRefsToCDS(cdsSeqDss, dnaSeq.getDatasetSequence(),
1761                   proteinProduct, aMapping);
1762           /*
1763            * add another mapping from original 'from' range to CDS
1764            */
1765           AlignedCodonFrame dnaToCdsMapping = new AlignedCodonFrame();
1766           MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
1767                   cdsRange, 1, 1);
1768           dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeqDss,
1769                   dnaToCdsMap);
1770           if (!mappings.contains(dnaToCdsMapping))
1771           {
1772             mappings.add(dnaToCdsMapping);
1773           }
1774
1775           /*
1776            * add DBRef with mapping from protein to CDS
1777            * (this enables Get Cross-References from protein alignment)
1778            * This is tricky because we can't have two DBRefs with the
1779            * same source and accession, so need a different accession for
1780            * the CDS from the dna sequence
1781            */
1782
1783           // specific use case:
1784           // Genomic contig ENSCHR:1, contains coding regions for ENSG01,
1785           // ENSG02, ENSG03, with transcripts and products similarly named.
1786           // cannot add distinct dbrefs mapping location on ENSCHR:1 to ENSG01
1787
1788           // JBPNote: ?? can't actually create an example that demonstrates we
1789           // need to
1790           // synthesize an xref.
1791
1792           for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
1793           {
1794             // creates a complementary cross-reference to the source sequence's
1795             // primary reference.
1796
1797             DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(),
1798                     primRef.getSource() + ":" + primRef.getVersion(),
1799                     primRef.getAccessionId());
1800             cdsCrossRef
1801                     .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap)));
1802             cdsSeqDss.addDBRef(cdsCrossRef);
1803
1804             // problem here is that the cross-reference is synthesized -
1805             // cdsSeq.getName() may be like 'CDS|dnaaccession' or
1806             // 'CDS|emblcdsacc'
1807             // assuming cds version same as dna ?!?
1808
1809             DBRefEntry proteinToCdsRef = new DBRefEntry(primRef.getSource(),
1810                     primRef.getVersion(), cdsSeq.getName());
1811             //
1812             proteinToCdsRef.setMap(
1813                     new Mapping(cdsSeqDss, cdsToProteinMap.getInverse()));
1814             proteinProduct.addDBRef(proteinToCdsRef);
1815           }
1816
1817           /*
1818            * transfer any features on dna that overlap the CDS
1819            */
1820           transferFeatures(dnaSeq, cdsSeq, dnaToCdsMap, null,
1821                   SequenceOntologyI.CDS);
1822         }
1823       }
1824     }
1825
1826     AlignmentI cds = new Alignment(
1827             cdsSeqs.toArray(new SequenceI[cdsSeqs.size()]));
1828     cds.setDataset(dataset);
1829
1830     return cds;
1831   }
1832
1833   /**
1834    * A helper method that finds a CDS sequence in the alignment dataset that is
1835    * mapped to the given protein sequence, and either is, or has a mapping from,
1836    * the given dna sequence.
1837    * 
1838    * @param mappings
1839    *          set of all mappings on the dataset
1840    * @param dnaSeq
1841    *          a dna (or cds) sequence we are searching from
1842    * @param seqMappings
1843    *          the set of mappings involving dnaSeq
1844    * @param aMapping
1845    *          an initial candidate from seqMappings
1846    * @return
1847    */
1848   static SequenceI findCdsForProtein(List<AlignedCodonFrame> mappings,
1849           SequenceI dnaSeq, List<AlignedCodonFrame> seqMappings,
1850           Mapping aMapping)
1851   {
1852     /*
1853      * TODO a better dna-cds-protein mapping data representation to allow easy
1854      * navigation; until then this clunky looping around lists of mappings
1855      */
1856     SequenceI seqDss = dnaSeq.getDatasetSequence() == null ? dnaSeq
1857             : dnaSeq.getDatasetSequence();
1858     SequenceI proteinProduct = aMapping.getTo();
1859
1860     /*
1861      * is this mapping from the whole dna sequence (i.e. CDS)?
1862      * allowing for possible stop codon on dna but not peptide
1863      */
1864     int mappedFromLength = MappingUtils
1865             .getLength(aMapping.getMap().getFromRanges());
1866     int dnaLength = seqDss.getLength();
1867     if (mappedFromLength == dnaLength
1868             || mappedFromLength == dnaLength - CODON_LENGTH)
1869     {
1870       return seqDss;
1871     }
1872
1873     /*
1874      * looks like we found the dna-to-protein mapping; search for the
1875      * corresponding cds-to-protein mapping
1876      */
1877     List<AlignedCodonFrame> mappingsToPeptide = MappingUtils
1878             .findMappingsForSequence(proteinProduct, mappings);
1879     for (AlignedCodonFrame acf : mappingsToPeptide)
1880     {
1881       for (SequenceToSequenceMapping map : acf.getMappings())
1882       {
1883         Mapping mapping = map.getMapping();
1884         if (mapping != aMapping
1885                 && mapping.getMap().getFromRatio() == CODON_LENGTH
1886                 && proteinProduct == mapping.getTo()
1887                 && seqDss != map.getFromSeq())
1888         {
1889           mappedFromLength = MappingUtils
1890                   .getLength(mapping.getMap().getFromRanges());
1891           if (mappedFromLength == map.getFromSeq().getLength())
1892           {
1893             /*
1894             * found a 3:1 mapping to the protein product which covers
1895             * the whole dna sequence i.e. is from CDS; finally check it
1896             * is from the dna start sequence
1897             */
1898             SequenceI cdsSeq = map.getFromSeq();
1899             List<AlignedCodonFrame> dnaToCdsMaps = MappingUtils
1900                     .findMappingsForSequence(cdsSeq, seqMappings);
1901             if (!dnaToCdsMaps.isEmpty())
1902             {
1903               return cdsSeq;
1904             }
1905           }
1906         }
1907       }
1908     }
1909     return null;
1910   }
1911
1912   /**
1913    * Helper method that makes a CDS sequence as defined by the mappings from the
1914    * given sequence i.e. extracts the 'mapped from' ranges (which may be on
1915    * forward or reverse strand).
1916    * 
1917    * @param seq
1918    * @param mapping
1919    * @param dataset
1920    *          - existing dataset. We check for sequences that look like the CDS
1921    *          we are about to construct, if one exists already, then we will
1922    *          just return that one.
1923    * @return CDS sequence (as a dataset sequence)
1924    */
1925   static SequenceI makeCdsSequence(SequenceI seq, Mapping mapping,
1926           AlignmentI dataset)
1927   {
1928     char[] seqChars = seq.getSequence();
1929     List<int[]> fromRanges = mapping.getMap().getFromRanges();
1930     int cdsWidth = MappingUtils.getLength(fromRanges);
1931     char[] newSeqChars = new char[cdsWidth];
1932
1933     int newPos = 0;
1934     for (int[] range : fromRanges)
1935     {
1936       if (range[0] <= range[1])
1937       {
1938         // forward strand mapping - just copy the range
1939         int length = range[1] - range[0] + 1;
1940         System.arraycopy(seqChars, range[0] - 1, newSeqChars, newPos,
1941                 length);
1942         newPos += length;
1943       }
1944       else
1945       {
1946         // reverse strand mapping - copy and complement one by one
1947         for (int i = range[0]; i >= range[1]; i--)
1948         {
1949           newSeqChars[newPos++] = Dna.getComplement(seqChars[i - 1]);
1950         }
1951       }
1952     }
1953
1954     /*
1955      * assign 'from id' held in the mapping if set (e.g. EMBL protein_id),
1956      * else generate a sequence name
1957      */
1958     String mapFromId = mapping.getMappedFromId();
1959     String seqId = "CDS|" + (mapFromId != null ? mapFromId : seq.getName());
1960     SequenceI newSeq = new Sequence(seqId, newSeqChars, 1, newPos);
1961     if (dataset != null)
1962     {
1963       SequenceI[] matches = dataset.findSequenceMatch(newSeq.getName());
1964       if (matches != null)
1965       {
1966         boolean matched = false;
1967         for (SequenceI mtch : matches)
1968         {
1969           if (mtch.getStart() != newSeq.getStart())
1970           {
1971             continue;
1972           }
1973           if (mtch.getEnd() != newSeq.getEnd())
1974           {
1975             continue;
1976           }
1977           if (!Arrays.equals(mtch.getSequence(), newSeq.getSequence()))
1978           {
1979             continue;
1980           }
1981           if (!matched)
1982           {
1983             matched = true;
1984             newSeq = mtch;
1985           }
1986           else
1987           {
1988             System.err.println(
1989                     "JAL-2154 regression: warning - found (and ignnored a duplicate CDS sequence):"
1990                             + mtch.toString());
1991           }
1992         }
1993       }
1994     }
1995     // newSeq.setDescription(mapFromId);
1996
1997     return newSeq;
1998   }
1999
2000   /**
2001    * add any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
2002    * the given mapping.
2003    * 
2004    * @param cdsSeq
2005    * @param contig
2006    * @param mapping
2007    * @return list of DBRefEntrys added.
2008    */
2009   public static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
2010           SequenceI contig, SequenceI proteinProduct, Mapping mapping)
2011   {
2012
2013     // gather direct refs from contig congrent with mapping
2014     List<DBRefEntry> direct = new ArrayList<DBRefEntry>();
2015     HashSet<String> directSources = new HashSet<String>();
2016     if (contig.getDBRefs() != null)
2017     {
2018       for (DBRefEntry dbr : contig.getDBRefs())
2019       {
2020         if (dbr.hasMap() && dbr.getMap().getMap().isTripletMap())
2021         {
2022           MapList map = dbr.getMap().getMap();
2023           // check if map is the CDS mapping
2024           if (mapping.getMap().equals(map))
2025           {
2026             direct.add(dbr);
2027             directSources.add(dbr.getSource());
2028           }
2029         }
2030       }
2031     }
2032     DBRefEntry[] onSource = DBRefUtils.selectRefs(
2033             proteinProduct.getDBRefs(),
2034             directSources.toArray(new String[0]));
2035     List<DBRefEntry> propagated = new ArrayList<DBRefEntry>();
2036
2037     // and generate appropriate mappings
2038     for (DBRefEntry cdsref : direct)
2039     {
2040       // clone maplist and mapping
2041       MapList cdsposmap = new MapList(
2042               Arrays.asList(new int[][]
2043               { new int[] { cdsSeq.getStart(), cdsSeq.getEnd() } }),
2044               cdsref.getMap().getMap().getToRanges(), 3, 1);
2045       Mapping cdsmap = new Mapping(cdsref.getMap().getTo(),
2046               cdsref.getMap().getMap());
2047
2048       // create dbref
2049       DBRefEntry newref = new DBRefEntry(cdsref.getSource(),
2050               cdsref.getVersion(), cdsref.getAccessionId(),
2051               new Mapping(cdsmap.getTo(), cdsposmap));
2052
2053       // and see if we can map to the protein product for this mapping.
2054       // onSource is the filtered set of accessions on protein that we are
2055       // tranferring, so we assume accession is the same.
2056       if (cdsmap.getTo() == null && onSource != null)
2057       {
2058         List<DBRefEntry> sourceRefs = DBRefUtils.searchRefs(onSource,
2059                 cdsref.getAccessionId());
2060         if (sourceRefs != null)
2061         {
2062           for (DBRefEntry srcref : sourceRefs)
2063           {
2064             if (srcref.getSource().equalsIgnoreCase(cdsref.getSource()))
2065             {
2066               // we have found a complementary dbref on the protein product, so
2067               // update mapping's getTo
2068               newref.getMap().setTo(proteinProduct);
2069             }
2070           }
2071         }
2072       }
2073       cdsSeq.addDBRef(newref);
2074       propagated.add(newref);
2075     }
2076     return propagated;
2077   }
2078
2079   /**
2080    * Transfers co-located features on 'fromSeq' to 'toSeq', adjusting the
2081    * feature start/end ranges, optionally omitting specified feature types.
2082    * Returns the number of features copied.
2083    * 
2084    * @param fromSeq
2085    * @param toSeq
2086    * @param mapping
2087    *          the mapping from 'fromSeq' to 'toSeq'
2088    * @param select
2089    *          if not null, only features of this type are copied (including
2090    *          subtypes in the Sequence Ontology)
2091    * @param omitting
2092    */
2093   public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
2094           MapList mapping, String select, String... omitting)
2095   {
2096     SequenceI copyTo = toSeq;
2097     while (copyTo.getDatasetSequence() != null)
2098     {
2099       copyTo = copyTo.getDatasetSequence();
2100     }
2101
2102     /*
2103      * get features, optionally restricted by an ontology term
2104      */
2105     List<SequenceFeature> sfs = select == null ? fromSeq.getFeatures()
2106             .getPositionalFeatures() : fromSeq.getFeatures()
2107             .getFeaturesByOntology(select);
2108
2109     int count = 0;
2110     for (SequenceFeature sf : sfs)
2111     {
2112       String type = sf.getType();
2113       boolean omit = false;
2114       for (String toOmit : omitting)
2115       {
2116         if (type.equals(toOmit))
2117         {
2118           omit = true;
2119         }
2120       }
2121       if (omit)
2122       {
2123         continue;
2124       }
2125
2126       /*
2127        * locate the mapped range - null if either start or end is
2128        * not mapped (no partial overlaps are calculated)
2129        */
2130       int start = sf.getBegin();
2131       int end = sf.getEnd();
2132       int[] mappedTo = mapping.locateInTo(start, end);
2133       /*
2134        * if whole exon range doesn't map, try interpreting it
2135        * as 5' or 3' exon overlapping the CDS range
2136        */
2137       if (mappedTo == null)
2138       {
2139         mappedTo = mapping.locateInTo(end, end);
2140         if (mappedTo != null)
2141         {
2142           /*
2143            * end of exon is in CDS range - 5' overlap
2144            * to a range from the start of the peptide
2145            */
2146           mappedTo[0] = 1;
2147         }
2148       }
2149       if (mappedTo == null)
2150       {
2151         mappedTo = mapping.locateInTo(start, start);
2152         if (mappedTo != null)
2153         {
2154           /*
2155            * start of exon is in CDS range - 3' overlap
2156            * to a range up to the end of the peptide
2157            */
2158           mappedTo[1] = toSeq.getLength();
2159         }
2160       }
2161       if (mappedTo != null)
2162       {
2163         int newBegin = Math.min(mappedTo[0], mappedTo[1]);
2164         int newEnd = Math.max(mappedTo[0], mappedTo[1]);
2165         SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
2166                 sf.getFeatureGroup(), sf.getScore());
2167         copyTo.addSequenceFeature(copy);
2168         count++;
2169       }
2170     }
2171     return count;
2172   }
2173
2174   /**
2175    * Returns a mapping from dna to protein by inspecting sequence features of
2176    * type "CDS" on the dna.
2177    * 
2178    * @param dnaSeq
2179    * @param proteinSeq
2180    * @return
2181    */
2182   public static MapList mapCdsToProtein(SequenceI dnaSeq,
2183           SequenceI proteinSeq)
2184   {
2185     List<int[]> ranges = findCdsPositions(dnaSeq);
2186     int mappedDnaLength = MappingUtils.getLength(ranges);
2187
2188     int proteinLength = proteinSeq.getLength();
2189     int proteinStart = proteinSeq.getStart();
2190     int proteinEnd = proteinSeq.getEnd();
2191
2192     /*
2193      * incomplete start codon may mean X at start of peptide
2194      * we ignore both for mapping purposes
2195      */
2196     if (proteinSeq.getCharAt(0) == 'X')
2197     {
2198       // todo JAL-2022 support startPhase > 0
2199       proteinStart++;
2200       proteinLength--;
2201     }
2202     List<int[]> proteinRange = new ArrayList<int[]>();
2203
2204     /*
2205      * dna length should map to protein (or protein plus stop codon)
2206      */
2207     int codesForResidues = mappedDnaLength / CODON_LENGTH;
2208     if (codesForResidues == (proteinLength + 1))
2209     {
2210       // assuming extra codon is for STOP and not in peptide
2211       codesForResidues--;
2212     }
2213     if (codesForResidues == proteinLength)
2214     {
2215       proteinRange.add(new int[] { proteinStart, proteinEnd });
2216       return new MapList(ranges, proteinRange, CODON_LENGTH, 1);
2217     }
2218     return null;
2219   }
2220
2221   /**
2222    * Returns a list of CDS ranges found (as sequence positions base 1), i.e. of
2223    * start/end positions of sequence features of type "CDS" (or a sub-type of
2224    * CDS in the Sequence Ontology). The ranges are sorted into ascending start
2225    * position order, so this method is only valid for linear CDS in the same
2226    * sense as the protein product.
2227    * 
2228    * @param dnaSeq
2229    * @return
2230    */
2231   public static List<int[]> findCdsPositions(SequenceI dnaSeq)
2232   {
2233     List<int[]> result = new ArrayList<int[]>();
2234
2235     List<SequenceFeature> sfs = dnaSeq.getFeatures().getFeaturesByOntology(
2236             SequenceOntologyI.CDS);
2237     if (sfs.isEmpty())
2238     {
2239       return result;
2240     }
2241     SequenceFeatures.sortFeatures(sfs, true);
2242     int startPhase = 0;
2243
2244     for (SequenceFeature sf : sfs)
2245     {
2246       int phase = 0;
2247       try
2248       {
2249         phase = Integer.parseInt(sf.getPhase());
2250       } catch (NumberFormatException e)
2251       {
2252         // ignore
2253       }
2254       /*
2255        * phase > 0 on first codon means 5' incomplete - skip to the start
2256        * of the next codon; example ENST00000496384
2257        */
2258       int begin = sf.getBegin();
2259       int end = sf.getEnd();
2260       if (result.isEmpty())
2261       {
2262         begin += phase;
2263         if (begin > end)
2264         {
2265           // shouldn't happen!
2266           System.err
2267                   .println("Error: start phase extends beyond start CDS in "
2268                           + dnaSeq.getName());
2269         }
2270       }
2271       result.add(new int[] { begin, end });
2272     }
2273
2274     /*
2275      * remove 'startPhase' positions (usually 0) from the first range 
2276      * so we begin at the start of a complete codon
2277      */
2278     if (!result.isEmpty())
2279     {
2280       // TODO JAL-2022 correctly model start phase > 0
2281       result.get(0)[0] += startPhase;
2282     }
2283
2284     /*
2285      * Finally sort ranges by start position. This avoids a dependency on 
2286      * keeping features in order on the sequence (if they are in order anyway,
2287      * the sort will have almost no work to do). The implicit assumption is CDS
2288      * ranges are assembled in order. Other cases should not use this method,
2289      * but instead construct an explicit mapping for CDS (e.g. EMBL parsing).
2290      */
2291     Collections.sort(result, IntRangeComparator.ASCENDING);
2292     return result;
2293   }
2294
2295   /**
2296    * Maps exon features from dna to protein, and computes variants in peptide
2297    * product generated by variants in dna, and adds them as sequence_variant
2298    * features on the protein sequence. Returns the number of variant features
2299    * added.
2300    * 
2301    * @param dnaSeq
2302    * @param peptide
2303    * @param dnaToProtein
2304    */
2305   public static int computeProteinFeatures(SequenceI dnaSeq,
2306           SequenceI peptide, MapList dnaToProtein)
2307   {
2308     while (dnaSeq.getDatasetSequence() != null)
2309     {
2310       dnaSeq = dnaSeq.getDatasetSequence();
2311     }
2312     while (peptide.getDatasetSequence() != null)
2313     {
2314       peptide = peptide.getDatasetSequence();
2315     }
2316
2317     transferFeatures(dnaSeq, peptide, dnaToProtein, SequenceOntologyI.EXON);
2318
2319     /*
2320      * compute protein variants from dna variants and codon mappings;
2321      * NB - alternatively we could retrieve this using the REST service e.g.
2322      * http://rest.ensembl.org/overlap/translation
2323      * /ENSP00000288602?feature=transcript_variation;content-type=text/xml
2324      * which would be a bit slower but possibly more reliable
2325      */
2326
2327     /*
2328      * build a map with codon variations for each potentially varying peptide
2329      */
2330     LinkedHashMap<Integer, List<DnaVariant>[]> variants = buildDnaVariantsMap(
2331             dnaSeq, dnaToProtein);
2332
2333     /*
2334      * scan codon variations, compute peptide variants and add to peptide sequence
2335      */
2336     int count = 0;
2337     for (Entry<Integer, List<DnaVariant>[]> variant : variants.entrySet())
2338     {
2339       int peptidePos = variant.getKey();
2340       List<DnaVariant>[] codonVariants = variant.getValue();
2341       count += computePeptideVariants(peptide, peptidePos, codonVariants);
2342     }
2343
2344     return count;
2345   }
2346
2347   /**
2348    * Computes non-synonymous peptide variants from codon variants and adds them
2349    * as sequence_variant features on the protein sequence (one feature per
2350    * allele variant). Selected attributes (variant id, clinical significance)
2351    * are copied over to the new features.
2352    * 
2353    * @param peptide
2354    *          the protein sequence
2355    * @param peptidePos
2356    *          the position to compute peptide variants for
2357    * @param codonVariants
2358    *          a list of dna variants per codon position
2359    * @return the number of features added
2360    */
2361   static int computePeptideVariants(SequenceI peptide, int peptidePos,
2362           List<DnaVariant>[] codonVariants)
2363   {
2364     String residue = String.valueOf(peptide.getCharAt(peptidePos - 1));
2365     int count = 0;
2366     String base1 = codonVariants[0].get(0).base;
2367     String base2 = codonVariants[1].get(0).base;
2368     String base3 = codonVariants[2].get(0).base;
2369
2370     /*
2371      * variants in first codon base
2372      */
2373     for (DnaVariant var : codonVariants[0])
2374     {
2375       if (var.variant != null)
2376       {
2377         String alleles = (String) var.variant.getValue("alleles");
2378         if (alleles != null)
2379         {
2380           for (String base : alleles.split(","))
2381           {
2382             String codon = base + base2 + base3;
2383             if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
2384             {
2385               count++;
2386             }
2387           }
2388         }
2389       }
2390     }
2391
2392     /*
2393      * variants in second codon base
2394      */
2395     for (DnaVariant var : codonVariants[1])
2396     {
2397       if (var.variant != null)
2398       {
2399         String alleles = (String) var.variant.getValue("alleles");
2400         if (alleles != null)
2401         {
2402           for (String base : alleles.split(","))
2403           {
2404             String codon = base1 + base + base3;
2405             if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
2406             {
2407               count++;
2408             }
2409           }
2410         }
2411       }
2412     }
2413
2414     /*
2415      * variants in third codon base
2416      */
2417     for (DnaVariant var : codonVariants[2])
2418     {
2419       if (var.variant != null)
2420       {
2421         String alleles = (String) var.variant.getValue("alleles");
2422         if (alleles != null)
2423         {
2424           for (String base : alleles.split(","))
2425           {
2426             String codon = base1 + base2 + base;
2427             if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
2428             {
2429               count++;
2430             }
2431           }
2432         }
2433       }
2434     }
2435
2436     return count;
2437   }
2438
2439   /**
2440    * Helper method that adds a peptide variant feature, provided the given codon
2441    * translates to a value different to the current residue (is a non-synonymous
2442    * variant). ID and clinical_significance attributes of the dna variant (if
2443    * present) are copied to the new feature.
2444    * 
2445    * @param peptide
2446    * @param peptidePos
2447    * @param residue
2448    * @param var
2449    * @param codon
2450    * @return true if a feature was added, else false
2451    */
2452   static boolean addPeptideVariant(SequenceI peptide, int peptidePos,
2453           String residue, DnaVariant var, String codon)
2454   {
2455     /*
2456      * get peptide translation of codon e.g. GAT -> D
2457      * note that variants which are not single alleles,
2458      * e.g. multibase variants or HGMD_MUTATION etc
2459      * are currently ignored here
2460      */
2461     String trans = codon.contains("-") ? "-"
2462             : (codon.length() > CODON_LENGTH ? null
2463                     : ResidueProperties.codonTranslate(codon));
2464     if (trans != null && !trans.equals(residue))
2465     {
2466       String residue3Char = StringUtils
2467               .toSentenceCase(ResidueProperties.aa2Triplet.get(residue));
2468       String trans3Char = StringUtils
2469               .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
2470       String desc = "p." + residue3Char + peptidePos + trans3Char;
2471       SequenceFeature sf = new SequenceFeature(
2472               SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
2473               peptidePos, var.getSource());
2474       StringBuilder attributes = new StringBuilder(32);
2475       String id = (String) var.variant.getValue(ID);
2476       if (id != null)
2477       {
2478         if (id.startsWith(SEQUENCE_VARIANT))
2479         {
2480           id = id.substring(SEQUENCE_VARIANT.length());
2481         }
2482         sf.setValue(ID, id);
2483         attributes.append(ID).append("=").append(id);
2484         // TODO handle other species variants JAL-2064
2485         StringBuilder link = new StringBuilder(32);
2486         try
2487         {
2488           link.append(desc).append(" ").append(id).append(
2489                   "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
2490                   .append(URLEncoder.encode(id, "UTF-8"));
2491           sf.addLink(link.toString());
2492         } catch (UnsupportedEncodingException e)
2493         {
2494           // as if
2495         }
2496       }
2497       String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
2498       if (clinSig != null)
2499       {
2500         sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
2501         attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
2502                 .append(clinSig);
2503       }
2504       peptide.addSequenceFeature(sf);
2505       if (attributes.length() > 0)
2506       {
2507         sf.setAttributes(attributes.toString());
2508       }
2509       return true;
2510     }
2511     return false;
2512   }
2513
2514   /**
2515    * Builds a map whose key is position in the protein sequence, and value is a
2516    * list of the base and all variants for each corresponding codon position
2517    * 
2518    * @param dnaSeq
2519    * @param dnaToProtein
2520    * @return
2521    */
2522   @SuppressWarnings("unchecked")
2523   static LinkedHashMap<Integer, List<DnaVariant>[]> buildDnaVariantsMap(
2524           SequenceI dnaSeq, MapList dnaToProtein)
2525   {
2526     /*
2527      * map from peptide position to all variants of the codon which codes for it
2528      * LinkedHashMap ensures we keep the peptide features in sequence order
2529      */
2530     LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<Integer, List<DnaVariant>[]>();
2531
2532     List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
2533             .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
2534     if (dnaFeatures.isEmpty())
2535     {
2536       return variants;
2537     }
2538
2539     int dnaStart = dnaSeq.getStart();
2540     int[] lastCodon = null;
2541     int lastPeptidePostion = 0;
2542
2543     /*
2544      * build a map of codon variations for peptides
2545      */
2546     for (SequenceFeature sf : dnaFeatures)
2547     {
2548       int dnaCol = sf.getBegin();
2549       if (dnaCol != sf.getEnd())
2550       {
2551         // not handling multi-locus variant features
2552         continue;
2553       }
2554
2555       /*
2556        * extract dna variants to a string array
2557        */
2558       String alls = (String) sf.getValue("alleles");
2559       if (alls == null)
2560       {
2561         continue; // non-SNP VCF variant perhaps - can't process this
2562       }
2563       String[] alleles = alls.toUpperCase().split(",");
2564       int i = 0;
2565       for (String allele : alleles)
2566       {
2567         alleles[i++] = allele.trim(); // lose any space characters "A, G"
2568       }
2569
2570       int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
2571       if (mapsTo == null)
2572       {
2573         // feature doesn't lie within coding region
2574         continue;
2575       }
2576       int peptidePosition = mapsTo[0];
2577       List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
2578       if (codonVariants == null)
2579       {
2580         codonVariants = new ArrayList[CODON_LENGTH];
2581         codonVariants[0] = new ArrayList<DnaVariant>();
2582         codonVariants[1] = new ArrayList<DnaVariant>();
2583         codonVariants[2] = new ArrayList<DnaVariant>();
2584         variants.put(peptidePosition, codonVariants);
2585       }
2586
2587       /*
2588        * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
2589        */
2590       int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
2591               : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
2592                       peptidePosition, peptidePosition));
2593       lastPeptidePostion = peptidePosition;
2594       lastCodon = codon;
2595
2596       /*
2597        * save nucleotide (and any variant) for each codon position
2598        */
2599       for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
2600       {
2601         String nucleotide = String.valueOf(
2602                 dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase();
2603         List<DnaVariant> codonVariant = codonVariants[codonPos];
2604         if (codon[codonPos] == dnaCol)
2605         {
2606           if (!codonVariant.isEmpty()
2607                   && codonVariant.get(0).variant == null)
2608           {
2609             /*
2610              * already recorded base value, add this variant
2611              */
2612             codonVariant.get(0).variant = sf;
2613           }
2614           else
2615           {
2616             /*
2617              * add variant with base value
2618              */
2619             codonVariant.add(new DnaVariant(nucleotide, sf));
2620           }
2621         }
2622         else if (codonVariant.isEmpty())
2623         {
2624           /*
2625            * record (possibly non-varying) base value
2626            */
2627           codonVariant.add(new DnaVariant(nucleotide));
2628         }
2629       }
2630     }
2631     return variants;
2632   }
2633
2634   /**
2635    * Makes an alignment with a copy of the given sequences, adding in any
2636    * non-redundant sequences which are mapped to by the cross-referenced
2637    * sequences.
2638    * 
2639    * @param seqs
2640    * @param xrefs
2641    * @param dataset
2642    *          the alignment dataset shared by the new copy
2643    * @return
2644    */
2645   public static AlignmentI makeCopyAlignment(SequenceI[] seqs,
2646           SequenceI[] xrefs, AlignmentI dataset)
2647   {
2648     AlignmentI copy = new Alignment(new Alignment(seqs));
2649     copy.setDataset(dataset);
2650     boolean isProtein = !copy.isNucleotide();
2651     SequenceIdMatcher matcher = new SequenceIdMatcher(seqs);
2652     if (xrefs != null)
2653     {
2654       for (SequenceI xref : xrefs)
2655       {
2656         DBRefEntry[] dbrefs = xref.getDBRefs();
2657         if (dbrefs != null)
2658         {
2659           for (DBRefEntry dbref : dbrefs)
2660           {
2661             if (dbref.getMap() == null || dbref.getMap().getTo() == null
2662                     || dbref.getMap().getTo().isProtein() != isProtein)
2663             {
2664               continue;
2665             }
2666             SequenceI mappedTo = dbref.getMap().getTo();
2667             SequenceI match = matcher.findIdMatch(mappedTo);
2668             if (match == null)
2669             {
2670               matcher.add(mappedTo);
2671               copy.addSequence(mappedTo);
2672             }
2673           }
2674         }
2675       }
2676     }
2677     return copy;
2678   }
2679
2680   /**
2681    * Try to align sequences in 'unaligned' to match the alignment of their
2682    * mapped regions in 'aligned'. For example, could use this to align CDS
2683    * sequences which are mapped to their parent cDNA sequences.
2684    * 
2685    * This method handles 1:1 mappings (dna-to-dna or protein-to-protein). For
2686    * dna-to-protein or protein-to-dna use alternative methods.
2687    * 
2688    * @param unaligned
2689    *          sequences to be aligned
2690    * @param aligned
2691    *          holds aligned sequences and their mappings
2692    * @return
2693    */
2694   public static int alignAs(AlignmentI unaligned, AlignmentI aligned)
2695   {
2696     /*
2697      * easy case - aligning a copy of aligned sequences
2698      */
2699     if (alignAsSameSequences(unaligned, aligned))
2700     {
2701       return unaligned.getHeight();
2702     }
2703
2704     /*
2705      * fancy case - aligning via mappings between sequences
2706      */
2707     List<SequenceI> unmapped = new ArrayList<SequenceI>();
2708     Map<Integer, Map<SequenceI, Character>> columnMap = buildMappedColumnsMap(
2709             unaligned, aligned, unmapped);
2710     int width = columnMap.size();
2711     char gap = unaligned.getGapCharacter();
2712     int realignedCount = 0;
2713     // TODO: verify this loop scales sensibly for very wide/high alignments
2714
2715     for (SequenceI seq : unaligned.getSequences())
2716     {
2717       if (!unmapped.contains(seq))
2718       {
2719         char[] newSeq = new char[width];
2720         Arrays.fill(newSeq, gap); // JBPComment - doubt this is faster than the
2721                                   // Integer iteration below
2722         int newCol = 0;
2723         int lastCol = 0;
2724
2725         /*
2726          * traverse the map to find columns populated
2727          * by our sequence
2728          */
2729         for (Integer column : columnMap.keySet())
2730         {
2731           Character c = columnMap.get(column).get(seq);
2732           if (c != null)
2733           {
2734             /*
2735              * sequence has a character at this position
2736              * 
2737              */
2738             newSeq[newCol] = c;
2739             lastCol = newCol;
2740           }
2741           newCol++;
2742         }
2743
2744         /*
2745          * trim trailing gaps
2746          */
2747         if (lastCol < width)
2748         {
2749           char[] tmp = new char[lastCol + 1];
2750           System.arraycopy(newSeq, 0, tmp, 0, lastCol + 1);
2751           newSeq = tmp;
2752         }
2753         // TODO: optimise SequenceI to avoid char[]->String->char[]
2754         seq.setSequence(String.valueOf(newSeq));
2755         realignedCount++;
2756       }
2757     }
2758     return realignedCount;
2759   }
2760
2761   /**
2762    * If unaligned and aligned sequences share the same dataset sequences, then
2763    * simply copies the aligned sequences to the unaligned sequences and returns
2764    * true; else returns false
2765    * 
2766    * @param unaligned
2767    *          - sequences to be aligned based on aligned
2768    * @param aligned
2769    *          - 'guide' alignment containing sequences derived from same dataset
2770    *          as unaligned
2771    * @return
2772    */
2773   static boolean alignAsSameSequences(AlignmentI unaligned,
2774           AlignmentI aligned)
2775   {
2776     if (aligned.getDataset() == null || unaligned.getDataset() == null)
2777     {
2778       return false; // should only pass alignments with datasets here
2779     }
2780
2781     // map from dataset sequence to alignment sequence(s)
2782     Map<SequenceI, List<SequenceI>> alignedDatasets = new HashMap<SequenceI, List<SequenceI>>();
2783     for (SequenceI seq : aligned.getSequences())
2784     {
2785       SequenceI ds = seq.getDatasetSequence();
2786       if (alignedDatasets.get(ds) == null)
2787       {
2788         alignedDatasets.put(ds, new ArrayList<SequenceI>());
2789       }
2790       alignedDatasets.get(ds).add(seq);
2791     }
2792
2793     /*
2794      * first pass - check whether all sequences to be aligned share a dataset
2795      * sequence with an aligned sequence
2796      */
2797     for (SequenceI seq : unaligned.getSequences())
2798     {
2799       if (!alignedDatasets.containsKey(seq.getDatasetSequence()))
2800       {
2801         return false;
2802       }
2803     }
2804
2805     /*
2806      * second pass - copy aligned sequences;
2807      * heuristic rule: pair off sequences in order for the case where 
2808      * more than one shares the same dataset sequence 
2809      */
2810     for (SequenceI seq : unaligned.getSequences())
2811     {
2812       List<SequenceI> alignedSequences = alignedDatasets
2813               .get(seq.getDatasetSequence());
2814       // TODO: getSequenceAsString() will be deprecated in the future
2815       // TODO: need to leave to SequenceI implementor to update gaps
2816       seq.setSequence(alignedSequences.get(0).getSequenceAsString());
2817       if (alignedSequences.size() > 0)
2818       {
2819         // pop off aligned sequences (except the last one)
2820         alignedSequences.remove(0);
2821       }
2822     }
2823
2824     return true;
2825   }
2826
2827   /**
2828    * Returns a map whose key is alignment column number (base 1), and whose
2829    * values are a map of sequence characters in that column.
2830    * 
2831    * @param unaligned
2832    * @param aligned
2833    * @param unmapped
2834    * @return
2835    */
2836   static SortedMap<Integer, Map<SequenceI, Character>> buildMappedColumnsMap(
2837           AlignmentI unaligned, AlignmentI aligned,
2838           List<SequenceI> unmapped)
2839   {
2840     /*
2841      * Map will hold, for each aligned column position, a map of
2842      * {unalignedSequence, characterPerSequence} at that position.
2843      * TreeMap keeps the entries in ascending column order. 
2844      */
2845     SortedMap<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
2846
2847     /*
2848      * record any sequences that have no mapping so can't be realigned
2849      */
2850     unmapped.addAll(unaligned.getSequences());
2851
2852     List<AlignedCodonFrame> mappings = aligned.getCodonFrames();
2853
2854     for (SequenceI seq : unaligned.getSequences())
2855     {
2856       for (AlignedCodonFrame mapping : mappings)
2857       {
2858         SequenceI fromSeq = mapping.findAlignedSequence(seq, aligned);
2859         if (fromSeq != null)
2860         {
2861           Mapping seqMap = mapping.getMappingBetween(fromSeq, seq);
2862           if (addMappedPositions(seq, fromSeq, seqMap, map))
2863           {
2864             unmapped.remove(seq);
2865           }
2866         }
2867       }
2868     }
2869     return map;
2870   }
2871
2872   /**
2873    * Helper method that adds to a map the mapped column positions of a sequence.
2874    * <br>
2875    * For example if aaTT-Tg-gAAA is mapped to TTTAAA then the map should record
2876    * that columns 3,4,6,10,11,12 map to characters T,T,T,A,A,A of the mapped to
2877    * sequence.
2878    * 
2879    * @param seq
2880    *          the sequence whose column positions we are recording
2881    * @param fromSeq
2882    *          a sequence that is mapped to the first sequence
2883    * @param seqMap
2884    *          the mapping from 'fromSeq' to 'seq'
2885    * @param map
2886    *          a map to add the column positions (in fromSeq) of the mapped
2887    *          positions of seq
2888    * @return
2889    */
2890   static boolean addMappedPositions(SequenceI seq, SequenceI fromSeq,
2891           Mapping seqMap, Map<Integer, Map<SequenceI, Character>> map)
2892   {
2893     if (seqMap == null)
2894     {
2895       return false;
2896     }
2897
2898     /*
2899      * invert mapping if it is from unaligned to aligned sequence
2900      */
2901     if (seqMap.getTo() == fromSeq.getDatasetSequence())
2902     {
2903       seqMap = new Mapping(seq.getDatasetSequence(),
2904               seqMap.getMap().getInverse());
2905     }
2906
2907     int toStart = seq.getStart();
2908
2909     /*
2910      * traverse [start, end, start, end...] ranges in fromSeq
2911      */
2912     for (int[] fromRange : seqMap.getMap().getFromRanges())
2913     {
2914       for (int i = 0; i < fromRange.length - 1; i += 2)
2915       {
2916         boolean forward = fromRange[i + 1] >= fromRange[i];
2917
2918         /*
2919          * find the range mapped to (sequence positions base 1)
2920          */
2921         int[] range = seqMap.locateMappedRange(fromRange[i],
2922                 fromRange[i + 1]);
2923         if (range == null)
2924         {
2925           System.err.println("Error in mapping " + seqMap + " from "
2926                   + fromSeq.getName());
2927           return false;
2928         }
2929         int fromCol = fromSeq.findIndex(fromRange[i]);
2930         int mappedCharPos = range[0];
2931
2932         /*
2933          * walk over the 'from' aligned sequence in forward or reverse
2934          * direction; when a non-gap is found, record the column position
2935          * of the next character of the mapped-to sequence; stop when all
2936          * the characters of the range have been counted
2937          */
2938         while (mappedCharPos <= range[1] && fromCol <= fromSeq.getLength()
2939                 && fromCol >= 0)
2940         {
2941           if (!Comparison.isGap(fromSeq.getCharAt(fromCol - 1)))
2942           {
2943             /*
2944              * mapped from sequence has a character in this column
2945              * record the column position for the mapped to character
2946              */
2947             Map<SequenceI, Character> seqsMap = map.get(fromCol);
2948             if (seqsMap == null)
2949             {
2950               seqsMap = new HashMap<SequenceI, Character>();
2951               map.put(fromCol, seqsMap);
2952             }
2953             seqsMap.put(seq, seq.getCharAt(mappedCharPos - toStart));
2954             mappedCharPos++;
2955           }
2956           fromCol += (forward ? 1 : -1);
2957         }
2958       }
2959     }
2960     return true;
2961   }
2962
2963   // strictly temporary hack until proper criteria for aligning protein to cds
2964   // are in place; this is so Ensembl -> fetch xrefs Uniprot aligns the Uniprot
2965   public static boolean looksLikeEnsembl(AlignmentI alignment)
2966   {
2967     for (SequenceI seq : alignment.getSequences())
2968     {
2969       String name = seq.getName();
2970       if (!name.startsWith("ENSG") && !name.startsWith("ENST"))
2971       {
2972         return false;
2973       }
2974     }
2975     return true;
2976   }
2977 }