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