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