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