JAL-3541 selectively merged build.gradle and gradle.properties
[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<>();
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<>();
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<>();
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<>();
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<>();
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 if none is found
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<>();
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         int start = dnaSeq.getStart();
509         char c1 = dnaSeq.getCharAt(codonPos[0] - start);
510         char c2 = dnaSeq.getCharAt(codonPos[1] - start);
511         char c3 = dnaSeq.getCharAt(codonPos[2] - start);
512         result.add(new char[] { c1, c2, c3 });
513       }
514     }
515     return result.isEmpty() ? null : result;
516   }
517
518   /**
519    * Returns any mappings found which are from the given sequence, and to
520    * distinct sequences.
521    * 
522    * @param seq
523    * @return
524    */
525   public List<Mapping> getMappingsFromSequence(SequenceI seq)
526   {
527     List<Mapping> result = new ArrayList<>();
528     List<SequenceI> related = new ArrayList<>();
529     SequenceI seqDs = seq.getDatasetSequence();
530     seqDs = seqDs != null ? seqDs : seq;
531
532     for (SequenceToSequenceMapping ssm : mappings)
533     {
534       final Mapping mapping = ssm.mapping;
535       if (ssm.fromSeq == seqDs)
536       {
537         if (!related.contains(mapping.to))
538         {
539           result.add(mapping);
540           related.add(mapping.to);
541         }
542       }
543     }
544     return result;
545   }
546
547   /**
548    * Test whether the given sequence is substitutable for one or more dummy
549    * sequences in this mapping
550    * 
551    * @param map
552    * @param seq
553    * @return
554    */
555   public boolean isRealisableWith(SequenceI seq)
556   {
557     return realiseWith(seq, false) > 0;
558   }
559
560   /**
561    * Replace any matchable mapped dummy sequences with the given real one.
562    * Returns the count of sequence mappings instantiated.
563    * 
564    * @param seq
565    * @return
566    */
567   public int realiseWith(SequenceI seq)
568   {
569     return realiseWith(seq, true);
570   }
571
572   /**
573    * Returns the number of mapped dummy sequences that could be replaced with
574    * the given real sequence.
575    * 
576    * @param seq
577    *          a dataset sequence
578    * @param doUpdate
579    *          if true, performs replacements, else only counts
580    * @return
581    */
582   protected int realiseWith(SequenceI seq, boolean doUpdate)
583   {
584     SequenceI ds = seq.getDatasetSequence() != null
585             ? seq.getDatasetSequence()
586             : seq;
587     int count = 0;
588
589     /*
590      * check for replaceable DNA ('map from') sequences
591      */
592     for (SequenceToSequenceMapping ssm : mappings)
593     {
594       SequenceI dna = ssm.fromSeq;
595       if (dna instanceof SequenceDummy
596               && dna.getName().equals(ds.getName()))
597       {
598         Mapping mapping = ssm.mapping;
599         int mapStart = mapping.getMap().getFromLowest();
600         int mapEnd = mapping.getMap().getFromHighest();
601         boolean mappable = couldRealiseSequence(dna, ds, mapStart, mapEnd);
602         if (mappable)
603         {
604           count++;
605           if (doUpdate)
606           {
607             // TODO: new method ? ds.realise(dna);
608             // might want to copy database refs as well
609             ds.setSequenceFeatures(dna.getSequenceFeatures());
610             // dnaSeqs[i] = ds;
611             ssm.fromSeq = ds;
612             System.out.println("Realised mapped sequence " + ds.getName());
613           }
614         }
615       }
616
617       /*
618        * check for replaceable protein ('map to') sequences
619        */
620       Mapping mapping = ssm.mapping;
621       SequenceI prot = mapping.getTo();
622       int mapStart = mapping.getMap().getToLowest();
623       int mapEnd = mapping.getMap().getToHighest();
624       boolean mappable = couldRealiseSequence(prot, ds, mapStart, mapEnd);
625       if (mappable)
626       {
627         count++;
628         if (doUpdate)
629         {
630           // TODO: new method ? ds.realise(dna);
631           // might want to copy database refs as well
632           ds.setSequenceFeatures(dna.getSequenceFeatures());
633           ssm.mapping.setTo(ds);
634         }
635       }
636     }
637     return count;
638   }
639
640   /**
641    * Helper method to test whether a 'real' sequence could replace a 'dummy'
642    * sequence in the map. The criteria are that they have the same name, and
643    * that the mapped region overlaps the candidate sequence.
644    * 
645    * @param existing
646    * @param replacement
647    * @param mapStart
648    * @param mapEnd
649    * @return
650    */
651   protected static boolean couldRealiseSequence(SequenceI existing,
652           SequenceI replacement, int mapStart, int mapEnd)
653   {
654     if (existing instanceof SequenceDummy
655             && !(replacement instanceof SequenceDummy)
656             && existing.getName().equals(replacement.getName()))
657     {
658       int start = replacement.getStart();
659       int end = replacement.getEnd();
660       boolean mappingOverlapsSequence = (mapStart >= start
661               && mapStart <= end) || (mapEnd >= start && mapEnd <= end);
662       if (mappingOverlapsSequence)
663       {
664         return true;
665       }
666     }
667     return false;
668   }
669
670   /**
671    * Change any mapping to the given sequence to be to its dataset sequence
672    * instead. For use when mappings are created before their referenced
673    * sequences are instantiated, for example when parsing GFF data.
674    * 
675    * @param seq
676    */
677   public void updateToDataset(SequenceI seq)
678   {
679     if (seq == null || seq.getDatasetSequence() == null)
680     {
681       return;
682     }
683     SequenceI ds = seq.getDatasetSequence();
684
685     for (SequenceToSequenceMapping ssm : mappings)
686     /*
687      * 'from' sequences
688      */
689     {
690       if (ssm.fromSeq == seq)
691       {
692         ssm.fromSeq = ds;
693       }
694
695       /*
696        * 'to' sequences
697        */
698       if (ssm.mapping.to == seq)
699       {
700         ssm.mapping.to = ds;
701       }
702     }
703   }
704
705   /**
706    * Answers true if this object contains no mappings
707    * 
708    * @return
709    */
710   public boolean isEmpty()
711   {
712     return mappings.isEmpty();
713   }
714
715   /**
716    * Method for debug / inspection purposes only, may change in future
717    */
718   @Override
719   public String toString()
720   {
721     return mappings == null ? "null" : mappings.toString();
722   }
723
724   /**
725    * Returns the first mapping found that is between 'fromSeq' and 'toSeq', or
726    * null if none found
727    * 
728    * @param fromSeq
729    *          aligned or dataset sequence
730    * @param toSeq
731    *          aligned or dataset sequence
732    * @return
733    */
734   public Mapping getMappingBetween(SequenceI fromSeq, SequenceI toSeq)
735   {
736     SequenceI dssFrom = fromSeq.getDatasetSequence() == null ? fromSeq
737             : fromSeq.getDatasetSequence();
738     SequenceI dssTo = toSeq.getDatasetSequence() == null ? toSeq
739             : toSeq.getDatasetSequence();
740
741     for (SequenceToSequenceMapping mapping : mappings)
742     {
743       SequenceI from = mapping.fromSeq;
744       SequenceI to = mapping.mapping.to;
745       if ((from == dssFrom && to == dssTo)
746               || (from == dssTo && to == dssFrom))
747       {
748         return mapping.mapping;
749       }
750     }
751     return null;
752   }
753
754   /**
755    * Returns a hashcode derived from the list of sequence mappings
756    * 
757    * @see SequenceToSequenceMapping#hashCode()
758    * @see AbstractList#hashCode()
759    */
760   @Override
761   public int hashCode()
762   {
763     return this.mappings.hashCode();
764   }
765
766   /**
767    * Two AlignedCodonFrame objects are equal if they hold the same ordered list
768    * of mappings
769    * 
770    * @see SequenceToSequenceMapping#
771    */
772   @Override
773   public boolean equals(Object obj)
774   {
775     if (!(obj instanceof AlignedCodonFrame))
776     {
777       return false;
778     }
779     return this.mappings.equals(((AlignedCodonFrame) obj).mappings);
780   }
781
782   public List<SequenceToSequenceMapping> getMappings()
783   {
784     return mappings;
785   }
786 }