83eeb3de75f9b46578a360a836dcc8ce6acc1bed
[jalview.git] / src / jalview / datamodel / AlignedCodonFrame.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.datamodel;
22
23 import jalview.util.MapList;
24 import jalview.util.MappingUtils;
25
26 import java.util.AbstractList;
27 import java.util.ArrayList;
28 import java.util.List;
29
30 /**
31  * Stores mapping between the columns of a protein alignment and a DNA alignment
32  * and a list of individual codon to amino acid mappings between sequences.
33  */
34 public class AlignedCodonFrame
35 {
36
37   /*
38    * Data bean to hold mappings from one sequence to another
39    */
40   public class SequenceToSequenceMapping
41   {
42     private SequenceI fromSeq;
43
44     private Mapping mapping;
45
46     SequenceToSequenceMapping(SequenceI from, Mapping map)
47     {
48       this.fromSeq = from;
49       this.mapping = map;
50     }
51
52     /**
53      * Readable representation for debugging only, not guaranteed not to change
54      */
55     @Override
56     public String toString()
57     {
58       return String.format("From %s %s", fromSeq.getName(),
59               mapping.toString());
60     }
61
62     /**
63      * Returns a hashCode derived from the hashcodes of the mappings and fromSeq
64      * 
65      * @see SequenceToSequenceMapping#hashCode()
66      */
67     @Override
68     public int hashCode()
69     {
70       return (fromSeq == null ? 0 : fromSeq.hashCode() * 31)
71               + mapping.hashCode();
72     }
73
74     /**
75      * Answers true if the objects hold the same mapping between the same two
76      * sequences
77      * 
78      * @see Mapping#equals
79      */
80     @Override
81     public boolean equals(Object obj)
82     {
83       if (!(obj instanceof SequenceToSequenceMapping))
84       {
85         return false;
86       }
87       SequenceToSequenceMapping that = (SequenceToSequenceMapping) obj;
88       if (this.mapping == null)
89       {
90         return that.mapping == null;
91       }
92       // TODO: can simplify by asserting fromSeq is a dataset sequence
93       return (this.fromSeq == that.fromSeq
94               || (this.fromSeq != null && that.fromSeq != null
95                       && this.fromSeq.getDatasetSequence() != null
96                       && this.fromSeq.getDatasetSequence() == that.fromSeq
97                               .getDatasetSequence()))
98               && this.mapping.equals(that.mapping);
99     }
100
101     public SequenceI getFromSeq()
102     {
103       return fromSeq;
104     }
105
106     public Mapping getMapping()
107     {
108       return mapping;
109     }
110   }
111
112   private List<SequenceToSequenceMapping> mappings;
113
114   /**
115    * Constructor
116    */
117   public AlignedCodonFrame()
118   {
119     mappings = new ArrayList<SequenceToSequenceMapping>();
120   }
121
122   /**
123    * Adds a mapping between the dataset sequences for the associated dna and
124    * protein sequence objects
125    * 
126    * @param dnaseq
127    * @param aaseq
128    * @param map
129    */
130   public void addMap(SequenceI dnaseq, SequenceI aaseq, MapList map)
131   {
132     addMap(dnaseq, aaseq, map, null);
133   }
134
135   /**
136    * Adds a mapping between the dataset sequences for the associated dna and
137    * protein sequence objects
138    * 
139    * @param dnaseq
140    * @param aaseq
141    * @param map
142    * @param mapFromId
143    */
144   public void addMap(SequenceI dnaseq, SequenceI aaseq, MapList map,
145           String mapFromId)
146   {
147     // JBPNote DEBUG! THIS !
148     // dnaseq.transferAnnotation(aaseq, mp);
149     // aaseq.transferAnnotation(dnaseq, new Mapping(map.getInverse()));
150
151     SequenceI fromSeq = (dnaseq.getDatasetSequence() == null) ? dnaseq
152             : dnaseq.getDatasetSequence();
153     SequenceI toSeq = (aaseq.getDatasetSequence() == null) ? aaseq
154             : aaseq.getDatasetSequence();
155
156     /*
157      * if we already hold a mapping between these sequences, just add to it 
158      * note that 'adding' a duplicate map does nothing; this protects against
159      * creating duplicate mappings in AlignedCodonFrame
160      */
161     for (SequenceToSequenceMapping ssm : mappings)
162     {
163       if (ssm.fromSeq == fromSeq && ssm.mapping.to == toSeq)
164       {
165         ssm.mapping.map.addMapList(map);
166         return;
167       }
168     }
169
170     /*
171      * otherwise, add a new sequence mapping
172      */
173     Mapping mp = new Mapping(toSeq, map);
174     mp.setMappedFromId(mapFromId);
175     mappings.add(new SequenceToSequenceMapping(fromSeq, mp));
176   }
177
178   public SequenceI[] getdnaSeqs()
179   {
180     // TODO return a list instead?
181     // return dnaSeqs;
182     List<SequenceI> seqs = new ArrayList<SequenceI>();
183     for (SequenceToSequenceMapping ssm : mappings)
184     {
185       seqs.add(ssm.fromSeq);
186     }
187     return seqs.toArray(new SequenceI[seqs.size()]);
188   }
189
190   public SequenceI[] getAaSeqs()
191   {
192     // TODO not used - remove?
193     List<SequenceI> seqs = new ArrayList<SequenceI>();
194     for (SequenceToSequenceMapping ssm : mappings)
195     {
196       seqs.add(ssm.mapping.to);
197     }
198     return seqs.toArray(new SequenceI[seqs.size()]);
199   }
200
201   public MapList[] getdnaToProt()
202   {
203     List<MapList> maps = new ArrayList<MapList>();
204     for (SequenceToSequenceMapping ssm : mappings)
205     {
206       maps.add(ssm.mapping.map);
207     }
208     return maps.toArray(new MapList[maps.size()]);
209   }
210
211   public Mapping[] getProtMappings()
212   {
213     List<Mapping> maps = new ArrayList<Mapping>();
214     for (SequenceToSequenceMapping ssm : mappings)
215     {
216       maps.add(ssm.mapping);
217     }
218     return maps.toArray(new Mapping[maps.size()]);
219   }
220
221   /**
222    * Returns the first mapping found which is to or from the given sequence, or
223    * null.
224    * 
225    * @param seq
226    * @return
227    */
228   public Mapping getMappingForSequence(SequenceI seq)
229   {
230     SequenceI seqDs = seq.getDatasetSequence();
231     seqDs = seqDs != null ? seqDs : seq;
232
233     for (SequenceToSequenceMapping ssm : mappings)
234     {
235       if (ssm.fromSeq == seqDs || ssm.mapping.to == seqDs)
236       {
237         return ssm.mapping;
238       }
239     }
240     return null;
241   }
242
243   /**
244    * Return the corresponding aligned or dataset aa sequence for given dna
245    * sequence, null if not found.
246    * 
247    * @param sequenceRef
248    * @return
249    */
250   public SequenceI getAaForDnaSeq(SequenceI dnaSeqRef)
251   {
252     SequenceI dnads = dnaSeqRef.getDatasetSequence();
253     for (SequenceToSequenceMapping ssm : mappings)
254     {
255       if (ssm.fromSeq == dnaSeqRef || ssm.fromSeq == dnads)
256       {
257         return ssm.mapping.to;
258       }
259     }
260     return null;
261   }
262
263   /**
264    * 
265    * @param sequenceRef
266    * @return null or corresponding aaSeq entry for dnaSeq entry
267    */
268   public SequenceI getDnaForAaSeq(SequenceI aaSeqRef)
269   {
270     SequenceI aads = aaSeqRef.getDatasetSequence();
271     for (SequenceToSequenceMapping ssm : mappings)
272     {
273       if (ssm.mapping.to == aaSeqRef || ssm.mapping.to == aads)
274       {
275         return ssm.fromSeq;
276       }
277     }
278     return null;
279   }
280
281   /**
282    * test to see if codon frame involves seq in any way
283    * 
284    * @param seq
285    *          a nucleotide or protein sequence
286    * @return true if a mapping exists to or from this sequence to any translated
287    *         sequence
288    */
289   public boolean involvesSequence(SequenceI seq)
290   {
291     return getAaForDnaSeq(seq) != null || getDnaForAaSeq(seq) != null;
292   }
293
294   /**
295    * Add search results for regions in other sequences that translate or are
296    * translated from a particular position in seq
297    * 
298    * @param seq
299    * @param index
300    *          position in seq
301    * @param results
302    *          where highlighted regions go
303    */
304   public void markMappedRegion(SequenceI seq, int index,
305           SearchResultsI results)
306   {
307     int[] codon;
308     SequenceI ds = seq.getDatasetSequence();
309     for (SequenceToSequenceMapping ssm : mappings)
310     {
311       if (ssm.fromSeq == seq || ssm.fromSeq == ds)
312       {
313         codon = ssm.mapping.map.locateInTo(index, index);
314         if (codon != null)
315         {
316           for (int i = 0; i < codon.length; i += 2)
317           {
318             results.addResult(ssm.mapping.to, codon[i], codon[i + 1]);
319           }
320         }
321       }
322       else if (ssm.mapping.to == seq || ssm.mapping.to == ds)
323       {
324         {
325           codon = ssm.mapping.map.locateInFrom(index, index);
326           if (codon != null)
327           {
328             for (int i = 0; i < codon.length; i += 2)
329             {
330               results.addResult(ssm.fromSeq, codon[i], codon[i + 1]);
331             }
332           }
333         }
334       }
335     }
336   }
337
338   /**
339    * Returns the DNA codon positions (base 1) for the given position (base 1) in
340    * a mapped protein sequence, or null if no mapping is found.
341    * 
342    * Intended for use in aligning cDNA to match aligned protein. Only the first
343    * mapping found is returned, so not suitable for use if multiple protein
344    * sequences are mapped to the same cDNA (but aligning cDNA as protein is
345    * ill-defined for this case anyway).
346    * 
347    * @param seq
348    *          the DNA dataset sequence
349    * @param aaPos
350    *          residue position (base 1) in a protein sequence
351    * @return
352    */
353   public int[] getDnaPosition(SequenceI seq, int aaPos)
354   {
355     /*
356      * Adapted from markMappedRegion().
357      */
358     MapList ml = null;
359     int i = 0;
360     for (SequenceToSequenceMapping ssm : mappings)
361     {
362       if (ssm.fromSeq == seq)
363       {
364         ml = getdnaToProt()[i];
365         break;
366       }
367       i++;
368     }
369     return ml == null ? null : ml.locateInFrom(aaPos, aaPos);
370   }
371
372   /**
373    * Convenience method to return the first aligned sequence in the given
374    * alignment whose dataset has a mapping with the given (aligned or dataset)
375    * sequence.
376    * 
377    * @param seq
378    * 
379    * @param al
380    * @return
381    */
382   public SequenceI findAlignedSequence(SequenceI seq, AlignmentI al)
383   {
384     /*
385      * Search mapped protein ('to') sequences first.
386      */
387     for (SequenceToSequenceMapping ssm : mappings)
388     {
389       if (ssm.fromSeq == seq || ssm.fromSeq == seq.getDatasetSequence())
390       {
391         for (SequenceI sourceAligned : al.getSequences())
392         {
393           if (ssm.mapping.to == sourceAligned.getDatasetSequence()
394                   || ssm.mapping.to == sourceAligned)
395           {
396             return sourceAligned;
397           }
398         }
399       }
400     }
401
402     /*
403      * Then try mapped dna sequences.
404      */
405     for (SequenceToSequenceMapping ssm : mappings)
406     {
407       if (ssm.mapping.to == seq
408               || ssm.mapping.to == seq.getDatasetSequence())
409       {
410         for (SequenceI sourceAligned : al.getSequences())
411         {
412           if (ssm.fromSeq == sourceAligned.getDatasetSequence())
413           {
414             return sourceAligned;
415           }
416         }
417       }
418     }
419
420     return null;
421   }
422
423   /**
424    * Returns the region in the target sequence's dataset that is mapped to the
425    * given position (base 1) in the query sequence's dataset. The region is a
426    * set of start/end position pairs.
427    * 
428    * @param target
429    * @param query
430    * @param queryPos
431    * @return
432    */
433   public int[] getMappedRegion(SequenceI target, SequenceI query,
434           int queryPos)
435   {
436     SequenceI targetDs = target.getDatasetSequence() == null ? target
437             : target.getDatasetSequence();
438     SequenceI queryDs = query.getDatasetSequence() == null ? query
439             : query.getDatasetSequence();
440     if (targetDs == null || queryDs == null /*|| dnaToProt == null*/)
441     {
442       return null;
443     }
444     for (SequenceToSequenceMapping ssm : mappings)
445     {
446       /*
447        * try mapping from target to query
448        */
449       if (ssm.fromSeq == targetDs && ssm.mapping.to == queryDs)
450       {
451         int[] codon = ssm.mapping.map.locateInFrom(queryPos, queryPos);
452         if (codon != null)
453         {
454           return codon;
455         }
456       }
457       /*
458        * else try mapping from query to target
459        */
460       else if (ssm.fromSeq == queryDs && ssm.mapping.to == targetDs)
461       {
462         int[] codon = ssm.mapping.map.locateInTo(queryPos, queryPos);
463         if (codon != null)
464         {
465           return codon;
466         }
467       }
468     }
469     return null;
470   }
471
472   /**
473    * Returns the mapped DNA codons for the given position in a protein sequence,
474    * or null if no mapping is found. Returns a list of (e.g.) ['g', 'c', 't']
475    * codons. There may be more than one codon mapped to the protein if (for
476    * example), there are mappings to cDNA variants.
477    * 
478    * @param protein
479    *          the peptide dataset sequence
480    * @param aaPos
481    *          residue position (base 1) in the peptide sequence
482    * @return
483    */
484   public List<char[]> getMappedCodons(SequenceI protein, int aaPos)
485   {
486     MapList ml = null;
487     SequenceI dnaSeq = null;
488     List<char[]> result = new ArrayList<char[]>();
489
490     for (SequenceToSequenceMapping ssm : mappings)
491     {
492       if (ssm.mapping.to == protein
493               && ssm.mapping.getMap().getFromRatio() == 3)
494       {
495         ml = ssm.mapping.map;
496         dnaSeq = ssm.fromSeq;
497
498         int[] codonPos = ml.locateInFrom(aaPos, aaPos);
499         if (codonPos == null)
500         {
501           return null;
502         }
503
504         /*
505          * Read off the mapped nucleotides (converting to position base 0)
506          */
507         codonPos = MappingUtils.flattenRanges(codonPos);
508         char[] dna = dnaSeq.getSequence();
509         int start = dnaSeq.getStart();
510         result.add(
511                 new char[]
512                 { dna[codonPos[0] - start], dna[codonPos[1] - start],
513                     dna[codonPos[2] - start] });
514       }
515     }
516     return result.isEmpty() ? null : result;
517   }
518
519   /**
520    * Returns any mappings found which are from the given sequence, and to
521    * distinct sequences.
522    * 
523    * @param seq
524    * @return
525    */
526   public List<Mapping> getMappingsFromSequence(SequenceI seq)
527   {
528     List<Mapping> result = new ArrayList<Mapping>();
529     List<SequenceI> related = new ArrayList<SequenceI>();
530     SequenceI seqDs = seq.getDatasetSequence();
531     seqDs = seqDs != null ? seqDs : seq;
532
533     for (SequenceToSequenceMapping ssm : mappings)
534     {
535       final Mapping mapping = ssm.mapping;
536       if (ssm.fromSeq == seqDs)
537       {
538         if (!related.contains(mapping.to))
539         {
540           result.add(mapping);
541           related.add(mapping.to);
542         }
543       }
544     }
545     return result;
546   }
547
548   /**
549    * Test whether the given sequence is substitutable for one or more dummy
550    * sequences in this mapping
551    * 
552    * @param map
553    * @param seq
554    * @return
555    */
556   public boolean isRealisableWith(SequenceI seq)
557   {
558     return realiseWith(seq, false) > 0;
559   }
560
561   /**
562    * Replace any matchable mapped dummy sequences with the given real one.
563    * Returns the count of sequence mappings instantiated.
564    * 
565    * @param seq
566    * @return
567    */
568   public int realiseWith(SequenceI seq)
569   {
570     return realiseWith(seq, true);
571   }
572
573   /**
574    * Returns the number of mapped dummy sequences that could be replaced with
575    * the given real sequence.
576    * 
577    * @param seq
578    *          a dataset sequence
579    * @param doUpdate
580    *          if true, performs replacements, else only counts
581    * @return
582    */
583   protected int realiseWith(SequenceI seq, boolean doUpdate)
584   {
585     SequenceI ds = seq.getDatasetSequence() != null
586             ? seq.getDatasetSequence()
587             : seq;
588     int count = 0;
589
590     /*
591      * check for replaceable DNA ('map from') sequences
592      */
593     for (SequenceToSequenceMapping ssm : mappings)
594     {
595       SequenceI dna = ssm.fromSeq;
596       if (dna instanceof SequenceDummy
597               && dna.getName().equals(ds.getName()))
598       {
599         Mapping mapping = ssm.mapping;
600         int mapStart = mapping.getMap().getFromLowest();
601         int mapEnd = mapping.getMap().getFromHighest();
602         boolean mappable = couldRealiseSequence(dna, ds, mapStart, mapEnd);
603         if (mappable)
604         {
605           count++;
606           if (doUpdate)
607           {
608             // TODO: new method ? ds.realise(dna);
609             // might want to copy database refs as well
610             ds.setSequenceFeatures(dna.getSequenceFeatures());
611             // dnaSeqs[i] = ds;
612             ssm.fromSeq = ds;
613             System.out.println("Realised mapped sequence " + ds.getName());
614           }
615         }
616       }
617
618       /*
619        * check for replaceable protein ('map to') sequences
620        */
621       Mapping mapping = ssm.mapping;
622       SequenceI prot = mapping.getTo();
623       int mapStart = mapping.getMap().getToLowest();
624       int mapEnd = mapping.getMap().getToHighest();
625       boolean mappable = couldRealiseSequence(prot, ds, mapStart, mapEnd);
626       if (mappable)
627       {
628         count++;
629         if (doUpdate)
630         {
631           // TODO: new method ? ds.realise(dna);
632           // might want to copy database refs as well
633           ds.setSequenceFeatures(dna.getSequenceFeatures());
634           ssm.mapping.setTo(ds);
635         }
636       }
637     }
638     return count;
639   }
640
641   /**
642    * Helper method to test whether a 'real' sequence could replace a 'dummy'
643    * sequence in the map. The criteria are that they have the same name, and
644    * that the mapped region overlaps the candidate sequence.
645    * 
646    * @param existing
647    * @param replacement
648    * @param mapStart
649    * @param mapEnd
650    * @return
651    */
652   protected static boolean couldRealiseSequence(SequenceI existing,
653           SequenceI replacement, int mapStart, int mapEnd)
654   {
655     if (existing instanceof SequenceDummy
656             && !(replacement instanceof SequenceDummy)
657             && existing.getName().equals(replacement.getName()))
658     {
659       int start = replacement.getStart();
660       int end = replacement.getEnd();
661       boolean mappingOverlapsSequence = (mapStart >= start
662               && mapStart <= end) || (mapEnd >= start && mapEnd <= end);
663       if (mappingOverlapsSequence)
664       {
665         return true;
666       }
667     }
668     return false;
669   }
670
671   /**
672    * Change any mapping to the given sequence to be to its dataset sequence
673    * instead. For use when mappings are created before their referenced
674    * sequences are instantiated, for example when parsing GFF data.
675    * 
676    * @param seq
677    */
678   public void updateToDataset(SequenceI seq)
679   {
680     if (seq == null || seq.getDatasetSequence() == null)
681     {
682       return;
683     }
684     SequenceI ds = seq.getDatasetSequence();
685
686     for (SequenceToSequenceMapping ssm : mappings)
687     /*
688      * 'from' sequences
689      */
690     {
691       if (ssm.fromSeq == seq)
692       {
693         ssm.fromSeq = ds;
694       }
695
696       /*
697        * 'to' sequences
698        */
699       if (ssm.mapping.to == seq)
700       {
701         ssm.mapping.to = ds;
702       }
703     }
704   }
705
706   /**
707    * Answers true if this object contains no mappings
708    * 
709    * @return
710    */
711   public boolean isEmpty()
712   {
713     return mappings.isEmpty();
714   }
715
716   /**
717    * Method for debug / inspection purposes only, may change in future
718    */
719   @Override
720   public String toString()
721   {
722     return mappings == null ? "null" : mappings.toString();
723   }
724
725   /**
726    * Returns the first mapping found that is between 'fromSeq' and 'toSeq', or
727    * null if none found
728    * 
729    * @param fromSeq
730    *          aligned or dataset sequence
731    * @param toSeq
732    *          aligned or dataset sequence
733    * @return
734    */
735   public Mapping getMappingBetween(SequenceI fromSeq, SequenceI toSeq)
736   {
737     SequenceI dssFrom = fromSeq.getDatasetSequence() == null ? fromSeq
738             : fromSeq.getDatasetSequence();
739     SequenceI dssTo = toSeq.getDatasetSequence() == null ? toSeq
740             : toSeq.getDatasetSequence();
741
742     for (SequenceToSequenceMapping mapping : mappings)
743     {
744       SequenceI from = mapping.fromSeq;
745       SequenceI to = mapping.mapping.to;
746       if ((from == dssFrom && to == dssTo)
747               || (from == dssTo && to == dssFrom))
748       {
749         return mapping.mapping;
750       }
751     }
752     return null;
753   }
754
755   /**
756    * Returns a hashcode derived from the list of sequence mappings
757    * 
758    * @see SequenceToSequenceMapping#hashCode()
759    * @see AbstractList#hashCode()
760    */
761   @Override
762   public int hashCode()
763   {
764     return this.mappings.hashCode();
765   }
766
767   /**
768    * Two AlignedCodonFrame objects are equal if they hold the same ordered list
769    * of mappings
770    * 
771    * @see SequenceToSequenceMapping#
772    */
773   @Override
774   public boolean equals(Object obj)
775   {
776     if (!(obj instanceof AlignedCodonFrame))
777     {
778       return false;
779     }
780     return this.mappings.equals(((AlignedCodonFrame) obj).mappings);
781   }
782
783   public List<SequenceToSequenceMapping> getMappings()
784   {
785     return mappings;
786   }
787 }