JAL-3567 report mapped location for virtual features in tooltip etc
[jalview.git] / src / jalview / datamodel / MappedFeatures.java
1 package jalview.datamodel;
2
3 import java.util.HashSet;
4 import java.util.List;
5 import java.util.Set;
6
7 import jalview.io.gff.Gff3Helper;
8 import jalview.schemes.ResidueProperties;
9 import jalview.util.MapList;
10 import jalview.util.MappingUtils;
11 import jalview.util.StringUtils;
12
13 /**
14  * A data bean to hold a list of mapped sequence features (e.g. CDS features
15  * mapped from protein), and the mapping between the sequences. It also provides
16  * a method to derive peptide variants from codon variants.
17  * 
18  * @author gmcarstairs
19  */
20 public class MappedFeatures
21 {
22   /*
23    * VEP CSQ:HGVSp (if present) is a short-cut to the protein variant consequence
24    */
25   private static final String HGV_SP = "HGVSp";
26
27   private static final String CSQ = "CSQ";
28
29   /*
30    * the sequence the mapped features are on
31    */
32   private final SequenceI linkedSeq;
33
34   /*
35    * the mapping between sequences;
36    * NB this could be in either sense
37    */
38   private final Mapping mapping;
39
40   /*
41    * if true, mapping is from the linked sequence, else to the linked sequence
42    */
43   private boolean mappingIsFromLinkedSequence;
44
45   /*
46    * features on linkedSeq that overlap the mapped positions
47    */
48   public final List<SequenceFeature> features;
49
50   /*
51    * the residue position in the sequence mapped to
52    */
53   private final int toPosition;
54
55   /*
56    * the residue at toPosition 
57    */
58   private final char toResidue;
59
60   /*
61    * if the mapping is 3:1 or 1:3 (peptide to CDS), this holds the
62    * mapped positions i.e. codon base positions in CDS; to
63    * support calculation of peptide variants from alleles
64    */
65   private final int[] codonPos;
66
67   private final char[] baseCodon;
68
69   /**
70    * Constructor
71    * 
72    * @param theMapping
73    * @param from
74    *                      the sequence mapped from (e.g. CDS)
75    * @param pos
76    *                      the residue position in the sequence mapped to
77    * @param res
78    *                      the residue character at position pos
79    * @param theFeatures
80    *                      list of mapped features found in the 'from' sequence at
81    *                      the mapped position(s)
82    */
83   public MappedFeatures(Mapping theMapping, SequenceI from, int pos,
84           char res, List<SequenceFeature> theFeatures)
85   {
86     mapping = theMapping;
87     linkedSeq = from;
88     mappingIsFromLinkedSequence = mapping.to != linkedSeq;
89     toPosition = pos;
90     toResidue = res;
91     features = theFeatures;
92
93     /*
94      * determine codon positions and canonical codon
95      * for a peptide-to-CDS mapping
96      */
97     int[] codonIntervals = mapping.getMap().locateInFrom(toPosition, toPosition);
98     int[] codonPositions = codonIntervals == null ? null
99             : MappingUtils.flattenRanges(codonIntervals);
100     if (codonPositions != null && codonPositions.length == 3)
101     {
102       codonPos = codonPositions;
103       baseCodon = new char[3];
104       int cdsStart = linkedSeq.getStart();
105       baseCodon[0] = Character
106               .toUpperCase(linkedSeq.getCharAt(codonPos[0] - cdsStart));
107       baseCodon[1] = Character
108               .toUpperCase(linkedSeq.getCharAt(codonPos[1] - cdsStart));
109       baseCodon[2] = Character
110               .toUpperCase(linkedSeq.getCharAt(codonPos[2] - cdsStart));
111     }
112     else
113     {
114       codonPos = null;
115       baseCodon = null;
116     }
117   }
118
119   /**
120    * Computes and returns comma-delimited HGVS notation peptide variants derived
121    * from codon allele variants. If no variants are found, answers an empty
122    * string.
123    * 
124    * @param sf
125    *             a sequence feature (which must be one of those held in this
126    *             object)
127    * @return
128    */
129   public String findProteinVariants(SequenceFeature sf)
130   {
131     if (!features.contains(sf) || baseCodon == null)
132     {
133       return "";
134     }
135
136     /*
137      * VCF data may already contain the protein consequence
138      */
139     String hgvsp = sf.getValueAsString(CSQ, HGV_SP);
140     if (hgvsp != null)
141     {
142       int colonPos = hgvsp.lastIndexOf(':');
143       if (colonPos >= 0)
144       {
145         String var = hgvsp.substring(colonPos + 1);
146         if (var.contains("p.")) // sanity check
147         {
148           return var;
149         }
150       }
151     }
152
153     /*
154      * otherwise, compute codon and peptide variant
155      */
156     int cdsPos = sf.getBegin();
157     if (cdsPos != sf.getEnd())
158     {
159       // not handling multi-locus variant features
160       return "";
161     }
162     if (cdsPos != codonPos[0] && cdsPos != codonPos[1]
163             && cdsPos != codonPos[2])
164     {
165       // e.g. feature on intron within spliced codon!
166       return "";
167     }
168
169     String alls = (String) sf.getValue(Gff3Helper.ALLELES);
170     if (alls == null)
171     {
172       return "";
173     }
174
175     String from3 = StringUtils.toSentenceCase(
176             ResidueProperties.aa2Triplet.get(String.valueOf(toResidue)));
177
178     /*
179      * make a peptide variant for each SNP allele 
180      * e.g. C,G,T gives variants G and T for base C
181      */
182     Set<String> variantPeptides = new HashSet<>();
183     String[] alleles = alls.toUpperCase().split(",");
184     StringBuilder vars = new StringBuilder();
185
186     for (String allele : alleles)
187     {
188       allele = allele.trim().toUpperCase();
189       if (allele.length() > 1 || "-".equals(allele))
190       {
191         continue; // multi-locus variant
192       }
193       char[] variantCodon = new char[3];
194       variantCodon[0] = baseCodon[0];
195       variantCodon[1] = baseCodon[1];
196       variantCodon[2] = baseCodon[2];
197
198       /*
199        * poke variant base into canonical codon;
200        * ignore first 'allele' (canonical base)
201        */
202       final int i = cdsPos == codonPos[0] ? 0
203               : (cdsPos == codonPos[1] ? 1 : 2);
204       variantCodon[i] = allele.toUpperCase().charAt(0);
205       if (variantCodon[i] == baseCodon[i])
206       {
207         continue;
208       }
209       String codon = new String(variantCodon);
210       String peptide = ResidueProperties.codonTranslate(codon);
211       boolean synonymous = toResidue == peptide.charAt(0);
212       StringBuilder var = new StringBuilder();
213       if (synonymous)
214       {
215         /*
216          * synonymous variant notation e.g. c.1062C>A(p.=)
217          */
218         var.append("c.").append(String.valueOf(cdsPos))
219                 .append(String.valueOf(baseCodon[i])).append(">")
220                 .append(String.valueOf(variantCodon[i]))
221                 .append("(p.=)");
222       }
223       else
224       {
225         /*
226          * missense variant notation e.g. p.Arg355Met
227          */
228         String to3 = ResidueProperties.STOP.equals(peptide) ? "Ter"
229                 : StringUtils.toSentenceCase(
230                         ResidueProperties.aa2Triplet.get(peptide));
231         var.append("p.").append(from3).append(String.valueOf(toPosition))
232                 .append(to3);
233       }
234       if (!variantPeptides.contains(peptide)) // duplicate consequence
235       {
236         variantPeptides.add(peptide);
237         if (vars.length() > 0)
238         {
239           vars.append(",");
240         }
241         vars.append(var);
242       }
243     }
244
245     return vars.toString();
246   }
247
248   /**
249    * Answers the name of the linked sequence holding any mapped features
250    * 
251    * @return
252    */
253   public String getLinkedSequenceName()
254   {
255     return linkedSeq == null ? null : linkedSeq.getName();
256   }
257
258   /**
259    * Answers the mapped ranges (as one or more [start, end] positions) which
260    * correspond to the given [begin, end] range of the linked sequence.
261    * 
262    * <pre>
263    * Example: MappedFeatures with CDS features mapped to peptide 
264    * CDS/200-220 gtc aac TGa acGt att AAC tta
265    * mapped to PEP/6-7 WN by mapping [206, 207, 210, 210, 215, 217] to [6, 7]
266    * getMappedPositions(206, 206) should return [6, 6]
267    * getMappedPositions(200, 214) should return [6, 6]
268    * getMappedPositions(210, 215) should return [6, 7]
269    * </pre>
270    * 
271    * @param begin
272    * @param end
273    * @return
274    */
275   public int[] getMappedPositions(int begin, int end)
276   {
277     MapList map = mapping.getMap();
278     return mappingIsFromLinkedSequence ? map.locateInTo(begin, end)
279             : map.locateInFrom(begin, end);
280   }
281
282   public boolean isFromCds()
283   {
284     if (mapping.getMap().getFromRatio() == 3)
285     {
286       return mappingIsFromLinkedSequence;
287     }
288     return !mappingIsFromLinkedSequence;
289   }
290 }